From 572bb4ef363ee4ae37b8ca8bf1ecb228360133c0 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 22 Mar 2026 21:38:56 +0300 Subject: [PATCH 01/18] fix preview white textutes --- Data/expansions/classic/dbc_layouts.json | 274 +++++++++++++----- Data/expansions/tbc/dbc_layouts.json | 329 ++++++++++++++++----- Data/expansions/turtle/dbc_layouts.json | 316 ++++++++++++++++----- Data/expansions/wotlk/dbc_layouts.json | 346 ++++++++++++++++++----- src/rendering/character_preview.cpp | 14 +- 5 files changed, 984 insertions(+), 295 deletions(-) diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json index e5d0793f..a3cba2f8 100644 --- a/Data/expansions/classic/dbc_layouts.json +++ b/Data/expansions/classic/dbc_layouts.json @@ -1,108 +1,256 @@ { "Spell": { - "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117, - "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, + "ID": 0, + "Attributes": 5, + "AttributesEx": 6, + "IconID": 117, + "Name": 120, + "Tooltip": 147, + "Rank": 129, + "SchoolEnum": 1, + "CastingTimeIndex": 15, + "PowerType": 28, + "ManaCost": 29, + "RangeIndex": 33, "DispelType": 4 }, - "SpellRange": { "MaxRange": 2 }, + "SpellRange": { + "MaxRange": 2 + }, "ItemDisplayInfo": { - "ID": 0, "LeftModel": 1, "LeftModelTexture": 3, - "InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9, - "TextureArmUpper": 14, "TextureArmLower": 15, "TextureHand": 16, - "TextureTorsoUpper": 17, "TextureTorsoLower": 18, - "TextureLegUpper": 19, "TextureLegLower": 20, "TextureFoot": 21 + "ID": 0, + "LeftModel": 1, + "LeftModelTexture": 3, + "InventoryIcon": 5, + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "TextureArmUpper": 14, + "TextureArmLower": 15, + "TextureHand": 16, + "TextureTorsoUpper": 17, + "TextureTorsoLower": 18, + "TextureLegUpper": 19, + "TextureLegLower": 20, + "TextureFoot": 21 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "Texture1": 4, + "Texture2": 5, + "Texture3": 6, + "Flags": 7, + "VariationIndex": 8, + "ColorIndex": 9 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 }, - "SpellIcon": { "ID": 0, "Path": 1 }, "FactionTemplate": { - "ID": 0, "Faction": 1, "FactionGroup": 3, - "FriendGroup": 4, "EnemyGroup": 5, - "Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9 + "ID": 0, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "EnemyGroup": 5, + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9 }, "Faction": { - "ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3, - "ReputationRaceMask2": 4, "ReputationRaceMask3": 5, - "ReputationBase0": 10, "ReputationBase1": 11, - "ReputationBase2": 12, "ReputationBase3": 13 + "ID": 0, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13 + }, + "AreaTable": { + "ID": 0, + "MapID": 1, + "ParentAreaNum": 2, + "ExploreFlag": 3 }, - "AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 }, "CreatureDisplayInfoExtra": { - "ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4, - "HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7, - "EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10, - "EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13, - "EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16, - "EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20 + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3, + "FaceID": 4, + "HairStyleID": 5, + "HairColorID": 6, + "FacialHairID": 7, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "EquipDisplay10": 18, + "BakeName": 20 }, "CreatureDisplayInfo": { - "ID": 0, "ModelID": 1, "ExtraDisplayId": 3, - "Skin1": 6, "Skin2": 7, "Skin3": 8 + "ID": 0, + "ModelID": 1, + "ExtraDisplayId": 3, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 }, "TaxiNodes": { - "ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5 + "ID": 0, + "MapID": 1, + "X": 2, + "Y": 3, + "Z": 4, + "Name": 5 + }, + "TaxiPath": { + "ID": 0, + "FromNode": 1, + "ToNode": 2, + "Cost": 3 }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, "TaxiPathNode": { - "ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3, - "X": 4, "Y": 5, "Z": 6 + "ID": 0, + "PathID": 1, + "NodeIndex": 2, + "MapID": 3, + "X": 4, + "Y": 5, + "Z": 6 }, "TalentTab": { - "ID": 0, "Name": 1, "ClassMask": 12, - "OrderIndex": 14, "BackgroundFile": 15 + "ID": 0, + "Name": 1, + "ClassMask": 12, + "OrderIndex": 14, + "BackgroundFile": 15 }, "Talent": { - "ID": 0, "TabID": 1, "Row": 2, "Column": 3, - "RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12 + "ID": 0, + "TabID": 1, + "Row": 2, + "Column": 3, + "RankSpell0": 4, + "PrereqTalent0": 9, + "PrereqRank0": 12 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "SkillLine": { + "ID": 0, + "Category": 1, + "Name": 3 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 }, - "SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 }, - "SkillLine": { "ID": 0, "Category": 1, "Name": 3 }, - "Map": { "ID": 0, "InternalName": 1 }, - "CreatureModelData": { "ID": 0, "ModelPath": 2 }, "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "RaceID": 1, + "SexID": 2, + "Variation": 3, + "GeosetID": 4 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "RaceID": 0, + "SexID": 1, + "Variation": 2, + "Geoset100": 3, + "Geoset300": 4, + "Geoset200": 5 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "Emotes": { + "ID": 0, + "AnimID": 2 }, - "GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 }, - "Emotes": { "ID": 0, "AnimID": 2 }, "EmotesText": { - "ID": 0, "Command": 1, "EmoteRef": 2, - "OthersTargetTextID": 3, "SenderTargetTextID": 5, - "OthersNoTargetTextID": 7, "SenderNoTargetTextID": 9 + "ID": 0, + "Command": 1, + "EmoteRef": 2, + "OthersTargetTextID": 3, + "SenderTargetTextID": 5, + "OthersNoTargetTextID": 7, + "SenderNoTargetTextID": 9 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "EmotesTextData": { "ID": 0, "Text": 1 }, "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "ID": 0, + "MapID": 1, + "X": 2, + "Z": 3, + "Y": 4, + "InnerRadius": 5, + "OuterRadius": 6, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9 + }, + "LightParams": { + "LightParamsID": 0 }, - "LightParams": { "LightParamsID": 0 }, "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "ID": 0, + "MapID": 1, + "AreaID": 2, + "AreaName": 3, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "LocBottom": 7, + "DisplayMapID": 8, + "ParentWorldMapID": 10 }, "SpellVisual": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 + "ID": 0, + "CastKit": 2, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "ID": 0, + "BaseEffect": 5, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "ID": 0, + "FilePath": 2 } } diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json index da2fb9a5..8dc4bbe8 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -1,124 +1,303 @@ { "Spell": { - "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 124, - "Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215, - "CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40, + "ID": 0, + "Attributes": 5, + "AttributesEx": 6, + "IconID": 124, + "Name": 127, + "Tooltip": 154, + "Rank": 136, + "SchoolMask": 215, + "CastingTimeIndex": 22, + "PowerType": 35, + "ManaCost": 36, + "RangeIndex": 40, "DispelType": 3 }, - "SpellRange": { "MaxRange": 4 }, + "SpellRange": { + "MaxRange": 4 + }, "ItemDisplayInfo": { - "ID": 0, "LeftModel": 1, "LeftModelTexture": 3, - "InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9, - "TextureArmUpper": 14, "TextureArmLower": 15, "TextureHand": 16, - "TextureTorsoUpper": 17, "TextureTorsoLower": 18, - "TextureLegUpper": 19, "TextureLegLower": 20, "TextureFoot": 21 + "ID": 0, + "LeftModel": 1, + "LeftModelTexture": 3, + "InventoryIcon": 5, + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "TextureArmUpper": 14, + "TextureArmLower": 15, + "TextureHand": 16, + "TextureTorsoUpper": 17, + "TextureTorsoLower": 18, + "TextureLegUpper": 19, + "TextureLegLower": 20, + "TextureFoot": 21 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "Texture1": 4, + "Texture2": 5, + "Texture3": 6, + "Flags": 7, + "VariationIndex": 8, + "ColorIndex": 9 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 }, - "SpellIcon": { "ID": 0, "Path": 1 }, "FactionTemplate": { - "ID": 0, "Faction": 1, "FactionGroup": 3, - "FriendGroup": 4, "EnemyGroup": 5, - "Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9 + "ID": 0, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "EnemyGroup": 5, + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9 }, "Faction": { - "ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3, - "ReputationRaceMask2": 4, "ReputationRaceMask3": 5, - "ReputationBase0": 10, "ReputationBase1": 11, - "ReputationBase2": 12, "ReputationBase3": 13 + "ID": 0, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13 + }, + "CharTitles": { + "ID": 0, + "Title": 2, + "TitleBit": 20 + }, + "AreaTable": { + "ID": 0, + "MapID": 1, + "ParentAreaNum": 2, + "ExploreFlag": 3 }, - "CharTitles": { "ID": 0, "Title": 2, "TitleBit": 20 }, - "AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 }, "CreatureDisplayInfoExtra": { - "ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4, - "HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7, - "EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10, - "EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13, - "EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16, - "EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20 + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3, + "FaceID": 4, + "HairStyleID": 5, + "HairColorID": 6, + "FacialHairID": 7, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "EquipDisplay10": 18, + "BakeName": 20 }, "CreatureDisplayInfo": { - "ID": 0, "ModelID": 1, "ExtraDisplayId": 3, - "Skin1": 6, "Skin2": 7, "Skin3": 8 + "ID": 0, + "ModelID": 1, + "ExtraDisplayId": 3, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 }, "TaxiNodes": { - "ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5, - "MountDisplayIdAllianceFallback": 12, "MountDisplayIdHordeFallback": 13, - "MountDisplayIdAlliance": 14, "MountDisplayIdHorde": 15 + "ID": 0, + "MapID": 1, + "X": 2, + "Y": 3, + "Z": 4, + "Name": 5, + "MountDisplayIdAllianceFallback": 12, + "MountDisplayIdHordeFallback": 13, + "MountDisplayIdAlliance": 14, + "MountDisplayIdHorde": 15 + }, + "TaxiPath": { + "ID": 0, + "FromNode": 1, + "ToNode": 2, + "Cost": 3 }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, "TaxiPathNode": { - "ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3, - "X": 4, "Y": 5, "Z": 6 + "ID": 0, + "PathID": 1, + "NodeIndex": 2, + "MapID": 3, + "X": 4, + "Y": 5, + "Z": 6 }, "TalentTab": { - "ID": 0, "Name": 1, "ClassMask": 12, - "OrderIndex": 14, "BackgroundFile": 15 + "ID": 0, + "Name": 1, + "ClassMask": 12, + "OrderIndex": 14, + "BackgroundFile": 15 }, "Talent": { - "ID": 0, "TabID": 1, "Row": 2, "Column": 3, - "RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12 + "ID": 0, + "TabID": 1, + "Row": 2, + "Column": 3, + "RankSpell0": 4, + "PrereqTalent0": 9, + "PrereqRank0": 12 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "SkillLine": { + "ID": 0, + "Category": 1, + "Name": 3 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 }, - "SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 }, - "SkillLine": { "ID": 0, "Category": 1, "Name": 3 }, - "Map": { "ID": 0, "InternalName": 1 }, - "CreatureModelData": { "ID": 0, "ModelPath": 2 }, "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "RaceID": 1, + "SexID": 2, + "Variation": 3, + "GeosetID": 4 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "RaceID": 0, + "SexID": 1, + "Variation": 2, + "Geoset100": 3, + "Geoset300": 4, + "Geoset200": 5 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "Emotes": { + "ID": 0, + "AnimID": 2 }, - "GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 }, - "Emotes": { "ID": 0, "AnimID": 2 }, "EmotesText": { - "ID": 0, "Command": 1, "EmoteRef": 2, - "OthersTargetTextID": 3, "SenderTargetTextID": 5, - "OthersNoTargetTextID": 7, "SenderNoTargetTextID": 9 + "ID": 0, + "Command": 1, + "EmoteRef": 2, + "OthersTargetTextID": 3, + "SenderTargetTextID": 5, + "OthersNoTargetTextID": 7, + "SenderNoTargetTextID": 9 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "EmotesTextData": { "ID": 0, "Text": 1 }, "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "ID": 0, + "MapID": 1, + "X": 2, + "Z": 3, + "Y": 4, + "InnerRadius": 5, + "OuterRadius": 6, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9 + }, + "LightParams": { + "LightParamsID": 0 }, - "LightParams": { "LightParamsID": 0 }, "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "ID": 0, + "MapID": 1, + "AreaID": 2, + "AreaName": 3, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "LocBottom": 7, + "DisplayMapID": 8, + "ParentWorldMapID": 10 }, "SpellItemEnchantment": { - "ID": 0, "Name": 8 + "ID": 0, + "Name": 8 }, "ItemSet": { - "ID": 0, "Name": 1, - "Item0": 18, "Item1": 19, "Item2": 20, "Item3": 21, "Item4": 22, - "Item5": 23, "Item6": 24, "Item7": 25, "Item8": 26, "Item9": 27, - "Spell0": 28, "Spell1": 29, "Spell2": 30, "Spell3": 31, "Spell4": 32, - "Spell5": 33, "Spell6": 34, "Spell7": 35, "Spell8": 36, "Spell9": 37, - "Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41, - "Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45, - "Threshold8": 46, "Threshold9": 47 + "ID": 0, + "Name": 1, + "Item0": 18, + "Item1": 19, + "Item2": 20, + "Item3": 21, + "Item4": 22, + "Item5": 23, + "Item6": 24, + "Item7": 25, + "Item8": 26, + "Item9": 27, + "Spell0": 28, + "Spell1": 29, + "Spell2": 30, + "Spell3": 31, + "Spell4": 32, + "Spell5": 33, + "Spell6": 34, + "Spell7": 35, + "Spell8": 36, + "Spell9": 37, + "Threshold0": 38, + "Threshold1": 39, + "Threshold2": 40, + "Threshold3": 41, + "Threshold4": 42, + "Threshold5": 43, + "Threshold6": 44, + "Threshold7": 45, + "Threshold8": 46, + "Threshold9": 47 }, "SpellVisual": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 + "ID": 0, + "CastKit": 2, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "ID": 0, + "BaseEffect": 5, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "ID": 0, + "FilePath": 2 } } diff --git a/Data/expansions/turtle/dbc_layouts.json b/Data/expansions/turtle/dbc_layouts.json index cb44c54a..b7650ef5 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -1,121 +1,293 @@ { "Spell": { - "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117, - "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, + "ID": 0, + "Attributes": 5, + "AttributesEx": 6, + "IconID": 117, + "Name": 120, + "Tooltip": 147, + "Rank": 129, + "SchoolEnum": 1, + "CastingTimeIndex": 15, + "PowerType": 28, + "ManaCost": 29, + "RangeIndex": 33, "DispelType": 4 }, - "SpellRange": { "MaxRange": 2 }, + "SpellRange": { + "MaxRange": 2 + }, "ItemDisplayInfo": { - "ID": 0, "LeftModel": 1, "LeftModelTexture": 3, - "InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9, - "TextureArmUpper": 14, "TextureArmLower": 15, "TextureHand": 16, - "TextureTorsoUpper": 17, "TextureTorsoLower": 18, - "TextureLegUpper": 19, "TextureLegLower": 20, "TextureFoot": 21 + "ID": 0, + "LeftModel": 1, + "LeftModelTexture": 3, + "InventoryIcon": 5, + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "TextureArmUpper": 14, + "TextureArmLower": 15, + "TextureHand": 16, + "TextureTorsoUpper": 17, + "TextureTorsoLower": 18, + "TextureLegUpper": 19, + "TextureLegLower": 20, + "TextureFoot": 21 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "Texture1": 4, + "Texture2": 5, + "Texture3": 6, + "Flags": 7, + "VariationIndex": 8, + "ColorIndex": 9 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 }, - "SpellIcon": { "ID": 0, "Path": 1 }, "FactionTemplate": { - "ID": 0, "Faction": 1, "FactionGroup": 3, - "FriendGroup": 4, "EnemyGroup": 5, - "Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9 + "ID": 0, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "EnemyGroup": 5, + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9 }, "Faction": { - "ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3, - "ReputationRaceMask2": 4, "ReputationRaceMask3": 5, - "ReputationBase0": 10, "ReputationBase1": 11, - "ReputationBase2": 12, "ReputationBase3": 13 + "ID": 0, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13 + }, + "AreaTable": { + "ID": 0, + "MapID": 1, + "ParentAreaNum": 2, + "ExploreFlag": 3 }, - "AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 }, "CreatureDisplayInfoExtra": { - "ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4, - "HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7, - "EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10, - "EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13, - "EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16, - "EquipDisplay9": 17, "BakeName": 18 + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3, + "FaceID": 4, + "HairStyleID": 5, + "HairColorID": 6, + "FacialHairID": 7, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "BakeName": 18 }, "CreatureDisplayInfo": { - "ID": 0, "ModelID": 1, "ExtraDisplayId": 3, - "Skin1": 6, "Skin2": 7, "Skin3": 8 + "ID": 0, + "ModelID": 1, + "ExtraDisplayId": 3, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 }, "TaxiNodes": { - "ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5 + "ID": 0, + "MapID": 1, + "X": 2, + "Y": 3, + "Z": 4, + "Name": 5 + }, + "TaxiPath": { + "ID": 0, + "FromNode": 1, + "ToNode": 2, + "Cost": 3 }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, "TaxiPathNode": { - "ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3, - "X": 4, "Y": 5, "Z": 6 + "ID": 0, + "PathID": 1, + "NodeIndex": 2, + "MapID": 3, + "X": 4, + "Y": 5, + "Z": 6 }, "TalentTab": { - "ID": 0, "Name": 1, "ClassMask": 12, - "OrderIndex": 14, "BackgroundFile": 15 + "ID": 0, + "Name": 1, + "ClassMask": 12, + "OrderIndex": 14, + "BackgroundFile": 15 }, "Talent": { - "ID": 0, "TabID": 1, "Row": 2, "Column": 3, - "RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12 + "ID": 0, + "TabID": 1, + "Row": 2, + "Column": 3, + "RankSpell0": 4, + "PrereqTalent0": 9, + "PrereqRank0": 12 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "SkillLine": { + "ID": 0, + "Category": 1, + "Name": 3 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 }, - "SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 }, - "SkillLine": { "ID": 0, "Category": 1, "Name": 3 }, - "Map": { "ID": 0, "InternalName": 1 }, - "CreatureModelData": { "ID": 0, "ModelPath": 2 }, "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "RaceID": 1, + "SexID": 2, + "Variation": 3, + "GeosetID": 4 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "RaceID": 0, + "SexID": 1, + "Variation": 2, + "Geoset100": 3, + "Geoset300": 4, + "Geoset200": 5 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "Emotes": { + "ID": 0, + "AnimID": 2 }, - "GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 }, - "Emotes": { "ID": 0, "AnimID": 2 }, "EmotesText": { - "ID": 0, "Command": 1, "EmoteRef": 2, - "OthersTargetTextID": 3, "SenderTargetTextID": 5, - "OthersNoTargetTextID": 7, "SenderNoTargetTextID": 9 + "ID": 0, + "Command": 1, + "EmoteRef": 2, + "OthersTargetTextID": 3, + "SenderTargetTextID": 5, + "OthersNoTargetTextID": 7, + "SenderNoTargetTextID": 9 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "EmotesTextData": { "ID": 0, "Text": 1 }, "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "ID": 0, + "MapID": 1, + "X": 2, + "Z": 3, + "Y": 4, + "InnerRadius": 5, + "OuterRadius": 6, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9 + }, + "LightParams": { + "LightParamsID": 0 }, - "LightParams": { "LightParamsID": 0 }, "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "ID": 0, + "MapID": 1, + "AreaID": 2, + "AreaName": 3, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "LocBottom": 7, + "DisplayMapID": 8, + "ParentWorldMapID": 10 }, "SpellItemEnchantment": { - "ID": 0, "Name": 8 + "ID": 0, + "Name": 8 }, "ItemSet": { - "ID": 0, "Name": 1, - "Item0": 10, "Item1": 11, "Item2": 12, "Item3": 13, "Item4": 14, - "Item5": 15, "Item6": 16, "Item7": 17, "Item8": 18, "Item9": 19, - "Spell0": 20, "Spell1": 21, "Spell2": 22, "Spell3": 23, "Spell4": 24, - "Spell5": 25, "Spell6": 26, "Spell7": 27, "Spell8": 28, "Spell9": 29, - "Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33, - "Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37, - "Threshold8": 38, "Threshold9": 39 + "ID": 0, + "Name": 1, + "Item0": 10, + "Item1": 11, + "Item2": 12, + "Item3": 13, + "Item4": 14, + "Item5": 15, + "Item6": 16, + "Item7": 17, + "Item8": 18, + "Item9": 19, + "Spell0": 20, + "Spell1": 21, + "Spell2": 22, + "Spell3": 23, + "Spell4": 24, + "Spell5": 25, + "Spell6": 26, + "Spell7": 27, + "Spell8": 28, + "Spell9": 29, + "Threshold0": 30, + "Threshold1": 31, + "Threshold2": 32, + "Threshold3": 33, + "Threshold4": 34, + "Threshold5": 35, + "Threshold6": 36, + "Threshold7": 37, + "Threshold8": 38, + "Threshold9": 39 }, "SpellVisual": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 + "ID": 0, + "CastKit": 2, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "ID": 0, + "BaseEffect": 5, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "ID": 0, + "FilePath": 2 } } diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json index 4ecbfc32..505ae150 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -1,129 +1,319 @@ { "Spell": { - "ID": 0, "Attributes": 4, "AttributesEx": 5, "IconID": 133, - "Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225, - "PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49, + "ID": 0, + "Attributes": 4, + "AttributesEx": 5, + "IconID": 133, + "Name": 136, + "Tooltip": 139, + "Rank": 153, + "SchoolMask": 225, + "PowerType": 14, + "ManaCost": 39, + "CastingTimeIndex": 47, + "RangeIndex": 49, "DispelType": 2 }, - "SpellRange": { "MaxRange": 4 }, + "SpellRange": { + "MaxRange": 4 + }, "ItemDisplayInfo": { - "ID": 0, "LeftModel": 1, "LeftModelTexture": 3, - "InventoryIcon": 5, "GeosetGroup1": 7, "GeosetGroup3": 9, - "TextureArmUpper": 14, "TextureArmLower": 15, "TextureHand": 16, - "TextureTorsoUpper": 17, "TextureTorsoLower": 18, - "TextureLegUpper": 19, "TextureLegLower": 20, "TextureFoot": 21 + "ID": 0, + "LeftModel": 1, + "LeftModelTexture": 3, + "InventoryIcon": 5, + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "TextureArmUpper": 14, + "TextureArmLower": 15, + "TextureHand": 16, + "TextureTorsoUpper": 17, + "TextureTorsoLower": 18, + "TextureLegUpper": 19, + "TextureLegLower": 20, + "TextureFoot": 21 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "Texture1": 4, + "Texture2": 5, + "Texture3": 6, + "Flags": 7, + "VariationIndex": 8, + "ColorIndex": 9 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 }, - "SpellIcon": { "ID": 0, "Path": 1 }, "FactionTemplate": { - "ID": 0, "Faction": 1, "FactionGroup": 3, - "FriendGroup": 4, "EnemyGroup": 5, - "Enemy0": 6, "Enemy1": 7, "Enemy2": 8, "Enemy3": 9 + "ID": 0, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "EnemyGroup": 5, + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9 }, "Faction": { - "ID": 0, "ReputationRaceMask0": 2, "ReputationRaceMask1": 3, - "ReputationRaceMask2": 4, "ReputationRaceMask3": 5, - "ReputationBase0": 10, "ReputationBase1": 11, - "ReputationBase2": 12, "ReputationBase3": 13 + "ID": 0, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13 + }, + "CharTitles": { + "ID": 0, + "Title": 2, + "TitleBit": 36 + }, + "Achievement": { + "ID": 0, + "Title": 4, + "Description": 21, + "Points": 39 + }, + "AchievementCriteria": { + "ID": 0, + "AchievementID": 1, + "Quantity": 4, + "Description": 9 + }, + "AreaTable": { + "ID": 0, + "MapID": 1, + "ParentAreaNum": 2, + "ExploreFlag": 3 }, - "CharTitles": { "ID": 0, "Title": 2, "TitleBit": 36 }, - "Achievement": { "ID": 0, "Title": 4, "Description": 21, "Points": 39 }, - "AchievementCriteria": { "ID": 0, "AchievementID": 1, "Quantity": 4, "Description": 9 }, - "AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 }, "CreatureDisplayInfoExtra": { - "ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4, - "HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7, - "EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10, - "EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13, - "EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16, - "EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20 + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3, + "FaceID": 4, + "HairStyleID": 5, + "HairColorID": 6, + "FacialHairID": 7, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "EquipDisplay10": 18, + "BakeName": 20 }, "CreatureDisplayInfo": { - "ID": 0, "ModelID": 1, "ExtraDisplayId": 3, - "Skin1": 6, "Skin2": 7, "Skin3": 8 + "ID": 0, + "ModelID": 1, + "ExtraDisplayId": 3, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 }, "TaxiNodes": { - "ID": 0, "MapID": 1, "X": 2, "Y": 3, "Z": 4, "Name": 5, - "MountDisplayIdAllianceFallback": 20, "MountDisplayIdHordeFallback": 21, - "MountDisplayIdAlliance": 22, "MountDisplayIdHorde": 23 + "ID": 0, + "MapID": 1, + "X": 2, + "Y": 3, + "Z": 4, + "Name": 5, + "MountDisplayIdAllianceFallback": 20, + "MountDisplayIdHordeFallback": 21, + "MountDisplayIdAlliance": 22, + "MountDisplayIdHorde": 23 + }, + "TaxiPath": { + "ID": 0, + "FromNode": 1, + "ToNode": 2, + "Cost": 3 }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, "TaxiPathNode": { - "ID": 0, "PathID": 1, "NodeIndex": 2, "MapID": 3, - "X": 4, "Y": 5, "Z": 6 + "ID": 0, + "PathID": 1, + "NodeIndex": 2, + "MapID": 3, + "X": 4, + "Y": 5, + "Z": 6 }, "TalentTab": { - "ID": 0, "Name": 1, "ClassMask": 20, - "OrderIndex": 22, "BackgroundFile": 23 + "ID": 0, + "Name": 1, + "ClassMask": 20, + "OrderIndex": 22, + "BackgroundFile": 23 }, "Talent": { - "ID": 0, "TabID": 1, "Row": 2, "Column": 3, - "RankSpell0": 4, "PrereqTalent0": 9, "PrereqRank0": 12 + "ID": 0, + "TabID": 1, + "Row": 2, + "Column": 3, + "RankSpell0": 4, + "PrereqTalent0": 9, + "PrereqRank0": 12 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "SkillLine": { + "ID": 0, + "Category": 1, + "Name": 3 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 }, - "SkillLineAbility": { "SkillLineID": 1, "SpellID": 2 }, - "SkillLine": { "ID": 0, "Category": 1, "Name": 3 }, - "Map": { "ID": 0, "InternalName": 1 }, - "CreatureModelData": { "ID": 0, "ModelPath": 2 }, "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "RaceID": 1, + "SexID": 2, + "Variation": 3, + "GeosetID": 4 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "RaceID": 0, + "SexID": 1, + "Variation": 2, + "Geoset100": 3, + "Geoset300": 4, + "Geoset200": 5 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "Emotes": { + "ID": 0, + "AnimID": 2 }, - "GameObjectDisplayInfo": { "ID": 0, "ModelName": 1 }, - "Emotes": { "ID": 0, "AnimID": 2 }, "EmotesText": { - "ID": 0, "Command": 1, "EmoteRef": 2, - "OthersTargetTextID": 3, "SenderTargetTextID": 5, - "OthersNoTargetTextID": 7, "SenderNoTargetTextID": 9 + "ID": 0, + "Command": 1, + "EmoteRef": 2, + "OthersTargetTextID": 3, + "SenderTargetTextID": 5, + "OthersNoTargetTextID": 7, + "SenderNoTargetTextID": 9 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "EmotesTextData": { "ID": 0, "Text": 1 }, "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "ID": 0, + "MapID": 1, + "X": 2, + "Z": 3, + "Y": 4, + "InnerRadius": 5, + "OuterRadius": 6, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9 + }, + "LightParams": { + "LightParamsID": 0 }, - "LightParams": { "LightParamsID": 0 }, "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "ID": 0, + "MapID": 1, + "AreaID": 2, + "AreaName": 3, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "LocBottom": 7, + "DisplayMapID": 8, + "ParentWorldMapID": 10 }, "SpellItemEnchantment": { - "ID": 0, "Name": 8 + "ID": 0, + "Name": 8 }, "ItemSet": { - "ID": 0, "Name": 1, - "Item0": 18, "Item1": 19, "Item2": 20, "Item3": 21, "Item4": 22, - "Item5": 23, "Item6": 24, "Item7": 25, "Item8": 26, "Item9": 27, - "Spell0": 28, "Spell1": 29, "Spell2": 30, "Spell3": 31, "Spell4": 32, - "Spell5": 33, "Spell6": 34, "Spell7": 35, "Spell8": 36, "Spell9": 37, - "Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41, - "Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45, - "Threshold8": 46, "Threshold9": 47 + "ID": 0, + "Name": 1, + "Item0": 18, + "Item1": 19, + "Item2": 20, + "Item3": 21, + "Item4": 22, + "Item5": 23, + "Item6": 24, + "Item7": 25, + "Item8": 26, + "Item9": 27, + "Spell0": 28, + "Spell1": 29, + "Spell2": 30, + "Spell3": 31, + "Spell4": 32, + "Spell5": 33, + "Spell6": 34, + "Spell7": 35, + "Spell8": 36, + "Spell9": 37, + "Threshold0": 38, + "Threshold1": 39, + "Threshold2": 40, + "Threshold3": 41, + "Threshold4": 42, + "Threshold5": 43, + "Threshold6": 44, + "Threshold7": 45, + "Threshold8": 46, + "Threshold9": 47 }, "LFGDungeons": { - "ID": 0, "Name": 1 + "ID": 0, + "Name": 1 }, "SpellVisual": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 + "ID": 0, + "CastKit": 2, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "ID": 0, + "BaseEffect": 5, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "ID": 0, + "FilePath": 2 } } diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 2cb6278e..31546026 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -336,8 +336,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, uint32_t fRace = csL ? (*csL)["RaceID"] : 1; uint32_t fSex = csL ? (*csL)["SexID"] : 2; uint32_t fBase = csL ? (*csL)["BaseSection"] : 3; - uint32_t fVar = csL ? (*csL)["VariationIndex"] : 4; - uint32_t fColor = csL ? (*csL)["ColorIndex"] : 5; + uint32_t fVar = csL ? (*csL)["VariationIndex"] : 8; + uint32_t fColor = csL ? (*csL)["ColorIndex"] : 9; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { uint32_t raceId = charSectionsDbc->getUInt32(r, fRace); uint32_t sexId = charSectionsDbc->getUInt32(r, fSex); @@ -350,7 +350,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // Section 0: Body skin (variation=0, colorIndex = skin color) if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); if (!tex1.empty()) { bodySkinPath_ = tex1; foundSkin = true; @@ -360,8 +360,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 1 && !foundFace && variationIndex == static_cast(face) && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); - std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); + std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5); if (!tex1.empty()) faceLowerPath = tex1; if (!tex2.empty()) faceUpperPath = tex2; foundFace = true; @@ -370,7 +370,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 3 && !foundHair && variationIndex == static_cast(hairStyle) && colorIndex == static_cast(hairColor)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); if (!tex1.empty()) { hairScalpPath = tex1; foundHair = true; @@ -379,7 +379,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // Section 4: Underwear (variation=0, colorIndex = skin color) else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == static_cast(skin)) { - uint32_t texBase = csL ? (*csL)["Texture1"] : 6; + uint32_t texBase = csL ? (*csL)["Texture1"] : 4; for (uint32_t f = texBase; f <= texBase + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) { From bd725f0bbe5da03091e1d16a414809297e6b579d Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 22 Mar 2026 21:39:40 +0300 Subject: [PATCH 02/18] build fix --- src/addons/lua_engine.cpp | 1 + src/game/warden_memory.cpp | 2 +- src/game/warden_module.cpp | 12 +++++------- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 6148ddd0..5eccdf36 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -3869,6 +3869,7 @@ void LuaEngine::registerCoreAPI() { "function StopSound() end\n" "function UIParent_OnEvent() end\n" "UIParent = CreateFrame('Frame', 'UIParent')\n" + "UIPanelWindows = {}\n" "WorldFrame = CreateFrame('Frame', 'WorldFrame')\n" // GameTooltip: global tooltip frame used by virtually all addons "GameTooltip = CreateFrame('Frame', 'GameTooltip')\n" diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index 33127e2c..5b13456a 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -861,7 +861,7 @@ void WardenMemory::verifyWardenScanEntries() { bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], uint8_t patternLen, bool imageOnly, uint32_t hintOffset, bool hintOnly) const { - if (!loaded_ || patternLen == 0 || patternLen > 255) return false; + if (!loaded_ || patternLen == 0) return false; // Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1) std::string cacheKey(26, '\0'); diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index eea0f0ee..bf44c26e 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -74,8 +74,7 @@ bool WardenModule::load(const std::vector& moduleData, // Step 3: Verify RSA signature if (!verifyRSASignature(decryptedData_)) { - LOG_ERROR("WardenModule: RSA signature verification failed!"); - // Note: Currently returns true (skipping verification) due to placeholder modulus + // Expected with placeholder modulus — verification is skipped gracefully } // Step 4: Strip RSA signature (last 256 bytes) then zlib decompress @@ -126,7 +125,7 @@ bool WardenModule::load(const std::vector& moduleData, return true; } -bool WardenModule::processCheckRequest(const std::vector& checkData, +bool WardenModule::processCheckRequest([[maybe_unused]] const std::vector& checkData, [[maybe_unused]] std::vector& responseOut) { if (!loaded_) { LOG_ERROR("WardenModule: Module not loaded, cannot process checks"); @@ -427,12 +426,11 @@ bool WardenModule::verifyRSASignature(const std::vector& data) { } } - LOG_ERROR("WardenModule: RSA signature verification FAILED (hash mismatch)"); - LOG_ERROR("WardenModule: NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification"); + LOG_WARNING("WardenModule: RSA signature verification skipped (placeholder modulus)"); + LOG_WARNING("WardenModule: Extract real modulus from WoW.exe for actual verification"); // For development, return true to proceed (since we don't have real modulus) // TODO: Set to false once real modulus is extracted - LOG_WARNING("WardenModule: Skipping RSA verification (placeholder modulus)"); return true; // TEMPORARY - change to false for production } @@ -705,7 +703,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize); } relocDataOffset_ = 0; - LOG_ERROR("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback"); + LOG_WARNING("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback"); return true; } From 7565019dc9e445a583d4fa350d9e6dc0288aef7f Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 22 Mar 2026 21:40:16 +0300 Subject: [PATCH 03/18] log falling --- src/main.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index d3811b3b..8ae707e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ #include #ifdef __linux__ #include +#include +#include // Keep a persistent X11 connection for emergency mouse release in signal handlers. // XOpenDisplay inside a signal handler is unreliable, so we open it once at startup. @@ -26,6 +28,27 @@ static void releaseMouseGrab() {} static void crashHandler(int sig) { releaseMouseGrab(); +#ifdef __linux__ + // Dump backtrace to debug log + { + void* frames[64]; + int n = backtrace(frames, 64); + const char* sigName = (sig == SIGSEGV) ? "SIGSEGV" : + (sig == SIGABRT) ? "SIGABRT" : + (sig == SIGFPE) ? "SIGFPE" : "UNKNOWN"; + // Write to stderr and to the debug log file + fprintf(stderr, "\n=== CRASH: signal %s (%d) ===\n", sigName, sig); + backtrace_symbols_fd(frames, n, STDERR_FILENO); + FILE* f = fopen("/tmp/wowee_debug.log", "a"); + if (f) { + fprintf(f, "\n=== CRASH: signal %s (%d) ===\n", sigName, sig); + fflush(f); + // Also write backtrace to the log file fd + backtrace_symbols_fd(frames, n, fileno(f)); + fclose(f); + } + } +#endif std::signal(sig, SIG_DFL); std::raise(sig); } From 027640189a5eb3d6670af2b529d843b8695d9ea2 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 22 Mar 2026 21:47:12 +0300 Subject: [PATCH 04/18] make start on ubuntu intel video cards --- include/game/world_packets.hpp | 7 +- include/rendering/m2_renderer.hpp | 7 ++ include/rendering/terrain_manager.hpp | 5 ++ src/core/application.cpp | 36 +++++----- src/game/world_packets.cpp | 21 +++--- src/rendering/m2_renderer.cpp | 96 +++++++++++++++++++++++++-- src/rendering/terrain_manager.cpp | 34 ++++++++-- src/ui/character_create_screen.cpp | 8 +-- 8 files changed, 170 insertions(+), 44 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c0408743..d72aebe6 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -399,9 +399,10 @@ enum class MovementFlags : uint32_t { WATER_WALK = 0x00008000, // Walk on water surface SWIMMING = 0x00200000, ASCENDING = 0x00400000, - CAN_FLY = 0x00800000, - FLYING = 0x01000000, - HOVER = 0x02000000, + DESCENDING = 0x00800000, + CAN_FLY = 0x01000000, + FLYING = 0x02000000, + HOVER = 0x40000000, }; /** diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 08d83d32..c50dfb0f 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -416,6 +416,13 @@ private: static constexpr uint32_t MAX_MATERIAL_SETS = 8192; static constexpr uint32_t MAX_BONE_SETS = 8192; + // Dummy identity bone buffer + descriptor set for non-animated models. + // The pipeline layout declares set 2 (bones) and some drivers (Intel ANV) + // require all declared sets to be bound even when the shader doesn't access them. + ::VkBuffer dummyBoneBuffer_ = VK_NULL_HANDLE; + VmaAllocation dummyBoneAlloc_ = VK_NULL_HANDLE; + VkDescriptorSet dummyBoneSet_ = VK_NULL_HANDLE; + // Dynamic ribbon vertex buffer (CPU-written triangle strip) static constexpr size_t MAX_RIBBON_VERTS = 2048; // 9 floats each ::VkBuffer ribbonVB_ = VK_NULL_HANDLE; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 9fa540b3..ab6e881f 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -394,6 +394,11 @@ private: std::unordered_set uploadedM2Ids_; std::mutex uploadedM2IdsMutex_; + // Cross-tile dedup for WMO doodad preparation on background workers + // (prevents re-parsing thousands of doodads when same WMO spans multiple tiles) + std::unordered_set preparedWmoUniqueIds_; + std::mutex preparedWmoUniqueIdsMutex_; + // Dedup set for doodad placements across tile boundaries std::unordered_set placedDoodadIds; diff --git a/src/core/application.cpp b/src/core/application.cpp index c5e7dccb..a4728379 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3671,13 +3671,13 @@ void Application::spawnPlayerCharacter() { uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); if (raceId != targetRaceId || sexId != targetSexId) continue; // Section 0 = skin: match by colorIndex = skin byte - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; + const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4; if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { std::string tex1 = charSectionsDbc->getString(r, csTex1); if (!tex1.empty()) { @@ -5353,9 +5353,9 @@ void Application::buildCharSectionsCache() { uint32_t raceF = csL ? (*csL)["RaceID"] : 1; uint32_t sexF = csL ? (*csL)["SexID"] : 2; uint32_t secF = csL ? (*csL)["BaseSection"] : 3; - uint32_t varF = csL ? (*csL)["VariationIndex"] : 4; - uint32_t colF = csL ? (*csL)["ColorIndex"] : 5; - uint32_t tex1F = csL ? (*csL)["Texture1"] : 6; + uint32_t varF = csL ? (*csL)["VariationIndex"] : 8; + uint32_t colF = csL ? (*csL)["ColorIndex"] : 9; + uint32_t tex1F = csL ? (*csL)["Texture1"] : 4; for (uint32_t r = 0; r < dbc->getRecordCount(); r++) { uint32_t race = dbc->getUInt32(r, raceF); uint32_t sex = dbc->getUInt32(r, sexF); @@ -5962,9 +5962,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (rId != npcRace || sId != npcSex) continue; uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); - uint32_t tex1F = csL ? (*csL)["Texture1"] : 6; + uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t tex1F = csL ? (*csL)["Texture1"] : 4; if (section == 0 && def.basePath.empty() && color == npcSkin) { def.basePath = csDbc->getString(r, tex1F); @@ -6080,11 +6080,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (raceId != targetRace || sexId != targetSex) continue; uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); if (section != 3) continue; - uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIdx = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t colorIdx = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); if (variation != static_cast(extraCopy.hairStyleId)) continue; if (colorIdx != static_cast(extraCopy.hairColorId)) continue; - def.hairTexturePath = csDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + def.hairTexturePath = csDbc->getString(r, csL ? (*csL)["Texture1"] : 4); break; } @@ -7193,7 +7193,7 @@ void Application::spawnOnlinePlayer(uint64_t guid, const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; uint32_t targetRaceId = raceId; uint32_t targetSexId = genderId; - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; + const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4; bool foundSkin = false; bool foundUnderwear = false; @@ -7204,8 +7204,8 @@ void Application::spawnOnlinePlayer(uint64_t guid, uint32_t rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t rSex = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); if (rRace != targetRaceId || rSex != targetSexId) continue; @@ -8189,9 +8189,9 @@ void Application::processCreatureSpawnQueue(bool unlimited) { uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); if (rId != nRace || sId != nSex) continue; uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); - uint32_t tex1F = csL ? (*csL)["Texture1"] : 6; + uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t tex1F = csL ? (*csL)["Texture1"] : 4; if (section == 0 && color == nSkin) { std::string t = csDbc->getString(r, tex1F); if (!t.empty()) displaySkinPaths.push_back(t); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 27051cb2..e740ea4c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -832,7 +832,7 @@ void MovementPacket::writeMovementPayload(network::Packet& packet, const Movemen packet.writeUInt8(static_cast(info.transportSeat)); // Optional second transport time for interpolated movement. - if (info.flags2 & 0x0200) { + if (info.flags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT packet.writeUInt32(info.transportTime2); } } @@ -994,26 +994,27 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec, " offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")"); - if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT + if (moveFlags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT if (rem() < 4) return false; /*uint32_t tTime2 =*/ packet.readUInt32(); } } // Swimming/flying pitch - // WotLK 3.3.5a movement flags relevant here: + // WotLK 3.3.5a movement flags (wire format): // SWIMMING = 0x00200000 - // FLYING = 0x01000000 (player/creature actively flying) - // SPLINE_ELEVATION = 0x02000000 (smooth vertical spline offset — no pitch field) + // CAN_FLY = 0x01000000 (ability to fly — no pitch field) + // FLYING = 0x02000000 (actively flying — has pitch field) + // SPLINE_ELEVATION = 0x04000000 (smooth vertical spline offset) // MovementFlags2: - // MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0010 + // MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0020 // // Pitch is present when SWIMMING or FLYING are set, or the always-allow flag is set. - // The original code checked 0x02000000 (SPLINE_ELEVATION) which neither covers SWIMMING - // nor FLYING, causing misaligned reads for swimming/flying entities in SMSG_UPDATE_OBJECT. + // Note: CAN_FLY (0x01000000) does NOT gate pitch; only FLYING (0x02000000) does. + // (TBC uses 0x01000000 for FLYING — see TbcMoveFlags in packet_parsers_tbc.cpp.) if ((moveFlags & 0x00200000) /* SWIMMING */ || - (moveFlags & 0x01000000) /* FLYING */ || - (moveFlags2 & 0x0010) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) { + (moveFlags & 0x02000000) /* FLYING */ || + (moveFlags2 & 0x0020) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) { if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index f711f542..b4bfa439 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -366,6 +366,41 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout vkCreateDescriptorPool(device, &ci, nullptr, &boneDescPool_); } + // Create a small identity-bone SSBO + descriptor set so that non-animated + // draws always have a valid set 2 bound. The Intel ANV driver segfaults + // on vkCmdDrawIndexed when a declared descriptor set slot is unbound. + { + // Single identity matrix (bone 0 = identity) + glm::mat4 identity(1.0f); + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = sizeof(glm::mat4); + bci.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + VmaAllocationCreateInfo aci{}; + aci.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + aci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo allocInfo{}; + vmaCreateBuffer(ctx->getAllocator(), &bci, &aci, + &dummyBoneBuffer_, &dummyBoneAlloc_, &allocInfo); + if (allocInfo.pMappedData) { + memcpy(allocInfo.pMappedData, &identity, sizeof(identity)); + } + + dummyBoneSet_ = allocateBoneSet(); + if (dummyBoneSet_) { + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = dummyBoneBuffer_; + bufInfo.offset = 0; + bufInfo.range = sizeof(glm::mat4); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = dummyBoneSet_; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + } + // --- Pipeline layouts --- // Main M2 pipeline layout: set 0 = perFrame, set 1 = material, set 2 = bones @@ -746,6 +781,9 @@ void M2Renderer::shutdown() { if (ribbonPipelineLayout_) { vkDestroyPipelineLayout(device, ribbonPipelineLayout_, nullptr); ribbonPipelineLayout_ = VK_NULL_HANDLE; } // Destroy descriptor pools and layouts + if (dummyBoneBuffer_) { vmaDestroyBuffer(alloc, dummyBoneBuffer_, dummyBoneAlloc_); dummyBoneBuffer_ = VK_NULL_HANDLE; } + // dummyBoneSet_ is freed implicitly when boneDescPool_ is destroyed + dummyBoneSet_ = VK_NULL_HANDLE; if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; } if (boneDescPool_) { vkDestroyDescriptorPool(device, boneDescPool_, nullptr); boneDescPool_ = VK_NULL_HANDLE; } if (materialSetLayout_) { vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); materialSetLayout_ = VK_NULL_HANDLE; } @@ -812,7 +850,11 @@ VkDescriptorSet M2Renderer::allocateMaterialSet() { ai.descriptorSetCount = 1; ai.pSetLayouts = &materialSetLayout_; VkDescriptorSet set = VK_NULL_HANDLE; - vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set); + VkResult result = vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set); + if (result != VK_SUCCESS) { + LOG_ERROR("M2Renderer: material descriptor set allocation failed (", result, ")"); + return VK_NULL_HANDLE; + } return set; } @@ -822,7 +864,11 @@ VkDescriptorSet M2Renderer::allocateBoneSet() { ai.descriptorSetCount = 1; ai.pSetLayouts = &boneSetLayout_; VkDescriptorSet set = VK_NULL_HANDLE; - vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set); + VkResult result = vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &set); + if (result != VK_SUCCESS) { + LOG_ERROR("M2Renderer: bone descriptor set allocation failed (", result, ")"); + return VK_NULL_HANDLE; + } return set; } @@ -1303,6 +1349,10 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { gpuModel.indexBuffer = buf.buffer; gpuModel.indexAlloc = buf.allocation; } + + if (!gpuModel.vertexBuffer || !gpuModel.indexBuffer) { + LOG_ERROR("M2Renderer::loadModel: GPU buffer upload failed for model ", modelId); + } } // Load ALL textures from the model into a local vector. @@ -1751,6 +1801,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } models[modelId] = std::move(gpuModel); + spatialIndexDirty_ = true; // Map may have rehashed — refresh cachedModel pointers LOG_DEBUG("Loaded M2 model: ", model.name, " (", models[modelId].vertexCount, " vertices, ", models[modelId].indexCount / 3, " triangles, ", models[modelId].batches.size(), " batches)"); @@ -2504,6 +2555,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const uint32_t currentModelId = UINT32_MAX; const M2ModelGPU* currentModel = nullptr; + bool currentModelValid = false; // State tracking VkPipeline currentPipeline = VK_NULL_HANDLE; @@ -2519,6 +2571,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const float fadeAlpha; }; + // Validate per-frame descriptor set before any Vulkan commands + if (!perFrameSet) { + LOG_ERROR("M2Renderer::render: perFrameSet is VK_NULL_HANDLE — skipping M2 render"); + return; + } + // Bind per-frame descriptor set (set 0) — shared across all draws vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); @@ -2528,6 +2586,13 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const currentPipeline = opaquePipeline_; bool opaquePass = true; // Pass 1 = opaque, pass 2 = transparent (set below for second pass) + // Bind dummy bone set (set 2) so non-animated draws have a valid binding. + // Animated instances override this with their real bone set per-instance. + if (dummyBoneSet_) { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + pipelineLayout_, 2, 1, &dummyBoneSet_, 0, nullptr); + } + for (const auto& entry : sortedVisible_) { if (entry.index >= instances.size()) continue; auto& instance = instances[entry.index]; @@ -2535,14 +2600,17 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // Bind vertex + index buffers once per model group if (entry.modelId != currentModelId) { currentModelId = entry.modelId; + currentModelValid = false; auto mdlIt = models.find(currentModelId); if (mdlIt == models.end()) continue; currentModel = &mdlIt->second; - if (!currentModel->vertexBuffer) continue; + if (!currentModel->vertexBuffer || !currentModel->indexBuffer) continue; + currentModelValid = true; VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset); vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16); } + if (!currentModelValid) continue; const M2ModelGPU& model = *currentModel; @@ -2785,7 +2853,6 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const continue; } vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc); - vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); lastDrawCallCount++; } @@ -2799,6 +2866,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const currentModelId = UINT32_MAX; currentModel = nullptr; + currentModelValid = false; // Reset pipeline to opaque so the first transparent bind always sets explicitly currentPipeline = opaquePipeline_; @@ -2817,14 +2885,17 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // `!opaquePass && !rawTransparent → continue` handles opaque skipping) if (entry.modelId != currentModelId) { currentModelId = entry.modelId; + currentModelValid = false; auto mdlIt = models.find(currentModelId); if (mdlIt == models.end()) continue; currentModel = &mdlIt->second; - if (!currentModel->vertexBuffer) continue; + if (!currentModel->vertexBuffer || !currentModel->indexBuffer) continue; + currentModelValid = true; VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, ¤tModel->vertexBuffer, &offset); vkCmdBindIndexBuffer(cmd, currentModel->indexBuffer, 0, VK_INDEX_TYPE_UINT16); } + if (!currentModelValid) continue; const M2ModelGPU& model = *currentModel; @@ -4168,6 +4239,21 @@ void M2Renderer::clear() { } if (boneDescPool_) { vkResetDescriptorPool(device, boneDescPool_, 0); + // Re-allocate the dummy bone set (invalidated by pool reset) + dummyBoneSet_ = allocateBoneSet(); + if (dummyBoneSet_ && dummyBoneBuffer_) { + VkDescriptorBufferInfo bufInfo{}; + bufInfo.buffer = dummyBoneBuffer_; + bufInfo.offset = 0; + bufInfo.range = sizeof(glm::mat4); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = dummyBoneSet_; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &bufInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } } } models.clear(); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index f380cc65..ba929d7c 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -562,7 +562,17 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { // Pre-load WMO doodads (M2 models inside WMO) if (!workerRunning.load()) return nullptr; - if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { + + // Skip WMO doodads if this placement was already prepared by another tile's worker. + // This prevents 15+ copies of Stormwind's ~6000 doodads from being parsed + // simultaneously, which was the primary cause of OOM during world load. + bool wmoAlreadyPrepared = false; + if (placement.uniqueId != 0) { + std::lock_guard lock(preparedWmoUniqueIdsMutex_); + wmoAlreadyPrepared = !preparedWmoUniqueIds_.insert(placement.uniqueId).second; + } + + if (!wmoAlreadyPrepared && !wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { glm::mat4 wmoMatrix(1.0f); wmoMatrix = glm::translate(wmoMatrix, pos); wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1)); @@ -575,6 +585,7 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { setsToLoad.push_back(placement.doodadSet); } std::unordered_set loadedDoodadIndices; + std::unordered_set wmoPreparedModelIds; // within-WMO model dedup for (uint32_t setIdx : setsToLoad) { const auto& doodadSet = wmoModel.doodadSets[setIdx]; for (uint32_t di = 0; di < doodadSet.count; di++) { @@ -599,15 +610,16 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); - // Skip file I/O if model already uploaded from a previous tile + // Skip file I/O if model already uploaded or already prepared within this WMO bool modelAlreadyUploaded = false; { std::lock_guard lock(uploadedM2IdsMutex_); modelAlreadyUploaded = uploadedM2Ids_.count(doodadModelId) > 0; } + bool modelAlreadyPreparedInWmo = !wmoPreparedModelIds.insert(doodadModelId).second; pipeline::M2Model m2Model; - if (!modelAlreadyUploaded) { + if (!modelAlreadyUploaded && !modelAlreadyPreparedInWmo) { std::vector m2Data = assetManager->readFile(m2Path); if (m2Data.empty()) continue; @@ -1404,7 +1416,11 @@ void TerrainManager::unloadTile(int x, int y) { wmoRenderer->removeInstances(fit->wmoInstanceIds); } for (uint32_t uid : fit->tileUniqueIds) placedDoodadIds.erase(uid); - for (uint32_t uid : fit->tileWmoUniqueIds) placedWmoIds.erase(uid); + for (uint32_t uid : fit->tileWmoUniqueIds) { + placedWmoIds.erase(uid); + std::lock_guard lock(preparedWmoUniqueIdsMutex_); + preparedWmoUniqueIds_.erase(uid); + } finalizingTiles_.erase(fit); return; } @@ -1425,6 +1441,8 @@ void TerrainManager::unloadTile(int x, int y) { } for (uint32_t uid : tile->wmoUniqueIds) { placedWmoIds.erase(uid); + std::lock_guard lock(preparedWmoUniqueIdsMutex_); + preparedWmoUniqueIds_.erase(uid); } // Remove M2 doodad instances @@ -1509,6 +1527,10 @@ void TerrainManager::unloadAll() { std::lock_guard lock(uploadedM2IdsMutex_); uploadedM2Ids_.clear(); } + { + std::lock_guard lock(preparedWmoUniqueIdsMutex_); + preparedWmoUniqueIds_.clear(); + } LOG_INFO("Unloading all terrain tiles"); loadedTiles.clear(); @@ -1561,6 +1583,10 @@ void TerrainManager::softReset() { std::lock_guard lock(uploadedM2IdsMutex_); uploadedM2Ids_.clear(); } + { + std::lock_guard lock(preparedWmoUniqueIdsMutex_); + preparedWmoUniqueIds_.clear(); + } // Clear tile cache — keys are (x,y) without map name, so stale entries from // a different map with overlapping coordinates would produce wrong geometry. diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index fa81756f..63933924 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -257,8 +257,8 @@ void CharacterCreateScreen::updateAppearanceRanges() { if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); if (baseSection == 0 && variationIndex == 0) { skinMax = std::max(skinMax, static_cast(colorIndex)); @@ -284,8 +284,8 @@ void CharacterCreateScreen::updateAppearanceRanges() { if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); + uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); if (baseSection == 1 && colorIndex == static_cast(skin)) { faceMax = std::max(faceMax, static_cast(variationIndex)); From ce4f93dfcb881a913b1d94e9ff735bec4074843c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:05:29 -0700 Subject: [PATCH 05/18] feat: add UnitCastingInfo/UnitChannelInfo Lua API and fix SMSG_CAST_FAILED events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose cast/channel state to Lua addons via UnitCastingInfo(unit) and UnitChannelInfo(unit), matching the WoW API signature (name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible). Works for player, target, focus, and pet units using existing UnitCastState tracking. Also fix handleCastFailed (SMSG_CAST_FAILED, Classic/TBC path) to fire UNIT_SPELLCAST_FAILED and UNIT_SPELLCAST_STOP events — previously only the WotLK SMSG_CAST_RESULT path fired these, leaving Classic/TBC addons unaware of cast failures. Adds isChannel field to UnitCastState and getCastTimeTotal() accessor. --- include/game/game_handler.hpp | 2 + src/addons/lua_engine.cpp | 80 +++++++++++++++++++++++++++++++++++ src/game/game_handler.cpp | 9 ++++ 3 files changed, 91 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d67bd0b2..033f8661 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -882,6 +882,7 @@ public: uint32_t getCurrentCastSpellId() const { return currentCastSpellId; } float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastTimeRemaining() const { return castTimeRemaining; } + float getCastTimeTotal() const { return castTimeTotal; } // Repeat-craft queue void startCraftQueue(uint32_t spellId, int count); @@ -896,6 +897,7 @@ public: // Unit cast state (tracked per GUID for target frame + boss frames) struct UnitCastState { bool casting = false; + bool isChannel = false; ///< true for channels (MSG_CHANNEL_START), false for casts (SMSG_SPELL_START) uint32_t spellId = 0; float timeRemaining = 0.0f; float timeTotal = 0.0f; diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 5eccdf36..ea1635e4 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -1077,6 +1077,84 @@ static int lua_UnitAuraGeneric(lua_State* L) { return lua_UnitAura(L, wantBuff); } +// ---------- UnitCastingInfo / UnitChannelInfo ---------- +// Internal helper: pushes cast/channel info for a unit. +// Returns number of Lua return values (0 if not casting/channeling the requested type). +static int lua_UnitCastInfo(lua_State* L, bool wantChannel) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnil(L); return 1; } + + const char* uid = luaL_optstring(L, 1, "player"); + std::string uidStr(uid ? uid : "player"); + + // GetTime epoch for consistent time values + static auto sStart = std::chrono::steady_clock::now(); + double nowSec = std::chrono::duration( + std::chrono::steady_clock::now() - sStart).count(); + + // Resolve cast state for the unit + bool isCasting = false; + bool isChannel = false; + uint32_t spellId = 0; + float timeTotal = 0.0f; + float timeRemaining = 0.0f; + bool interruptible = true; + + if (uidStr == "player") { + isCasting = gh->isCasting(); + isChannel = gh->isChanneling(); + spellId = gh->getCurrentCastSpellId(); + timeTotal = gh->getCastTimeTotal(); + timeRemaining = gh->getCastTimeRemaining(); + // Player interruptibility: always true for own casts (server controls actual interrupt) + interruptible = true; + } else { + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushnil(L); return 1; } + const auto* state = gh->getUnitCastState(guid); + if (!state) { lua_pushnil(L); return 1; } + isCasting = state->casting; + isChannel = state->isChannel; + spellId = state->spellId; + timeTotal = state->timeTotal; + timeRemaining = state->timeRemaining; + interruptible = state->interruptible; + } + + if (!isCasting) { lua_pushnil(L); return 1; } + + // UnitCastingInfo: only returns for non-channel casts + // UnitChannelInfo: only returns for channels + if (wantChannel != isChannel) { lua_pushnil(L); return 1; } + + // Spell name + icon + const std::string& name = gh->getSpellName(spellId); + std::string iconPath = gh->getSpellIconPath(spellId); + + // Time values in milliseconds (WoW API convention) + double startTimeMs = (nowSec - (timeTotal - timeRemaining)) * 1000.0; + double endTimeMs = (nowSec + timeRemaining) * 1000.0; + + // Return values match WoW API: + // UnitCastingInfo: name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible + // UnitChannelInfo: name, text, texture, startTime, endTime, isTradeSkill, notInterruptible + lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name + lua_pushstring(L, ""); // text (sub-text, usually empty) + if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); + else lua_pushstring(L, "Interface\\Icons\\INV_Misc_QuestionMark"); // texture + lua_pushnumber(L, startTimeMs); // startTime (ms) + lua_pushnumber(L, endTimeMs); // endTime (ms) + lua_pushboolean(L, gh->isProfessionSpell(spellId) ? 1 : 0); // isTradeSkill + if (!wantChannel) { + lua_pushnumber(L, spellId); // castID (UnitCastingInfo only) + } + lua_pushboolean(L, interruptible ? 0 : 1); // notInterruptible + return wantChannel ? 7 : 8; +} + +static int lua_UnitCastingInfo(lua_State* L) { return lua_UnitCastInfo(L, false); } +static int lua_UnitChannelInfo(lua_State* L) { return lua_UnitCastInfo(L, true); } + // --- Action API --- static int lua_SendChatMessage(lua_State* L) { @@ -3486,6 +3564,8 @@ void LuaEngine::registerCoreAPI() { {"UnitBuff", lua_UnitBuff}, {"UnitDebuff", lua_UnitDebuff}, {"UnitAura", lua_UnitAuraGeneric}, + {"UnitCastingInfo", lua_UnitCastingInfo}, + {"UnitChannelInfo", lua_UnitChannelInfo}, {"GetNumAddOns", lua_GetNumAddOns}, {"GetAddOnInfo", lua_GetAddOnInfo}, {"GetAddOnMetadata", lua_GetAddOnMetadata}, diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f81f0ef0..58fbf032 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7519,6 +7519,7 @@ void GameHandler::handlePacket(network::Packet& packet) { } else { auto& s = unitCastStates_[chanCaster]; s.casting = true; + s.isChannel = true; s.spellId = chanSpellId; s.timeTotal = chanTotalMs / 1000.0f; s.timeRemaining = s.timeTotal; @@ -19363,6 +19364,13 @@ void GameHandler::handleCastFailed(network::Packet& packet) { if (auto* sfx = renderer->getUiSoundManager()) sfx->playError(); } + + // Fire UNIT_SPELLCAST_FAILED + UNIT_SPELLCAST_STOP so Lua addons can react + if (addonEventCallback_) { + addonEventCallback_("UNIT_SPELLCAST_FAILED", {"player", std::to_string(data.spellId)}); + addonEventCallback_("UNIT_SPELLCAST_STOP", {"player", std::to_string(data.spellId)}); + } + if (spellCastFailedCallback_) spellCastFailedCallback_(data.spellId); } static audio::SpellSoundManager::MagicSchool schoolMaskToMagicSchool(uint32_t mask) { @@ -19383,6 +19391,7 @@ void GameHandler::handleSpellStart(network::Packet& packet) { if (data.casterUnit != playerGuid && data.castTime > 0) { auto& s = unitCastStates_[data.casterUnit]; s.casting = true; + s.isChannel = false; s.spellId = data.spellId; s.timeTotal = data.castTime / 1000.0f; s.timeRemaining = s.timeTotal; From 329a1f4b12b54f8b1cc3e9d07c1963899fc9c68c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:11:29 -0700 Subject: [PATCH 06/18] feat: add IsActionInRange, GetActionInfo, and GetActionCount Lua API IsActionInRange(slot) checks if the spell on an action bar slot is within range of the current target, using DBC spell range data and entity positions. Returns 1/0/nil matching the WoW API contract. GetActionInfo(slot) returns action type ("spell"/"item"/"macro"), id, and subType for action bar interrogation by bar addons. GetActionCount(slot) returns item stack count across backpack and bags for consumable tracking on action bars. --- src/addons/lua_engine.cpp | 112 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index ea1635e4..9b4ed756 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -2919,6 +2919,115 @@ static int lua_IsUsableAction(lua_State* L) { return 2; } +// IsActionInRange(slot) → 1 if in range, 0 if out, nil if no range check applicable +static int lua_IsActionInRange(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnil(L); return 1; } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size()) || bar[slot].isEmpty()) { + lua_pushnil(L); + return 1; + } + const auto& action = bar[slot]; + uint32_t spellId = 0; + if (action.type == game::ActionBarSlot::SPELL) { + spellId = action.id; + } else { + // Items/macros: no range check for now + lua_pushnil(L); + return 1; + } + if (spellId == 0) { lua_pushnil(L); return 1; } + + auto data = gh->getSpellData(spellId); + if (data.maxRange <= 0.0f) { + // Melee or self-cast spells: no range indicator + lua_pushnil(L); + return 1; + } + + // Need a target to check range against + uint64_t targetGuid = gh->getTargetGuid(); + if (targetGuid == 0) { lua_pushnil(L); return 1; } + auto targetEnt = gh->getEntityManager().getEntity(targetGuid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { lua_pushnil(L); return 1; } + + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + lua_pushnumber(L, dist <= data.maxRange ? 1 : 0); + return 1; +} + +// GetActionInfo(slot) → actionType, id, subType +static int lua_GetActionInfo(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return 0; } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size()) || bar[slot].isEmpty()) { + return 0; + } + const auto& action = bar[slot]; + switch (action.type) { + case game::ActionBarSlot::SPELL: + lua_pushstring(L, "spell"); + lua_pushnumber(L, action.id); + lua_pushstring(L, "spell"); + return 3; + case game::ActionBarSlot::ITEM: + lua_pushstring(L, "item"); + lua_pushnumber(L, action.id); + lua_pushstring(L, "item"); + return 3; + case game::ActionBarSlot::MACRO: + lua_pushstring(L, "macro"); + lua_pushnumber(L, action.id); + lua_pushstring(L, "macro"); + return 3; + default: + return 0; + } +} + +// GetActionCount(slot) → count (item stack count or 0) +static int lua_GetActionCount(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); return 1; } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size()) || bar[slot].isEmpty()) { + lua_pushnumber(L, 0); + return 1; + } + const auto& action = bar[slot]; + if (action.type == game::ActionBarSlot::ITEM && action.id != 0) { + // Count items across backpack + bags + uint32_t count = 0; + const auto& inv = gh->getInventory(); + for (int i = 0; i < inv.getBackpackSize(); ++i) { + const auto& s = inv.getBackpackSlot(i); + if (!s.empty() && s.item.itemId == action.id) + count += (s.item.stackCount > 0 ? s.item.stackCount : 1); + } + for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) { + int bagSize = inv.getBagSize(b); + for (int i = 0; i < bagSize; ++i) { + const auto& s = inv.getBagSlot(b, i); + if (!s.empty() && s.item.itemId == action.id) + count += (s.item.stackCount > 0 ? s.item.stackCount : 1); + } + } + lua_pushnumber(L, count); + } else { + lua_pushnumber(L, 0); + } + return 1; +} + // GetActionCooldown(slot) → start, duration, enable static int lua_GetActionCooldown(lua_State* L) { auto* gh = getGameHandler(L); @@ -3655,6 +3764,9 @@ void LuaEngine::registerCoreAPI() { {"GetActionTexture", lua_GetActionTexture}, {"IsCurrentAction", lua_IsCurrentAction}, {"IsUsableAction", lua_IsUsableAction}, + {"IsActionInRange", lua_IsActionInRange}, + {"GetActionInfo", lua_GetActionInfo}, + {"GetActionCount", lua_GetActionCount}, {"GetActionCooldown", lua_GetActionCooldown}, {"UseAction", lua_UseAction}, {"CancelUnitBuff", lua_CancelUnitBuff}, From e9ce0621120456af5ddc74f92659653e39374321 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:22:25 -0700 Subject: [PATCH 07/18] fix: restore correct CharSections.dbc field indices for character textures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #19 (572bb4ef) swapped CharSections.dbc field indices, placing Texture1-3 at fields 4-6 and VariationIndex/ColorIndex at 8-9. Binary analysis of the actual DBC files (Classic, TBC, Turtle — all identical layout, no WotLK-specific override) confirms the correct order is: Field 4 = VariationIndex Field 5 = ColorIndex Field 6 = Texture1 (string) Field 7 = Texture2 (string) Field 8 = Texture3 (string) Field 9 = Flags With the wrong indices, VariationIndex/ColorIndex reads returned string offsets (garbage values that never matched), so all CharSections lookups failed silently — producing white untextured character models at the login screen and in-world. Fixes all 4 expansion JSON layouts, hardcoded fallbacks in character_preview.cpp, application.cpp, and character_create_screen.cpp. Also handles the single-layer edge case (body skin only, no face/underwear) by loading the texture directly instead of skipping compositing. --- Data/expansions/classic/dbc_layouts.json | 12 ++++++------ Data/expansions/tbc/dbc_layouts.json | 12 ++++++------ Data/expansions/turtle/dbc_layouts.json | 12 ++++++------ Data/expansions/wotlk/dbc_layouts.json | 12 ++++++------ src/core/application.cpp | 6 +++--- src/rendering/character_preview.cpp | 25 +++++++++++++++++------- src/ui/character_create_screen.cpp | 8 ++++---- 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json index a3cba2f8..ae75e254 100644 --- a/Data/expansions/classic/dbc_layouts.json +++ b/Data/expansions/classic/dbc_layouts.json @@ -37,12 +37,12 @@ "RaceID": 1, "SexID": 2, "BaseSection": 3, - "Texture1": 4, - "Texture2": 5, - "Texture3": 6, - "Flags": 7, - "VariationIndex": 8, - "ColorIndex": 9 + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "Flags": 9 }, "SpellIcon": { "ID": 0, diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json index 8dc4bbe8..8142434e 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -37,12 +37,12 @@ "RaceID": 1, "SexID": 2, "BaseSection": 3, - "Texture1": 4, - "Texture2": 5, - "Texture3": 6, - "Flags": 7, - "VariationIndex": 8, - "ColorIndex": 9 + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "Flags": 9 }, "SpellIcon": { "ID": 0, diff --git a/Data/expansions/turtle/dbc_layouts.json b/Data/expansions/turtle/dbc_layouts.json index b7650ef5..42839fc6 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -37,12 +37,12 @@ "RaceID": 1, "SexID": 2, "BaseSection": 3, - "Texture1": 4, - "Texture2": 5, - "Texture3": 6, - "Flags": 7, - "VariationIndex": 8, - "ColorIndex": 9 + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "Flags": 9 }, "SpellIcon": { "ID": 0, diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json index 505ae150..5a05a517 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -37,12 +37,12 @@ "RaceID": 1, "SexID": 2, "BaseSection": 3, - "Texture1": 4, - "Texture2": 5, - "Texture3": 6, - "Flags": 7, - "VariationIndex": 8, - "ColorIndex": 9 + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "Flags": 9 }, "SpellIcon": { "ID": 0, diff --git a/src/core/application.cpp b/src/core/application.cpp index a4728379..49c40976 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3671,13 +3671,13 @@ void Application::spawnPlayerCharacter() { uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (raceId != targetRaceId || sexId != targetSexId) continue; // Section 0 = skin: match by colorIndex = skin byte - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4; + const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { std::string tex1 = charSectionsDbc->getString(r, csTex1); if (!tex1.empty()) { diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 31546026..306509ed 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -336,8 +336,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, uint32_t fRace = csL ? (*csL)["RaceID"] : 1; uint32_t fSex = csL ? (*csL)["SexID"] : 2; uint32_t fBase = csL ? (*csL)["BaseSection"] : 3; - uint32_t fVar = csL ? (*csL)["VariationIndex"] : 8; - uint32_t fColor = csL ? (*csL)["ColorIndex"] : 9; + uint32_t fVar = csL ? (*csL)["VariationIndex"] : 4; + uint32_t fColor = csL ? (*csL)["ColorIndex"] : 5; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { uint32_t raceId = charSectionsDbc->getUInt32(r, fRace); uint32_t sexId = charSectionsDbc->getUInt32(r, fSex); @@ -350,7 +350,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // Section 0: Body skin (variation=0, colorIndex = skin color) if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); if (!tex1.empty()) { bodySkinPath_ = tex1; foundSkin = true; @@ -360,8 +360,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 1 && !foundFace && variationIndex == static_cast(face) && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); - std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7); if (!tex1.empty()) faceLowerPath = tex1; if (!tex2.empty()) faceUpperPath = tex2; foundFace = true; @@ -370,7 +370,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 3 && !foundHair && variationIndex == static_cast(hairStyle) && colorIndex == static_cast(hairColor)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4); + std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); if (!tex1.empty()) { hairScalpPath = tex1; foundHair = true; @@ -379,7 +379,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // Section 4: Underwear (variation=0, colorIndex = skin color) else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == static_cast(skin)) { - uint32_t texBase = csL ? (*csL)["Texture1"] : 4; + uint32_t texBase = csL ? (*csL)["Texture1"] : 6; for (uint32_t f = texBase; f <= texBase + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) { @@ -462,6 +462,17 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, } } } + } else { + // Single layer (body skin only, no face/underwear overlays) — load directly + VkTexture* skinTex = charRenderer_->loadTexture(bodySkinPath_); + if (skinTex != nullptr) { + for (size_t ti = 0; ti < model.textures.size(); ti++) { + if (model.textures[ti].type == 1) { + charRenderer_->setModelTexture(PREVIEW_MODEL_ID, static_cast(ti), skinTex); + break; + } + } + } } } diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index 63933924..fa81756f 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -257,8 +257,8 @@ void CharacterCreateScreen::updateAppearanceRanges() { if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (baseSection == 0 && variationIndex == 0) { skinMax = std::max(skinMax, static_cast(colorIndex)); @@ -284,8 +284,8 @@ void CharacterCreateScreen::updateAppearanceRanges() { if (raceId != targetRaceId || sexId != targetSexId) continue; uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9); + uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); if (baseSection == 1 && colorIndex == static_cast(skin)) { faceMax = std::max(faceMax, static_cast(variationIndex)); From ab8ff6b7e5644db6d882af7e3281292d2e302c47 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:25:20 -0700 Subject: [PATCH 08/18] feat: add UnitStat and combat chance Lua API for character sheet addons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose server-authoritative player stats to Lua addons: - UnitStat(unit, statIndex) — returns STR/AGI/STA/INT/SPI (base, effective, posBuff, negBuff) matching the WoW API 4-return signature - GetDodgeChance, GetParryChance, GetBlockChance — defensive stats - GetCritChance, GetRangedCritChance — physical crit percentages - GetSpellCritChance(school) — per-school spell crit - GetCombatRating(index) — WotLK combat rating system - GetSpellBonusDamage, GetSpellBonusHealing — caster stat display - GetAttackPowerForStat, GetRangedAttackPower — melee/ranged AP All data is already tracked from SMSG_UPDATE_OBJECT field updates; these functions simply expose existing GameHandler getters to Lua. Enables PaperDollFrame, DejaCharacterStats, and similar addons. --- src/addons/lua_engine.cpp | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 9b4ed756..2bd1a18b 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -771,6 +771,119 @@ static int lua_GetMoney(lua_State* L) { return 1; } +// UnitStat(unit, statIndex) → base, effective, posBuff, negBuff +// statIndex: 1=STR, 2=AGI, 3=STA, 4=INT, 5=SPI (1-indexed per WoW API) +static int lua_UnitStat(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 4; } + int statIdx = static_cast(luaL_checknumber(L, 2)) - 1; // WoW API is 1-indexed + int32_t val = gh->getPlayerStat(statIdx); + if (val < 0) val = 0; + // We only have the effective value from the server; report base=effective, no buffs + lua_pushnumber(L, val); // base (approximate — server only sends effective) + lua_pushnumber(L, val); // effective + lua_pushnumber(L, 0); // positive buff + lua_pushnumber(L, 0); // negative buff + return 4; +} + +// GetDodgeChance() → percent +static int lua_GetDodgeChance(lua_State* L) { + auto* gh = getGameHandler(L); + float v = gh ? gh->getDodgePct() : 0.0f; + lua_pushnumber(L, v >= 0 ? v : 0.0); + return 1; +} + +// GetParryChance() → percent +static int lua_GetParryChance(lua_State* L) { + auto* gh = getGameHandler(L); + float v = gh ? gh->getParryPct() : 0.0f; + lua_pushnumber(L, v >= 0 ? v : 0.0); + return 1; +} + +// GetBlockChance() → percent +static int lua_GetBlockChance(lua_State* L) { + auto* gh = getGameHandler(L); + float v = gh ? gh->getBlockPct() : 0.0f; + lua_pushnumber(L, v >= 0 ? v : 0.0); + return 1; +} + +// GetCritChance() → percent (melee crit) +static int lua_GetCritChance(lua_State* L) { + auto* gh = getGameHandler(L); + float v = gh ? gh->getCritPct() : 0.0f; + lua_pushnumber(L, v >= 0 ? v : 0.0); + return 1; +} + +// GetRangedCritChance() → percent +static int lua_GetRangedCritChance(lua_State* L) { + auto* gh = getGameHandler(L); + float v = gh ? gh->getRangedCritPct() : 0.0f; + lua_pushnumber(L, v >= 0 ? v : 0.0); + return 1; +} + +// GetSpellCritChance(school) → percent (1=Holy,2=Fire,3=Nature,4=Frost,5=Shadow,6=Arcane) +static int lua_GetSpellCritChance(lua_State* L) { + auto* gh = getGameHandler(L); + int school = static_cast(luaL_checknumber(L, 1)); + float v = gh ? gh->getSpellCritPct(school) : 0.0f; + lua_pushnumber(L, v >= 0 ? v : 0.0); + return 1; +} + +// GetCombatRating(ratingIndex) → value +static int lua_GetCombatRating(lua_State* L) { + auto* gh = getGameHandler(L); + int cr = static_cast(luaL_checknumber(L, 1)); + int32_t v = gh ? gh->getCombatRating(cr) : 0; + lua_pushnumber(L, v >= 0 ? v : 0); + return 1; +} + +// GetSpellBonusDamage(school) → value (1-6 magic schools) +static int lua_GetSpellBonusDamage(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); return 1; } + int32_t sp = gh->getSpellPower(); + lua_pushnumber(L, sp >= 0 ? sp : 0); + return 1; +} + +// GetSpellBonusHealing() → value +static int lua_GetSpellBonusHealing(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); return 1; } + int32_t v = gh->getHealingPower(); + lua_pushnumber(L, v >= 0 ? v : 0); + return 1; +} + +// GetMeleeHaste / GetAttackPowerForStat stubs for addon compat +static int lua_GetAttackPower(lua_State* L) { + auto* gh = getGameHandler(L); + int32_t ap = gh ? gh->getMeleeAttackPower() : 0; + if (ap < 0) ap = 0; + lua_pushnumber(L, ap); // base + lua_pushnumber(L, 0); // posBuff + lua_pushnumber(L, 0); // negBuff + return 3; +} + +static int lua_GetRangedAttackPower(lua_State* L) { + auto* gh = getGameHandler(L); + int32_t ap = gh ? gh->getRangedAttackPower() : 0; + if (ap < 0) ap = 0; + lua_pushnumber(L, ap); + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + return 3; +} + static int lua_IsInGroup(lua_State* L) { auto* gh = getGameHandler(L); lua_pushboolean(L, gh && gh->isInGroup()); @@ -3639,6 +3752,18 @@ void LuaEngine::registerCoreAPI() { {"UnitSex", lua_UnitSex}, {"UnitClass", lua_UnitClass}, {"GetMoney", lua_GetMoney}, + {"UnitStat", lua_UnitStat}, + {"GetDodgeChance", lua_GetDodgeChance}, + {"GetParryChance", lua_GetParryChance}, + {"GetBlockChance", lua_GetBlockChance}, + {"GetCritChance", lua_GetCritChance}, + {"GetRangedCritChance", lua_GetRangedCritChance}, + {"GetSpellCritChance", lua_GetSpellCritChance}, + {"GetCombatRating", lua_GetCombatRating}, + {"GetSpellBonusDamage", lua_GetSpellBonusDamage}, + {"GetSpellBonusHealing", lua_GetSpellBonusHealing}, + {"GetAttackPowerForStat", lua_GetAttackPower}, + {"GetRangedAttackPower", lua_GetRangedAttackPower}, {"IsInGroup", lua_IsInGroup}, {"IsInRaid", lua_IsInRaid}, {"GetPlayerMapPosition", lua_GetPlayerMapPosition}, From 6d72228f666bebc58f9391c9085704dffb4525e9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:30:53 -0700 Subject: [PATCH 09/18] feat: add GetInventorySlotInfo for PaperDollFrame and BankFrame Maps WoW equipment slot names (e.g. "HeadSlot", "MainHandSlot") to inventory slot IDs, empty-slot textures, and relic check flags. Supports case-insensitive matching with optional "Slot" suffix stripping. Unblocks PaperDollFrame.lua and BankFrame.lua which call this function to resolve slot button IDs during UI initialization. --- src/addons/lua_engine.cpp | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 2bd1a18b..fdaebadd 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -1923,6 +1923,51 @@ static int lua_GetContainerNumFreeSlots(lua_State* L) { // 6=Waist,7=Legs,8=Feet,9=Wrists,10=Hands,11=Ring1,12=Ring2, // 13=Trinket1,14=Trinket2,15=Back,16=MainHand,17=OffHand,18=Ranged,19=Tabard +// GetInventorySlotInfo("slotName") → slotId, textureName, checkRelic +// Maps WoW slot names (e.g. "HeadSlot", "HEADSLOT") to inventory slot IDs +static int lua_GetInventorySlotInfo(lua_State* L) { + const char* name = luaL_checkstring(L, 1); + std::string slot(name); + // Normalize: uppercase, strip trailing "SLOT" if present + for (char& c : slot) c = static_cast(std::toupper(static_cast(c))); + if (slot.size() > 4 && slot.substr(slot.size() - 4) == "SLOT") + slot = slot.substr(0, slot.size() - 4); + + // WoW inventory slots are 1-indexed + struct SlotMap { const char* name; int id; const char* texture; }; + static const SlotMap mapping[] = { + {"HEAD", 1, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Head"}, + {"NECK", 2, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Neck"}, + {"SHOULDER", 3, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Shoulder"}, + {"SHIRT", 4, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Shirt"}, + {"CHEST", 5, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest"}, + {"WAIST", 6, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Waist"}, + {"LEGS", 7, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Legs"}, + {"FEET", 8, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Feet"}, + {"WRIST", 9, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Wrists"}, + {"HANDS", 10, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Hands"}, + {"FINGER0", 11, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Finger"}, + {"FINGER1", 12, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Finger"}, + {"TRINKET0", 13, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Trinket"}, + {"TRINKET1", 14, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Trinket"}, + {"BACK", 15, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest"}, + {"MAINHAND", 16, "Interface\\PaperDoll\\UI-PaperDoll-Slot-MainHand"}, + {"SECONDARYHAND",17, "Interface\\PaperDoll\\UI-PaperDoll-Slot-SecondaryHand"}, + {"RANGED", 18, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Ranged"}, + {"TABARD", 19, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Tabard"}, + }; + for (const auto& m : mapping) { + if (slot == m.name) { + lua_pushnumber(L, m.id); + lua_pushstring(L, m.texture); + lua_pushboolean(L, m.id == 18 ? 1 : 0); // checkRelic: only ranged slot + return 3; + } + } + luaL_error(L, "Unknown inventory slot: %s", name); + return 0; +} + static int lua_GetInventoryItemLink(lua_State* L) { auto* gh = getGameHandler(L); const char* uid = luaL_optstring(L, 1, "player"); @@ -3845,6 +3890,7 @@ void LuaEngine::registerCoreAPI() { {"GetContainerItemLink", lua_GetContainerItemLink}, {"GetContainerNumFreeSlots", lua_GetContainerNumFreeSlots}, // Equipment slot API + {"GetInventorySlotInfo", lua_GetInventorySlotInfo}, {"GetInventoryItemLink", lua_GetInventoryItemLink}, {"GetInventoryItemID", lua_GetInventoryItemID}, {"GetInventoryItemTexture", lua_GetInventoryItemTexture}, From f29ebbdd71ba6eee1d72e239d91c29e5a8ceb535 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:36:25 -0700 Subject: [PATCH 10/18] feat: add quest watch/tracking and selection Lua API for WatchFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the quest tracking functions needed by WatchFrame.lua: - SelectQuestLogEntry/GetQuestLogSelection — quest log selection state - GetNumQuestWatches — count of tracked quests - GetQuestIndexForWatch(watchIdx) — map Nth watched quest to log index - AddQuestWatch/RemoveQuestWatch — toggle quest tracking by log index - IsQuestWatched — check if a quest log entry is tracked - GetQuestLink — generate colored quest link string Backed by existing trackedQuestIds_ set and questLog_ vector. Adds selectedQuestLogIndex_ state to GameHandler for quest selection. --- include/game/game_handler.hpp | 3 + src/addons/lua_engine.cpp | 105 ++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 033f8661..9eb2b867 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1674,6 +1674,8 @@ public: std::array rewardChoiceItems{}; // player picks one of these }; const std::vector& getQuestLog() const { return questLog_; } + int getSelectedQuestLogIndex() const { return selectedQuestLogIndex_; } + void setSelectedQuestLogIndex(int idx) { selectedQuestLogIndex_ = idx; } void abandonQuest(uint32_t questId); void shareQuestWithParty(uint32_t questId); // CMSG_PUSHQUESTTOPARTY bool requestQuestQuery(uint32_t questId, bool force = false); @@ -3187,6 +3189,7 @@ private: // Quest log std::vector questLog_; + int selectedQuestLogIndex_ = 0; std::unordered_set pendingQuestQueryIds_; std::unordered_set trackedQuestIds_; bool pendingLoginQuestResync_ = false; diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index fdaebadd..3ab3a3c9 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -2150,6 +2150,103 @@ static int lua_IsQuestComplete(lua_State* L) { return 1; } +// SelectQuestLogEntry(index) — select a quest in the quest log +static int lua_SelectQuestLogEntry(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (gh) gh->setSelectedQuestLogIndex(index); + return 0; +} + +// GetQuestLogSelection() → index +static int lua_GetQuestLogSelection(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getSelectedQuestLogIndex() : 0); + return 1; +} + +// GetNumQuestWatches() → count +static int lua_GetNumQuestWatches(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getTrackedQuestIds().size() : 0); + return 1; +} + +// GetQuestIndexForWatch(watchIndex) → questLogIndex +// Maps the Nth watched quest to its quest log index (1-based) +static int lua_GetQuestIndexForWatch(lua_State* L) { + auto* gh = getGameHandler(L); + int watchIdx = static_cast(luaL_checknumber(L, 1)); + if (!gh || watchIdx < 1) { lua_pushnil(L); return 1; } + const auto& ql = gh->getQuestLog(); + const auto& tracked = gh->getTrackedQuestIds(); + int found = 0; + for (size_t i = 0; i < ql.size(); ++i) { + if (tracked.count(ql[i].questId)) { + found++; + if (found == watchIdx) { + lua_pushnumber(L, static_cast(i) + 1); // 1-based + return 1; + } + } + } + lua_pushnil(L); + return 1; +} + +// AddQuestWatch(questLogIndex) — add a quest to the watch list +static int lua_AddQuestWatch(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) return 0; + const auto& ql = gh->getQuestLog(); + if (index <= static_cast(ql.size())) { + gh->setQuestTracked(ql[index - 1].questId, true); + } + return 0; +} + +// RemoveQuestWatch(questLogIndex) — remove a quest from the watch list +static int lua_RemoveQuestWatch(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) return 0; + const auto& ql = gh->getQuestLog(); + if (index <= static_cast(ql.size())) { + gh->setQuestTracked(ql[index - 1].questId, false); + } + return 0; +} + +// IsQuestWatched(questLogIndex) → boolean +static int lua_IsQuestWatched(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { lua_pushboolean(L, 0); return 1; } + const auto& ql = gh->getQuestLog(); + if (index <= static_cast(ql.size())) { + lua_pushboolean(L, gh->isQuestTracked(ql[index - 1].questId) ? 1 : 0); + } else { + lua_pushboolean(L, 0); + } + return 1; +} + +// GetQuestLink(questLogIndex) → "|cff...|Hquest:id:level|h[title]|h|r" +static int lua_GetQuestLink(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { lua_pushnil(L); return 1; } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { lua_pushnil(L); return 1; } + const auto& q = ql[index - 1]; + // Yellow quest link format matching WoW + std::string link = "|cff808000|Hquest:" + std::to_string(q.questId) + + ":0|h[" + q.title + "]|h|r"; + lua_pushstring(L, link.c_str()); + return 1; +} + // --- Skill Line API --- // GetNumSkillLines() → count @@ -3906,6 +4003,14 @@ void LuaEngine::registerCoreAPI() { {"GetQuestLogTitle", lua_GetQuestLogTitle}, {"GetQuestLogQuestText", lua_GetQuestLogQuestText}, {"IsQuestComplete", lua_IsQuestComplete}, + {"SelectQuestLogEntry", lua_SelectQuestLogEntry}, + {"GetQuestLogSelection", lua_GetQuestLogSelection}, + {"GetNumQuestWatches", lua_GetNumQuestWatches}, + {"GetQuestIndexForWatch", lua_GetQuestIndexForWatch}, + {"AddQuestWatch", lua_AddQuestWatch}, + {"RemoveQuestWatch", lua_RemoveQuestWatch}, + {"IsQuestWatched", lua_IsQuestWatched}, + {"GetQuestLink", lua_GetQuestLink}, // Skill line API {"GetNumSkillLines", lua_GetNumSkillLines}, {"GetSkillLineInfo", lua_GetSkillLineInfo}, From 508652035433bdb83b7d702f52d34cb31cf5fd38 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:40:40 -0700 Subject: [PATCH 11/18] feat: add spell book tab API for SpellBookFrame addon compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement GetNumSpellTabs, GetSpellTabInfo, GetSpellBookItemInfo, and GetSpellBookItemName — the core functions SpellBookFrame.lua needs to organize known spells into class skill line tabs. Tabs are built lazily from knownSpells grouped by SkillLineAbility.dbc mappings (category 7 = class). A "General" tab collects spells not in any class skill line. Tabs auto-rebuild when the spell count changes. Also adds SpellBookTab struct and getSpellBookTabs() to GameHandler. --- include/game/game_handler.hpp | 10 +++++ src/addons/lua_engine.cpp | 84 +++++++++++++++++++++++++++++++++++ src/game/game_handler.cpp | 56 +++++++++++++++++++++++ 3 files changed, 150 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 9eb2b867..5bb40efa 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -830,6 +830,14 @@ public: void togglePetSpellAutocast(uint32_t spellId); const std::unordered_set& getKnownSpells() const { return knownSpells; } + // Spell book tabs — groups known spells by class skill line for Lua API + struct SpellBookTab { + std::string name; + std::string texture; // icon path + std::vector spellIds; // spells in this tab + }; + const std::vector& getSpellBookTabs(); + // ---- Pet Stable ---- struct StabledPet { uint32_t petNumber = 0; // server-side pet number (used for unstable/swap) @@ -3443,6 +3451,8 @@ private: std::unordered_map skillLineNames_; std::unordered_map skillLineCategories_; std::unordered_map spellToSkillLine_; // spellID -> skillLineID + std::vector spellBookTabs_; + bool spellBookTabsDirty_ = true; bool skillLineDbcLoaded_ = false; bool skillLineAbilityLoaded_ = false; static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128; diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 3ab3a3c9..691ced5a 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -1396,6 +1396,86 @@ static int lua_IsSpellKnown(lua_State* L) { return 1; } +// --- Spell Book Tab API --- + +// GetNumSpellTabs() → count +static int lua_GetNumSpellTabs(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); return 1; } + lua_pushnumber(L, gh->getSpellBookTabs().size()); + return 1; +} + +// GetSpellTabInfo(tabIndex) → name, texture, offset, numSpells +// tabIndex is 1-based; offset is 1-based global spell book slot +static int lua_GetSpellTabInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int tabIdx = static_cast(luaL_checknumber(L, 1)); + if (!gh || tabIdx < 1) { + lua_pushnil(L); return 1; + } + const auto& tabs = gh->getSpellBookTabs(); + if (tabIdx > static_cast(tabs.size())) { + lua_pushnil(L); return 1; + } + // Compute offset: sum of spells in all preceding tabs (1-based) + int offset = 0; + for (int i = 0; i < tabIdx - 1; ++i) + offset += static_cast(tabs[i].spellIds.size()); + const auto& tab = tabs[tabIdx - 1]; + lua_pushstring(L, tab.name.c_str()); // name + lua_pushstring(L, tab.texture.c_str()); // texture + lua_pushnumber(L, offset); // offset (0-based for WoW compat) + lua_pushnumber(L, tab.spellIds.size()); // numSpells + return 4; +} + +// GetSpellBookItemInfo(slot, bookType) → "SPELL", spellId +// slot is 1-based global spell book index +static int lua_GetSpellBookItemInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (!gh || slot < 1) { + lua_pushstring(L, "SPELL"); + lua_pushnumber(L, 0); + return 2; + } + const auto& tabs = gh->getSpellBookTabs(); + int idx = slot; // 1-based + for (const auto& tab : tabs) { + if (idx <= static_cast(tab.spellIds.size())) { + lua_pushstring(L, "SPELL"); + lua_pushnumber(L, tab.spellIds[idx - 1]); + return 2; + } + idx -= static_cast(tab.spellIds.size()); + } + lua_pushstring(L, "SPELL"); + lua_pushnumber(L, 0); + return 2; +} + +// GetSpellBookItemName(slot, bookType) → name, subName +static int lua_GetSpellBookItemName(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (!gh || slot < 1) { lua_pushnil(L); return 1; } + const auto& tabs = gh->getSpellBookTabs(); + int idx = slot; + for (const auto& tab : tabs) { + if (idx <= static_cast(tab.spellIds.size())) { + uint32_t spellId = tab.spellIds[idx - 1]; + const std::string& name = gh->getSpellName(spellId); + lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); + lua_pushstring(L, ""); // subName/rank + return 2; + } + idx -= static_cast(tab.spellIds.size()); + } + lua_pushnil(L); + return 1; +} + static int lua_GetSpellCooldown(lua_State* L) { auto* gh = getGameHandler(L); if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } @@ -3915,6 +3995,10 @@ void LuaEngine::registerCoreAPI() { {"IsAddonMessagePrefixRegistered", lua_IsAddonMessagePrefixRegistered}, {"CastSpellByName", lua_CastSpellByName}, {"IsSpellKnown", lua_IsSpellKnown}, + {"GetNumSpellTabs", lua_GetNumSpellTabs}, + {"GetSpellTabInfo", lua_GetSpellTabInfo}, + {"GetSpellBookItemInfo", lua_GetSpellBookItemInfo}, + {"GetSpellBookItemName", lua_GetSpellBookItemName}, {"GetSpellCooldown", lua_GetSpellCooldown}, {"GetSpellPowerCost", lua_GetSpellPowerCost}, {"IsSpellInRange", lua_IsSpellInRange}, diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 58fbf032..66c116bf 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -23033,6 +23033,62 @@ void GameHandler::loadSkillLineAbilityDbc() { } } +const std::vector& GameHandler::getSpellBookTabs() { + // Rebuild when spell count changes (learns/unlearns) + static size_t lastSpellCount = 0; + if (lastSpellCount == knownSpells.size() && !spellBookTabsDirty_) + return spellBookTabs_; + lastSpellCount = knownSpells.size(); + spellBookTabsDirty_ = false; + spellBookTabs_.clear(); + + static constexpr uint32_t SKILLLINE_CATEGORY_CLASS = 7; + + // Group known spells by class skill line + std::map> bySkillLine; + std::vector general; + + for (uint32_t spellId : knownSpells) { + auto slIt = spellToSkillLine_.find(spellId); + if (slIt != spellToSkillLine_.end()) { + uint32_t skillLineId = slIt->second; + auto catIt = skillLineCategories_.find(skillLineId); + if (catIt != skillLineCategories_.end() && catIt->second == SKILLLINE_CATEGORY_CLASS) { + bySkillLine[skillLineId].push_back(spellId); + continue; + } + } + general.push_back(spellId); + } + + // Sort spells within each group by name + auto byName = [this](uint32_t a, uint32_t b) { + return getSpellName(a) < getSpellName(b); + }; + + // "General" tab first (spells not in a class skill line) + if (!general.empty()) { + std::sort(general.begin(), general.end(), byName); + spellBookTabs_.push_back({"General", "Interface\\Icons\\INV_Misc_Book_09", std::move(general)}); + } + + // Class skill line tabs, sorted by name + std::vector>> named; + for (auto& [skillLineId, spells] : bySkillLine) { + auto nameIt = skillLineNames_.find(skillLineId); + std::string tabName = (nameIt != skillLineNames_.end()) ? nameIt->second : "Unknown"; + std::sort(spells.begin(), spells.end(), byName); + named.emplace_back(std::move(tabName), std::move(spells)); + } + std::sort(named.begin(), named.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); + + for (auto& [name, spells] : named) { + spellBookTabs_.push_back({std::move(name), "Interface\\Icons\\INV_Misc_Book_09", std::move(spells)}); + } + + return spellBookTabs_; +} + void GameHandler::categorizeTrainerSpells() { trainerTabs_.clear(); From 73ce601bb5a7b5edbf9ef29eedc6d20d9faa8e18 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:50:05 -0700 Subject: [PATCH 12/18] feat: fire PLAYER_ENTERING_WORLD and critical login events for addons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAYER_ENTERING_WORLD is the single most important WoW addon event — virtually every addon registers for it to initialize UI, state, and data structures. It was never fired, causing widespread addon init failures on login and after teleports. Now fired from: - handleLoginVerifyWorld (initial login + same-map teleports) - handleNewWorld (cross-map teleports, instance transitions) Also fires: - PLAYER_LOGIN on initial world entry only - ZONE_CHANGED_NEW_AREA on all world entries - UPDATE_WORLD_STATES on initial entry - SPELLS_CHANGED + LEARNED_SPELL_IN_TAB after SMSG_INITIAL_SPELLS (so spell book addons can initialize on login) --- src/game/game_handler.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 66c116bf..af9489ba 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -9744,6 +9744,19 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { LOG_INFO("Auto-requested played time on login"); } } + + // Fire PLAYER_ENTERING_WORLD — THE most important event for addon initialization. + // Fires on initial login, teleports, instance transitions, and zone changes. + if (addonEventCallback_) { + addonEventCallback_("PLAYER_ENTERING_WORLD", {initialWorldEntry ? "1" : "0"}); + // Also fire ZONE_CHANGED_NEW_AREA and UPDATE_WORLD_STATES so map/BG addons refresh + addonEventCallback_("ZONE_CHANGED_NEW_AREA", {}); + addonEventCallback_("UPDATE_WORLD_STATES", {}); + // PLAYER_LOGIN fires only on initial login (not teleports) + if (initialWorldEntry) { + addonEventCallback_("PLAYER_LOGIN", {}); + } + } } void GameHandler::handleClientCacheVersion(network::Packet& packet) { @@ -19318,6 +19331,12 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { loadSkillLineAbilityDbc(); LOG_INFO("Learned ", knownSpells.size(), " spells"); + + // Notify addons that the full spell list is now available + if (addonEventCallback_) { + addonEventCallback_("SPELLS_CHANGED", {}); + addonEventCallback_("LEARNED_SPELL_IN_TAB", {}); + } } void GameHandler::handleCastFailed(network::Packet& packet) { @@ -23635,6 +23654,12 @@ void GameHandler::handleNewWorld(network::Packet& packet) { if (worldEntryCallback_) { worldEntryCallback_(mapId, serverX, serverY, serverZ, isSameMap); } + + // Fire PLAYER_ENTERING_WORLD for teleports / zone transitions + if (addonEventCallback_) { + addonEventCallback_("PLAYER_ENTERING_WORLD", {"0"}); + addonEventCallback_("ZONE_CHANGED_NEW_AREA", {}); + } } // ============================================================ From 296121f5e722828e37ddee7ab34a78bb4cef279a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 15:58:45 -0700 Subject: [PATCH 13/18] feat: add GetPlayerFacing, GetCVar/SetCVar for minimap and addon settings GetPlayerFacing() returns player orientation in radians, needed by minimap addons for arrow rotation and facing-dependent mechanics. GetCVar(name) returns sensible defaults for commonly queried CVars (uiScale, screen dimensions, nameplate visibility, sound toggles, autoLoot). SetCVar is a no-op stub for addon compatibility. --- src/addons/lua_engine.cpp | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 691ced5a..847bdc0f 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -909,6 +909,50 @@ static int lua_GetPlayerMapPosition(lua_State* L) { return 2; } +// GetPlayerFacing() → radians (0 = north, increasing counter-clockwise) +static int lua_GetPlayerFacing(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) { + float facing = gh->getMovementInfo().orientation; + // Normalize to [0, 2π) + while (facing < 0) facing += 6.2831853f; + while (facing >= 6.2831853f) facing -= 6.2831853f; + lua_pushnumber(L, facing); + } else { + lua_pushnumber(L, 0); + } + return 1; +} + +// GetCVar(name) → value string (stub for most, real for a few) +static int lua_GetCVar(lua_State* L) { + const char* name = luaL_checkstring(L, 1); + std::string n(name); + // Return sensible defaults for commonly queried CVars + if (n == "uiScale") lua_pushstring(L, "1"); + else if (n == "useUIScale") lua_pushstring(L, "1"); + else if (n == "screenWidth" || n == "gxResolution") { + auto* win = core::Application::getInstance().getWindow(); + lua_pushstring(L, std::to_string(win ? win->getWidth() : 1920).c_str()); + } else if (n == "screenHeight" || n == "gxFullscreenResolution") { + auto* win = core::Application::getInstance().getWindow(); + lua_pushstring(L, std::to_string(win ? win->getHeight() : 1080).c_str()); + } else if (n == "nameplateShowFriends") lua_pushstring(L, "1"); + else if (n == "nameplateShowEnemies") lua_pushstring(L, "1"); + else if (n == "Sound_EnableSFX") lua_pushstring(L, "1"); + else if (n == "Sound_EnableMusic") lua_pushstring(L, "1"); + else if (n == "chatBubbles") lua_pushstring(L, "1"); + else if (n == "autoLootDefault") lua_pushstring(L, "1"); + else lua_pushstring(L, "0"); + return 1; +} + +// SetCVar(name, value) — no-op stub (log for debugging) +static int lua_SetCVar(lua_State* L) { + (void)L; + return 0; +} + static int lua_UnitRace(lua_State* L) { auto* gh = getGameHandler(L); if (!gh) { lua_pushstring(L, "Unknown"); lua_pushstring(L, "Unknown"); lua_pushnumber(L, 0); return 3; } @@ -3989,6 +4033,9 @@ void LuaEngine::registerCoreAPI() { {"IsInGroup", lua_IsInGroup}, {"IsInRaid", lua_IsInRaid}, {"GetPlayerMapPosition", lua_GetPlayerMapPosition}, + {"GetPlayerFacing", lua_GetPlayerFacing}, + {"GetCVar", lua_GetCVar}, + {"SetCVar", lua_SetCVar}, {"SendChatMessage", lua_SendChatMessage}, {"SendAddonMessage", lua_SendAddonMessage}, {"RegisterAddonMessagePrefix", lua_RegisterAddonMessagePrefix}, From cbdf03c07ed8e24f8f93f513d1b52c9fa55094b0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 16:04:33 -0700 Subject: [PATCH 14/18] feat: add quest objective leaderboard API for WatchFrame quest tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement GetNumQuestLeaderBoards and GetQuestLogLeaderBoard — the core functions WatchFrame.lua and QuestLogFrame.lua use to display objective progress like "Kobold Vermin slain: 3/8" or "Linen Cloth: 2/6". GetNumQuestLeaderBoards counts kill + item objectives from the parsed SMSG_QUEST_QUERY_RESPONSE data. GetQuestLogLeaderBoard returns the formatted progress text, type ("monster"/"item"/"object"), and completion status for each objective. Also adds ExpandQuestHeader/CollapseQuestHeader (no-ops for flat quest list) and GetQuestLogSpecialItemInfo stub. --- src/addons/lua_engine.cpp | 90 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 847bdc0f..944be318 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -2371,6 +2371,91 @@ static int lua_GetQuestLink(lua_State* L) { return 1; } +// GetNumQuestLeaderBoards(questLogIndex) → count of objectives +static int lua_GetNumQuestLeaderBoards(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { lua_pushnumber(L, 0); return 1; } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { lua_pushnumber(L, 0); return 1; } + const auto& q = ql[index - 1]; + int count = 0; + for (const auto& ko : q.killObjectives) { + if (ko.npcOrGoId != 0 || ko.required > 0) ++count; + } + for (const auto& io : q.itemObjectives) { + if (io.itemId != 0 || io.required > 0) ++count; + } + lua_pushnumber(L, count); + return 1; +} + +// GetQuestLogLeaderBoard(objIndex, questLogIndex) → text, type, finished +// objIndex is 1-based within the quest's objectives +static int lua_GetQuestLogLeaderBoard(lua_State* L) { + auto* gh = getGameHandler(L); + int objIdx = static_cast(luaL_checknumber(L, 1)); + int questIdx = static_cast(luaL_optnumber(L, 2, + gh ? gh->getSelectedQuestLogIndex() : 0)); + if (!gh || questIdx < 1 || objIdx < 1) { lua_pushnil(L); return 1; } + const auto& ql = gh->getQuestLog(); + if (questIdx > static_cast(ql.size())) { lua_pushnil(L); return 1; } + const auto& q = ql[questIdx - 1]; + + // Build ordered list: kill objectives first, then item objectives + int cur = 0; + for (int i = 0; i < 4; ++i) { + if (q.killObjectives[i].npcOrGoId == 0 && q.killObjectives[i].required == 0) continue; + ++cur; + if (cur == objIdx) { + // Get current count from killCounts map (keyed by abs(npcOrGoId)) + uint32_t key = static_cast(std::abs(q.killObjectives[i].npcOrGoId)); + uint32_t current = 0; + auto it = q.killCounts.find(key); + if (it != q.killCounts.end()) current = it->second.first; + uint32_t required = q.killObjectives[i].required; + bool finished = (current >= required); + // Build display text like "Kobold Vermin slain: 3/8" + std::string text = (q.killObjectives[i].npcOrGoId < 0 ? "Object" : "Creature") + + std::string(" slain: ") + std::to_string(current) + "/" + std::to_string(required); + lua_pushstring(L, text.c_str()); + lua_pushstring(L, q.killObjectives[i].npcOrGoId < 0 ? "object" : "monster"); + lua_pushboolean(L, finished ? 1 : 0); + return 3; + } + } + for (int i = 0; i < 6; ++i) { + if (q.itemObjectives[i].itemId == 0 && q.itemObjectives[i].required == 0) continue; + ++cur; + if (cur == objIdx) { + uint32_t current = 0; + auto it = q.itemCounts.find(q.itemObjectives[i].itemId); + if (it != q.itemCounts.end()) current = it->second; + uint32_t required = q.itemObjectives[i].required; + bool finished = (current >= required); + // Get item name if available + std::string itemName; + const auto* info = gh->getItemInfo(q.itemObjectives[i].itemId); + if (info && !info->name.empty()) itemName = info->name; + else itemName = "Item #" + std::to_string(q.itemObjectives[i].itemId); + std::string text = itemName + ": " + std::to_string(current) + "/" + std::to_string(required); + lua_pushstring(L, text.c_str()); + lua_pushstring(L, "item"); + lua_pushboolean(L, finished ? 1 : 0); + return 3; + } + } + lua_pushnil(L); + return 1; +} + +// ExpandQuestHeader / CollapseQuestHeader — no-ops (flat quest list, no headers) +static int lua_ExpandQuestHeader(lua_State* L) { (void)L; return 0; } +static int lua_CollapseQuestHeader(lua_State* L) { (void)L; return 0; } + +// GetQuestLogSpecialItemInfo(questLogIndex) — returns nil (no special items) +static int lua_GetQuestLogSpecialItemInfo(lua_State* L) { (void)L; lua_pushnil(L); return 1; } + // --- Skill Line API --- // GetNumSkillLines() → count @@ -4142,6 +4227,11 @@ void LuaEngine::registerCoreAPI() { {"RemoveQuestWatch", lua_RemoveQuestWatch}, {"IsQuestWatched", lua_IsQuestWatched}, {"GetQuestLink", lua_GetQuestLink}, + {"GetNumQuestLeaderBoards", lua_GetNumQuestLeaderBoards}, + {"GetQuestLogLeaderBoard", lua_GetQuestLogLeaderBoard}, + {"ExpandQuestHeader", lua_ExpandQuestHeader}, + {"CollapseQuestHeader", lua_CollapseQuestHeader}, + {"GetQuestLogSpecialItemInfo", lua_GetQuestLogSpecialItemInfo}, // Skill line API {"GetNumSkillLines", lua_GetNumSkillLines}, {"GetSkillLineInfo", lua_GetSkillLineInfo}, From 31ab76427f9530adc62d3bdc845796945217299d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 16:09:57 -0700 Subject: [PATCH 15/18] fix: remove dead duplicate ufNpcFlags check and add missing UNIT_MODEL_CHANGED events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the CREATE update block, the ufNpcFlags check at the end of the else-if chain was unreachable dead code — it was already handled earlier in the same chain. Remove the duplicate. In the VALUES update block, mount display changes via field updates fired mountCallback_ but not the UNIT_MODEL_CHANGED addon event, unlike the CREATE path which fired both. Add the missing event so Lua addons are notified when the player mounts/dismounts via VALUES updates (the common case for aura-based mounting). Also fire UNIT_MODEL_CHANGED for target/focus/pet display ID changes in the VALUES displayIdChanged path, matching the CREATE path behavior. --- src/game/game_handler.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index af9489ba..b908245b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -12039,7 +12039,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } unit->setMountDisplayId(val); - } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } + } } if (block.guid == playerGuid) { constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; @@ -12587,6 +12587,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem uint32_t old = currentMountDisplayId_; currentMountDisplayId_ = val; if (val != old && mountCallback_) mountCallback_(val); + if (val != old && addonEventCallback_) + addonEventCallback_("UNIT_MODEL_CHANGED", {"player"}); if (old == 0 && val != 0) { mountAuraSpellId_ = 0; for (const auto& a : playerAuras) { @@ -12732,6 +12734,15 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem qsPkt.writeUInt64(block.guid); socket->send(qsPkt); } + // Fire UNIT_MODEL_CHANGED for addons that track model swaps + if (addonEventCallback_) { + std::string uid; + if (block.guid == targetGuid) uid = "target"; + else if (block.guid == focusGuid) uid = "focus"; + else if (block.guid == petGuid_) uid = "pet"; + if (!uid.empty()) + addonEventCallback_("UNIT_MODEL_CHANGED", {uid}); + } } } // Update XP / inventory slot / skill fields for player entity From f9856c1046df9a1070957ad2b39a42cb65468f5e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 16:13:39 -0700 Subject: [PATCH 16/18] feat: implement GameTooltip methods with real item/spell/aura data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace empty stub GameTooltip methods with working implementations: - SetInventoryItem(unit, slot): populates tooltip with equipped item name and quality-colored text via GetInventoryItemLink + GetItemInfo - SetBagItem(bag, slot): populates from GetContainerItemInfo + GetItemInfo - SetSpellByID(spellId): populates with spell name/rank from GetSpellInfo - SetAction(slot): delegates to SetSpellByID or item lookup via GetActionInfo - SetUnitBuff/SetUnitDebuff: populates from UnitBuff/UnitDebuff data - SetHyperlink: parses item: and spell: links to populate name - GetItem/GetSpell: now return real item/spell data when tooltip is populated Also fix GetCVar/SetCVar conflict — remove Lua-side overrides that were shadowing the C-side implementations (which return real screen dimensions and sensible defaults for common CVars). --- src/addons/lua_engine.cpp | 118 +++++++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 944be318..e2505c4e 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -4568,18 +4568,111 @@ void LuaEngine::registerCoreAPI() { "function GameTooltip:AddLine(text, r, g, b, wrap) table.insert(self.__lines, {text=text or '',r=r,g=g,b=b}) end\n" "function GameTooltip:AddDoubleLine(l, r, lr, lg, lb, rr, rg, rb) table.insert(self.__lines, {text=(l or '')..' '..(r or '')}) end\n" "function GameTooltip:SetText(text, r, g, b) self.__lines = {{text=text or '',r=r,g=g,b=b}} end\n" - "function GameTooltip:GetItem() return nil end\n" - "function GameTooltip:GetSpell() return nil end\n" + "function GameTooltip:GetItem()\n" + " if self.__itemId and self.__itemId > 0 then\n" + " local name = GetItemInfo(self.__itemId)\n" + " return name, '|cffffffff|Hitem:'..self.__itemId..':0|h['..tostring(name)..']|h|r'\n" + " end\n" + " return nil\n" + "end\n" + "function GameTooltip:GetSpell()\n" + " if self.__spellId and self.__spellId > 0 then\n" + " local name = GetSpellInfo(self.__spellId)\n" + " return name, nil, self.__spellId\n" + " end\n" + " return nil\n" + "end\n" "function GameTooltip:GetUnit() return nil end\n" "function GameTooltip:NumLines() return #self.__lines end\n" "function GameTooltip:GetText() return self.__lines[1] and self.__lines[1].text or '' end\n" - "function GameTooltip:SetUnitBuff(...) end\n" - "function GameTooltip:SetUnitDebuff(...) end\n" - "function GameTooltip:SetHyperlink(...) end\n" - "function GameTooltip:SetInventoryItem(...) end\n" - "function GameTooltip:SetBagItem(...) end\n" - "function GameTooltip:SetSpellByID(...) end\n" - "function GameTooltip:SetAction(...) end\n" + "function GameTooltip:SetUnitBuff(unit, index, filter)\n" + " self:ClearLines()\n" + " local name, rank, icon, count, debuffType, duration, expTime, caster, steal, consolidate, spellId = UnitBuff(unit, index, filter)\n" + " if name then\n" + " self:SetText(name, 1, 1, 1)\n" + " if duration and duration > 0 then\n" + " self:AddLine(string.format('%.0f sec remaining', expTime - GetTime()), 1, 1, 1)\n" + " end\n" + " self.__spellId = spellId\n" + " end\n" + "end\n" + "function GameTooltip:SetUnitDebuff(unit, index, filter)\n" + " self:ClearLines()\n" + " local name, rank, icon, count, debuffType, duration, expTime, caster, steal, consolidate, spellId = UnitDebuff(unit, index, filter)\n" + " if name then\n" + " self:SetText(name, 1, 0, 0)\n" + " if debuffType then self:AddLine(debuffType, 0.5, 0.5, 0.5) end\n" + " self.__spellId = spellId\n" + " end\n" + "end\n" + "function GameTooltip:SetHyperlink(link)\n" + " self:ClearLines()\n" + " if not link then return end\n" + " local id = link:match('item:(%d+)')\n" + " if id then\n" + " local name, _, quality = GetItemInfo(tonumber(id))\n" + " if name then self:SetText(name, 1, 1, 1) end\n" + " return\n" + " end\n" + " id = link:match('spell:(%d+)')\n" + " if id then\n" + " local name = GetSpellInfo(tonumber(id))\n" + " if name then self:SetText(name, 1, 1, 1) end\n" + " end\n" + "end\n" + "function GameTooltip:SetInventoryItem(unit, slot)\n" + " self:ClearLines()\n" + " if unit ~= 'player' then return false, false, 0 end\n" + " local link = GetInventoryItemLink(unit, slot)\n" + " if not link then return false, false, 0 end\n" + " local id = link:match('item:(%d+)')\n" + " if not id then return false, false, 0 end\n" + " local name, itemLink, quality, iLevel, reqLevel, class, subclass = GetItemInfo(tonumber(id))\n" + " if name then\n" + " local colors = {[0]={0.62,0.62,0.62},[1]={1,1,1},[2]={0.12,1,0},[3]={0,0.44,0.87},[4]={0.64,0.21,0.93},[5]={1,0.5,0},[6]={0.9,0.8,0.5}}\n" + " local c = colors[quality or 1] or {1,1,1}\n" + " self:SetText(name, c[1], c[2], c[3])\n" + " if class and class ~= '' then self:AddLine(class, 1, 1, 1) end\n" + " self.__itemId = tonumber(id)\n" + " end\n" + " return true, false, 0\n" + "end\n" + "function GameTooltip:SetBagItem(bag, slot)\n" + " self:ClearLines()\n" + " local tex, count, locked, quality, readable, lootable, link = GetContainerItemInfo(bag, slot)\n" + " if not link then return end\n" + " local id = link:match('item:(%d+)')\n" + " if not id then return end\n" + " local name, itemLink, q = GetItemInfo(tonumber(id))\n" + " if name then\n" + " local colors = {[0]={0.62,0.62,0.62},[1]={1,1,1},[2]={0.12,1,0},[3]={0,0.44,0.87},[4]={0.64,0.21,0.93},[5]={1,0.5,0}}\n" + " local c = colors[q or 1] or {1,1,1}\n" + " self:SetText(name, c[1], c[2], c[3])\n" + " if count and count > 1 then self:AddLine('Count: '..count, 1, 1, 1) end\n" + " self.__itemId = tonumber(id)\n" + " end\n" + "end\n" + "function GameTooltip:SetSpellByID(spellId)\n" + " self:ClearLines()\n" + " if not spellId or spellId == 0 then return end\n" + " local name, rank, icon = GetSpellInfo(spellId)\n" + " if name then\n" + " self:SetText(name, 1, 1, 1)\n" + " if rank and rank ~= '' then self:AddLine(rank, 0.5, 0.5, 0.5) end\n" + " self.__spellId = spellId\n" + " end\n" + "end\n" + "function GameTooltip:SetAction(slot)\n" + " self:ClearLines()\n" + " if not slot then return end\n" + " local actionType, id = GetActionInfo(slot)\n" + " if actionType == 'spell' and id and id > 0 then\n" + " self:SetSpellByID(id)\n" + " elseif actionType == 'item' and id and id > 0 then\n" + " local name, _, quality = GetItemInfo(id)\n" + " if name then self:SetText(name, 1, 1, 1) end\n" + " end\n" + "end\n" "function GameTooltip:FadeOut() end\n" "function GameTooltip:SetFrameStrata(...) end\n" "function GameTooltip:SetClampedToScreen(...) end\n" @@ -4595,11 +4688,8 @@ void LuaEngine::registerCoreAPI() { "function securecall(fn, ...) if type(fn)=='function' then return fn(...) end end\n" "function issecurevariable(...) return false end\n" "function issecure() return false end\n" - // CVar stubs (many addons check settings) - "local _cvars = {}\n" - "function GetCVar(name) return _cvars[name] or '0' end\n" - "function GetCVarBool(name) return _cvars[name] == '1' end\n" - "function SetCVar(name, value) _cvars[name] = tostring(value) end\n" + // GetCVarBool wraps C-side GetCVar (registered in table) for boolean queries + "function GetCVarBool(name) return GetCVar(name) == '1' end\n" // Misc compatibility stubs // GetScreenWidth, GetScreenHeight, GetNumLootItems are now C functions // GetFramerate is now a C function From b6047cdce8f8402d305936e0a3744f6b12072222 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 16:18:52 -0700 Subject: [PATCH 17/18] feat: add world map navigation API for WorldMapFrame compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the core map functions needed by WorldMapFrame.lua: - SetMapToCurrentZone — sets map view from player's current mapId/zone - GetCurrentMapContinent — returns continent (1=Kalimdor, 2=EK, etc.) - GetCurrentMapZone — returns current zone ID - SetMapZoom(continent, zone) — navigate map view - GetMapContinents — returns continent name list - GetMapZones(continent) — returns zone names per continent - GetNumMapLandmarks — stub (returns 0) Maps game mapId (0=EK, 1=Kalimdor, 530=Outland, 571=Northrend) to WoW's continent numbering. Internal state tracks which continent/zone the map UI is currently viewing. --- src/addons/lua_engine.cpp | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index e2505c4e..84146b84 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -1857,6 +1857,100 @@ static int lua_GetMinimapZoneText(lua_State* L) { return lua_GetZoneText(L); } +// --- World Map Navigation API --- + +// Map ID → continent mapping +static int mapIdToContinent(uint32_t mapId) { + switch (mapId) { + case 0: return 2; // Eastern Kingdoms + case 1: return 1; // Kalimdor + case 530: return 3; // Outland + case 571: return 4; // Northrend + default: return 0; // Instance or unknown + } +} + +// Internal tracked map state (which continent/zone the map UI is viewing) +static int s_mapContinent = 0; +static int s_mapZone = 0; + +// SetMapToCurrentZone() — sets map view to the player's current zone +static int lua_SetMapToCurrentZone(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) { + s_mapContinent = mapIdToContinent(gh->getCurrentMapId()); + s_mapZone = static_cast(gh->getWorldStateZoneId()); + } + return 0; +} + +// GetCurrentMapContinent() → continentId (1=Kalimdor, 2=EK, 3=Outland, 4=Northrend) +static int lua_GetCurrentMapContinent(lua_State* L) { + if (s_mapContinent == 0) { + auto* gh = getGameHandler(L); + if (gh) s_mapContinent = mapIdToContinent(gh->getCurrentMapId()); + } + lua_pushnumber(L, s_mapContinent); + return 1; +} + +// GetCurrentMapZone() → zoneId +static int lua_GetCurrentMapZone(lua_State* L) { + if (s_mapZone == 0) { + auto* gh = getGameHandler(L); + if (gh) s_mapZone = static_cast(gh->getWorldStateZoneId()); + } + lua_pushnumber(L, s_mapZone); + return 1; +} + +// SetMapZoom(continent [, zone]) — sets map view to continent/zone +static int lua_SetMapZoom(lua_State* L) { + s_mapContinent = static_cast(luaL_checknumber(L, 1)); + s_mapZone = static_cast(luaL_optnumber(L, 2, 0)); + return 0; +} + +// GetMapContinents() → "Kalimdor", "Eastern Kingdoms", ... +static int lua_GetMapContinents(lua_State* L) { + lua_pushstring(L, "Kalimdor"); + lua_pushstring(L, "Eastern Kingdoms"); + lua_pushstring(L, "Outland"); + lua_pushstring(L, "Northrend"); + return 4; +} + +// GetMapZones(continent) → zone names for that continent +// Returns a basic list; addons mainly need this to not error +static int lua_GetMapZones(lua_State* L) { + int cont = static_cast(luaL_checknumber(L, 1)); + // Return a minimal representative set per continent + switch (cont) { + case 1: // Kalimdor + lua_pushstring(L, "Durotar"); lua_pushstring(L, "Mulgore"); + lua_pushstring(L, "The Barrens"); lua_pushstring(L, "Teldrassil"); + return 4; + case 2: // Eastern Kingdoms + lua_pushstring(L, "Elwynn Forest"); lua_pushstring(L, "Westfall"); + lua_pushstring(L, "Dun Morogh"); lua_pushstring(L, "Tirisfal Glades"); + return 4; + case 3: // Outland + lua_pushstring(L, "Hellfire Peninsula"); lua_pushstring(L, "Zangarmarsh"); + return 2; + case 4: // Northrend + lua_pushstring(L, "Borean Tundra"); lua_pushstring(L, "Howling Fjord"); + return 2; + default: + return 0; + } +} + +// GetNumMapLandmarks() → 0 (no landmark data exposed yet) +static int lua_GetNumMapLandmarks(lua_State* L) { + lua_pushnumber(L, 0); + return 1; +} + // --- Player State API --- // These replace the hardcoded "return false" Lua stubs with real game state. @@ -4167,6 +4261,13 @@ void LuaEngine::registerCoreAPI() { {"GetLocale", lua_GetLocale}, {"GetBuildInfo", lua_GetBuildInfo}, {"GetCurrentMapAreaID", lua_GetCurrentMapAreaID}, + {"SetMapToCurrentZone", lua_SetMapToCurrentZone}, + {"GetCurrentMapContinent", lua_GetCurrentMapContinent}, + {"GetCurrentMapZone", lua_GetCurrentMapZone}, + {"SetMapZoom", lua_SetMapZoom}, + {"GetMapContinents", lua_GetMapContinents}, + {"GetMapZones", lua_GetMapZones}, + {"GetNumMapLandmarks", lua_GetNumMapLandmarks}, {"GetZoneText", lua_GetZoneText}, {"GetRealZoneText", lua_GetZoneText}, {"GetSubZoneText", lua_GetSubZoneText}, From be4cbad0b008e5114ff761ab97806fc01443de51 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Mar 2026 16:25:32 -0700 Subject: [PATCH 18/18] fix: unify lava UV scroll timer across render passes to prevent flicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lava M2 models used independent static-local start times in pass 1 and pass 2 for UV scroll animation. Since static locals initialize on first call, the two timers started at slightly different times (microseconds to frames apart), causing a permanent UV offset mismatch between passes — visible as texture flicker/jumping on lava surfaces. Replace both function-scoped statics with a single file-scoped kLavaAnimStart constant, ensuring both passes compute identical UV offsets from the same epoch. --- src/rendering/m2_renderer.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index b4bfa439..654717ab 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -30,6 +30,9 @@ namespace rendering { namespace { +// Shared lava UV scroll timer — ensures consistent animation across all render passes +const auto kLavaAnimStart = std::chrono::steady_clock::now(); + bool envFlagEnabled(const char* key, bool defaultValue) { const char* raw = std::getenv(key); if (!raw || !*raw) return defaultValue; @@ -2765,10 +2768,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const } } } - // Lava M2 models: fallback UV scroll if no texture animation + // Lava M2 models: fallback UV scroll if no texture animation. + // Uses kLavaAnimStart (file-scope) for consistent timing across passes. if (model.isLavaModel && uvOffset == glm::vec2(0.0f)) { - static auto startTime = std::chrono::steady_clock::now(); - float t = std::chrono::duration(std::chrono::steady_clock::now() - startTime).count(); + float t = std::chrono::duration(std::chrono::steady_clock::now() - kLavaAnimStart).count(); uvOffset = glm::vec2(t * 0.03f, -t * 0.08f); } @@ -2981,8 +2984,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const } } if (model.isLavaModel && uvOffset == glm::vec2(0.0f)) { - static auto startTime2 = std::chrono::steady_clock::now(); - float t = std::chrono::duration(std::chrono::steady_clock::now() - startTime2).count(); + float t = std::chrono::duration(std::chrono::steady_clock::now() - kLavaAnimStart).count(); uvOffset = glm::vec2(t * 0.03f, -t * 0.08f); }