diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json index e5d0793f..ae75e254 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, + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, "Flags": 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..8142434e 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, + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, "Flags": 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..42839fc6 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, + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, "Flags": 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..5a05a517 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, + "RaceID": 1, + "SexID": 2, + "BaseSection": 3, + "VariationIndex": 4, + "ColorIndex": 5, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, "Flags": 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/include/game/game_handler.hpp b/include/game/game_handler.hpp index d67bd0b2..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) @@ -882,6 +890,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 +905,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; @@ -1672,6 +1682,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); @@ -3185,6 +3197,7 @@ private: // Quest log std::vector questLog_; + int selectedQuestLogIndex_ = 0; std::unordered_set pendingQuestQueryIds_; std::unordered_set trackedQuestIds_; bool pendingLoginQuestResync_ = false; @@ -3438,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/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/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 6148ddd0..84146b84 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()); @@ -796,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; } @@ -1077,6 +1234,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) { @@ -1205,6 +1440,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; } @@ -1542,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. @@ -1732,6 +2141,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"); @@ -1914,6 +2368,188 @@ 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; +} + +// 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 @@ -2841,6 +3477,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); @@ -3452,15 +4197,34 @@ 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}, + {"GetPlayerFacing", lua_GetPlayerFacing}, + {"GetCVar", lua_GetCVar}, + {"SetCVar", lua_SetCVar}, {"SendChatMessage", lua_SendChatMessage}, {"SendAddonMessage", lua_SendAddonMessage}, {"RegisterAddonMessagePrefix", lua_RegisterAddonMessagePrefix}, {"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}, @@ -3486,6 +4250,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}, @@ -3495,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}, @@ -3531,6 +4304,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}, @@ -3546,6 +4320,19 @@ 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}, + {"GetNumQuestLeaderBoards", lua_GetNumQuestLeaderBoards}, + {"GetQuestLogLeaderBoard", lua_GetQuestLogLeaderBoard}, + {"ExpandQuestHeader", lua_ExpandQuestHeader}, + {"CollapseQuestHeader", lua_CollapseQuestHeader}, + {"GetQuestLogSpecialItemInfo", lua_GetQuestLogSpecialItemInfo}, // Skill line API {"GetNumSkillLines", lua_GetNumSkillLines}, {"GetSkillLineInfo", lua_GetSkillLineInfo}, @@ -3575,6 +4362,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}, @@ -3869,6 +4659,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" @@ -3878,18 +4669,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" @@ -3905,11 +4789,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 diff --git a/src/core/application.cpp b/src/core/application.cpp index c5e7dccb..49c40976 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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/game_handler.cpp b/src/game/game_handler.cpp index f81f0ef0..b908245b 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; @@ -9743,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) { @@ -12025,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; @@ -12573,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) { @@ -12718,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 @@ -19317,6 +19342,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) { @@ -19363,6 +19394,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 +19421,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; @@ -23024,6 +23063,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(); @@ -23570,6 +23665,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", {}); + } } // ============================================================ 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; } 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/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); } diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 2cb6278e..306509ed 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -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/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index f711f542..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; @@ -366,6 +369,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 +784,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 +853,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 +867,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 +1352,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 +1804,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 +2558,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 +2574,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 +2589,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 +2603,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; @@ -2697,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); } @@ -2785,7 +2856,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 +2869,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 +2888,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; @@ -2910,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); } @@ -4168,6 +4241,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.