Compare commits

...

19 commits

Author SHA1 Message Date
Kelsi
be4cbad0b0 fix: unify lava UV scroll timer across render passes to prevent flicker
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Lava M2 models used independent static-local start times in pass 1
and pass 2 for UV scroll animation. Since static locals initialize
on first call, the two timers started at slightly different times
(microseconds to frames apart), causing a permanent UV offset mismatch
between passes — visible as texture flicker/jumping on lava surfaces.

Replace both function-scoped statics with a single file-scoped
kLavaAnimStart constant, ensuring both passes compute identical UV
offsets from the same epoch.
2026-03-22 16:25:32 -07:00
Kelsi
b6047cdce8 feat: add world map navigation API for WorldMapFrame compatibility
Implement the core map functions needed by WorldMapFrame.lua:
- SetMapToCurrentZone — sets map view from player's current mapId/zone
- GetCurrentMapContinent — returns continent (1=Kalimdor, 2=EK, etc.)
- GetCurrentMapZone — returns current zone ID
- SetMapZoom(continent, zone) — navigate map view
- GetMapContinents — returns continent name list
- GetMapZones(continent) — returns zone names per continent
- GetNumMapLandmarks — stub (returns 0)

Maps game mapId (0=EK, 1=Kalimdor, 530=Outland, 571=Northrend) to
WoW's continent numbering. Internal state tracks which continent/zone
the map UI is currently viewing.
2026-03-22 16:18:52 -07:00
Kelsi
f9856c1046 feat: implement GameTooltip methods with real item/spell/aura data
Replace empty stub GameTooltip methods with working implementations:

- SetInventoryItem(unit, slot): populates tooltip with equipped item
  name and quality-colored text via GetInventoryItemLink + GetItemInfo
- SetBagItem(bag, slot): populates from GetContainerItemInfo + GetItemInfo
- SetSpellByID(spellId): populates with spell name/rank from GetSpellInfo
- SetAction(slot): delegates to SetSpellByID or item lookup via GetActionInfo
- SetUnitBuff/SetUnitDebuff: populates from UnitBuff/UnitDebuff data
- SetHyperlink: parses item: and spell: links to populate name
- GetItem/GetSpell: now return real item/spell data when tooltip is populated

Also fix GetCVar/SetCVar conflict — remove Lua-side overrides that
were shadowing the C-side implementations (which return real screen
dimensions and sensible defaults for common CVars).
2026-03-22 16:13:39 -07:00
Kelsi
31ab76427f fix: remove dead duplicate ufNpcFlags check and add missing UNIT_MODEL_CHANGED events
In the CREATE update block, the ufNpcFlags check at the end of the
else-if chain was unreachable dead code — it was already handled
earlier in the same chain. Remove the duplicate.

In the VALUES update block, mount display changes via field updates
fired mountCallback_ but not the UNIT_MODEL_CHANGED addon event,
unlike the CREATE path which fired both. Add the missing event so
Lua addons are notified when the player mounts/dismounts via VALUES
updates (the common case for aura-based mounting).

Also fire UNIT_MODEL_CHANGED for target/focus/pet display ID changes
in the VALUES displayIdChanged path, matching the CREATE path behavior.
2026-03-22 16:09:57 -07:00
Kelsi
cbdf03c07e feat: add quest objective leaderboard API for WatchFrame quest tracking
Implement GetNumQuestLeaderBoards and GetQuestLogLeaderBoard — the core
functions WatchFrame.lua and QuestLogFrame.lua use to display objective
progress like "Kobold Vermin slain: 3/8" or "Linen Cloth: 2/6".

GetNumQuestLeaderBoards counts kill + item objectives from the parsed
SMSG_QUEST_QUERY_RESPONSE data. GetQuestLogLeaderBoard returns the
formatted progress text, type ("monster"/"item"/"object"), and
completion status for each objective.

Also adds ExpandQuestHeader/CollapseQuestHeader (no-ops for flat quest
list) and GetQuestLogSpecialItemInfo stub.
2026-03-22 16:04:33 -07:00
Kelsi
296121f5e7 feat: add GetPlayerFacing, GetCVar/SetCVar for minimap and addon settings
GetPlayerFacing() returns player orientation in radians, needed by
minimap addons for arrow rotation and facing-dependent mechanics.

GetCVar(name) returns sensible defaults for commonly queried CVars
(uiScale, screen dimensions, nameplate visibility, sound toggles,
autoLoot). SetCVar is a no-op stub for addon compatibility.
2026-03-22 15:58:45 -07:00
Kelsi
73ce601bb5 feat: fire PLAYER_ENTERING_WORLD and critical login events for addons
PLAYER_ENTERING_WORLD is the single most important WoW addon event —
virtually every addon registers for it to initialize UI, state, and
data structures. It was never fired, causing widespread addon init
failures on login and after teleports.

Now fired from:
- handleLoginVerifyWorld (initial login + same-map teleports)
- handleNewWorld (cross-map teleports, instance transitions)

Also fires:
- PLAYER_LOGIN on initial world entry only
- ZONE_CHANGED_NEW_AREA on all world entries
- UPDATE_WORLD_STATES on initial entry
- SPELLS_CHANGED + LEARNED_SPELL_IN_TAB after SMSG_INITIAL_SPELLS
  (so spell book addons can initialize on login)
2026-03-22 15:50:05 -07:00
Kelsi
5086520354 feat: add spell book tab API for SpellBookFrame addon compatibility
Implement GetNumSpellTabs, GetSpellTabInfo, GetSpellBookItemInfo, and
GetSpellBookItemName — the core functions SpellBookFrame.lua needs to
organize known spells into class skill line tabs.

Tabs are built lazily from knownSpells grouped by SkillLineAbility.dbc
mappings (category 7 = class). A "General" tab collects spells not in
any class skill line. Tabs auto-rebuild when the spell count changes.

Also adds SpellBookTab struct and getSpellBookTabs() to GameHandler.
2026-03-22 15:40:40 -07:00
Kelsi
f29ebbdd71 feat: add quest watch/tracking and selection Lua API for WatchFrame
Implement the quest tracking functions needed by WatchFrame.lua:
- SelectQuestLogEntry/GetQuestLogSelection — quest log selection state
- GetNumQuestWatches — count of tracked quests
- GetQuestIndexForWatch(watchIdx) — map Nth watched quest to log index
- AddQuestWatch/RemoveQuestWatch — toggle quest tracking by log index
- IsQuestWatched — check if a quest log entry is tracked
- GetQuestLink — generate colored quest link string

Backed by existing trackedQuestIds_ set and questLog_ vector.
Adds selectedQuestLogIndex_ state to GameHandler for quest selection.
2026-03-22 15:36:25 -07:00
Kelsi
6d72228f66 feat: add GetInventorySlotInfo for PaperDollFrame and BankFrame
Maps WoW equipment slot names (e.g. "HeadSlot", "MainHandSlot") to
inventory slot IDs, empty-slot textures, and relic check flags.
Supports case-insensitive matching with optional "Slot" suffix stripping.

Unblocks PaperDollFrame.lua and BankFrame.lua which call this function
to resolve slot button IDs during UI initialization.
2026-03-22 15:30:53 -07:00
Kelsi
ab8ff6b7e5 feat: add UnitStat and combat chance Lua API for character sheet addons
Expose server-authoritative player stats to Lua addons:

- UnitStat(unit, statIndex) — returns STR/AGI/STA/INT/SPI (base,
  effective, posBuff, negBuff) matching the WoW API 4-return signature
- GetDodgeChance, GetParryChance, GetBlockChance — defensive stats
- GetCritChance, GetRangedCritChance — physical crit percentages
- GetSpellCritChance(school) — per-school spell crit
- GetCombatRating(index) — WotLK combat rating system
- GetSpellBonusDamage, GetSpellBonusHealing — caster stat display
- GetAttackPowerForStat, GetRangedAttackPower — melee/ranged AP

All data is already tracked from SMSG_UPDATE_OBJECT field updates;
these functions simply expose existing GameHandler getters to Lua.
Enables PaperDollFrame, DejaCharacterStats, and similar addons.
2026-03-22 15:25:20 -07:00
Kelsi
e9ce062112 fix: restore correct CharSections.dbc field indices for character textures
PR #19 (572bb4ef) swapped CharSections.dbc field indices, placing
Texture1-3 at fields 4-6 and VariationIndex/ColorIndex at 8-9. Binary
analysis of the actual DBC files (Classic, TBC, Turtle — all identical
layout, no WotLK-specific override) confirms the correct order is:

  Field 4 = VariationIndex
  Field 5 = ColorIndex
  Field 6 = Texture1 (string)
  Field 7 = Texture2 (string)
  Field 8 = Texture3 (string)
  Field 9 = Flags

With the wrong indices, VariationIndex/ColorIndex reads returned string
offsets (garbage values that never matched), so all CharSections lookups
failed silently — producing white untextured character models at the
login screen and in-world.

Fixes all 4 expansion JSON layouts, hardcoded fallbacks in
character_preview.cpp, application.cpp, and character_create_screen.cpp.
Also handles the single-layer edge case (body skin only, no face/underwear)
by loading the texture directly instead of skipping compositing.
2026-03-22 15:22:25 -07:00
Kelsi
329a1f4b12 feat: add IsActionInRange, GetActionInfo, and GetActionCount Lua API
IsActionInRange(slot) checks if the spell on an action bar slot is within
range of the current target, using DBC spell range data and entity positions.
Returns 1/0/nil matching the WoW API contract.

GetActionInfo(slot) returns action type ("spell"/"item"/"macro"), id, and
subType for action bar interrogation by bar addons.

GetActionCount(slot) returns item stack count across backpack and bags for
consumable tracking on action bars.
2026-03-22 15:11:29 -07:00
Kelsi
ce4f93dfcb feat: add UnitCastingInfo/UnitChannelInfo Lua API and fix SMSG_CAST_FAILED events
Expose cast/channel state to Lua addons via UnitCastingInfo(unit) and
UnitChannelInfo(unit), matching the WoW API signature (name, text, texture,
startTime, endTime, isTradeSkill, castID, notInterruptible). Works for
player, target, focus, and pet units using existing UnitCastState tracking.

Also fix handleCastFailed (SMSG_CAST_FAILED, Classic/TBC path) to fire
UNIT_SPELLCAST_FAILED and UNIT_SPELLCAST_STOP events — previously only
the WotLK SMSG_CAST_RESULT path fired these, leaving Classic/TBC addons
unaware of cast failures.

Adds isChannel field to UnitCastState and getCastTimeTotal() accessor.
2026-03-22 15:05:29 -07:00
Kelsi Rae Davis
1482694495
Merge pull request #19 from ldmonster/fix/wotlk-render
fix/wotlk-render: WotLK rendering stability, Intel GPU compatibility & terrain OOM fixes
2026-03-22 13:40:10 -07:00
Paul
027640189a make start on ubuntu intel video cards 2026-03-22 21:47:12 +03:00
Paul
7565019dc9 log falling 2026-03-22 21:40:16 +03:00
Paul
bd725f0bbe build fix 2026-03-22 21:39:40 +03:00
Paul
572bb4ef36 fix preview white textutes 2026-03-22 21:38:56 +03:00
18 changed files with 2195 additions and 349 deletions

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -830,6 +830,14 @@ public:
void togglePetSpellAutocast(uint32_t spellId);
const std::unordered_set<uint32_t>& 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<uint32_t> spellIds; // spells in this tab
};
const std::vector<SpellBookTab>& 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<QuestRewardItem, 6> rewardChoiceItems{}; // player picks one of these
};
const std::vector<QuestLogEntry>& 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<QuestLogEntry> questLog_;
int selectedQuestLogIndex_ = 0;
std::unordered_set<uint32_t> pendingQuestQueryIds_;
std::unordered_set<uint32_t> trackedQuestIds_;
bool pendingLoginQuestResync_ = false;
@ -3438,6 +3451,8 @@ private:
std::unordered_map<uint32_t, std::string> skillLineNames_;
std::unordered_map<uint32_t, uint32_t> skillLineCategories_;
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
std::vector<SpellBookTab> spellBookTabs_;
bool spellBookTabsDirty_ = true;
bool skillLineDbcLoaded_ = false;
bool skillLineAbilityLoaded_ = false;
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;

View file

@ -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,
};
/**

View file

@ -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;

View file

@ -394,6 +394,11 @@ private:
std::unordered_set<uint32_t> 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<uint32_t> preparedWmoUniqueIds_;
std::mutex preparedWmoUniqueIdsMutex_;
// Dedup set for doodad placements across tile boundaries
std::unordered_set<uint32_t> placedDoodadIds;

File diff suppressed because it is too large Load diff

View file

@ -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<uint32_t>(extraCopy.hairStyleId)) continue;
if (colorIdx != static_cast<uint32_t>(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);

View file

@ -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::SpellBookTab>& 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<uint32_t, std::vector<uint32_t>> bySkillLine;
std::vector<uint32_t> 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<std::pair<std::string, std::vector<uint32_t>>> 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", {});
}
}
// ============================================================

View file

@ -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');

View file

@ -74,8 +74,7 @@ bool WardenModule::load(const std::vector<uint8_t>& 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<uint8_t>& moduleData,
return true;
}
bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
bool WardenModule::processCheckRequest([[maybe_unused]] const std::vector<uint8_t>& checkData,
[[maybe_unused]] std::vector<uint8_t>& responseOut) {
if (!loaded_) {
LOG_ERROR("WardenModule: Module not loaded, cannot process checks");
@ -427,12 +426,11 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& 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<uint8_t>& 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;
}

View file

@ -832,7 +832,7 @@ void MovementPacket::writeMovementPayload(network::Packet& packet, const Movemen
packet.writeUInt8(static_cast<uint8_t>(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();
}

View file

@ -8,6 +8,8 @@
#include <SDL2/SDL.h>
#ifdef __linux__
#include <X11/Xlib.h>
#include <execinfo.h>
#include <unistd.h>
// 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);
}

View file

@ -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<uint32_t>(ti), skinTex);
break;
}
}
}
}
}

View file

@ -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, &currentModel->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<float>(std::chrono::steady_clock::now() - startTime).count();
float t = std::chrono::duration<float>(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, &currentModel->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<float>(std::chrono::steady_clock::now() - startTime2).count();
float t = std::chrono::duration<float>(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();

View file

@ -562,7 +562,17 @@ std::shared_ptr<PendingTile> 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<std::mutex> 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<PendingTile> TerrainManager::prepareTile(int x, int y) {
setsToLoad.push_back(placement.doodadSet);
}
std::unordered_set<uint32_t> loadedDoodadIndices;
std::unordered_set<uint32_t> 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<PendingTile> TerrainManager::prepareTile(int x, int y) {
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(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<std::mutex> lock(uploadedM2IdsMutex_);
modelAlreadyUploaded = uploadedM2Ids_.count(doodadModelId) > 0;
}
bool modelAlreadyPreparedInWmo = !wmoPreparedModelIds.insert(doodadModelId).second;
pipeline::M2Model m2Model;
if (!modelAlreadyUploaded) {
if (!modelAlreadyUploaded && !modelAlreadyPreparedInWmo) {
std::vector<uint8_t> 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<std::mutex> 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<std::mutex> lock(preparedWmoUniqueIdsMutex_);
preparedWmoUniqueIds_.erase(uid);
}
// Remove M2 doodad instances
@ -1509,6 +1527,10 @@ void TerrainManager::unloadAll() {
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
uploadedM2Ids_.clear();
}
{
std::lock_guard<std::mutex> lock(preparedWmoUniqueIdsMutex_);
preparedWmoUniqueIds_.clear();
}
LOG_INFO("Unloading all terrain tiles");
loadedTiles.clear();
@ -1561,6 +1583,10 @@ void TerrainManager::softReset() {
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
uploadedM2Ids_.clear();
}
{
std::lock_guard<std::mutex> 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.