diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6893f717..0a3c81d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: branches: [master] env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true WOWEE_AMD_FSR2_REPO: https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git WOWEE_AMD_FSR2_REF: master WOWEE_FFX_SDK_REPO: https://github.com/Kelsidavis/FidelityFX-SDK.git diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0698607a..34917dd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,9 @@ on: push: tags: ['v*'] +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + permissions: contents: write diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 4709225b..6e5f63af 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -7,6 +7,9 @@ on: branches: [master] workflow_dispatch: +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + permissions: contents: read diff --git a/.gitignore b/.gitignore index 4ddf59ab..e4348ceb 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ extern/* !extern/imgui !extern/vk-bootstrap !extern/vk_mem_alloc.h +!extern/lua-5.1.5 # ImGui state imgui.ini @@ -100,3 +101,10 @@ node_modules/ # Python cache artifacts tools/__pycache__/ *.pyc + +# artifacts +.codex-loop/ + +# Local agent instructions +AGENTS.md +codex-loop.sh diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000..eb36847a --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,8 @@ +# Vendored third-party code (frozen releases, not ours to modify) +extern/lua-5.1.5/ +extern/imgui/ +extern/stb_image.h +extern/stb_image_write.h +extern/vk-bootstrap/ +extern/FidelityFX-FSR2/ +extern/FidelityFX-SDK/ diff --git a/CMakeLists.txt b/CMakeLists.txt index e4c37e70..219b88ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(wowee VERSION 1.0.0 LANGUAGES CXX) +project(wowee VERSION 1.0.0 LANGUAGES C CXX) include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 20) @@ -529,6 +529,7 @@ set(WOWEE_SOURCES src/rendering/character_preview.cpp src/rendering/wmo_renderer.cpp src/rendering/m2_renderer.cpp + src/rendering/m2_model_classifier.cpp src/rendering/quest_marker_renderer.cpp src/rendering/minimap.cpp src/rendering/world_map.cpp @@ -552,6 +553,11 @@ set(WOWEE_SOURCES src/ui/talent_screen.cpp src/ui/keybinding_manager.cpp + # Addons + src/addons/addon_manager.cpp + src/addons/lua_engine.cpp + src/addons/toc_parser.cpp + # Main src/main.cpp ) @@ -668,6 +674,27 @@ if(WIN32) list(APPEND WOWEE_PLATFORM_SOURCES resources/wowee.rc) endif() +# ---- Lua 5.1.5 (vendored, static library) ---- +set(LUA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/lua-5.1.5/src) +set(LUA_SOURCES + ${LUA_DIR}/lapi.c ${LUA_DIR}/lcode.c ${LUA_DIR}/ldebug.c + ${LUA_DIR}/ldo.c ${LUA_DIR}/ldump.c ${LUA_DIR}/lfunc.c + ${LUA_DIR}/lgc.c ${LUA_DIR}/llex.c ${LUA_DIR}/lmem.c + ${LUA_DIR}/lobject.c ${LUA_DIR}/lopcodes.c ${LUA_DIR}/lparser.c + ${LUA_DIR}/lstate.c ${LUA_DIR}/lstring.c ${LUA_DIR}/ltable.c + ${LUA_DIR}/ltm.c ${LUA_DIR}/lundump.c ${LUA_DIR}/lvm.c + ${LUA_DIR}/lzio.c ${LUA_DIR}/lauxlib.c ${LUA_DIR}/lbaselib.c + ${LUA_DIR}/ldblib.c ${LUA_DIR}/liolib.c ${LUA_DIR}/lmathlib.c + ${LUA_DIR}/loslib.c ${LUA_DIR}/ltablib.c ${LUA_DIR}/lstrlib.c + ${LUA_DIR}/linit.c +) +add_library(lua51 STATIC ${LUA_SOURCES}) +set_target_properties(lua51 PROPERTIES LINKER_LANGUAGE C C_STANDARD 99 POSITION_INDEPENDENT_CODE ON) +target_include_directories(lua51 PUBLIC ${LUA_DIR}) +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(lua51 PRIVATE -w) +endif() + # Create executable add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS} ${WOWEE_PLATFORM_SOURCES}) if(TARGET opcodes-generate) @@ -709,6 +736,7 @@ target_link_libraries(wowee PRIVATE OpenSSL::Crypto Threads::Threads ZLIB::ZLIB + lua51 ${CMAKE_DL_LIBS} ) diff --git a/Data/expansions/classic/dbc_layouts.json b/Data/expansions/classic/dbc_layouts.json index ca8c8a50..459c9046 100644 --- a/Data/expansions/classic/dbc_layouts.json +++ b/Data/expansions/classic/dbc_layouts.json @@ -1,98 +1,260 @@ { - "Spell": { - "ID": 0, "Attributes": 5, "IconID": 117, - "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33 + "AreaTable": { + "ExploreFlag": 3, + "ID": 0, + "MapID": 1, + "ParentAreaNum": 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 + "CharHairGeosets": { + "GeosetID": 4, + "RaceID": 1, + "SexID": 2, + "Variation": 3 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 - }, - "SpellIcon": { "ID": 0, "Path": 1 }, - "FactionTemplate": { - "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 - }, - "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 - }, - "CreatureDisplayInfo": { - "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 - }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, - "TaxiPathNode": { - "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 - }, - "Talent": { - "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 }, - "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "BaseSection": 3, + "ColorIndex": 5, + "Flags": 9, + "RaceID": 1, + "SexID": 2, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "VariationIndex": 4 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "Geoset100": 3, + "Geoset200": 5, + "Geoset300": 4, + "RaceID": 0, + "SexID": 1, + "Variation": 2 + }, + "CreatureDisplayInfo": { + "ExtraDisplayId": 3, + "ID": 0, + "ModelID": 1, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 + }, + "CreatureDisplayInfoExtra": { + "BakeName": 20, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay10": 18, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "FaceID": 4, + "FacialHairID": 7, + "HairColorID": 6, + "HairStyleID": 5, + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 + }, + "Emotes": { + "AnimID": 2, + "ID": 0 }, - "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 + "Command": 1, + "EmoteRef": 2, + "ID": 0, + "OthersNoTargetTextID": 7, + "OthersTargetTextID": 3, + "SenderNoTargetTextID": 9, + "SenderTargetTextID": 5 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 + }, + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 + }, + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "ItemDisplayInfo": { + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "ID": 0, + "InventoryIcon": 5, + "LeftModel": 1, + "LeftModelTexture": 3, + "TextureArmLower": 15, + "TextureArmUpper": 14, + "TextureFoot": 21, + "TextureHand": 16, + "TextureLegLower": 20, + "TextureLegUpper": 19, + "TextureTorsoLower": 18, + "TextureTorsoUpper": 17 }, - "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 - }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "ID": 0, + "InnerRadius": 5, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9, + "MapID": 1, + "OuterRadius": 6, + "X": 2, + "Y": 4, + "Z": 3 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightIntBand": { + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightParams": { + "LightParamsID": 0 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "SkillLine": { + "Category": 1, + "ID": 0, + "Name": 3 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "Spell": { + "Attributes": 5, + "AttributesEx": 6, + "CastingTimeIndex": 15, + "DispelType": 4, + "DurationIndex": 40, + "EffectBasePoints0": 80, + "EffectBasePoints1": 81, + "EffectBasePoints2": 82, + "ID": 0, + "IconID": 117, + "ManaCost": 29, + "Name": 120, + "PowerType": 28, + "RangeIndex": 33, + "Rank": 129, + "SchoolEnum": 1, + "Tooltip": 147 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 + }, + "SpellRange": { + "MaxRange": 2 + }, + "SpellVisual": { + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 + }, + "SpellVisualEffectName": { + "FilePath": 2, + "ID": 0 + }, + "SpellVisualKit": { + "BaseEffect": 5, + "ID": 0, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 + }, + "Talent": { + "Column": 3, + "ID": 0, + "PrereqRank0": 12, + "PrereqTalent0": 9, + "RankSpell0": 4, + "Row": 2, + "TabID": 1 + }, + "TalentTab": { + "BackgroundFile": 15, + "ClassMask": 12, + "ID": 0, + "Name": 1, + "OrderIndex": 14 + }, + "TaxiNodes": { + "ID": 0, + "MapID": 1, + "Name": 5, + "X": 2, + "Y": 3, + "Z": 4 + }, + "TaxiPath": { + "Cost": 3, + "FromNode": 1, + "ID": 0, + "ToNode": 2 + }, + "TaxiPathNode": { + "ID": 0, + "MapID": 3, + "NodeIndex": 2, + "PathID": 1, + "X": 4, + "Y": 5, + "Z": 6 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "AreaID": 2, + "AreaName": 3, + "DisplayMapID": 8, + "ID": 0, + "LocBottom": 7, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "MapID": 1, + "ParentWorldMapID": 10 } } diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index b99e4223..760647d9 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -273,7 +273,7 @@ "SMSG_INVENTORY_CHANGE_FAILURE": "0x112", "SMSG_OPEN_CONTAINER": "0x113", "CMSG_INSPECT": "0x114", - "SMSG_INSPECT": "0x115", + "SMSG_INSPECT_RESULTS_UPDATE": "0x115", "CMSG_INITIATE_TRADE": "0x116", "CMSG_BEGIN_TRADE": "0x117", "CMSG_BUSY_TRADE": "0x118", @@ -300,7 +300,7 @@ "CMSG_NEW_SPELL_SLOT": "0x12D", "CMSG_CAST_SPELL": "0x12E", "CMSG_CANCEL_CAST": "0x12F", - "SMSG_CAST_RESULT": "0x130", + "SMSG_CAST_FAILED": "0x130", "SMSG_SPELL_START": "0x131", "SMSG_SPELL_GO": "0x132", "SMSG_SPELL_FAILURE": "0x133", @@ -504,8 +504,7 @@ "CMSG_GM_SET_SECURITY_GROUP": "0x1F9", "CMSG_GM_NUKE": "0x1FA", "MSG_RANDOM_ROLL": "0x1FB", - "SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC", - "CMSG_RWHOIS_OBSOLETE": "0x1FD", + "SMSG_ENVIRONMENTAL_DAMAGE_LOG": "0x1FC", "SMSG_RWHOIS": "0x1FE", "MSG_LOOKING_FOR_GROUP": "0x1FF", "CMSG_SET_LOOKING_FOR_GROUP": "0x200", @@ -528,7 +527,6 @@ "CMSG_GMTICKET_GETTICKET": "0x211", "SMSG_GMTICKET_GETTICKET": "0x212", "CMSG_UNLEARN_TALENTS": "0x213", - "SMSG_GAMEOBJECT_SPAWN_ANIM_OBSOLETE": "0x214", "SMSG_GAMEOBJECT_DESPAWN_ANIM": "0x215", "MSG_CORPSE_QUERY": "0x216", "CMSG_GMTICKET_DELETETICKET": "0x217", @@ -538,7 +536,7 @@ "SMSG_GMTICKET_SYSTEMSTATUS": "0x21B", "CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C", "CMSG_SET_STAT_CHEAT": "0x21D", - "SMSG_SET_REST_START": "0x21E", + "SMSG_QUEST_FORCE_REMOVE": "0x21E", "CMSG_SKILL_BUY_STEP": "0x21F", "CMSG_SKILL_BUY_RANK": "0x220", "CMSG_XP_CHEAT": "0x221", @@ -571,8 +569,6 @@ "CMSG_BATTLEFIELD_LIST": "0x23C", "SMSG_BATTLEFIELD_LIST": "0x23D", "CMSG_BATTLEFIELD_JOIN": "0x23E", - "SMSG_BATTLEFIELD_WIN_OBSOLETE": "0x23F", - "SMSG_BATTLEFIELD_LOSE_OBSOLETE": "0x240", "CMSG_TAXICLEARNODE": "0x241", "CMSG_TAXIENABLENODE": "0x242", "CMSG_ITEM_TEXT_QUERY": "0x243", @@ -605,7 +601,6 @@ "SMSG_AUCTION_BIDDER_NOTIFICATION": "0x25E", "SMSG_AUCTION_OWNER_NOTIFICATION": "0x25F", "SMSG_PROCRESIST": "0x260", - "SMSG_STANDSTATE_CHANGE_FAILURE_OBSOLETE": "0x261", "SMSG_DISPEL_FAILED": "0x262", "SMSG_SPELLORDAMAGE_IMMUNE": "0x263", "CMSG_AUCTION_LIST_BIDDER_ITEMS": "0x264", @@ -693,8 +688,8 @@ "SMSG_SCRIPT_MESSAGE": "0x2B6", "SMSG_DUEL_COUNTDOWN": "0x2B7", "SMSG_AREA_TRIGGER_MESSAGE": "0x2B8", - "CMSG_TOGGLE_HELM": "0x2B9", - "CMSG_TOGGLE_CLOAK": "0x2BA", + "CMSG_SHOWING_HELM": "0x2B9", + "CMSG_SHOWING_CLOAK": "0x2BA", "SMSG_MEETINGSTONE_JOINFAILED": "0x2BB", "SMSG_PLAYER_SKINNED": "0x2BC", "SMSG_DURABILITY_DAMAGE_DEATH": "0x2BD", @@ -821,6 +816,5 @@ "SMSG_LOTTERY_RESULT_OBSOLETE": "0x337", "SMSG_CHARACTER_PROFILE": "0x338", "SMSG_CHARACTER_PROFILE_REALM_CONNECTED": "0x339", - "SMSG_UNK": "0x33A", "SMSG_DEFENSE_MESSAGE": "0x33B" } diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json index bb269d8a..4a602e91 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -1,47 +1,49 @@ { + "CONTAINER_FIELD_NUM_SLOTS": 48, + "CONTAINER_FIELD_SLOT_1": 50, + "GAMEOBJECT_DISPLAYID": 8, + "ITEM_FIELD_DURABILITY": 48, + "ITEM_FIELD_MAXDURABILITY": 49, + "ITEM_FIELD_STACK_COUNT": 14, "OBJECT_FIELD_ENTRY": 3, "OBJECT_FIELD_SCALE_X": 4, - "UNIT_FIELD_TARGET_LO": 16, - "UNIT_FIELD_TARGET_HI": 17, + "PLAYER_BYTES": 191, + "PLAYER_BYTES_2": 192, + "PLAYER_END": 1282, + "PLAYER_EXPLORED_ZONES_START": 1111, + "PLAYER_FIELD_BANKBAG_SLOT_1": 612, + "PLAYER_FIELD_BANK_SLOT_1": 564, + "PLAYER_FIELD_COINAGE": 1176, + "PLAYER_FIELD_INV_SLOT_HEAD": 486, + "PLAYER_FIELD_PACK_SLOT_1": 532, + "PLAYER_FLAGS": 190, + "PLAYER_NEXT_LEVEL_XP": 717, + "PLAYER_QUEST_LOG_START": 198, + "PLAYER_REST_STATE_EXPERIENCE": 1175, + "PLAYER_SKILL_INFO_START": 718, + "PLAYER_XP": 716, + "UNIT_DYNAMIC_FLAGS": 143, + "UNIT_END": 188, + "UNIT_FIELD_AURAFLAGS": 98, + "UNIT_FIELD_AURAS": 50, "UNIT_FIELD_BYTES_0": 36, - "UNIT_FIELD_HEALTH": 22, - "UNIT_FIELD_POWER1": 23, - "UNIT_FIELD_MAXHEALTH": 28, - "UNIT_FIELD_MAXPOWER1": 29, - "UNIT_FIELD_LEVEL": 34, + "UNIT_FIELD_BYTES_1": 133, + "UNIT_FIELD_DISPLAYID": 131, "UNIT_FIELD_FACTIONTEMPLATE": 35, "UNIT_FIELD_FLAGS": 46, - "UNIT_FIELD_DISPLAYID": 131, + "UNIT_FIELD_HEALTH": 22, + "UNIT_FIELD_LEVEL": 34, + "UNIT_FIELD_MAXHEALTH": 28, + "UNIT_FIELD_MAXPOWER1": 29, "UNIT_FIELD_MOUNTDISPLAYID": 133, - "UNIT_FIELD_AURAS": 50, - "UNIT_NPC_FLAGS": 147, - "UNIT_DYNAMIC_FLAGS": 143, + "UNIT_FIELD_POWER1": 23, "UNIT_FIELD_RESISTANCES": 154, "UNIT_FIELD_STAT0": 138, "UNIT_FIELD_STAT1": 139, "UNIT_FIELD_STAT2": 140, "UNIT_FIELD_STAT3": 141, "UNIT_FIELD_STAT4": 142, - "UNIT_END": 188, - "PLAYER_FLAGS": 190, - "PLAYER_BYTES": 191, - "PLAYER_BYTES_2": 192, - "PLAYER_XP": 716, - "PLAYER_NEXT_LEVEL_XP": 717, - "PLAYER_REST_STATE_EXPERIENCE": 1175, - "PLAYER_FIELD_COINAGE": 1176, - "PLAYER_QUEST_LOG_START": 198, - "PLAYER_FIELD_INV_SLOT_HEAD": 486, - "PLAYER_FIELD_PACK_SLOT_1": 532, - "PLAYER_FIELD_BANK_SLOT_1": 564, - "PLAYER_FIELD_BANKBAG_SLOT_1": 612, - "PLAYER_SKILL_INFO_START": 718, - "PLAYER_EXPLORED_ZONES_START": 1111, - "PLAYER_END": 1282, - "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14, - "ITEM_FIELD_DURABILITY": 48, - "ITEM_FIELD_MAXDURABILITY": 49, - "CONTAINER_FIELD_NUM_SLOTS": 48, - "CONTAINER_FIELD_SLOT_1": 50 + "UNIT_FIELD_TARGET_HI": 17, + "UNIT_FIELD_TARGET_LO": 16, + "UNIT_NPC_FLAGS": 147 } diff --git a/Data/expansions/tbc/dbc_layouts.json b/Data/expansions/tbc/dbc_layouts.json index fdc9e07d..e11682cf 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -1,100 +1,307 @@ { - "Spell": { - "ID": 0, "Attributes": 5, "IconID": 124, - "Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215, - "CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40 + "AreaTable": { + "ExploreFlag": 3, + "ID": 0, + "MapID": 1, + "ParentAreaNum": 2 }, - "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 + "CharHairGeosets": { + "GeosetID": 4, + "RaceID": 1, + "SexID": 2, + "Variation": 3 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 + "BaseSection": 3, + "ColorIndex": 5, + "Flags": 9, + "RaceID": 1, + "SexID": 2, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "VariationIndex": 4 }, - "SpellIcon": { "ID": 0, "Path": 1 }, - "FactionTemplate": { - "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 - }, - "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 - }, - "CreatureDisplayInfo": { - "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 - }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, - "TaxiPathNode": { - "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 - }, - "Talent": { - "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 }, - "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "CharTitles": { + "ID": 0, + "Title": 2, + "TitleBit": 20 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "Geoset100": 3, + "Geoset200": 5, + "Geoset300": 4, + "RaceID": 0, + "SexID": 1, + "Variation": 2 + }, + "CreatureDisplayInfo": { + "ExtraDisplayId": 3, + "ID": 0, + "ModelID": 1, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 + }, + "CreatureDisplayInfoExtra": { + "BakeName": 20, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay10": 18, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "FaceID": 4, + "FacialHairID": 7, + "HairColorID": 6, + "HairStyleID": 5, + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 + }, + "Emotes": { + "AnimID": 2, + "ID": 0 }, - "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 + "Command": 1, + "EmoteRef": 2, + "ID": 0, + "OthersNoTargetTextID": 7, + "OthersTargetTextID": 3, + "SenderNoTargetTextID": 9, + "SenderTargetTextID": 5 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 + }, + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 + }, + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "ItemDisplayInfo": { + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "ID": 0, + "InventoryIcon": 5, + "LeftModel": 1, + "LeftModelTexture": 3, + "TextureArmLower": 15, + "TextureArmUpper": 14, + "TextureFoot": 21, + "TextureHand": 16, + "TextureLegLower": 20, + "TextureLegUpper": 19, + "TextureTorsoLower": 18, + "TextureTorsoUpper": 17 + }, + "ItemSet": { + "ID": 0, + "Item0": 18, + "Item1": 19, + "Item2": 20, + "Item3": 21, + "Item4": 22, + "Item5": 23, + "Item6": 24, + "Item7": 25, + "Item8": 26, + "Item9": 27, + "Name": 1, + "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 }, - "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 - }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "ID": 0, + "InnerRadius": 5, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9, + "MapID": 1, + "OuterRadius": 6, + "X": 2, + "Y": 4, + "Z": 3 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightIntBand": { + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightParams": { + "LightParamsID": 0 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "SkillLine": { + "Category": 1, + "ID": 0, + "Name": 3 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "Spell": { + "Attributes": 5, + "AttributesEx": 6, + "CastingTimeIndex": 22, + "DispelType": 3, + "DurationIndex": 40, + "EffectBasePoints0": 80, + "EffectBasePoints1": 81, + "EffectBasePoints2": 82, + "ID": 0, + "IconID": 124, + "ManaCost": 36, + "Name": 127, + "PowerType": 35, + "RangeIndex": 40, + "Rank": 136, + "SchoolMask": 215, + "Tooltip": 154 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 + }, + "SpellItemEnchantment": { + "ID": 0, + "Name": 8 + }, + "SpellRange": { + "MaxRange": 4 + }, + "SpellVisual": { + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 + }, + "SpellVisualEffectName": { + "FilePath": 2, + "ID": 0 + }, + "SpellVisualKit": { + "BaseEffect": 5, + "ID": 0, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 + }, + "Talent": { + "Column": 3, + "ID": 0, + "PrereqRank0": 12, + "PrereqTalent0": 9, + "RankSpell0": 4, + "Row": 2, + "TabID": 1 + }, + "TalentTab": { + "BackgroundFile": 15, + "ClassMask": 12, + "ID": 0, + "Name": 1, + "OrderIndex": 14 + }, + "TaxiNodes": { + "ID": 0, + "MapID": 1, + "MountDisplayIdAlliance": 14, + "MountDisplayIdAllianceFallback": 12, + "MountDisplayIdHorde": 15, + "MountDisplayIdHordeFallback": 13, + "Name": 5, + "X": 2, + "Y": 3, + "Z": 4 + }, + "TaxiPath": { + "Cost": 3, + "FromNode": 1, + "ID": 0, + "ToNode": 2 + }, + "TaxiPathNode": { + "ID": 0, + "MapID": 3, + "NodeIndex": 2, + "PathID": 1, + "X": 4, + "Y": 5, + "Z": 6 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "AreaID": 2, + "AreaName": 3, + "DisplayMapID": 8, + "ID": 0, + "LocBottom": 7, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "MapID": 1, + "ParentWorldMapID": 10 } } diff --git a/Data/expansions/tbc/update_fields.json b/Data/expansions/tbc/update_fields.json index 05e37180..471ac235 100644 --- a/Data/expansions/tbc/update_fields.json +++ b/Data/expansions/tbc/update_fields.json @@ -1,46 +1,49 @@ { + "CONTAINER_FIELD_NUM_SLOTS": 64, + "CONTAINER_FIELD_SLOT_1": 66, + "GAMEOBJECT_DISPLAYID": 8, + "ITEM_FIELD_DURABILITY": 60, + "ITEM_FIELD_MAXDURABILITY": 61, + "ITEM_FIELD_STACK_COUNT": 14, "OBJECT_FIELD_ENTRY": 3, "OBJECT_FIELD_SCALE_X": 4, - "UNIT_FIELD_TARGET_LO": 16, - "UNIT_FIELD_TARGET_HI": 17, + "PLAYER_BYTES": 237, + "PLAYER_BYTES_2": 238, + "PLAYER_EXPLORED_ZONES_START": 1312, + "PLAYER_FIELD_ARENA_CURRENCY": 1506, + "PLAYER_FIELD_BANKBAG_SLOT_1": 784, + "PLAYER_FIELD_BANK_SLOT_1": 728, + "PLAYER_FIELD_COINAGE": 1441, + "PLAYER_FIELD_HONOR_CURRENCY": 1505, + "PLAYER_FIELD_INV_SLOT_HEAD": 650, + "PLAYER_FIELD_PACK_SLOT_1": 696, + "PLAYER_FLAGS": 236, + "PLAYER_NEXT_LEVEL_XP": 927, + "PLAYER_QUEST_LOG_START": 244, + "PLAYER_REST_STATE_EXPERIENCE": 1440, + "PLAYER_SKILL_INFO_START": 928, + "PLAYER_XP": 926, + "UNIT_DYNAMIC_FLAGS": 164, + "UNIT_END": 234, "UNIT_FIELD_BYTES_0": 36, - "UNIT_FIELD_HEALTH": 22, - "UNIT_FIELD_POWER1": 23, - "UNIT_FIELD_MAXHEALTH": 28, - "UNIT_FIELD_MAXPOWER1": 29, - "UNIT_FIELD_LEVEL": 34, + "UNIT_FIELD_BYTES_1": 137, + "UNIT_FIELD_DISPLAYID": 152, "UNIT_FIELD_FACTIONTEMPLATE": 35, "UNIT_FIELD_FLAGS": 46, "UNIT_FIELD_FLAGS_2": 47, - "UNIT_FIELD_DISPLAYID": 152, + "UNIT_FIELD_HEALTH": 22, + "UNIT_FIELD_LEVEL": 34, + "UNIT_FIELD_MAXHEALTH": 28, + "UNIT_FIELD_MAXPOWER1": 29, "UNIT_FIELD_MOUNTDISPLAYID": 154, - "UNIT_NPC_FLAGS": 168, - "UNIT_DYNAMIC_FLAGS": 164, + "UNIT_FIELD_POWER1": 23, "UNIT_FIELD_RESISTANCES": 185, "UNIT_FIELD_STAT0": 159, "UNIT_FIELD_STAT1": 160, "UNIT_FIELD_STAT2": 161, "UNIT_FIELD_STAT3": 162, "UNIT_FIELD_STAT4": 163, - "UNIT_END": 234, - "PLAYER_FLAGS": 236, - "PLAYER_BYTES": 237, - "PLAYER_BYTES_2": 238, - "PLAYER_XP": 926, - "PLAYER_NEXT_LEVEL_XP": 927, - "PLAYER_REST_STATE_EXPERIENCE": 1440, - "PLAYER_FIELD_COINAGE": 1441, - "PLAYER_QUEST_LOG_START": 244, - "PLAYER_FIELD_INV_SLOT_HEAD": 650, - "PLAYER_FIELD_PACK_SLOT_1": 696, - "PLAYER_FIELD_BANK_SLOT_1": 728, - "PLAYER_FIELD_BANKBAG_SLOT_1": 784, - "PLAYER_SKILL_INFO_START": 928, - "PLAYER_EXPLORED_ZONES_START": 1312, - "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14, - "ITEM_FIELD_DURABILITY": 60, - "ITEM_FIELD_MAXDURABILITY": 61, - "CONTAINER_FIELD_NUM_SLOTS": 64, - "CONTAINER_FIELD_SLOT_1": 66 + "UNIT_FIELD_TARGET_HI": 17, + "UNIT_FIELD_TARGET_LO": 16, + "UNIT_NPC_FLAGS": 168 } diff --git a/Data/expansions/turtle/dbc_layouts.json b/Data/expansions/turtle/dbc_layouts.json index a2482e0d..2f580109 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -1,98 +1,297 @@ { - "Spell": { - "ID": 0, "Attributes": 5, "IconID": 117, - "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33 + "AreaTable": { + "ExploreFlag": 3, + "ID": 0, + "MapID": 1, + "ParentAreaNum": 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 + "CharHairGeosets": { + "GeosetID": 4, + "RaceID": 1, + "SexID": 2, + "Variation": 3 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 - }, - "SpellIcon": { "ID": 0, "Path": 1 }, - "FactionTemplate": { - "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 - }, - "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 - }, - "CreatureDisplayInfo": { - "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 - }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, - "TaxiPathNode": { - "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 - }, - "Talent": { - "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 }, - "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "BaseSection": 3, + "ColorIndex": 5, + "Flags": 9, + "RaceID": 1, + "SexID": 2, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "VariationIndex": 4 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "Geoset100": 3, + "Geoset200": 5, + "Geoset300": 4, + "RaceID": 0, + "SexID": 1, + "Variation": 2 + }, + "CreatureDisplayInfo": { + "ExtraDisplayId": 3, + "ID": 0, + "ModelID": 1, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 + }, + "CreatureDisplayInfoExtra": { + "BakeName": 18, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "FaceID": 4, + "FacialHairID": 7, + "HairColorID": 6, + "HairStyleID": 5, + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 + }, + "Emotes": { + "AnimID": 2, + "ID": 0 }, - "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 + "Command": 1, + "EmoteRef": 2, + "ID": 0, + "OthersNoTargetTextID": 7, + "OthersTargetTextID": 3, + "SenderNoTargetTextID": 9, + "SenderTargetTextID": 5 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 + }, + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 + }, + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "ItemDisplayInfo": { + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "ID": 0, + "InventoryIcon": 5, + "LeftModel": 1, + "LeftModelTexture": 3, + "TextureArmLower": 15, + "TextureArmUpper": 14, + "TextureFoot": 21, + "TextureHand": 16, + "TextureLegLower": 20, + "TextureLegUpper": 19, + "TextureTorsoLower": 18, + "TextureTorsoUpper": 17 + }, + "ItemSet": { + "ID": 0, + "Item0": 10, + "Item1": 11, + "Item2": 12, + "Item3": 13, + "Item4": 14, + "Item5": 15, + "Item6": 16, + "Item7": 17, + "Item8": 18, + "Item9": 19, + "Name": 1, + "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 }, - "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 - }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "ID": 0, + "InnerRadius": 5, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9, + "MapID": 1, + "OuterRadius": 6, + "X": 2, + "Y": 4, + "Z": 3 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightIntBand": { + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightParams": { + "LightParamsID": 0 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "SkillLine": { + "Category": 1, + "ID": 0, + "Name": 3 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "Spell": { + "Attributes": 5, + "AttributesEx": 6, + "CastingTimeIndex": 15, + "DispelType": 4, + "DurationIndex": 40, + "EffectBasePoints0": 80, + "EffectBasePoints1": 81, + "EffectBasePoints2": 82, + "ID": 0, + "IconID": 117, + "ManaCost": 29, + "Name": 120, + "PowerType": 28, + "RangeIndex": 33, + "Rank": 129, + "SchoolEnum": 1, + "Tooltip": 147 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 + }, + "SpellItemEnchantment": { + "ID": 0, + "Name": 8 + }, + "SpellRange": { + "MaxRange": 2 + }, + "SpellVisual": { + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 + }, + "SpellVisualEffectName": { + "FilePath": 2, + "ID": 0 + }, + "SpellVisualKit": { + "BaseEffect": 5, + "ID": 0, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 + }, + "Talent": { + "Column": 3, + "ID": 0, + "PrereqRank0": 12, + "PrereqTalent0": 9, + "RankSpell0": 4, + "Row": 2, + "TabID": 1 + }, + "TalentTab": { + "BackgroundFile": 15, + "ClassMask": 12, + "ID": 0, + "Name": 1, + "OrderIndex": 14 + }, + "TaxiNodes": { + "ID": 0, + "MapID": 1, + "Name": 5, + "X": 2, + "Y": 3, + "Z": 4 + }, + "TaxiPath": { + "Cost": 3, + "FromNode": 1, + "ID": 0, + "ToNode": 2 + }, + "TaxiPathNode": { + "ID": 0, + "MapID": 3, + "NodeIndex": 2, + "PathID": 1, + "X": 4, + "Y": 5, + "Z": 6 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "AreaID": 2, + "AreaName": 3, + "DisplayMapID": 8, + "ID": 0, + "LocBottom": 7, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "MapID": 1, + "ParentWorldMapID": 10 } } diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index 95a22888..d0f84599 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -1,300 +1,6 @@ { - "CMSG_PING": "0x1DC", - "CMSG_AUTH_SESSION": "0x1ED", - "CMSG_CHAR_CREATE": "0x036", - "CMSG_CHAR_ENUM": "0x037", - "CMSG_CHAR_DELETE": "0x038", - "CMSG_PLAYER_LOGIN": "0x03D", - "MSG_MOVE_START_FORWARD": "0x0B5", - "MSG_MOVE_START_BACKWARD": "0x0B6", - "MSG_MOVE_STOP": "0x0B7", - "MSG_MOVE_START_STRAFE_LEFT": "0x0B8", - "MSG_MOVE_START_STRAFE_RIGHT": "0x0B9", - "MSG_MOVE_STOP_STRAFE": "0x0BA", - "MSG_MOVE_JUMP": "0x0BB", - "MSG_MOVE_START_TURN_LEFT": "0x0BC", - "MSG_MOVE_START_TURN_RIGHT": "0x0BD", - "MSG_MOVE_STOP_TURN": "0x0BE", - "MSG_MOVE_SET_FACING": "0x0DA", - "MSG_MOVE_FALL_LAND": "0x0C9", - "MSG_MOVE_START_SWIM": "0x0CA", - "MSG_MOVE_STOP_SWIM": "0x0CB", - "MSG_MOVE_HEARTBEAT": "0x0EE", - "SMSG_AUTH_CHALLENGE": "0x1EC", - "SMSG_AUTH_RESPONSE": "0x1EE", - "SMSG_CHAR_CREATE": "0x03A", - "SMSG_CHAR_ENUM": "0x03B", - "SMSG_CHAR_DELETE": "0x03C", - "SMSG_CHARACTER_LOGIN_FAILED": "0x041", - "SMSG_PONG": "0x1DD", - "SMSG_LOGIN_VERIFY_WORLD": "0x236", - "SMSG_INIT_WORLD_STATES": "0x2C2", - "SMSG_LOGIN_SETTIMESPEED": "0x042", - "SMSG_TUTORIAL_FLAGS": "0x0FD", - "SMSG_INITIALIZE_FACTIONS": "0x122", - "SMSG_WARDEN_DATA": "0x2E6", - "CMSG_WARDEN_DATA": "0x2E7", - "SMSG_NOTIFICATION": "0x1CB", - "SMSG_ACCOUNT_DATA_TIMES": "0x209", - "SMSG_UPDATE_OBJECT": "0x0A9", - "SMSG_COMPRESSED_UPDATE_OBJECT": "0x1F6", - "SMSG_PARTYKILLLOG": "0x1F5", - "SMSG_MONSTER_MOVE_TRANSPORT": "0x2AE", - "SMSG_SPLINE_MOVE_SET_WALK_MODE": "0x30E", - "SMSG_SPLINE_MOVE_SET_RUN_MODE": "0x30D", - "SMSG_SPLINE_SET_RUN_SPEED": "0x2FE", - "SMSG_SPLINE_SET_RUN_BACK_SPEED": "0x2FF", - "SMSG_SPLINE_SET_SWIM_SPEED": "0x300", - "SMSG_DESTROY_OBJECT": "0x0AA", - "CMSG_MESSAGECHAT": "0x095", - "SMSG_MESSAGECHAT": "0x096", - "CMSG_WHO": "0x062", - "SMSG_WHO": "0x063", - "CMSG_PLAYED_TIME": "0x1CC", - "SMSG_PLAYED_TIME": "0x1CD", - "CMSG_QUERY_TIME": "0x1CE", - "SMSG_QUERY_TIME_RESPONSE": "0x1CF", - "SMSG_FRIEND_STATUS": "0x068", - "SMSG_CONTACT_LIST": "0x067", - "CMSG_ADD_FRIEND": "0x069", - "CMSG_DEL_FRIEND": "0x06A", - "CMSG_ADD_IGNORE": "0x06C", - "CMSG_DEL_IGNORE": "0x06D", - "CMSG_PLAYER_LOGOUT": "0x04A", - "CMSG_LOGOUT_REQUEST": "0x04B", - "CMSG_LOGOUT_CANCEL": "0x04E", - "SMSG_LOGOUT_RESPONSE": "0x04C", - "SMSG_LOGOUT_COMPLETE": "0x04D", - "CMSG_STANDSTATECHANGE": "0x101", - "CMSG_SHOWING_HELM": "0x2B9", - "CMSG_SHOWING_CLOAK": "0x2BA", - "CMSG_TOGGLE_PVP": "0x253", - "CMSG_GUILD_INVITE": "0x082", - "CMSG_GUILD_ACCEPT": "0x084", - "CMSG_GUILD_DECLINE": "0x085", - "CMSG_GUILD_INFO": "0x087", - "CMSG_GUILD_ROSTER": "0x089", - "CMSG_GUILD_PROMOTE": "0x08B", - "CMSG_GUILD_DEMOTE": "0x08C", - "CMSG_GUILD_LEAVE": "0x08D", - "CMSG_GUILD_MOTD": "0x091", - "SMSG_GUILD_INFO": "0x088", - "SMSG_GUILD_ROSTER": "0x08A", - "CMSG_GUILD_QUERY": "0x054", - "SMSG_GUILD_QUERY_RESPONSE": "0x055", - "SMSG_GUILD_INVITE": "0x083", - "CMSG_GUILD_REMOVE": "0x08E", - "SMSG_GUILD_EVENT": "0x092", - "SMSG_GUILD_COMMAND_RESULT": "0x093", - "MSG_RAID_READY_CHECK": "0x322", - "SMSG_ITEM_PUSH_RESULT": "0x166", - "CMSG_DUEL_ACCEPTED": "0x16C", - "CMSG_DUEL_CANCELLED": "0x16D", - "SMSG_DUEL_REQUESTED": "0x167", - "CMSG_INITIATE_TRADE": "0x116", - "MSG_RANDOM_ROLL": "0x1FB", - "CMSG_SET_SELECTION": "0x13D", - "CMSG_NAME_QUERY": "0x050", - "SMSG_NAME_QUERY_RESPONSE": "0x051", - "CMSG_CREATURE_QUERY": "0x060", - "SMSG_CREATURE_QUERY_RESPONSE": "0x061", - "CMSG_GAMEOBJECT_QUERY": "0x05E", - "SMSG_GAMEOBJECT_QUERY_RESPONSE": "0x05F", - "CMSG_SET_ACTIVE_MOVER": "0x26A", - "CMSG_BINDER_ACTIVATE": "0x1B5", - "SMSG_LOG_XPGAIN": "0x1D0", - "_NOTE_MONSTER_MOVE": "These look swapped vs vanilla (0x0DD/0x2FB) but may be intentional Turtle WoW changes. Check if NPC movement breaks.", - "SMSG_MONSTER_MOVE": "0x2FB", - "SMSG_COMPRESSED_MOVES": "0x06B", - "CMSG_ATTACKSWING": "0x141", - "CMSG_ATTACKSTOP": "0x142", - "SMSG_ATTACKSTART": "0x143", - "SMSG_ATTACKSTOP": "0x144", - "SMSG_ATTACKERSTATEUPDATE": "0x14A", - "SMSG_AI_REACTION": "0x13C", - "SMSG_SPELLNONMELEEDAMAGELOG": "0x250", - "SMSG_PLAY_SPELL_VISUAL": "0x1F3", - "SMSG_SPELLHEALLOG": "0x150", - "SMSG_SPELLENERGIZELOG": "0x151", - "SMSG_PERIODICAURALOG": "0x24E", - "SMSG_ENVIRONMENTAL_DAMAGE_LOG": "0x1FC", - "CMSG_CAST_SPELL": "0x12E", - "CMSG_CANCEL_CAST": "0x12F", - "CMSG_CANCEL_AURA": "0x136", - "SMSG_CAST_FAILED": "0x130", - "SMSG_SPELL_START": "0x131", - "SMSG_SPELL_GO": "0x132", - "SMSG_SPELL_FAILURE": "0x133", - "SMSG_SPELL_COOLDOWN": "0x134", - "SMSG_COOLDOWN_EVENT": "0x135", - "SMSG_EQUIPMENT_SET_SAVED": "0x137", - "SMSG_INITIAL_SPELLS": "0x12A", - "SMSG_LEARNED_SPELL": "0x12B", - "SMSG_SUPERCEDED_SPELL": "0x12C", - "SMSG_REMOVED_SPELL": "0x203", - "SMSG_SPELL_DELAYED": "0x1E2", - "SMSG_SET_FLAT_SPELL_MODIFIER": "0x266", - "SMSG_SET_PCT_SPELL_MODIFIER": "0x267", - "CMSG_LEARN_TALENT": "0x251", - "MSG_TALENT_WIPE_CONFIRM": "0x2AA", - "CMSG_GROUP_INVITE": "0x06E", - "SMSG_GROUP_INVITE": "0x06F", - "CMSG_GROUP_ACCEPT": "0x072", - "CMSG_GROUP_DECLINE": "0x073", - "SMSG_GROUP_DECLINE": "0x074", - "CMSG_GROUP_UNINVITE_GUID": "0x076", - "SMSG_GROUP_UNINVITE": "0x077", - "CMSG_GROUP_SET_LEADER": "0x078", - "SMSG_GROUP_SET_LEADER": "0x079", - "CMSG_GROUP_DISBAND": "0x07B", - "SMSG_GROUP_LIST": "0x07D", - "SMSG_PARTY_COMMAND_RESULT": "0x07F", - "MSG_RAID_TARGET_UPDATE": "0x321", - "CMSG_REQUEST_RAID_INFO": "0x2CD", - "SMSG_RAID_INSTANCE_INFO": "0x2CC", - "CMSG_AUTOSTORE_LOOT_ITEM": "0x108", - "CMSG_LOOT": "0x15D", - "CMSG_LOOT_MONEY": "0x15E", - "CMSG_LOOT_RELEASE": "0x15F", - "SMSG_LOOT_RESPONSE": "0x160", - "SMSG_LOOT_RELEASE_RESPONSE": "0x161", - "SMSG_LOOT_REMOVED": "0x162", - "SMSG_LOOT_MONEY_NOTIFY": "0x163", - "SMSG_LOOT_CLEAR_MONEY": "0x165", - "CMSG_ACTIVATETAXI": "0x1AD", - "CMSG_GOSSIP_HELLO": "0x17B", - "CMSG_GOSSIP_SELECT_OPTION": "0x17C", - "SMSG_GOSSIP_MESSAGE": "0x17D", - "SMSG_GOSSIP_COMPLETE": "0x17E", - "SMSG_NPC_TEXT_UPDATE": "0x180", - "CMSG_GAMEOBJ_USE": "0x0B1", - "CMSG_QUESTGIVER_STATUS_QUERY": "0x182", - "SMSG_QUESTGIVER_STATUS": "0x183", - "CMSG_QUESTGIVER_HELLO": "0x184", - "SMSG_QUESTGIVER_QUEST_LIST": "0x185", - "CMSG_QUESTGIVER_QUERY_QUEST": "0x186", - "SMSG_QUESTGIVER_QUEST_DETAILS": "0x188", - "CMSG_QUESTGIVER_ACCEPT_QUEST": "0x189", - "CMSG_QUESTGIVER_COMPLETE_QUEST": "0x18A", - "SMSG_QUESTGIVER_REQUEST_ITEMS": "0x18B", - "CMSG_QUESTGIVER_REQUEST_REWARD": "0x18C", - "SMSG_QUESTGIVER_OFFER_REWARD": "0x18D", - "CMSG_QUESTGIVER_CHOOSE_REWARD": "0x18E", - "SMSG_QUESTGIVER_QUEST_INVALID": "0x18F", - "SMSG_QUESTGIVER_QUEST_COMPLETE": "0x191", - "CMSG_QUESTLOG_REMOVE_QUEST": "0x194", - "SMSG_QUESTUPDATE_ADD_KILL": "0x199", - "SMSG_QUESTUPDATE_COMPLETE": "0x198", - "SMSG_QUEST_FORCE_REMOVE": "0x21E", - "CMSG_QUEST_QUERY": "0x05C", - "SMSG_QUEST_QUERY_RESPONSE": "0x05D", - "SMSG_QUESTLOG_FULL": "0x195", - "CMSG_LIST_INVENTORY": "0x19E", - "SMSG_LIST_INVENTORY": "0x19F", - "CMSG_SELL_ITEM": "0x1A0", - "SMSG_SELL_ITEM": "0x1A1", - "CMSG_BUY_ITEM": "0x1A2", - "CMSG_BUYBACK_ITEM": "0x1A6", - "SMSG_BUY_FAILED": "0x1A5", - "CMSG_TRAINER_LIST": "0x1B0", - "SMSG_TRAINER_LIST": "0x1B1", - "CMSG_TRAINER_BUY_SPELL": "0x1B2", - "SMSG_TRAINER_BUY_FAILED": "0x1B4", - "CMSG_ITEM_QUERY_SINGLE": "0x056", - "SMSG_ITEM_QUERY_SINGLE_RESPONSE": "0x058", - "CMSG_USE_ITEM": "0x0AB", - "CMSG_AUTOEQUIP_ITEM": "0x10A", - "CMSG_SWAP_ITEM": "0x10C", - "CMSG_SWAP_INV_ITEM": "0x10D", - "SMSG_INVENTORY_CHANGE_FAILURE": "0x112", - "CMSG_INSPECT": "0x114", - "SMSG_INSPECT_RESULTS_UPDATE": "0x115", - "CMSG_REPOP_REQUEST": "0x15A", - "SMSG_RESURRECT_REQUEST": "0x15B", - "CMSG_RESURRECT_RESPONSE": "0x15C", - "CMSG_SPIRIT_HEALER_ACTIVATE": "0x21C", - "SMSG_SPIRIT_HEALER_CONFIRM": "0x222", - "MSG_MOVE_TELEPORT_ACK": "0x0C7", - "SMSG_TRANSFER_PENDING": "0x03F", - "SMSG_NEW_WORLD": "0x03E", - "MSG_MOVE_WORLDPORT_ACK": "0x0DC", - "SMSG_TRANSFER_ABORTED": "0x040", - "SMSG_FORCE_RUN_SPEED_CHANGE": "0x0E2", - "SMSG_CLIENT_CONTROL_UPDATE": "0x159", - "CMSG_FORCE_RUN_SPEED_CHANGE_ACK": "0x0E3", - "SMSG_SHOWTAXINODES": "0x1A9", - "SMSG_ACTIVATETAXIREPLY": "0x1AE", - "SMSG_NEW_TAXI_PATH": "0x1AF", - "CMSG_ACTIVATETAXIEXPRESS": "0x312", - "CMSG_TAXINODE_STATUS_QUERY": "0x1AA", - "SMSG_TAXINODE_STATUS": "0x1AB", - "SMSG_TRAINER_BUY_SUCCEEDED": "0x1B3", - "SMSG_BINDPOINTUPDATE": "0x155", - "SMSG_SET_PROFICIENCY": "0x127", - "SMSG_ACTION_BUTTONS": "0x129", - "SMSG_LEVELUP_INFO": "0x1D4", - "SMSG_PLAY_SOUND": "0x2D2", - "CMSG_UPDATE_ACCOUNT_DATA": "0x20B", - "CMSG_BATTLEFIELD_LIST": "0x23C", - "SMSG_BATTLEFIELD_LIST": "0x23D", - "CMSG_BATTLEFIELD_JOIN": "0x23E", - "CMSG_BATTLEFIELD_STATUS": "0x2D3", - "SMSG_BATTLEFIELD_STATUS": "0x2D4", - "CMSG_BATTLEFIELD_PORT": "0x2D5", - "CMSG_BATTLEMASTER_HELLO": "0x2D7", - "MSG_PVP_LOG_DATA": "0x2E0", - "CMSG_LEAVE_BATTLEFIELD": "0x2E1", - "SMSG_GROUP_JOINED_BATTLEGROUND": "0x2E8", - "MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9", - "SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC", - "SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED", - "CMSG_BATTLEMASTER_JOIN": "0x2EE", - "CMSG_EMOTE": "0x102", - "SMSG_EMOTE": "0x103", - "CMSG_TEXT_EMOTE": "0x104", - "SMSG_TEXT_EMOTE": "0x105", - "CMSG_JOIN_CHANNEL": "0x097", - "CMSG_LEAVE_CHANNEL": "0x098", - "SMSG_CHANNEL_NOTIFY": "0x099", - "CMSG_CHANNEL_LIST": "0x09A", - "SMSG_CHANNEL_LIST": "0x09B", - "SMSG_INSPECT_TALENT": "0x3F4", - "SMSG_SHOW_MAILBOX": "0x297", - "CMSG_GET_MAIL_LIST": "0x23A", - "SMSG_MAIL_LIST_RESULT": "0x23B", - "CMSG_SEND_MAIL": "0x238", - "SMSG_SEND_MAIL_RESULT": "0x239", - "CMSG_MAIL_TAKE_MONEY": "0x245", - "CMSG_MAIL_TAKE_ITEM": "0x246", - "CMSG_MAIL_DELETE": "0x249", - "CMSG_MAIL_MARK_AS_READ": "0x247", - "SMSG_RECEIVED_MAIL": "0x285", - "MSG_QUERY_NEXT_MAIL_TIME": "0x284", - "CMSG_BANKER_ACTIVATE": "0x1B7", - "SMSG_SHOW_BANK": "0x1B8", - "CMSG_BUY_BANK_SLOT": "0x1B9", - "SMSG_BUY_BANK_SLOT_RESULT": "0x1BA", - "CMSG_AUTOSTORE_BANK_ITEM": "0x282", - "CMSG_AUTOBANK_ITEM": "0x283", - "MSG_AUCTION_HELLO": "0x255", - "CMSG_AUCTION_SELL_ITEM": "0x256", - "CMSG_AUCTION_REMOVE_ITEM": "0x257", - "CMSG_AUCTION_LIST_ITEMS": "0x258", - "CMSG_AUCTION_LIST_OWNER_ITEMS": "0x259", - "CMSG_AUCTION_PLACE_BID": "0x25A", - "SMSG_AUCTION_COMMAND_RESULT": "0x25B", - "SMSG_AUCTION_LIST_RESULT": "0x25C", - "SMSG_AUCTION_OWNER_LIST_RESULT": "0x25D", - "SMSG_AUCTION_OWNER_NOTIFICATION": "0x25E", - "SMSG_AUCTION_BIDDER_NOTIFICATION": "0x260", - "CMSG_AUCTION_LIST_BIDDER_ITEMS": "0x264", - "SMSG_AUCTION_BIDDER_LIST_RESULT": "0x265", - "MSG_MOVE_TIME_SKIPPED": "0x319", - "SMSG_CANCEL_AUTO_REPEAT": "0x29C", - "SMSG_WEATHER": "0x2F4", - "SMSG_QUESTUPDATE_ADD_ITEM": "0x19A", - "CMSG_GUILD_DISBAND": "0x08F", - "CMSG_GUILD_LEADER": "0x090", - "CMSG_GUILD_SET_PUBLIC_NOTE": "0x234", - "CMSG_GUILD_SET_OFFICER_NOTE": "0x235" + "_extends": "../classic/opcodes.json", + "_remove": [ + "MSG_SET_DUNGEON_DIFFICULTY" + ] } diff --git a/Data/expansions/turtle/update_fields.json b/Data/expansions/turtle/update_fields.json index 74b873ae..4a602e91 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -1,47 +1,49 @@ { + "CONTAINER_FIELD_NUM_SLOTS": 48, + "CONTAINER_FIELD_SLOT_1": 50, + "GAMEOBJECT_DISPLAYID": 8, + "ITEM_FIELD_DURABILITY": 48, + "ITEM_FIELD_MAXDURABILITY": 49, + "ITEM_FIELD_STACK_COUNT": 14, "OBJECT_FIELD_ENTRY": 3, "OBJECT_FIELD_SCALE_X": 4, - "UNIT_FIELD_TARGET_LO": 16, - "UNIT_FIELD_TARGET_HI": 17, + "PLAYER_BYTES": 191, + "PLAYER_BYTES_2": 192, + "PLAYER_END": 1282, + "PLAYER_EXPLORED_ZONES_START": 1111, + "PLAYER_FIELD_BANKBAG_SLOT_1": 612, + "PLAYER_FIELD_BANK_SLOT_1": 564, + "PLAYER_FIELD_COINAGE": 1176, + "PLAYER_FIELD_INV_SLOT_HEAD": 486, + "PLAYER_FIELD_PACK_SLOT_1": 532, + "PLAYER_FLAGS": 190, + "PLAYER_NEXT_LEVEL_XP": 717, + "PLAYER_QUEST_LOG_START": 198, + "PLAYER_REST_STATE_EXPERIENCE": 1175, + "PLAYER_SKILL_INFO_START": 718, + "PLAYER_XP": 716, + "UNIT_DYNAMIC_FLAGS": 143, + "UNIT_END": 188, + "UNIT_FIELD_AURAFLAGS": 98, + "UNIT_FIELD_AURAS": 50, "UNIT_FIELD_BYTES_0": 36, - "UNIT_FIELD_HEALTH": 22, - "UNIT_FIELD_POWER1": 23, - "UNIT_FIELD_MAXHEALTH": 28, - "UNIT_FIELD_MAXPOWER1": 29, - "UNIT_FIELD_LEVEL": 34, + "UNIT_FIELD_BYTES_1": 133, + "UNIT_FIELD_DISPLAYID": 131, "UNIT_FIELD_FACTIONTEMPLATE": 35, "UNIT_FIELD_FLAGS": 46, - "UNIT_FIELD_DISPLAYID": 131, + "UNIT_FIELD_HEALTH": 22, + "UNIT_FIELD_LEVEL": 34, + "UNIT_FIELD_MAXHEALTH": 28, + "UNIT_FIELD_MAXPOWER1": 29, "UNIT_FIELD_MOUNTDISPLAYID": 133, - "UNIT_FIELD_AURAS": 50, - "UNIT_NPC_FLAGS": 147, - "UNIT_DYNAMIC_FLAGS": 143, + "UNIT_FIELD_POWER1": 23, "UNIT_FIELD_RESISTANCES": 154, "UNIT_FIELD_STAT0": 138, "UNIT_FIELD_STAT1": 139, "UNIT_FIELD_STAT2": 140, "UNIT_FIELD_STAT3": 141, "UNIT_FIELD_STAT4": 142, - "UNIT_END": 188, - "PLAYER_FLAGS": 190, - "PLAYER_BYTES": 191, - "PLAYER_BYTES_2": 192, - "PLAYER_XP": 716, - "PLAYER_NEXT_LEVEL_XP": 717, - "PLAYER_REST_STATE_EXPERIENCE": 1175, - "PLAYER_FIELD_COINAGE": 1176, - "PLAYER_QUEST_LOG_START": 198, - "PLAYER_FIELD_INV_SLOT_HEAD": 486, - "PLAYER_FIELD_PACK_SLOT_1": 532, - "PLAYER_FIELD_BANK_SLOT_1": 564, - "PLAYER_FIELD_BANKBAG_SLOT_1": 612, - "PLAYER_SKILL_INFO_START": 718, - "PLAYER_EXPLORED_ZONES_START": 1111, - "PLAYER_END": 1282, - "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14, - "ITEM_FIELD_DURABILITY": 48, - "ITEM_FIELD_MAXDURABILITY": 49, - "CONTAINER_FIELD_NUM_SLOTS": 48, - "CONTAINER_FIELD_SLOT_1": 50 -} \ No newline at end of file + "UNIT_FIELD_TARGET_HI": 17, + "UNIT_FIELD_TARGET_LO": 16, + "UNIT_NPC_FLAGS": 147 +} diff --git a/Data/expansions/wotlk/dbc_layouts.json b/Data/expansions/wotlk/dbc_layouts.json index 0d1667a1..e563d2c9 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -1,101 +1,323 @@ { - "Spell": { - "ID": 0, "Attributes": 4, "IconID": 133, - "Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225, - "PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49 + "Achievement": { + "Description": 21, + "ID": 0, + "Points": 39, + "Title": 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 + "AchievementCriteria": { + "AchievementID": 1, + "Description": 9, + "ID": 0, + "Quantity": 4 + }, + "AreaTable": { + "ExploreFlag": 3, + "ID": 0, + "MapID": 1, + "ParentAreaNum": 2 + }, + "CharHairGeosets": { + "GeosetID": 4, + "RaceID": 1, + "SexID": 2, + "Variation": 3 }, "CharSections": { - "RaceID": 1, "SexID": 2, "BaseSection": 3, - "VariationIndex": 4, "ColorIndex": 5, - "Texture1": 6, "Texture2": 7, "Texture3": 8, - "Flags": 9 + "BaseSection": 3, + "ColorIndex": 5, + "Flags": 9, + "RaceID": 1, + "SexID": 2, + "Texture1": 6, + "Texture2": 7, + "Texture3": 8, + "VariationIndex": 4 }, - "SpellIcon": { "ID": 0, "Path": 1 }, - "FactionTemplate": { - "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 - }, - "Achievement": { "ID": 0, "Title": 4, "Description": 21 }, - "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 - }, - "CreatureDisplayInfo": { - "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 - }, - "TaxiPath": { "ID": 0, "FromNode": 1, "ToNode": 2, "Cost": 3 }, - "TaxiPathNode": { - "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 - }, - "Talent": { - "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 }, - "CharHairGeosets": { - "RaceID": 1, "SexID": 2, "Variation": 3, "GeosetID": 4 + "CharTitles": { + "ID": 0, + "Title": 2, + "TitleBit": 36 }, "CharacterFacialHairStyles": { - "RaceID": 0, "SexID": 1, "Variation": 2, - "Geoset100": 3, "Geoset300": 4, "Geoset200": 5 + "Geoset100": 3, + "Geoset200": 5, + "Geoset300": 4, + "RaceID": 0, + "SexID": 1, + "Variation": 2 + }, + "CreatureDisplayInfo": { + "ExtraDisplayId": 3, + "ID": 0, + "ModelID": 1, + "Skin1": 6, + "Skin2": 7, + "Skin3": 8 + }, + "CreatureDisplayInfoExtra": { + "BakeName": 20, + "EquipDisplay0": 8, + "EquipDisplay1": 9, + "EquipDisplay10": 18, + "EquipDisplay2": 10, + "EquipDisplay3": 11, + "EquipDisplay4": 12, + "EquipDisplay5": 13, + "EquipDisplay6": 14, + "EquipDisplay7": 15, + "EquipDisplay8": 16, + "EquipDisplay9": 17, + "FaceID": 4, + "FacialHairID": 7, + "HairColorID": 6, + "HairStyleID": 5, + "ID": 0, + "RaceID": 1, + "SexID": 2, + "SkinID": 3 + }, + "CreatureModelData": { + "ID": 0, + "ModelPath": 2 + }, + "Emotes": { + "AnimID": 2, + "ID": 0 }, - "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 + "Command": 1, + "EmoteRef": 2, + "ID": 0, + "OthersNoTargetTextID": 7, + "OthersTargetTextID": 3, + "SenderNoTargetTextID": 9, + "SenderTargetTextID": 5 + }, + "EmotesTextData": { + "ID": 0, + "Text": 1 + }, + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 + }, + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 + }, + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 + }, + "ItemDisplayInfo": { + "GeosetGroup1": 7, + "GeosetGroup3": 9, + "ID": 0, + "InventoryIcon": 5, + "LeftModel": 1, + "LeftModelTexture": 3, + "TextureArmLower": 15, + "TextureArmUpper": 14, + "TextureFoot": 21, + "TextureHand": 16, + "TextureLegLower": 20, + "TextureLegUpper": 19, + "TextureTorsoLower": 18, + "TextureTorsoUpper": 17 + }, + "ItemSet": { + "ID": 0, + "Item0": 18, + "Item1": 19, + "Item2": 20, + "Item3": 21, + "Item4": 22, + "Item5": 23, + "Item6": 24, + "Item7": 25, + "Item8": 26, + "Item9": 27, + "Name": 1, + "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 }, - "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 - }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "ID": 0, + "InnerRadius": 5, + "LightParamsID": 7, + "LightParamsIDRain": 8, + "LightParamsIDUnderwater": 9, + "MapID": 1, + "OuterRadius": 6, + "X": 2, + "Y": 4, + "Z": 3 }, "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightIntBand": { + "BlockIndex": 1, + "NumKeyframes": 2, + "TimeKey0": 3, + "Value0": 19 + }, + "LightParams": { + "LightParamsID": 0 + }, + "Map": { + "ID": 0, + "InternalName": 1 + }, + "SkillLine": { + "Category": 1, + "ID": 0, + "Name": 3 + }, + "SkillLineAbility": { + "SkillLineID": 1, + "SpellID": 2 + }, + "Spell": { + "Attributes": 4, + "AttributesEx": 5, + "CastingTimeIndex": 47, + "DispelType": 2, + "DurationIndex": 40, + "EffectBasePoints0": 80, + "EffectBasePoints1": 81, + "EffectBasePoints2": 82, + "ID": 0, + "IconID": 133, + "ManaCost": 39, + "Name": 136, + "PowerType": 14, + "RangeIndex": 49, + "Rank": 153, + "SchoolMask": 225, + "Tooltip": 139 + }, + "SpellIcon": { + "ID": 0, + "Path": 1 + }, + "SpellItemEnchantment": { + "ID": 0, + "Name": 8 + }, + "SpellRange": { + "MaxRange": 4 + }, + "SpellVisual": { + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 + }, + "SpellVisualEffectName": { + "FilePath": 2, + "ID": 0 + }, + "SpellVisualKit": { + "BaseEffect": 5, + "ID": 0, + "SpecialEffect0": 11, + "SpecialEffect1": 12, + "SpecialEffect2": 13 + }, + "Talent": { + "Column": 3, + "ID": 0, + "PrereqRank0": 12, + "PrereqTalent0": 9, + "RankSpell0": 4, + "Row": 2, + "TabID": 1 + }, + "TalentTab": { + "BackgroundFile": 23, + "ClassMask": 20, + "ID": 0, + "Name": 1, + "OrderIndex": 22 + }, + "TaxiNodes": { + "ID": 0, + "MapID": 1, + "MountDisplayIdAlliance": 22, + "MountDisplayIdAllianceFallback": 20, + "MountDisplayIdHorde": 23, + "MountDisplayIdHordeFallback": 21, + "Name": 5, + "X": 2, + "Y": 3, + "Z": 4 + }, + "TaxiPath": { + "Cost": 3, + "FromNode": 1, + "ID": 0, + "ToNode": 2 + }, + "TaxiPathNode": { + "ID": 0, + "MapID": 3, + "NodeIndex": 2, + "PathID": 1, + "X": 4, + "Y": 5, + "Z": 6 }, "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "AreaID": 2, + "AreaName": 3, + "DisplayMapID": 8, + "ID": 0, + "LocBottom": 7, + "LocLeft": 4, + "LocRight": 5, + "LocTop": 6, + "MapID": 1, + "ParentWorldMapID": 10 } } diff --git a/Data/expansions/wotlk/update_fields.json b/Data/expansions/wotlk/update_fields.json index 1532f628..06bcbd62 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -1,46 +1,61 @@ { + "CONTAINER_FIELD_NUM_SLOTS": 64, + "CONTAINER_FIELD_SLOT_1": 66, + "GAMEOBJECT_DISPLAYID": 8, + "ITEM_FIELD_DURABILITY": 60, + "ITEM_FIELD_MAXDURABILITY": 61, + "ITEM_FIELD_STACK_COUNT": 14, "OBJECT_FIELD_ENTRY": 3, "OBJECT_FIELD_SCALE_X": 4, - "UNIT_FIELD_TARGET_LO": 6, - "UNIT_FIELD_TARGET_HI": 7, + "PLAYER_BLOCK_PERCENTAGE": 1024, + "PLAYER_BYTES": 153, + "PLAYER_BYTES_2": 154, + "PLAYER_CHOSEN_TITLE": 1349, + "PLAYER_CRIT_PERCENTAGE": 1029, + "PLAYER_DODGE_PERCENTAGE": 1025, + "PLAYER_EXPLORED_ZONES_START": 1041, + "PLAYER_FIELD_ARENA_CURRENCY": 1423, + "PLAYER_FIELD_BANKBAG_SLOT_1": 458, + "PLAYER_FIELD_BANK_SLOT_1": 402, + "PLAYER_FIELD_COINAGE": 1170, + "PLAYER_FIELD_COMBAT_RATING_1": 1231, + "PLAYER_FIELD_HONOR_CURRENCY": 1422, + "PLAYER_FIELD_INV_SLOT_HEAD": 324, + "PLAYER_FIELD_MOD_DAMAGE_DONE_POS": 1171, + "PLAYER_FIELD_MOD_HEALING_DONE_POS": 1192, + "PLAYER_FIELD_PACK_SLOT_1": 370, + "PLAYER_FLAGS": 150, + "PLAYER_NEXT_LEVEL_XP": 635, + "PLAYER_PARRY_PERCENTAGE": 1026, + "PLAYER_QUEST_LOG_START": 158, + "PLAYER_RANGED_CRIT_PERCENTAGE": 1030, + "PLAYER_REST_STATE_EXPERIENCE": 1169, + "PLAYER_SKILL_INFO_START": 636, + "PLAYER_SPELL_CRIT_PERCENTAGE1": 1032, + "PLAYER_XP": 634, + "UNIT_DYNAMIC_FLAGS": 147, + "UNIT_END": 148, + "UNIT_FIELD_ATTACK_POWER": 123, "UNIT_FIELD_BYTES_0": 23, - "UNIT_FIELD_HEALTH": 24, - "UNIT_FIELD_POWER1": 25, - "UNIT_FIELD_MAXHEALTH": 32, - "UNIT_FIELD_MAXPOWER1": 33, - "UNIT_FIELD_LEVEL": 54, + "UNIT_FIELD_BYTES_1": 137, + "UNIT_FIELD_DISPLAYID": 67, "UNIT_FIELD_FACTIONTEMPLATE": 55, "UNIT_FIELD_FLAGS": 59, "UNIT_FIELD_FLAGS_2": 60, - "UNIT_FIELD_DISPLAYID": 67, + "UNIT_FIELD_HEALTH": 24, + "UNIT_FIELD_LEVEL": 54, + "UNIT_FIELD_MAXHEALTH": 32, + "UNIT_FIELD_MAXPOWER1": 33, "UNIT_FIELD_MOUNTDISPLAYID": 69, - "UNIT_NPC_FLAGS": 82, - "UNIT_DYNAMIC_FLAGS": 147, + "UNIT_FIELD_POWER1": 25, + "UNIT_FIELD_RANGED_ATTACK_POWER": 126, "UNIT_FIELD_RESISTANCES": 99, "UNIT_FIELD_STAT0": 84, "UNIT_FIELD_STAT1": 85, "UNIT_FIELD_STAT2": 86, "UNIT_FIELD_STAT3": 87, "UNIT_FIELD_STAT4": 88, - "UNIT_END": 148, - "PLAYER_FLAGS": 150, - "PLAYER_BYTES": 153, - "PLAYER_BYTES_2": 154, - "PLAYER_XP": 634, - "PLAYER_NEXT_LEVEL_XP": 635, - "PLAYER_REST_STATE_EXPERIENCE": 1169, - "PLAYER_FIELD_COINAGE": 1170, - "PLAYER_QUEST_LOG_START": 158, - "PLAYER_FIELD_INV_SLOT_HEAD": 324, - "PLAYER_FIELD_PACK_SLOT_1": 370, - "PLAYER_FIELD_BANK_SLOT_1": 402, - "PLAYER_FIELD_BANKBAG_SLOT_1": 458, - "PLAYER_SKILL_INFO_START": 636, - "PLAYER_EXPLORED_ZONES_START": 1041, - "GAMEOBJECT_DISPLAYID": 8, - "ITEM_FIELD_STACK_COUNT": 14, - "ITEM_FIELD_DURABILITY": 60, - "ITEM_FIELD_MAXDURABILITY": 61, - "CONTAINER_FIELD_NUM_SLOTS": 64, - "CONTAINER_FIELD_SLOT_1": 66 + "UNIT_FIELD_TARGET_HI": 7, + "UNIT_FIELD_TARGET_LO": 6, + "UNIT_NPC_FLAGS": 82 } diff --git a/Data/interface/AddOns/HelloWorld/HelloWorld.lua b/Data/interface/AddOns/HelloWorld/HelloWorld.lua new file mode 100644 index 00000000..5ee38fd6 --- /dev/null +++ b/Data/interface/AddOns/HelloWorld/HelloWorld.lua @@ -0,0 +1,36 @@ +-- HelloWorld addon — demonstrates the WoWee addon system + +-- Initialize saved variables (persisted across sessions) +if not HelloWorldDB then + HelloWorldDB = { loginCount = 0 } +end +HelloWorldDB.loginCount = (HelloWorldDB.loginCount or 0) + 1 + +-- Create a frame and register for events (standard WoW addon pattern) +local f = CreateFrame("Frame", "HelloWorldFrame") +f:RegisterEvent("PLAYER_ENTERING_WORLD") +f:RegisterEvent("CHAT_MSG_SAY") + +f:SetScript("OnEvent", function(self, event, ...) + if event == "PLAYER_ENTERING_WORLD" then + local name = UnitName("player") + local level = UnitLevel("player") + print("|cff00ff00[HelloWorld]|r Welcome, " .. name .. "! (Level " .. level .. ")") + print("|cff00ff00[HelloWorld]|r Login count: " .. HelloWorldDB.loginCount) + elseif event == "CHAT_MSG_SAY" then + local msg, sender = ... + if msg and sender then + print("|cff00ff00[HelloWorld]|r " .. sender .. " said: " .. msg) + end + end +end) + +-- Register a custom slash command +SLASH_HELLOWORLD1 = "/hello" +SLASH_HELLOWORLD2 = "/hw" +SlashCmdList["HELLOWORLD"] = function(args) + print("|cff00ff00[HelloWorld]|r Hello! " .. (args ~= "" and args or "Type /hello ")) + print("|cff00ff00[HelloWorld]|r Sessions: " .. HelloWorldDB.loginCount) +end + +print("|cff00ff00[HelloWorld]|r Addon loaded. Type /hello to test.") diff --git a/Data/interface/AddOns/HelloWorld/HelloWorld.toc b/Data/interface/AddOns/HelloWorld/HelloWorld.toc new file mode 100644 index 00000000..f50ef105 --- /dev/null +++ b/Data/interface/AddOns/HelloWorld/HelloWorld.toc @@ -0,0 +1,5 @@ +## Interface: 30300 +## Title: Hello World +## Notes: Test addon for the WoWee addon system +## SavedVariables: HelloWorldDB +HelloWorld.lua diff --git a/Data/opcodes/aliases.json b/Data/opcodes/aliases.json index e3a67348..4677cd5d 100644 --- a/Data/opcodes/aliases.json +++ b/Data/opcodes/aliases.json @@ -41,7 +41,6 @@ "SMSG_SPLINE_MOVE_SET_RUN_BACK_SPEED": "SMSG_SPLINE_SET_RUN_BACK_SPEED", "SMSG_SPLINE_MOVE_SET_RUN_SPEED": "SMSG_SPLINE_SET_RUN_SPEED", "SMSG_SPLINE_MOVE_SET_SWIM_SPEED": "SMSG_SPLINE_SET_SWIM_SPEED", - "SMSG_UPDATE_AURA_DURATION": "SMSG_EQUIPMENT_SET_SAVED", "SMSG_VICTIMSTATEUPDATE_OBSOLETE": "SMSG_BATTLEFIELD_PORT_DENIED" } } diff --git a/README.md b/README.md index d983133c..50a09bfa 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,14 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. > **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction. -## Status & Direction (2026-03-07) +## Status & Direction (2026-03-24) -- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par — no single one is significantly more complete than the others. -- **Tested against**: AzerothCore, TrinityCore, Mangos, and Turtle WoW (1.17). -- **Current focus**: instance dungeons, visual accuracy (lava/water, shadow mapping, WMO interiors), and multi-expansion coverage. +- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers. All three expansions are roughly on par. +- **Tested against**: AzerothCore/ChromieCraft, TrinityCore, Mangos, and Turtle WoW (1.17). +- **Current focus**: gameplay correctness (quest/GO interaction, NPC visibility), rendering stability, and multi-expansion coverage. - **Warden**: Full module execution via Unicorn Engine CPU emulation. Decrypts (RC4→RSA→zlib), parses and relocates the PE module, executes via x86 emulation with Windows API interception. Module cache at `~/.local/share/wowee/warden_cache/`. -- **CI**: GitHub Actions builds for Linux (x86-64, ARM64), Windows (MSYS2), and macOS (ARM64). Security scans via CodeQL, Semgrep, and sanitizers. +- **CI**: GitHub Actions builds for Linux (x86-64, ARM64), Windows (MSYS2 x86-64 + ARM64), and macOS (ARM64). Security scans via CodeQL, Semgrep, and sanitizers. +- **Release**: v1.8.2-preview — 530+ WoW API functions, 140+ events, 664 opcode handlers. ## Features @@ -52,7 +53,7 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. - **Movement** -- WASD movement, camera orbit, spline path following, transport riding (trams, ships, zeppelins), movement ACK responses - **Combat** -- Auto-attack, spell casting with cooldowns, damage calculation, death handling, spirit healer resurrection - **Targeting** -- Tab-cycling with hostility filtering, click-to-target, faction-based hostility (using Faction.dbc) -- **Inventory** -- 23 equipment slots, 16 backpack slots, drag-drop, auto-equip, item tooltips with weapon damage/speed +- **Inventory** -- 23 equipment slots, 16 backpack slots, drag-drop, auto-equip, item tooltips with weapon damage/speed, server-synced bag sort (quality/type/stack), independent bag windows - **Bank** -- Full bank support for all expansions, bag slots, drag-drop, right-click deposit (non-equippable items) - **Spells** -- Spellbook with specialty, general, profession, mount, and companion tabs; drag-drop to action bar; item use support - **Talents** -- Talent tree UI with proper visuals and functionality @@ -67,7 +68,8 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**. - **Chat** -- Tabs/channels, emotes, chat bubbles, clickable URLs, clickable item links with tooltips - **Party** -- Group invites, party list, out-of-range member health via SMSG_PARTY_MEMBER_STATS - **Pets** -- Pet tracking via SMSG_PET_SPELLS, action bar (10 slots with icon/autocast tinting/tooltips), dismiss button -- **Map Exploration** -- Subzone-level fog-of-war reveal matching retail behavior +- **Map Exploration** -- Subzone-level fog-of-war reveal, world map with continent/zone views, quest POI markers, taxi node markers, party member dots +- **NPC Voices** -- Race/gender-specific NPC greeting, farewell, vendor, pissed, aggro, and flee sounds for all playable races including Blood Elf and Draenei - **Warden** -- Warden anti-cheat module execution via Unicorn Engine x86 emulation (cross-platform, no Wine) - **UI** -- Loading screens with progress bar, settings window with graphics quality presets (LOW/MEDIUM/HIGH/ULTRA), shadow distance slider, minimap with zoom/rotation/square mode, top-right minimap mute speaker, separate bag windows with compact-empty mode (aggregate view) diff --git a/assets/shaders/fsr2_sharpen.frag.glsl b/assets/shaders/fsr2_sharpen.frag.glsl index 9cd1271c..392a3a37 100644 --- a/assets/shaders/fsr2_sharpen.frag.glsl +++ b/assets/shaders/fsr2_sharpen.frag.glsl @@ -10,9 +10,7 @@ layout(push_constant) uniform PushConstants { } pc; void main() { - // Undo the vertex shader Y flip (postprocess.vert flips for Vulkan overlay, - // but we need standard UV coords for texture sampling) - vec2 tc = vec2(TexCoord.x, 1.0 - TexCoord.y); + vec2 tc = TexCoord; vec2 texelSize = pc.params.xy; float sharpness = pc.params.z; diff --git a/assets/shaders/fsr2_sharpen.frag.spv b/assets/shaders/fsr2_sharpen.frag.spv index 20672a9e..24519b93 100644 Binary files a/assets/shaders/fsr2_sharpen.frag.spv and b/assets/shaders/fsr2_sharpen.frag.spv differ diff --git a/assets/shaders/fsr_easu.frag.glsl b/assets/shaders/fsr_easu.frag.glsl index 20e5ed32..6a36be75 100644 --- a/assets/shaders/fsr_easu.frag.glsl +++ b/assets/shaders/fsr_easu.frag.glsl @@ -21,9 +21,7 @@ vec3 fsrFetch(vec2 p, vec2 off) { } void main() { - // Undo the vertex shader Y flip (postprocess.vert flips for Vulkan overlay, - // but we need standard UV coords for texture sampling) - vec2 tc = vec2(TexCoord.x, 1.0 - TexCoord.y); + vec2 tc = TexCoord; // Map output pixel to input space vec2 pp = tc * fsr.con2.xy; // output pixel position diff --git a/assets/shaders/fsr_easu.frag.spv b/assets/shaders/fsr_easu.frag.spv index 5ddc2ea8..12780757 100644 Binary files a/assets/shaders/fsr_easu.frag.spv and b/assets/shaders/fsr_easu.frag.spv differ diff --git a/assets/shaders/fxaa.frag.glsl b/assets/shaders/fxaa.frag.glsl new file mode 100644 index 00000000..3da10854 --- /dev/null +++ b/assets/shaders/fxaa.frag.glsl @@ -0,0 +1,155 @@ +#version 450 + +// FXAA 3.11 — Fast Approximate Anti-Aliasing post-process pass. +// Reads the resolved scene color and outputs a smoothed result. +// Push constant: rcpFrame = vec2(1/width, 1/height), sharpness (0=off, 2=max), desaturate (1=ghost grayscale). + +layout(set = 0, binding = 0) uniform sampler2D uScene; + +layout(location = 0) in vec2 TexCoord; +layout(location = 0) out vec4 outColor; + +layout(push_constant) uniform PC { + vec2 rcpFrame; + float sharpness; // 0 = no sharpen, 2 = max (matches FSR2 RCAS range) + float desaturate; // 1 = full grayscale (ghost mode), 0 = normal color +} pc; + +// Quality tuning +#define FXAA_EDGE_THRESHOLD (1.0/8.0) // minimum edge contrast to process +#define FXAA_EDGE_THRESHOLD_MIN (1.0/24.0) // ignore very dark regions +#define FXAA_SEARCH_STEPS 12 +#define FXAA_SEARCH_THRESHOLD (1.0/4.0) +#define FXAA_SUBPIX 0.75 +#define FXAA_SUBPIX_TRIM (1.0/4.0) +#define FXAA_SUBPIX_TRIM_SCALE (1.0/(1.0 - FXAA_SUBPIX_TRIM)) +#define FXAA_SUBPIX_CAP (3.0/4.0) + +float luma(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void main() { + vec2 uv = TexCoord; + vec2 rcp = pc.rcpFrame; + + // --- Centre and cardinal neighbours --- + vec3 rgbM = texture(uScene, uv).rgb; + vec3 rgbN = texture(uScene, uv + vec2( 0.0, -1.0) * rcp).rgb; + vec3 rgbS = texture(uScene, uv + vec2( 0.0, 1.0) * rcp).rgb; + vec3 rgbE = texture(uScene, uv + vec2( 1.0, 0.0) * rcp).rgb; + vec3 rgbW = texture(uScene, uv + vec2(-1.0, 0.0) * rcp).rgb; + + float lumaN = luma(rgbN); + float lumaS = luma(rgbS); + float lumaE = luma(rgbE); + float lumaW = luma(rgbW); + float lumaM = luma(rgbM); + + float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW))); + float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW))); + float range = lumaMax - lumaMin; + + // Early exit on smooth regions + if (range < max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD)) { + outColor = vec4(rgbM, 1.0); + return; + } + + // --- Diagonal neighbours --- + vec3 rgbNW = texture(uScene, uv + vec2(-1.0, -1.0) * rcp).rgb; + vec3 rgbNE = texture(uScene, uv + vec2( 1.0, -1.0) * rcp).rgb; + vec3 rgbSW = texture(uScene, uv + vec2(-1.0, 1.0) * rcp).rgb; + vec3 rgbSE = texture(uScene, uv + vec2( 1.0, 1.0) * rcp).rgb; + + float lumaNW = luma(rgbNW); + float lumaNE = luma(rgbNE); + float lumaSW = luma(rgbSW); + float lumaSE = luma(rgbSE); + + // --- Sub-pixel blend factor --- + float lumaL = (lumaN + lumaS + lumaE + lumaW) * 0.25; + float rangeL = abs(lumaL - lumaM); + float blendL = max(0.0, (rangeL / range) - FXAA_SUBPIX_TRIM) * FXAA_SUBPIX_TRIM_SCALE; + blendL = min(FXAA_SUBPIX_CAP, blendL) * FXAA_SUBPIX; + + // --- Edge orientation (horizontal vs. vertical) --- + float edgeHorz = + abs(-2.0*lumaW + lumaNW + lumaSW) + + 2.0*abs(-2.0*lumaM + lumaN + lumaS) + + abs(-2.0*lumaE + lumaNE + lumaSE); + float edgeVert = + abs(-2.0*lumaS + lumaSW + lumaSE) + + 2.0*abs(-2.0*lumaM + lumaW + lumaE) + + abs(-2.0*lumaN + lumaNW + lumaNE); + + bool horzSpan = (edgeHorz >= edgeVert); + float lengthSign = horzSpan ? rcp.y : rcp.x; + + float luma1 = horzSpan ? lumaN : lumaW; + float luma2 = horzSpan ? lumaS : lumaE; + float grad1 = abs(luma1 - lumaM); + float grad2 = abs(luma2 - lumaM); + lengthSign = (grad1 >= grad2) ? -lengthSign : lengthSign; + + // --- Edge search --- + vec2 posB = uv; + vec2 offNP = horzSpan ? vec2(rcp.x, 0.0) : vec2(0.0, rcp.y); + if (!horzSpan) posB.x += lengthSign * 0.5; + if ( horzSpan) posB.y += lengthSign * 0.5; + + float lumaMLSS = lumaM - (luma1 + luma2) * 0.5; + float gradientScaled = max(grad1, grad2) * 0.25; + + vec2 posN = posB - offNP; + vec2 posP = posB + offNP; + bool done1 = false, done2 = false; + float lumaEnd1 = 0.0, lumaEnd2 = 0.0; + + for (int i = 0; i < FXAA_SEARCH_STEPS; ++i) { + if (!done1) lumaEnd1 = luma(texture(uScene, posN).rgb) - lumaMLSS; + if (!done2) lumaEnd2 = luma(texture(uScene, posP).rgb) - lumaMLSS; + done1 = done1 || (abs(lumaEnd1) >= gradientScaled * FXAA_SEARCH_THRESHOLD); + done2 = done2 || (abs(lumaEnd2) >= gradientScaled * FXAA_SEARCH_THRESHOLD); + if (done1 && done2) break; + if (!done1) posN -= offNP; + if (!done2) posP += offNP; + } + + float dstN = horzSpan ? (uv.x - posN.x) : (uv.y - posN.y); + float dstP = horzSpan ? (posP.x - uv.x) : (posP.y - uv.y); + bool dirN = (dstN < dstP); + float lumaEndFinal = dirN ? lumaEnd1 : lumaEnd2; + + float spanLength = dstN + dstP; + float pixelOffset = (dirN ? dstN : dstP) / spanLength; + bool goodSpan = ((lumaEndFinal < 0.0) != (lumaMLSS < 0.0)); + float pixelOffsetFinal = max(goodSpan ? pixelOffset : 0.0, blendL); + + vec2 finalUV = uv; + if ( horzSpan) finalUV.y += pixelOffsetFinal * lengthSign; + if (!horzSpan) finalUV.x += pixelOffsetFinal * lengthSign; + + vec3 fxaaResult = texture(uScene, finalUV).rgb; + + // Post-FXAA contrast-adaptive sharpening (unsharp mask). + // Counteracts FXAA's sub-pixel blur when sharpness > 0. + if (pc.sharpness > 0.0) { + vec2 r = pc.rcpFrame; + vec3 blur = (texture(uScene, uv + vec2(-r.x, 0)).rgb + + texture(uScene, uv + vec2( r.x, 0)).rgb + + texture(uScene, uv + vec2(0, -r.y)).rgb + + texture(uScene, uv + vec2(0, r.y)).rgb) * 0.25; + // scale sharpness from [0,2] to a modest [0, 0.3] boost factor + float s = pc.sharpness * 0.15; + fxaaResult = clamp(fxaaResult + s * (fxaaResult - blur), 0.0, 1.0); + } + + // Ghost mode: desaturate to grayscale (with a slight cool blue tint). + if (pc.desaturate > 0.5) { + float gray = dot(fxaaResult, vec3(0.299, 0.587, 0.114)); + fxaaResult = mix(fxaaResult, vec3(gray, gray, gray * 1.05), pc.desaturate); + } + + outColor = vec4(fxaaResult, 1.0); +} diff --git a/assets/shaders/fxaa.frag.spv b/assets/shaders/fxaa.frag.spv new file mode 100644 index 00000000..b87b3dee Binary files /dev/null and b/assets/shaders/fxaa.frag.spv differ diff --git a/assets/shaders/m2_particle.frag.glsl b/assets/shaders/m2_particle.frag.glsl index f91a3fb7..49fac7e1 100644 --- a/assets/shaders/m2_particle.frag.glsl +++ b/assets/shaders/m2_particle.frag.glsl @@ -25,6 +25,9 @@ void main() { if (lum < 0.05) discard; } - float edge = smoothstep(0.5, 0.4, length(p - 0.5)); - outColor = texColor * vColor * vec4(vec3(1.0), edge); + // Soft circular falloff for point-sprite edges. + float edge = 1.0 - smoothstep(0.4, 0.5, length(p - 0.5)); + float alpha = texColor.a * vColor.a * edge; + vec3 rgb = texColor.rgb * vColor.rgb * alpha; + outColor = vec4(rgb, alpha); } diff --git a/assets/shaders/m2_ribbon.frag.glsl b/assets/shaders/m2_ribbon.frag.glsl new file mode 100644 index 00000000..4e0e483e --- /dev/null +++ b/assets/shaders/m2_ribbon.frag.glsl @@ -0,0 +1,25 @@ +#version 450 + +// M2 ribbon emitter fragment shader. +// Samples the ribbon texture, multiplied by vertex color and alpha. +// Uses additive blending (pipeline-level) for magic/spell trails. + +layout(set = 1, binding = 0) uniform sampler2D uTexture; + +layout(location = 0) in vec3 vColor; +layout(location = 1) in float vAlpha; +layout(location = 2) in vec2 vUV; +layout(location = 3) in float vFogFactor; + +layout(location = 0) out vec4 outColor; + +void main() { + vec4 tex = texture(uTexture, vUV); + // For additive ribbons alpha comes from texture luminance; multiply by vertex alpha. + float a = tex.a * vAlpha; + if (a < 0.01) discard; + vec3 rgb = tex.rgb * vColor; + // Ribbons fade slightly with fog (additive blend attenuated toward black = invisible in fog). + rgb *= vFogFactor; + outColor = vec4(rgb, a); +} diff --git a/assets/shaders/m2_ribbon.frag.spv b/assets/shaders/m2_ribbon.frag.spv new file mode 100644 index 00000000..9b0a3fe9 Binary files /dev/null and b/assets/shaders/m2_ribbon.frag.spv differ diff --git a/assets/shaders/m2_ribbon.vert.glsl b/assets/shaders/m2_ribbon.vert.glsl new file mode 100644 index 00000000..492a295e --- /dev/null +++ b/assets/shaders/m2_ribbon.vert.glsl @@ -0,0 +1,43 @@ +#version 450 + +// M2 ribbon emitter vertex shader. +// Ribbon geometry is generated CPU-side as a triangle strip. +// Vertex format: pos(3) + color(3) + alpha(1) + uv(2) = 9 floats. + +layout(set = 0, binding = 0) uniform PerFrame { + mat4 view; + mat4 projection; + mat4 lightSpaceMatrix; + vec4 lightDir; + vec4 lightColor; + vec4 ambientColor; + vec4 viewPos; + vec4 fogColor; + vec4 fogParams; + vec4 shadowParams; +}; + +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aColor; +layout(location = 2) in float aAlpha; +layout(location = 3) in vec2 aUV; + +layout(location = 0) out vec3 vColor; +layout(location = 1) out float vAlpha; +layout(location = 2) out vec2 vUV; +layout(location = 3) out float vFogFactor; + +void main() { + vec4 worldPos = vec4(aPos, 1.0); + vec4 viewPos4 = view * worldPos; + gl_Position = projection * viewPos4; + + float dist = length(viewPos4.xyz); + float fogStart = fogParams.x; + float fogEnd = fogParams.y; + vFogFactor = clamp((fogEnd - dist) / max(fogEnd - fogStart, 0.001), 0.0, 1.0); + + vColor = aColor; + vAlpha = aAlpha; + vUV = aUV; +} diff --git a/assets/shaders/m2_ribbon.vert.spv b/assets/shaders/m2_ribbon.vert.spv new file mode 100644 index 00000000..d74ba750 Binary files /dev/null and b/assets/shaders/m2_ribbon.vert.spv differ diff --git a/assets/shaders/minimap_display.frag.glsl b/assets/shaders/minimap_display.frag.glsl index dacaaed1..476017b9 100644 --- a/assets/shaders/minimap_display.frag.glsl +++ b/assets/shaders/minimap_display.frag.glsl @@ -40,23 +40,27 @@ void main() { float cs = cos(push.rotation); float sn = sin(push.rotation); vec2 rotated = vec2(center.x * cs - center.y * sn, center.x * sn + center.y * cs); - vec2 mapUV = push.playerUV + vec2(-rotated.x, rotated.y) * push.zoomRadius * 2.0; + vec2 mapUV = push.playerUV + vec2(rotated.x, rotated.y) * push.zoomRadius * 2.0; vec4 mapColor = texture(uComposite, mapUV); - // Player arrow - float acs = cos(push.arrowRotation); - float asn = sin(push.arrowRotation); - vec2 ac = center; - vec2 arrowPos = vec2(-(ac.x * acs - ac.y * asn), ac.x * asn + ac.y * acs); - - vec2 tip = vec2(0.0, -0.04); - vec2 left = vec2(-0.02, 0.02); - vec2 right = vec2(0.02, 0.02); - - if (pointInTriangle(arrowPos, tip, left, right)) { - mapColor = vec4(1.0, 0.8, 0.0, 1.0); + // Single player direction indicator (center arrow) rendered in-shader. + vec2 local = center; // [-0.5, 0.5] around minimap center + float ac = cos(push.arrowRotation); + float as = sin(push.arrowRotation); + // TexCoord Y grows downward on screen; use negative Y so 0-angle points North (up). + vec2 tip = vec2(0.0, -0.09); + vec2 left = vec2(-0.045, 0.02); + vec2 right = vec2( 0.045, 0.02); + mat2 rot = mat2(ac, -as, as, ac); + tip = rot * tip; + left = rot * left; + right = rot * right; + if (pointInTriangle(local, tip, left, right)) { + mapColor.rgb = vec3(1.0, 0.86, 0.05); } + float centerDot = smoothstep(0.016, 0.0, length(local)); + mapColor.rgb = mix(mapColor.rgb, vec3(1.0), centerDot * 0.95); // Dark border ring float border = smoothstep(0.48, 0.5, dist); diff --git a/assets/shaders/minimap_display.frag.spv b/assets/shaders/minimap_display.frag.spv index 5c0ac7b0..f33deef2 100644 Binary files a/assets/shaders/minimap_display.frag.spv and b/assets/shaders/minimap_display.frag.spv differ diff --git a/assets/shaders/postprocess.vert.glsl b/assets/shaders/postprocess.vert.glsl index aa78b1b5..2ed8f784 100644 --- a/assets/shaders/postprocess.vert.glsl +++ b/assets/shaders/postprocess.vert.glsl @@ -6,5 +6,7 @@ void main() { // Fullscreen triangle trick: 3 vertices, no vertex buffer TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); gl_Position = vec4(TexCoord * 2.0 - 1.0, 0.0, 1.0); - TexCoord.y = 1.0 - TexCoord.y; // flip Y for Vulkan + // No Y-flip: scene textures use Vulkan convention (v=0 at top), + // and NDC y=-1 already maps to framebuffer top, so the triangle + // naturally samples the correct row without any inversion. } diff --git a/assets/shaders/postprocess.vert.spv b/assets/shaders/postprocess.vert.spv index afc10472..89065a80 100644 Binary files a/assets/shaders/postprocess.vert.spv and b/assets/shaders/postprocess.vert.spv differ diff --git a/docs/status.md b/docs/status.md index 06722c2f..bb1e9614 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,6 +1,6 @@ # Project Status -**Last updated**: 2026-03-11 +**Last updated**: 2026-03-24 ## What This Repo Is @@ -25,19 +25,23 @@ Implemented (working in normal use): - Talent tree UI with proper visuals and functionality - Pet tracking (SMSG_PET_SPELLS), dismiss pet button - Party: group invites, party list, out-of-range member health (SMSG_PARTY_MEMBER_STATS) +- Nameplates: NPC subtitles, guild names, elite/boss/rare borders, quest/raid indicators, cast bars, debuff dots +- Floating combat text: world-space damage/heal numbers above entities with 3D projection +- Target/focus frames: guild name, creature type, rank badges, combo points, cast bars - Map exploration: subzone-level fog-of-war reveal - Warden anti-cheat: full module execution via Unicorn Engine x86 emulation; module caching -- Audio: ambient, movement, combat, spell, and UI sound systems -- Bag UI: separate bag windows, open-bag indicator on bag bar, optional collapse-empty mode in aggregate bag view +- Audio: ambient, movement, combat, spell, and UI sound systems; NPC voice lines for all playable races (greeting/farewell/vendor/pissed/aggro/flee) +- Bag UI: independent bag windows (any bag closable independently), open-bag indicator on bag bar, server-synced bag sort, off-screen position reset, optional collapse-empty mode in aggregate view +- DBC auto-detection: CharSections.dbc field layout auto-detected at runtime (handles stock WotLK vs HD-textured clients) - Multi-expansion: Classic/Vanilla, TBC, WotLK, and Turtle WoW (1.17) protocol and asset variants -- CI: GitHub Actions for Linux (x86-64, ARM64), Windows (MSYS2), macOS (ARM64); container builds via Podman +- CI: GitHub Actions for Linux (x86-64, ARM64), Windows (MSYS2 x86-64 + ARM64), macOS (ARM64); container builds via Podman In progress / known gaps: - Transports: M2 transports (trams) working with position-delta riding; WMO transports (ships, zeppelins) working with path following; some edge cases remain -- Visual edge cases: some M2/WMO rendering gaps (character shin mesh, some particle effects) -- Lava steam particles: sparse in some areas (tuning opportunity) -- Water refraction: implemented but disabled by default (can cause VK_ERROR_DEVICE_LOST on some GPUs); currently requires FSR to be active +- Quest GO interaction: CMSG_GAMEOBJ_USE + CMSG_LOOT sent correctly, but some AzerothCore/ChromieCraft servers don't grant quest credit for chest-type GOs (server-side limitation) +- Visual edge cases: some M2/WMO rendering gaps (some particle effects) +- Water refraction: enabled by default; srcAccessMask barrier fix (2026-03-18) resolved prior VK_ERROR_DEVICE_LOST on AMD/Mali GPUs ## Where To Look diff --git a/extern/lua-5.1.5/COPYRIGHT b/extern/lua-5.1.5/COPYRIGHT new file mode 100644 index 00000000..a8602680 --- /dev/null +++ b/extern/lua-5.1.5/COPYRIGHT @@ -0,0 +1,34 @@ +Lua License +----------- + +Lua is licensed under the terms of the MIT license reproduced below. +This means that Lua is free software and can be used for both academic +and commercial purposes at absolutely no cost. + +For details and rationale, see http://www.lua.org/license.html . + +=============================================================================== + +Copyright (C) 1994-2012 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=============================================================================== + +(end of COPYRIGHT) diff --git a/extern/lua-5.1.5/HISTORY b/extern/lua-5.1.5/HISTORY new file mode 100644 index 00000000..ce0c95bc --- /dev/null +++ b/extern/lua-5.1.5/HISTORY @@ -0,0 +1,183 @@ +HISTORY for Lua 5.1 + +* Changes from version 5.0 to 5.1 + ------------------------------- + Language: + + new module system. + + new semantics for control variables of fors. + + new semantics for setn/getn. + + new syntax/semantics for varargs. + + new long strings and comments. + + new `mod' operator (`%') + + new length operator #t + + metatables for all types + API: + + new functions: lua_createtable, lua_get(set)field, lua_push(to)integer. + + user supplies memory allocator (lua_open becomes lua_newstate). + + luaopen_* functions must be called through Lua. + Implementation: + + new configuration scheme via luaconf.h. + + incremental garbage collection. + + better handling of end-of-line in the lexer. + + fully reentrant parser (new Lua function `load') + + better support for 64-bit machines. + + native loadlib support for Mac OS X. + + standard distribution in only one library (lualib.a merged into lua.a) + +* Changes from version 4.0 to 5.0 + ------------------------------- + Language: + + lexical scoping. + + Lua coroutines. + + standard libraries now packaged in tables. + + tags replaced by metatables and tag methods replaced by metamethods, + stored in metatables. + + proper tail calls. + + each function can have its own global table, which can be shared. + + new __newindex metamethod, called when we insert a new key into a table. + + new block comments: --[[ ... ]]. + + new generic for. + + new weak tables. + + new boolean type. + + new syntax "local function". + + (f()) returns the first value returned by f. + + {f()} fills a table with all values returned by f. + + \n ignored in [[\n . + + fixed and-or priorities. + + more general syntax for function definition (e.g. function a.x.y:f()...end). + + more general syntax for function calls (e.g. (print or write)(9)). + + new functions (time/date, tmpfile, unpack, require, load*, etc.). + API: + + chunks are loaded by using lua_load; new luaL_loadfile and luaL_loadbuffer. + + introduced lightweight userdata, a simple "void*" without a metatable. + + new error handling protocol: the core no longer prints error messages; + all errors are reported to the caller on the stack. + + new lua_atpanic for host cleanup. + + new, signal-safe, hook scheme. + Implementation: + + new license: MIT. + + new, faster, register-based virtual machine. + + support for external multithreading and coroutines. + + new and consistent error message format. + + the core no longer needs "stdio.h" for anything (except for a single + use of sprintf to convert numbers to strings). + + lua.c now runs the environment variable LUA_INIT, if present. It can + be "@filename", to run a file, or the chunk itself. + + support for user extensions in lua.c. + sample implementation given for command line editing. + + new dynamic loading library, active by default on several platforms. + + safe garbage-collector metamethods. + + precompiled bytecodes checked for integrity (secure binary dostring). + + strings are fully aligned. + + position capture in string.find. + + read('*l') can read lines with embedded zeros. + +* Changes from version 3.2 to 4.0 + ------------------------------- + Language: + + new "break" and "for" statements (both numerical and for tables). + + uniform treatment of globals: globals are now stored in a Lua table. + + improved error messages. + + no more '$debug': full speed *and* full debug information. + + new read form: read(N) for next N bytes. + + general read patterns now deprecated. + (still available with -DCOMPAT_READPATTERNS.) + + all return values are passed as arguments for the last function + (old semantics still available with -DLUA_COMPAT_ARGRET) + + garbage collection tag methods for tables now deprecated. + + there is now only one tag method for order. + API: + + New API: fully re-entrant, simpler, and more efficient. + + New debug API. + Implementation: + + faster than ever: cleaner virtual machine and new hashing algorithm. + + non-recursive garbage-collector algorithm. + + reduced memory usage for programs with many strings. + + improved treatment for memory allocation errors. + + improved support for 16-bit machines (we hope). + + code now compiles unmodified as both ANSI C and C++. + + numbers in bases other than 10 are converted using strtoul. + + new -f option in Lua to support #! scripts. + + luac can now combine text and binaries. + +* Changes from version 3.1 to 3.2 + ------------------------------- + + redirected all output in Lua's core to _ERRORMESSAGE and _ALERT. + + increased limit on the number of constants and globals per function + (from 2^16 to 2^24). + + debugging info (lua_debug and hooks) moved into lua_state and new API + functions provided to get and set this info. + + new debug lib gives full debugging access within Lua. + + new table functions "foreachi", "sort", "tinsert", "tremove", "getn". + + new io functions "flush", "seek". + +* Changes from version 3.0 to 3.1 + ------------------------------- + + NEW FEATURE: anonymous functions with closures (via "upvalues"). + + new syntax: + - local variables in chunks. + - better scope control with DO block END. + - constructors can now be also written: { record-part; list-part }. + - more general syntax for function calls and lvalues, e.g.: + f(x).y=1 + o:f(x,y):g(z) + f"string" is sugar for f("string") + + strings may now contain arbitrary binary data (e.g., embedded zeros). + + major code re-organization and clean-up; reduced module interdependecies. + + no arbitrary limits on the total number of constants and globals. + + support for multiple global contexts. + + better syntax error messages. + + new traversal functions "foreach" and "foreachvar". + + the default for numbers is now double. + changing it to use floats or longs is easy. + + complete debug information stored in pre-compiled chunks. + + sample interpreter now prompts user when run interactively, and also + handles control-C interruptions gracefully. + +* Changes from version 2.5 to 3.0 + ------------------------------- + + NEW CONCEPT: "tag methods". + Tag methods replace fallbacks as the meta-mechanism for extending the + semantics of Lua. Whereas fallbacks had a global nature, tag methods + work on objects having the same tag (e.g., groups of tables). + Existing code that uses fallbacks should work without change. + + new, general syntax for constructors {[exp] = exp, ... }. + + support for handling variable number of arguments in functions (varargs). + + support for conditional compilation ($if ... $else ... $end). + + cleaner semantics in API simplifies host code. + + better support for writing libraries (auxlib.h). + + better type checking and error messages in the standard library. + + luac can now also undump. + +* Changes from version 2.4 to 2.5 + ------------------------------- + + io and string libraries are now based on pattern matching; + the old libraries are still available for compatibility + + dofile and dostring can now return values (via return statement) + + better support for 16- and 64-bit machines + + expanded documentation, with more examples + +* Changes from version 2.2 to 2.4 + ------------------------------- + + external compiler creates portable binary files that can be loaded faster + + interface for debugging and profiling + + new "getglobal" fallback + + new functions for handling references to Lua objects + + new functions in standard lib + + only one copy of each string is stored + + expanded documentation, with more examples + +* Changes from version 2.1 to 2.2 + ------------------------------- + + functions now may be declared with any "lvalue" as a name + + garbage collection of functions + + support for pipes + +* Changes from version 1.1 to 2.1 + ------------------------------- + + object-oriented support + + fallbacks + + simplified syntax for tables + + many internal improvements + +(end of HISTORY) diff --git a/extern/lua-5.1.5/INSTALL b/extern/lua-5.1.5/INSTALL new file mode 100644 index 00000000..17eb8aee --- /dev/null +++ b/extern/lua-5.1.5/INSTALL @@ -0,0 +1,99 @@ +INSTALL for Lua 5.1 + +* Building Lua + ------------ + Lua is built in the src directory, but the build process can be + controlled from the top-level Makefile. + + Building Lua on Unix systems should be very easy. First do "make" and + see if your platform is listed. If so, just do "make xxx", where xxx + is your platform name. The platforms currently supported are: + aix ansi bsd freebsd generic linux macosx mingw posix solaris + + If your platform is not listed, try the closest one or posix, generic, + ansi, in this order. + + See below for customization instructions and for instructions on how + to build with other Windows compilers. + + If you want to check that Lua has been built correctly, do "make test" + after building Lua. Also, have a look at the example programs in test. + +* Installing Lua + -------------- + Once you have built Lua, you may want to install it in an official + place in your system. In this case, do "make install". The official + place and the way to install files are defined in Makefile. You must + have the right permissions to install files. + + If you want to build and install Lua in one step, do "make xxx install", + where xxx is your platform name. + + If you want to install Lua locally, then do "make local". This will + create directories bin, include, lib, man, and install Lua there as + follows: + + bin: lua luac + include: lua.h luaconf.h lualib.h lauxlib.h lua.hpp + lib: liblua.a + man/man1: lua.1 luac.1 + + These are the only directories you need for development. + + There are man pages for lua and luac, in both nroff and html, and a + reference manual in html in doc, some sample code in test, and some + useful stuff in etc. You don't need these directories for development. + + If you want to install Lua locally, but in some other directory, do + "make install INSTALL_TOP=xxx", where xxx is your chosen directory. + + See below for instructions for Windows and other systems. + +* Customization + ------------- + Three things can be customized by editing a file: + - Where and how to install Lua -- edit Makefile. + - How to build Lua -- edit src/Makefile. + - Lua features -- edit src/luaconf.h. + + You don't actually need to edit the Makefiles because you may set the + relevant variables when invoking make. + + On the other hand, if you need to select some Lua features, you'll need + to edit src/luaconf.h. The edited file will be the one installed, and + it will be used by any Lua clients that you build, to ensure consistency. + + We strongly recommend that you enable dynamic loading. This is done + automatically for all platforms listed above that have this feature + (and also Windows). See src/luaconf.h and also src/Makefile. + +* Building Lua on Windows and other systems + ----------------------------------------- + If you're not using the usual Unix tools, then the instructions for + building Lua depend on the compiler you use. You'll need to create + projects (or whatever your compiler uses) for building the library, + the interpreter, and the compiler, as follows: + + library: lapi.c lcode.c ldebug.c ldo.c ldump.c lfunc.c lgc.c llex.c + lmem.c lobject.c lopcodes.c lparser.c lstate.c lstring.c + ltable.c ltm.c lundump.c lvm.c lzio.c + lauxlib.c lbaselib.c ldblib.c liolib.c lmathlib.c loslib.c + ltablib.c lstrlib.c loadlib.c linit.c + + interpreter: library, lua.c + + compiler: library, luac.c print.c + + If you use Visual Studio .NET, you can use etc/luavs.bat in its + "Command Prompt". + + If all you want is to build the Lua interpreter, you may put all .c files + in a single project, except for luac.c and print.c. Or just use etc/all.c. + + To use Lua as a library in your own programs, you'll need to know how to + create and use libraries with your compiler. + + As mentioned above, you may edit luaconf.h to select some features before + building Lua. + +(end of INSTALL) diff --git a/extern/lua-5.1.5/Makefile b/extern/lua-5.1.5/Makefile new file mode 100644 index 00000000..209a1324 --- /dev/null +++ b/extern/lua-5.1.5/Makefile @@ -0,0 +1,128 @@ +# makefile for installing Lua +# see INSTALL for installation instructions +# see src/Makefile and src/luaconf.h for further customization + +# == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT ======================= + +# Your platform. See PLATS for possible values. +PLAT= none + +# Where to install. The installation starts in the src and doc directories, +# so take care if INSTALL_TOP is not an absolute path. +INSTALL_TOP= /usr/local +INSTALL_BIN= $(INSTALL_TOP)/bin +INSTALL_INC= $(INSTALL_TOP)/include +INSTALL_LIB= $(INSTALL_TOP)/lib +INSTALL_MAN= $(INSTALL_TOP)/man/man1 +# +# You probably want to make INSTALL_LMOD and INSTALL_CMOD consistent with +# LUA_ROOT, LUA_LDIR, and LUA_CDIR in luaconf.h (and also with etc/lua.pc). +INSTALL_LMOD= $(INSTALL_TOP)/share/lua/$V +INSTALL_CMOD= $(INSTALL_TOP)/lib/lua/$V + +# How to install. If your install program does not support "-p", then you +# may have to run ranlib on the installed liblua.a (do "make ranlib"). +INSTALL= install -p +INSTALL_EXEC= $(INSTALL) -m 0755 +INSTALL_DATA= $(INSTALL) -m 0644 +# +# If you don't have install you can use cp instead. +# INSTALL= cp -p +# INSTALL_EXEC= $(INSTALL) +# INSTALL_DATA= $(INSTALL) + +# Utilities. +MKDIR= mkdir -p +RANLIB= ranlib + +# == END OF USER SETTINGS. NO NEED TO CHANGE ANYTHING BELOW THIS LINE ========= + +# Convenience platforms targets. +PLATS= aix ansi bsd freebsd generic linux macosx mingw posix solaris + +# What to install. +TO_BIN= lua luac +TO_INC= lua.h luaconf.h lualib.h lauxlib.h ../etc/lua.hpp +TO_LIB= liblua.a +TO_MAN= lua.1 luac.1 + +# Lua version and release. +V= 5.1 +R= 5.1.5 + +all: $(PLAT) + +$(PLATS) clean: + cd src && $(MAKE) $@ + +test: dummy + src/lua test/hello.lua + +install: dummy + cd src && $(MKDIR) $(INSTALL_BIN) $(INSTALL_INC) $(INSTALL_LIB) $(INSTALL_MAN) $(INSTALL_LMOD) $(INSTALL_CMOD) + cd src && $(INSTALL_EXEC) $(TO_BIN) $(INSTALL_BIN) + cd src && $(INSTALL_DATA) $(TO_INC) $(INSTALL_INC) + cd src && $(INSTALL_DATA) $(TO_LIB) $(INSTALL_LIB) + cd doc && $(INSTALL_DATA) $(TO_MAN) $(INSTALL_MAN) + +ranlib: + cd src && cd $(INSTALL_LIB) && $(RANLIB) $(TO_LIB) + +local: + $(MAKE) install INSTALL_TOP=.. + +none: + @echo "Please do" + @echo " make PLATFORM" + @echo "where PLATFORM is one of these:" + @echo " $(PLATS)" + @echo "See INSTALL for complete instructions." + +# make may get confused with test/ and INSTALL in a case-insensitive OS +dummy: + +# echo config parameters +echo: + @echo "" + @echo "These are the parameters currently set in src/Makefile to build Lua $R:" + @echo "" + @cd src && $(MAKE) -s echo + @echo "" + @echo "These are the parameters currently set in Makefile to install Lua $R:" + @echo "" + @echo "PLAT = $(PLAT)" + @echo "INSTALL_TOP = $(INSTALL_TOP)" + @echo "INSTALL_BIN = $(INSTALL_BIN)" + @echo "INSTALL_INC = $(INSTALL_INC)" + @echo "INSTALL_LIB = $(INSTALL_LIB)" + @echo "INSTALL_MAN = $(INSTALL_MAN)" + @echo "INSTALL_LMOD = $(INSTALL_LMOD)" + @echo "INSTALL_CMOD = $(INSTALL_CMOD)" + @echo "INSTALL_EXEC = $(INSTALL_EXEC)" + @echo "INSTALL_DATA = $(INSTALL_DATA)" + @echo "" + @echo "See also src/luaconf.h ." + @echo "" + +# echo private config parameters +pecho: + @echo "V = $(V)" + @echo "R = $(R)" + @echo "TO_BIN = $(TO_BIN)" + @echo "TO_INC = $(TO_INC)" + @echo "TO_LIB = $(TO_LIB)" + @echo "TO_MAN = $(TO_MAN)" + +# echo config parameters as Lua code +# uncomment the last sed expression if you want nil instead of empty strings +lecho: + @echo "-- installation parameters for Lua $R" + @echo "VERSION = '$V'" + @echo "RELEASE = '$R'" + @$(MAKE) echo | grep = | sed -e 's/= /= "/' -e 's/$$/"/' #-e 's/""/nil/' + @echo "-- EOF" + +# list targets that do not create files (but not all makes understand .PHONY) +.PHONY: all $(PLATS) clean test install local none dummy echo pecho lecho + +# (end of Makefile) diff --git a/extern/lua-5.1.5/README b/extern/lua-5.1.5/README new file mode 100644 index 00000000..11b4dff7 --- /dev/null +++ b/extern/lua-5.1.5/README @@ -0,0 +1,37 @@ +README for Lua 5.1 + +See INSTALL for installation instructions. +See HISTORY for a summary of changes since the last released version. + +* What is Lua? + ------------ + Lua is a powerful, light-weight programming language designed for extending + applications. Lua is also frequently used as a general-purpose, stand-alone + language. Lua is free software. + + For complete information, visit Lua's web site at http://www.lua.org/ . + For an executive summary, see http://www.lua.org/about.html . + + Lua has been used in many different projects around the world. + For a short list, see http://www.lua.org/uses.html . + +* Availability + ------------ + Lua is freely available for both academic and commercial purposes. + See COPYRIGHT and http://www.lua.org/license.html for details. + Lua can be downloaded at http://www.lua.org/download.html . + +* Installation + ------------ + Lua is implemented in pure ANSI C, and compiles unmodified in all known + platforms that have an ANSI C compiler. In most Unix-like platforms, simply + do "make" with a suitable target. See INSTALL for detailed instructions. + +* Origin + ------ + Lua is developed at Lua.org, a laboratory of the Department of Computer + Science of PUC-Rio (the Pontifical Catholic University of Rio de Janeiro + in Brazil). + For more information about the authors, see http://www.lua.org/authors.html . + +(end of README) diff --git a/extern/lua-5.1.5/doc/contents.html b/extern/lua-5.1.5/doc/contents.html new file mode 100644 index 00000000..3d83da98 --- /dev/null +++ b/extern/lua-5.1.5/doc/contents.html @@ -0,0 +1,497 @@ + + + +Lua 5.1 Reference Manual - contents + + + + + + + +
+

+ +Lua 5.1 Reference Manual +

+ +

+The reference manual is the official definition of the Lua language. +For a complete introduction to Lua programming, see the book +Programming in Lua. + +

+This manual is also available as a book: +

+ + + +Lua 5.1 Reference Manual +
by R. Ierusalimschy, L. H. de Figueiredo, W. Celes +
Lua.org, August 2006 +
ISBN 85-903798-3-3 +
+
+ +

+Buy a copy +of this book and +help to support +the Lua project. + +

+start +· +contents +· +index +· +other versions +


+ +Copyright © 2006–2012 Lua.org, PUC-Rio. +Freely available under the terms of the +Lua license. + + +

Contents

+ + +

Index

+ + + + + + + +
+

Lua functions

+_G
+_VERSION
+

+ +assert
+collectgarbage
+dofile
+error
+getfenv
+getmetatable
+ipairs
+load
+loadfile
+loadstring
+module
+next
+pairs
+pcall
+print
+rawequal
+rawget
+rawset
+require
+select
+setfenv
+setmetatable
+tonumber
+tostring
+type
+unpack
+xpcall
+

+ +coroutine.create
+coroutine.resume
+coroutine.running
+coroutine.status
+coroutine.wrap
+coroutine.yield
+

+ +debug.debug
+debug.getfenv
+debug.gethook
+debug.getinfo
+debug.getlocal
+debug.getmetatable
+debug.getregistry
+debug.getupvalue
+debug.setfenv
+debug.sethook
+debug.setlocal
+debug.setmetatable
+debug.setupvalue
+debug.traceback
+ +

+

 

+file:close
+file:flush
+file:lines
+file:read
+file:seek
+file:setvbuf
+file:write
+

+ +io.close
+io.flush
+io.input
+io.lines
+io.open
+io.output
+io.popen
+io.read
+io.stderr
+io.stdin
+io.stdout
+io.tmpfile
+io.type
+io.write
+

+ +math.abs
+math.acos
+math.asin
+math.atan
+math.atan2
+math.ceil
+math.cos
+math.cosh
+math.deg
+math.exp
+math.floor
+math.fmod
+math.frexp
+math.huge
+math.ldexp
+math.log
+math.log10
+math.max
+math.min
+math.modf
+math.pi
+math.pow
+math.rad
+math.random
+math.randomseed
+math.sin
+math.sinh
+math.sqrt
+math.tan
+math.tanh
+

+ +os.clock
+os.date
+os.difftime
+os.execute
+os.exit
+os.getenv
+os.remove
+os.rename
+os.setlocale
+os.time
+os.tmpname
+

+ +package.cpath
+package.loaded
+package.loaders
+package.loadlib
+package.path
+package.preload
+package.seeall
+

+ +string.byte
+string.char
+string.dump
+string.find
+string.format
+string.gmatch
+string.gsub
+string.len
+string.lower
+string.match
+string.rep
+string.reverse
+string.sub
+string.upper
+

+ +table.concat
+table.insert
+table.maxn
+table.remove
+table.sort
+ +

+

C API

+lua_Alloc
+lua_CFunction
+lua_Debug
+lua_Hook
+lua_Integer
+lua_Number
+lua_Reader
+lua_State
+lua_Writer
+

+ +lua_atpanic
+lua_call
+lua_checkstack
+lua_close
+lua_concat
+lua_cpcall
+lua_createtable
+lua_dump
+lua_equal
+lua_error
+lua_gc
+lua_getallocf
+lua_getfenv
+lua_getfield
+lua_getglobal
+lua_gethook
+lua_gethookcount
+lua_gethookmask
+lua_getinfo
+lua_getlocal
+lua_getmetatable
+lua_getstack
+lua_gettable
+lua_gettop
+lua_getupvalue
+lua_insert
+lua_isboolean
+lua_iscfunction
+lua_isfunction
+lua_islightuserdata
+lua_isnil
+lua_isnone
+lua_isnoneornil
+lua_isnumber
+lua_isstring
+lua_istable
+lua_isthread
+lua_isuserdata
+lua_lessthan
+lua_load
+lua_newstate
+lua_newtable
+lua_newthread
+lua_newuserdata
+lua_next
+lua_objlen
+lua_pcall
+lua_pop
+lua_pushboolean
+lua_pushcclosure
+lua_pushcfunction
+lua_pushfstring
+lua_pushinteger
+lua_pushlightuserdata
+lua_pushliteral
+lua_pushlstring
+lua_pushnil
+lua_pushnumber
+lua_pushstring
+lua_pushthread
+lua_pushvalue
+lua_pushvfstring
+lua_rawequal
+lua_rawget
+lua_rawgeti
+lua_rawset
+lua_rawseti
+lua_register
+lua_remove
+lua_replace
+lua_resume
+lua_setallocf
+lua_setfenv
+lua_setfield
+lua_setglobal
+lua_sethook
+lua_setlocal
+lua_setmetatable
+lua_settable
+lua_settop
+lua_setupvalue
+lua_status
+lua_toboolean
+lua_tocfunction
+lua_tointeger
+lua_tolstring
+lua_tonumber
+lua_topointer
+lua_tostring
+lua_tothread
+lua_touserdata
+lua_type
+lua_typename
+lua_upvalueindex
+lua_xmove
+lua_yield
+ +

+

auxiliary library

+luaL_Buffer
+luaL_Reg
+

+ +luaL_addchar
+luaL_addlstring
+luaL_addsize
+luaL_addstring
+luaL_addvalue
+luaL_argcheck
+luaL_argerror
+luaL_buffinit
+luaL_callmeta
+luaL_checkany
+luaL_checkint
+luaL_checkinteger
+luaL_checklong
+luaL_checklstring
+luaL_checknumber
+luaL_checkoption
+luaL_checkstack
+luaL_checkstring
+luaL_checktype
+luaL_checkudata
+luaL_dofile
+luaL_dostring
+luaL_error
+luaL_getmetafield
+luaL_getmetatable
+luaL_gsub
+luaL_loadbuffer
+luaL_loadfile
+luaL_loadstring
+luaL_newmetatable
+luaL_newstate
+luaL_openlibs
+luaL_optint
+luaL_optinteger
+luaL_optlong
+luaL_optlstring
+luaL_optnumber
+luaL_optstring
+luaL_prepbuffer
+luaL_pushresult
+luaL_ref
+luaL_register
+luaL_typename
+luaL_typerror
+luaL_unref
+luaL_where
+ +

+

+ +


+ +Last update: +Mon Feb 13 18:53:32 BRST 2012 + + + + + diff --git a/extern/lua-5.1.5/doc/cover.png b/extern/lua-5.1.5/doc/cover.png new file mode 100644 index 00000000..2dbb1981 Binary files /dev/null and b/extern/lua-5.1.5/doc/cover.png differ diff --git a/extern/lua-5.1.5/doc/logo.gif b/extern/lua-5.1.5/doc/logo.gif new file mode 100644 index 00000000..2f5e4ac2 Binary files /dev/null and b/extern/lua-5.1.5/doc/logo.gif differ diff --git a/extern/lua-5.1.5/doc/lua.1 b/extern/lua-5.1.5/doc/lua.1 new file mode 100644 index 00000000..24809cc6 --- /dev/null +++ b/extern/lua-5.1.5/doc/lua.1 @@ -0,0 +1,163 @@ +.\" $Id: lua.man,v 1.11 2006/01/06 16:03:34 lhf Exp $ +.TH LUA 1 "$Date: 2006/01/06 16:03:34 $" +.SH NAME +lua \- Lua interpreter +.SH SYNOPSIS +.B lua +[ +.I options +] +[ +.I script +[ +.I args +] +] +.SH DESCRIPTION +.B lua +is the stand-alone Lua interpreter. +It loads and executes Lua programs, +either in textual source form or +in precompiled binary form. +(Precompiled binaries are output by +.BR luac , +the Lua compiler.) +.B lua +can be used as a batch interpreter and also interactively. +.LP +The given +.I options +(see below) +are executed and then +the Lua program in file +.I script +is loaded and executed. +The given +.I args +are available to +.I script +as strings in a global table named +.BR arg . +If these arguments contain spaces or other characters special to the shell, +then they should be quoted +(but note that the quotes will be removed by the shell). +The arguments in +.B arg +start at 0, +which contains the string +.RI ' script '. +The index of the last argument is stored in +.BR arg.n . +The arguments given in the command line before +.IR script , +including the name of the interpreter, +are available in negative indices in +.BR arg . +.LP +At the very start, +before even handling the command line, +.B lua +executes the contents of the environment variable +.BR LUA_INIT , +if it is defined. +If the value of +.B LUA_INIT +is of the form +.RI '@ filename ', +then +.I filename +is executed. +Otherwise, the string is assumed to be a Lua statement and is executed. +.LP +Options start with +.B '\-' +and are described below. +You can use +.B "'\--'" +to signal the end of options. +.LP +If no arguments are given, +then +.B "\-v \-i" +is assumed when the standard input is a terminal; +otherwise, +.B "\-" +is assumed. +.LP +In interactive mode, +.B lua +prompts the user, +reads lines from the standard input, +and executes them as they are read. +If a line does not contain a complete statement, +then a secondary prompt is displayed and +lines are read until a complete statement is formed or +a syntax error is found. +So, one way to interrupt the reading of an incomplete statement is +to force a syntax error: +adding a +.B ';' +in the middle of a statement is a sure way of forcing a syntax error +(except inside multiline strings and comments; these must be closed explicitly). +If a line starts with +.BR '=' , +then +.B lua +displays the values of all the expressions in the remainder of the +line. The expressions must be separated by commas. +The primary prompt is the value of the global variable +.BR _PROMPT , +if this value is a string; +otherwise, the default prompt is used. +Similarly, the secondary prompt is the value of the global variable +.BR _PROMPT2 . +So, +to change the prompts, +set the corresponding variable to a string of your choice. +You can do that after calling the interpreter +or on the command line +(but in this case you have to be careful with quotes +if the prompt string contains a space; otherwise you may confuse the shell.) +The default prompts are "> " and ">> ". +.SH OPTIONS +.TP +.B \- +load and execute the standard input as a file, +that is, +not interactively, +even when the standard input is a terminal. +.TP +.BI \-e " stat" +execute statement +.IR stat . +You need to quote +.I stat +if it contains spaces, quotes, +or other characters special to the shell. +.TP +.B \-i +enter interactive mode after +.I script +is executed. +.TP +.BI \-l " name" +call +.BI require(' name ') +before executing +.IR script . +Typically used to load libraries. +.TP +.B \-v +show version information. +.SH "SEE ALSO" +.BR luac (1) +.br +http://www.lua.org/ +.SH DIAGNOSTICS +Error messages should be self explanatory. +.SH AUTHORS +R. Ierusalimschy, +L. H. de Figueiredo, +and +W. Celes +.\" EOF diff --git a/extern/lua-5.1.5/doc/lua.css b/extern/lua-5.1.5/doc/lua.css new file mode 100644 index 00000000..7fafbb1b --- /dev/null +++ b/extern/lua-5.1.5/doc/lua.css @@ -0,0 +1,83 @@ +body { + color: #000000 ; + background-color: #FFFFFF ; + font-family: Helvetica, Arial, sans-serif ; + text-align: justify ; + margin-right: 30px ; + margin-left: 30px ; +} + +h1, h2, h3, h4 { + font-family: Verdana, Geneva, sans-serif ; + font-weight: normal ; + font-style: italic ; +} + +h2 { + padding-top: 0.4em ; + padding-bottom: 0.4em ; + padding-left: 30px ; + padding-right: 30px ; + margin-left: -30px ; + background-color: #E0E0FF ; +} + +h3 { + padding-left: 0.5em ; + border-left: solid #E0E0FF 1em ; +} + +table h3 { + padding-left: 0px ; + border-left: none ; +} + +a:link { + color: #000080 ; + background-color: inherit ; + text-decoration: none ; +} + +a:visited { + background-color: inherit ; + text-decoration: none ; +} + +a:link:hover, a:visited:hover { + color: #000080 ; + background-color: #E0E0FF ; +} + +a:link:active, a:visited:active { + color: #FF0000 ; +} + +hr { + border: 0 ; + height: 1px ; + color: #a0a0a0 ; + background-color: #a0a0a0 ; +} + +:target { + background-color: #F8F8F8 ; + padding: 8px ; + border: solid #a0a0a0 2px ; +} + +.footer { + color: gray ; + font-size: small ; +} + +input[type=text] { + border: solid #a0a0a0 2px ; + border-radius: 2em ; + -moz-border-radius: 2em ; + background-image: url('images/search.png') ; + background-repeat: no-repeat; + background-position: 4px center ; + padding-left: 20px ; + height: 2em ; +} + diff --git a/extern/lua-5.1.5/doc/lua.html b/extern/lua-5.1.5/doc/lua.html new file mode 100644 index 00000000..1d435ab0 --- /dev/null +++ b/extern/lua-5.1.5/doc/lua.html @@ -0,0 +1,172 @@ + + + +LUA man page + + + + + +

NAME

+lua - Lua interpreter +

SYNOPSIS

+lua +[ +options +] +[ +script +[ +args +] +] +

DESCRIPTION

+lua +is the stand-alone Lua interpreter. +It loads and executes Lua programs, +either in textual source form or +in precompiled binary form. +(Precompiled binaries are output by +luac, +the Lua compiler.) +lua +can be used as a batch interpreter and also interactively. +

+The given +options +(see below) +are executed and then +the Lua program in file +script +is loaded and executed. +The given +args +are available to +script +as strings in a global table named +arg. +If these arguments contain spaces or other characters special to the shell, +then they should be quoted +(but note that the quotes will be removed by the shell). +The arguments in +arg +start at 0, +which contains the string +'script'. +The index of the last argument is stored in +arg.n. +The arguments given in the command line before +script, +including the name of the interpreter, +are available in negative indices in +arg. +

+At the very start, +before even handling the command line, +lua +executes the contents of the environment variable +LUA_INIT, +if it is defined. +If the value of +LUA_INIT +is of the form +'@filename', +then +filename +is executed. +Otherwise, the string is assumed to be a Lua statement and is executed. +

+Options start with +'-' +and are described below. +You can use +'--' +to signal the end of options. +

+If no arguments are given, +then +"-v -i" +is assumed when the standard input is a terminal; +otherwise, +"-" +is assumed. +

+In interactive mode, +lua +prompts the user, +reads lines from the standard input, +and executes them as they are read. +If a line does not contain a complete statement, +then a secondary prompt is displayed and +lines are read until a complete statement is formed or +a syntax error is found. +So, one way to interrupt the reading of an incomplete statement is +to force a syntax error: +adding a +';' +in the middle of a statement is a sure way of forcing a syntax error +(except inside multiline strings and comments; these must be closed explicitly). +If a line starts with +'=', +then +lua +displays the values of all the expressions in the remainder of the +line. The expressions must be separated by commas. +The primary prompt is the value of the global variable +_PROMPT, +if this value is a string; +otherwise, the default prompt is used. +Similarly, the secondary prompt is the value of the global variable +_PROMPT2. +So, +to change the prompts, +set the corresponding variable to a string of your choice. +You can do that after calling the interpreter +or on the command line +(but in this case you have to be careful with quotes +if the prompt string contains a space; otherwise you may confuse the shell.) +The default prompts are "> " and ">> ". +

OPTIONS

+

+- +load and execute the standard input as a file, +that is, +not interactively, +even when the standard input is a terminal. +

+-e stat +execute statement +stat. +You need to quote +stat +if it contains spaces, quotes, +or other characters special to the shell. +

+-i +enter interactive mode after +script +is executed. +

+-l name +call +require('name') +before executing +script. +Typically used to load libraries. +

+-v +show version information. +

SEE ALSO

+luac(1) +
+http://www.lua.org/ +

DIAGNOSTICS

+Error messages should be self explanatory. +

AUTHORS

+R. Ierusalimschy, +L. H. de Figueiredo, +and +W. Celes + + + diff --git a/extern/lua-5.1.5/doc/luac.1 b/extern/lua-5.1.5/doc/luac.1 new file mode 100644 index 00000000..d8146782 --- /dev/null +++ b/extern/lua-5.1.5/doc/luac.1 @@ -0,0 +1,136 @@ +.\" $Id: luac.man,v 1.28 2006/01/06 16:03:34 lhf Exp $ +.TH LUAC 1 "$Date: 2006/01/06 16:03:34 $" +.SH NAME +luac \- Lua compiler +.SH SYNOPSIS +.B luac +[ +.I options +] [ +.I filenames +] +.SH DESCRIPTION +.B luac +is the Lua compiler. +It translates programs written in the Lua programming language +into binary files that can be later loaded and executed. +.LP +The main advantages of precompiling chunks are: +faster loading, +protecting source code from accidental user changes, +and +off-line syntax checking. +.LP +Pre-compiling does not imply faster execution +because in Lua chunks are always compiled into bytecodes before being executed. +.B luac +simply allows those bytecodes to be saved in a file for later execution. +.LP +Pre-compiled chunks are not necessarily smaller than the corresponding source. +The main goal in pre-compiling is faster loading. +.LP +The binary files created by +.B luac +are portable only among architectures with the same word size and byte order. +.LP +.B luac +produces a single output file containing the bytecodes +for all source files given. +By default, +the output file is named +.BR luac.out , +but you can change this with the +.B \-o +option. +.LP +In the command line, +you can mix +text files containing Lua source and +binary files containing precompiled chunks. +This is useful to combine several precompiled chunks, +even from different (but compatible) platforms, +into a single precompiled chunk. +.LP +You can use +.B "'\-'" +to indicate the standard input as a source file +and +.B "'\--'" +to signal the end of options +(that is, +all remaining arguments will be treated as files even if they start with +.BR "'\-'" ). +.LP +The internal format of the binary files produced by +.B luac +is likely to change when a new version of Lua is released. +So, +save the source files of all Lua programs that you precompile. +.LP +.SH OPTIONS +Options must be separate. +.TP +.B \-l +produce a listing of the compiled bytecode for Lua's virtual machine. +Listing bytecodes is useful to learn about Lua's virtual machine. +If no files are given, then +.B luac +loads +.B luac.out +and lists its contents. +.TP +.BI \-o " file" +output to +.IR file , +instead of the default +.BR luac.out . +(You can use +.B "'\-'" +for standard output, +but not on platforms that open standard output in text mode.) +The output file may be a source file because +all files are loaded before the output file is written. +Be careful not to overwrite precious files. +.TP +.B \-p +load files but do not generate any output file. +Used mainly for syntax checking and for testing precompiled chunks: +corrupted files will probably generate errors when loaded. +Lua always performs a thorough integrity test on precompiled chunks. +Bytecode that passes this test is completely safe, +in the sense that it will not break the interpreter. +However, +there is no guarantee that such code does anything sensible. +(None can be given, because the halting problem is unsolvable.) +If no files are given, then +.B luac +loads +.B luac.out +and tests its contents. +No messages are displayed if the file passes the integrity test. +.TP +.B \-s +strip debug information before writing the output file. +This saves some space in very large chunks, +but if errors occur when running a stripped chunk, +then the error messages may not contain the full information they usually do. +For instance, +line numbers and names of local variables are lost. +.TP +.B \-v +show version information. +.SH FILES +.TP 15 +.B luac.out +default output file +.SH "SEE ALSO" +.BR lua (1) +.br +http://www.lua.org/ +.SH DIAGNOSTICS +Error messages should be self explanatory. +.SH AUTHORS +L. H. de Figueiredo, +R. Ierusalimschy and +W. Celes +.\" EOF diff --git a/extern/lua-5.1.5/doc/luac.html b/extern/lua-5.1.5/doc/luac.html new file mode 100644 index 00000000..179ffe82 --- /dev/null +++ b/extern/lua-5.1.5/doc/luac.html @@ -0,0 +1,145 @@ + + + +LUAC man page + + + + + +

NAME

+luac - Lua compiler +

SYNOPSIS

+luac +[ +options +] [ +filenames +] +

DESCRIPTION

+luac +is the Lua compiler. +It translates programs written in the Lua programming language +into binary files that can be later loaded and executed. +

+The main advantages of precompiling chunks are: +faster loading, +protecting source code from accidental user changes, +and +off-line syntax checking. +

+Precompiling does not imply faster execution +because in Lua chunks are always compiled into bytecodes before being executed. +luac +simply allows those bytecodes to be saved in a file for later execution. +

+Precompiled chunks are not necessarily smaller than the corresponding source. +The main goal in precompiling is faster loading. +

+The binary files created by +luac +are portable only among architectures with the same word size and byte order. +

+luac +produces a single output file containing the bytecodes +for all source files given. +By default, +the output file is named +luac.out, +but you can change this with the +-o +option. +

+In the command line, +you can mix +text files containing Lua source and +binary files containing precompiled chunks. +This is useful because several precompiled chunks, +even from different (but compatible) platforms, +can be combined into a single precompiled chunk. +

+You can use +'-' +to indicate the standard input as a source file +and +'--' +to signal the end of options +(that is, +all remaining arguments will be treated as files even if they start with +'-'). +

+The internal format of the binary files produced by +luac +is likely to change when a new version of Lua is released. +So, +save the source files of all Lua programs that you precompile. +

+

OPTIONS

+Options must be separate. +

+-l +produce a listing of the compiled bytecode for Lua's virtual machine. +Listing bytecodes is useful to learn about Lua's virtual machine. +If no files are given, then +luac +loads +luac.out +and lists its contents. +

+-o file +output to +file, +instead of the default +luac.out. +(You can use +'-' +for standard output, +but not on platforms that open standard output in text mode.) +The output file may be a source file because +all files are loaded before the output file is written. +Be careful not to overwrite precious files. +

+-p +load files but do not generate any output file. +Used mainly for syntax checking and for testing precompiled chunks: +corrupted files will probably generate errors when loaded. +Lua always performs a thorough integrity test on precompiled chunks. +Bytecode that passes this test is completely safe, +in the sense that it will not break the interpreter. +However, +there is no guarantee that such code does anything sensible. +(None can be given, because the halting problem is unsolvable.) +If no files are given, then +luac +loads +luac.out +and tests its contents. +No messages are displayed if the file passes the integrity test. +

+-s +strip debug information before writing the output file. +This saves some space in very large chunks, +but if errors occur when running a stripped chunk, +then the error messages may not contain the full information they usually do. +For instance, +line numbers and names of local variables are lost. +

+-v +show version information. +

FILES

+

+luac.out +default output file +

SEE ALSO

+lua(1) +
+http://www.lua.org/ +

DIAGNOSTICS

+Error messages should be self explanatory. +

AUTHORS

+L. H. de Figueiredo, +R. Ierusalimschy and +W. Celes + + + diff --git a/extern/lua-5.1.5/doc/manual.css b/extern/lua-5.1.5/doc/manual.css new file mode 100644 index 00000000..b49b3629 --- /dev/null +++ b/extern/lua-5.1.5/doc/manual.css @@ -0,0 +1,24 @@ +h3 code { + font-family: inherit ; + font-size: inherit ; +} + +pre, code { + font-size: 12pt ; +} + +span.apii { + float: right ; + font-family: inherit ; + font-style: normal ; + font-size: small ; + color: gray ; +} + +p+h1, ul+h1 { + padding-top: 0.4em ; + padding-bottom: 0.4em ; + padding-left: 30px ; + margin-left: -30px ; + background-color: #E0E0FF ; +} diff --git a/extern/lua-5.1.5/doc/manual.html b/extern/lua-5.1.5/doc/manual.html new file mode 100644 index 00000000..4e41683d --- /dev/null +++ b/extern/lua-5.1.5/doc/manual.html @@ -0,0 +1,8804 @@ + + + + +Lua 5.1 Reference Manual + + + + + + + +
+

+ +Lua 5.1 Reference Manual +

+ +by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes +

+ +Copyright © 2006–2012 Lua.org, PUC-Rio. +Freely available under the terms of the +Lua license. + +


+

+ +contents +· +index +· +other versions + + +

+ + + + + + +

1 - Introduction

+ +

+Lua is an extension programming language designed to support +general procedural programming with data description +facilities. +It also offers good support for object-oriented programming, +functional programming, and data-driven programming. +Lua is intended to be used as a powerful, light-weight +scripting language for any program that needs one. +Lua is implemented as a library, written in clean C +(that is, in the common subset of ANSI C and C++). + + +

+Being an extension language, Lua has no notion of a "main" program: +it only works embedded in a host client, +called the embedding program or simply the host. +This host program can invoke functions to execute a piece of Lua code, +can write and read Lua variables, +and can register C functions to be called by Lua code. +Through the use of C functions, Lua can be augmented to cope with +a wide range of different domains, +thus creating customized programming languages sharing a syntactical framework. +The Lua distribution includes a sample host program called lua, +which uses the Lua library to offer a complete, stand-alone Lua interpreter. + + +

+Lua is free software, +and is provided as usual with no guarantees, +as stated in its license. +The implementation described in this manual is available +at Lua's official web site, www.lua.org. + + +

+Like any other reference manual, +this document is dry in places. +For a discussion of the decisions behind the design of Lua, +see the technical papers available at Lua's web site. +For a detailed introduction to programming in Lua, +see Roberto's book, Programming in Lua (Second Edition). + + + +

2 - The Language

+ +

+This section describes the lexis, the syntax, and the semantics of Lua. +In other words, +this section describes +which tokens are valid, +how they can be combined, +and what their combinations mean. + + +

+The language constructs will be explained using the usual extended BNF notation, +in which +{a} means 0 or more a's, and +[a] means an optional a. +Non-terminals are shown like non-terminal, +keywords are shown like kword, +and other terminal symbols are shown like `=´. +The complete syntax of Lua can be found in §8 +at the end of this manual. + + + +

2.1 - Lexical Conventions

+ +

+Names +(also called identifiers) +in Lua can be any string of letters, +digits, and underscores, +not beginning with a digit. +This coincides with the definition of names in most languages. +(The definition of letter depends on the current locale: +any character considered alphabetic by the current locale +can be used in an identifier.) +Identifiers are used to name variables and table fields. + + +

+The following keywords are reserved +and cannot be used as names: + + +

+     and       break     do        else      elseif
+     end       false     for       function  if
+     in        local     nil       not       or
+     repeat    return    then      true      until     while
+
+ +

+Lua is a case-sensitive language: +and is a reserved word, but And and AND +are two different, valid names. +As a convention, names starting with an underscore followed by +uppercase letters (such as _VERSION) +are reserved for internal global variables used by Lua. + + +

+The following strings denote other tokens: + +

+     +     -     *     /     %     ^     #
+     ==    ~=    <=    >=    <     >     =
+     (     )     {     }     [     ]
+     ;     :     ,     .     ..    ...
+
+ +

+Literal strings +can be delimited by matching single or double quotes, +and can contain the following C-like escape sequences: +'\a' (bell), +'\b' (backspace), +'\f' (form feed), +'\n' (newline), +'\r' (carriage return), +'\t' (horizontal tab), +'\v' (vertical tab), +'\\' (backslash), +'\"' (quotation mark [double quote]), +and '\'' (apostrophe [single quote]). +Moreover, a backslash followed by a real newline +results in a newline in the string. +A character in a string can also be specified by its numerical value +using the escape sequence \ddd, +where ddd is a sequence of up to three decimal digits. +(Note that if a numerical escape is to be followed by a digit, +it must be expressed using exactly three digits.) +Strings in Lua can contain any 8-bit value, including embedded zeros, +which can be specified as '\0'. + + +

+Literal strings can also be defined using a long format +enclosed by long brackets. +We define an opening long bracket of level n as an opening +square bracket followed by n equal signs followed by another +opening square bracket. +So, an opening long bracket of level 0 is written as [[, +an opening long bracket of level 1 is written as [=[, +and so on. +A closing long bracket is defined similarly; +for instance, a closing long bracket of level 4 is written as ]====]. +A long string starts with an opening long bracket of any level and +ends at the first closing long bracket of the same level. +Literals in this bracketed form can run for several lines, +do not interpret any escape sequences, +and ignore long brackets of any other level. +They can contain anything except a closing bracket of the proper level. + + +

+For convenience, +when the opening long bracket is immediately followed by a newline, +the newline is not included in the string. +As an example, in a system using ASCII +(in which 'a' is coded as 97, +newline is coded as 10, and '1' is coded as 49), +the five literal strings below denote the same string: + +

+     a = 'alo\n123"'
+     a = "alo\n123\""
+     a = '\97lo\10\04923"'
+     a = [[alo
+     123"]]
+     a = [==[
+     alo
+     123"]==]
+
+ +

+A numerical constant can be written with an optional decimal part +and an optional decimal exponent. +Lua also accepts integer hexadecimal constants, +by prefixing them with 0x. +Examples of valid numerical constants are + +

+     3   3.0   3.1416   314.16e-2   0.31416E1   0xff   0x56
+
+ +

+A comment starts with a double hyphen (--) +anywhere outside a string. +If the text immediately after -- is not an opening long bracket, +the comment is a short comment, +which runs until the end of the line. +Otherwise, it is a long comment, +which runs until the corresponding closing long bracket. +Long comments are frequently used to disable code temporarily. + + + + + +

2.2 - Values and Types

+ +

+Lua is a dynamically typed language. +This means that +variables do not have types; only values do. +There are no type definitions in the language. +All values carry their own type. + + +

+All values in Lua are first-class values. +This means that all values can be stored in variables, +passed as arguments to other functions, and returned as results. + + +

+There are eight basic types in Lua: +nil, boolean, number, +string, function, userdata, +thread, and table. +Nil is the type of the value nil, +whose main property is to be different from any other value; +it usually represents the absence of a useful value. +Boolean is the type of the values false and true. +Both nil and false make a condition false; +any other value makes it true. +Number represents real (double-precision floating-point) numbers. +(It is easy to build Lua interpreters that use other +internal representations for numbers, +such as single-precision float or long integers; +see file luaconf.h.) +String represents arrays of characters. + +Lua is 8-bit clean: +strings can contain any 8-bit character, +including embedded zeros ('\0') (see §2.1). + + +

+Lua can call (and manipulate) functions written in Lua and +functions written in C +(see §2.5.8). + + +

+The type userdata is provided to allow arbitrary C data to +be stored in Lua variables. +This type corresponds to a block of raw memory +and has no pre-defined operations in Lua, +except assignment and identity test. +However, by using metatables, +the programmer can define operations for userdata values +(see §2.8). +Userdata values cannot be created or modified in Lua, +only through the C API. +This guarantees the integrity of data owned by the host program. + + +

+The type thread represents independent threads of execution +and it is used to implement coroutines (see §2.11). +Do not confuse Lua threads with operating-system threads. +Lua supports coroutines on all systems, +even those that do not support threads. + + +

+The type table implements associative arrays, +that is, arrays that can be indexed not only with numbers, +but with any value (except nil). +Tables can be heterogeneous; +that is, they can contain values of all types (except nil). +Tables are the sole data structuring mechanism in Lua; +they can be used to represent ordinary arrays, +symbol tables, sets, records, graphs, trees, etc. +To represent records, Lua uses the field name as an index. +The language supports this representation by +providing a.name as syntactic sugar for a["name"]. +There are several convenient ways to create tables in Lua +(see §2.5.7). + + +

+Like indices, +the value of a table field can be of any type (except nil). +In particular, +because functions are first-class values, +table fields can contain functions. +Thus tables can also carry methods (see §2.5.9). + + +

+Tables, functions, threads, and (full) userdata values are objects: +variables do not actually contain these values, +only references to them. +Assignment, parameter passing, and function returns +always manipulate references to such values; +these operations do not imply any kind of copy. + + +

+The library function type returns a string describing the type +of a given value. + + + +

2.2.1 - Coercion

+ +

+Lua provides automatic conversion between +string and number values at run time. +Any arithmetic operation applied to a string tries to convert +this string to a number, following the usual conversion rules. +Conversely, whenever a number is used where a string is expected, +the number is converted to a string, in a reasonable format. +For complete control over how numbers are converted to strings, +use the format function from the string library +(see string.format). + + + + + + + +

2.3 - Variables

+ +

+Variables are places that store values. + +There are three kinds of variables in Lua: +global variables, local variables, and table fields. + + +

+A single name can denote a global variable or a local variable +(or a function's formal parameter, +which is a particular kind of local variable): + +

+	var ::= Name
+

+Name denotes identifiers, as defined in §2.1. + + +

+Any variable is assumed to be global unless explicitly declared +as a local (see §2.4.7). +Local variables are lexically scoped: +local variables can be freely accessed by functions +defined inside their scope (see §2.6). + + +

+Before the first assignment to a variable, its value is nil. + + +

+Square brackets are used to index a table: + +

+	var ::= prefixexp `[´ exp `]´
+

+The meaning of accesses to global variables +and table fields can be changed via metatables. +An access to an indexed variable t[i] is equivalent to +a call gettable_event(t,i). +(See §2.8 for a complete description of the +gettable_event function. +This function is not defined or callable in Lua. +We use it here only for explanatory purposes.) + + +

+The syntax var.Name is just syntactic sugar for +var["Name"]: + +

+	var ::= prefixexp `.´ Name
+
+ +

+All global variables live as fields in ordinary Lua tables, +called environment tables or simply +environments (see §2.9). +Each function has its own reference to an environment, +so that all global variables in this function +will refer to this environment table. +When a function is created, +it inherits the environment from the function that created it. +To get the environment table of a Lua function, +you call getfenv. +To replace it, +you call setfenv. +(You can only manipulate the environment of C functions +through the debug library; (see §5.9).) + + +

+An access to a global variable x +is equivalent to _env.x, +which in turn is equivalent to + +

+     gettable_event(_env, "x")
+

+where _env is the environment of the running function. +(See §2.8 for a complete description of the +gettable_event function. +This function is not defined or callable in Lua. +Similarly, the _env variable is not defined in Lua. +We use them here only for explanatory purposes.) + + + + + +

2.4 - Statements

+ +

+Lua supports an almost conventional set of statements, +similar to those in Pascal or C. +This set includes +assignments, control structures, function calls, +and variable declarations. + + + +

2.4.1 - Chunks

+ +

+The unit of execution of Lua is called a chunk. +A chunk is simply a sequence of statements, +which are executed sequentially. +Each statement can be optionally followed by a semicolon: + +

+	chunk ::= {stat [`;´]}
+

+There are no empty statements and thus ';;' is not legal. + + +

+Lua handles a chunk as the body of an anonymous function +with a variable number of arguments +(see §2.5.9). +As such, chunks can define local variables, +receive arguments, and return values. + + +

+A chunk can be stored in a file or in a string inside the host program. +To execute a chunk, +Lua first pre-compiles the chunk into instructions for a virtual machine, +and then it executes the compiled code +with an interpreter for the virtual machine. + + +

+Chunks can also be pre-compiled into binary form; +see program luac for details. +Programs in source and compiled forms are interchangeable; +Lua automatically detects the file type and acts accordingly. + + + + + + +

2.4.2 - Blocks

+A block is a list of statements; +syntactically, a block is the same as a chunk: + +

+	block ::= chunk
+
+ +

+A block can be explicitly delimited to produce a single statement: + +

+	stat ::= do block end
+

+Explicit blocks are useful +to control the scope of variable declarations. +Explicit blocks are also sometimes used to +add a return or break statement in the middle +of another block (see §2.4.4). + + + + + +

2.4.3 - Assignment

+ +

+Lua allows multiple assignments. +Therefore, the syntax for assignment +defines a list of variables on the left side +and a list of expressions on the right side. +The elements in both lists are separated by commas: + +

+	stat ::= varlist `=´ explist
+	varlist ::= var {`,´ var}
+	explist ::= exp {`,´ exp}
+

+Expressions are discussed in §2.5. + + +

+Before the assignment, +the list of values is adjusted to the length of +the list of variables. +If there are more values than needed, +the excess values are thrown away. +If there are fewer values than needed, +the list is extended with as many nil's as needed. +If the list of expressions ends with a function call, +then all values returned by that call enter the list of values, +before the adjustment +(except when the call is enclosed in parentheses; see §2.5). + + +

+The assignment statement first evaluates all its expressions +and only then are the assignments performed. +Thus the code + +

+     i = 3
+     i, a[i] = i+1, 20
+

+sets a[3] to 20, without affecting a[4] +because the i in a[i] is evaluated (to 3) +before it is assigned 4. +Similarly, the line + +

+     x, y = y, x
+

+exchanges the values of x and y, +and + +

+     x, y, z = y, z, x
+

+cyclically permutes the values of x, y, and z. + + +

+The meaning of assignments to global variables +and table fields can be changed via metatables. +An assignment to an indexed variable t[i] = val is equivalent to +settable_event(t,i,val). +(See §2.8 for a complete description of the +settable_event function. +This function is not defined or callable in Lua. +We use it here only for explanatory purposes.) + + +

+An assignment to a global variable x = val +is equivalent to the assignment +_env.x = val, +which in turn is equivalent to + +

+     settable_event(_env, "x", val)
+

+where _env is the environment of the running function. +(The _env variable is not defined in Lua. +We use it here only for explanatory purposes.) + + + + + +

2.4.4 - Control Structures

+The control structures +if, while, and repeat have the usual meaning and +familiar syntax: + + + + +

+	stat ::= while exp do block end
+	stat ::= repeat block until exp
+	stat ::= if exp then block {elseif exp then block} [else block] end
+

+Lua also has a for statement, in two flavors (see §2.4.5). + + +

+The condition expression of a +control structure can return any value. +Both false and nil are considered false. +All values different from nil and false are considered true +(in particular, the number 0 and the empty string are also true). + + +

+In the repeatuntil loop, +the inner block does not end at the until keyword, +but only after the condition. +So, the condition can refer to local variables +declared inside the loop block. + + +

+The return statement is used to return values +from a function or a chunk (which is just a function). + +Functions and chunks can return more than one value, +and so the syntax for the return statement is + +

+	stat ::= return [explist]
+
+ +

+The break statement is used to terminate the execution of a +while, repeat, or for loop, +skipping to the next statement after the loop: + + +

+	stat ::= break
+

+A break ends the innermost enclosing loop. + + +

+The return and break +statements can only be written as the last statement of a block. +If it is really necessary to return or break in the +middle of a block, +then an explicit inner block can be used, +as in the idioms +do return end and do break end, +because now return and break are the last statements in +their (inner) blocks. + + + + + +

2.4.5 - For Statement

+ +

+ +The for statement has two forms: +one numeric and one generic. + + +

+The numeric for loop repeats a block of code while a +control variable runs through an arithmetic progression. +It has the following syntax: + +

+	stat ::= for Name `=´ exp `,´ exp [`,´ exp] do block end
+

+The block is repeated for name starting at the value of +the first exp, until it passes the second exp by steps of the +third exp. +More precisely, a for statement like + +

+     for v = e1, e2, e3 do block end
+

+is equivalent to the code: + +

+     do
+       local var, limit, step = tonumber(e1), tonumber(e2), tonumber(e3)
+       if not (var and limit and step) then error() end
+       while (step > 0 and var <= limit) or (step <= 0 and var >= limit) do
+         local v = var
+         block
+         var = var + step
+       end
+     end
+

+Note the following: + +

+ +

+The generic for statement works over functions, +called iterators. +On each iteration, the iterator function is called to produce a new value, +stopping when this new value is nil. +The generic for loop has the following syntax: + +

+	stat ::= for namelist in explist do block end
+	namelist ::= Name {`,´ Name}
+

+A for statement like + +

+     for var_1, ···, var_n in explist do block end
+

+is equivalent to the code: + +

+     do
+       local f, s, var = explist
+       while true do
+         local var_1, ···, var_n = f(s, var)
+         var = var_1
+         if var == nil then break end
+         block
+       end
+     end
+

+Note the following: + +

+ + + + +

2.4.6 - Function Calls as Statements

+To allow possible side-effects, +function calls can be executed as statements: + +

+	stat ::= functioncall
+

+In this case, all returned values are thrown away. +Function calls are explained in §2.5.8. + + + + + +

2.4.7 - Local Declarations

+Local variables can be declared anywhere inside a block. +The declaration can include an initial assignment: + +

+	stat ::= local namelist [`=´ explist]
+

+If present, an initial assignment has the same semantics +of a multiple assignment (see §2.4.3). +Otherwise, all variables are initialized with nil. + + +

+A chunk is also a block (see §2.4.1), +and so local variables can be declared in a chunk outside any explicit block. +The scope of such local variables extends until the end of the chunk. + + +

+The visibility rules for local variables are explained in §2.6. + + + + + + + +

2.5 - Expressions

+ +

+The basic expressions in Lua are the following: + +

+	exp ::= prefixexp
+	exp ::= nil | false | true
+	exp ::= Number
+	exp ::= String
+	exp ::= function
+	exp ::= tableconstructor
+	exp ::= `...´
+	exp ::= exp binop exp
+	exp ::= unop exp
+	prefixexp ::= var | functioncall | `(´ exp `)´
+
+ +

+Numbers and literal strings are explained in §2.1; +variables are explained in §2.3; +function definitions are explained in §2.5.9; +function calls are explained in §2.5.8; +table constructors are explained in §2.5.7. +Vararg expressions, +denoted by three dots ('...'), can only be used when +directly inside a vararg function; +they are explained in §2.5.9. + + +

+Binary operators comprise arithmetic operators (see §2.5.1), +relational operators (see §2.5.2), logical operators (see §2.5.3), +and the concatenation operator (see §2.5.4). +Unary operators comprise the unary minus (see §2.5.1), +the unary not (see §2.5.3), +and the unary length operator (see §2.5.5). + + +

+Both function calls and vararg expressions can result in multiple values. +If an expression is used as a statement +(only possible for function calls (see §2.4.6)), +then its return list is adjusted to zero elements, +thus discarding all returned values. +If an expression is used as the last (or the only) element +of a list of expressions, +then no adjustment is made +(unless the call is enclosed in parentheses). +In all other contexts, +Lua adjusts the result list to one element, +discarding all values except the first one. + + +

+Here are some examples: + +

+     f()                -- adjusted to 0 results
+     g(f(), x)          -- f() is adjusted to 1 result
+     g(x, f())          -- g gets x plus all results from f()
+     a,b,c = f(), x     -- f() is adjusted to 1 result (c gets nil)
+     a,b = ...          -- a gets the first vararg parameter, b gets
+                        -- the second (both a and b can get nil if there
+                        -- is no corresponding vararg parameter)
+     
+     a,b,c = x, f()     -- f() is adjusted to 2 results
+     a,b,c = f()        -- f() is adjusted to 3 results
+     return f()         -- returns all results from f()
+     return ...         -- returns all received vararg parameters
+     return x,y,f()     -- returns x, y, and all results from f()
+     {f()}              -- creates a list with all results from f()
+     {...}              -- creates a list with all vararg parameters
+     {f(), nil}         -- f() is adjusted to 1 result
+
+ +

+Any expression enclosed in parentheses always results in only one value. +Thus, +(f(x,y,z)) is always a single value, +even if f returns several values. +(The value of (f(x,y,z)) is the first value returned by f +or nil if f does not return any values.) + + + +

2.5.1 - Arithmetic Operators

+Lua supports the usual arithmetic operators: +the binary + (addition), +- (subtraction), * (multiplication), +/ (division), % (modulo), and ^ (exponentiation); +and unary - (negation). +If the operands are numbers, or strings that can be converted to +numbers (see §2.2.1), +then all operations have the usual meaning. +Exponentiation works for any exponent. +For instance, x^(-0.5) computes the inverse of the square root of x. +Modulo is defined as + +

+     a % b == a - math.floor(a/b)*b
+

+That is, it is the remainder of a division that rounds +the quotient towards minus infinity. + + + + + +

2.5.2 - Relational Operators

+The relational operators in Lua are + +

+     ==    ~=    <     >     <=    >=
+

+These operators always result in false or true. + + +

+Equality (==) first compares the type of its operands. +If the types are different, then the result is false. +Otherwise, the values of the operands are compared. +Numbers and strings are compared in the usual way. +Objects (tables, userdata, threads, and functions) +are compared by reference: +two objects are considered equal only if they are the same object. +Every time you create a new object +(a table, userdata, thread, or function), +this new object is different from any previously existing object. + + +

+You can change the way that Lua compares tables and userdata +by using the "eq" metamethod (see §2.8). + + +

+The conversion rules of §2.2.1 +do not apply to equality comparisons. +Thus, "0"==0 evaluates to false, +and t[0] and t["0"] denote different +entries in a table. + + +

+The operator ~= is exactly the negation of equality (==). + + +

+The order operators work as follows. +If both arguments are numbers, then they are compared as such. +Otherwise, if both arguments are strings, +then their values are compared according to the current locale. +Otherwise, Lua tries to call the "lt" or the "le" +metamethod (see §2.8). +A comparison a > b is translated to b < a +and a >= b is translated to b <= a. + + + + + +

2.5.3 - Logical Operators

+The logical operators in Lua are +and, or, and not. +Like the control structures (see §2.4.4), +all logical operators consider both false and nil as false +and anything else as true. + + +

+The negation operator not always returns false or true. +The conjunction operator and returns its first argument +if this value is false or nil; +otherwise, and returns its second argument. +The disjunction operator or returns its first argument +if this value is different from nil and false; +otherwise, or returns its second argument. +Both and and or use short-cut evaluation; +that is, +the second operand is evaluated only if necessary. +Here are some examples: + +

+     10 or 20            --> 10
+     10 or error()       --> 10
+     nil or "a"          --> "a"
+     nil and 10          --> nil
+     false and error()   --> false
+     false and nil       --> false
+     false or nil        --> nil
+     10 and 20           --> 20
+

+(In this manual, +--> indicates the result of the preceding expression.) + + + + + +

2.5.4 - Concatenation

+The string concatenation operator in Lua is +denoted by two dots ('..'). +If both operands are strings or numbers, then they are converted to +strings according to the rules mentioned in §2.2.1. +Otherwise, the "concat" metamethod is called (see §2.8). + + + + + +

2.5.5 - The Length Operator

+ +

+The length operator is denoted by the unary operator #. +The length of a string is its number of bytes +(that is, the usual meaning of string length when each +character is one byte). + + +

+The length of a table t is defined to be any +integer index n +such that t[n] is not nil and t[n+1] is nil; +moreover, if t[1] is nil, n can be zero. +For a regular array, with non-nil values from 1 to a given n, +its length is exactly that n, +the index of its last value. +If the array has "holes" +(that is, nil values between other non-nil values), +then #t can be any of the indices that +directly precedes a nil value +(that is, it may consider any such nil value as the end of +the array). + + + + + +

2.5.6 - Precedence

+Operator precedence in Lua follows the table below, +from lower to higher priority: + +

+     or
+     and
+     <     >     <=    >=    ~=    ==
+     ..
+     +     -
+     *     /     %
+     not   #     - (unary)
+     ^
+

+As usual, +you can use parentheses to change the precedences of an expression. +The concatenation ('..') and exponentiation ('^') +operators are right associative. +All other binary operators are left associative. + + + + + +

2.5.7 - Table Constructors

+Table constructors are expressions that create tables. +Every time a constructor is evaluated, a new table is created. +A constructor can be used to create an empty table +or to create a table and initialize some of its fields. +The general syntax for constructors is + +

+	tableconstructor ::= `{´ [fieldlist] `}´
+	fieldlist ::= field {fieldsep field} [fieldsep]
+	field ::= `[´ exp `]´ `=´ exp | Name `=´ exp | exp
+	fieldsep ::= `,´ | `;´
+
+ +

+Each field of the form [exp1] = exp2 adds to the new table an entry +with key exp1 and value exp2. +A field of the form name = exp is equivalent to +["name"] = exp. +Finally, fields of the form exp are equivalent to +[i] = exp, where i are consecutive numerical integers, +starting with 1. +Fields in the other formats do not affect this counting. +For example, + +

+     a = { [f(1)] = g; "x", "y"; x = 1, f(x), [30] = 23; 45 }
+

+is equivalent to + +

+     do
+       local t = {}
+       t[f(1)] = g
+       t[1] = "x"         -- 1st exp
+       t[2] = "y"         -- 2nd exp
+       t.x = 1            -- t["x"] = 1
+       t[3] = f(x)        -- 3rd exp
+       t[30] = 23
+       t[4] = 45          -- 4th exp
+       a = t
+     end
+
+ +

+If the last field in the list has the form exp +and the expression is a function call or a vararg expression, +then all values returned by this expression enter the list consecutively +(see §2.5.8). +To avoid this, +enclose the function call or the vararg expression +in parentheses (see §2.5). + + +

+The field list can have an optional trailing separator, +as a convenience for machine-generated code. + + + + + +

2.5.8 - Function Calls

+A function call in Lua has the following syntax: + +

+	functioncall ::= prefixexp args
+

+In a function call, +first prefixexp and args are evaluated. +If the value of prefixexp has type function, +then this function is called +with the given arguments. +Otherwise, the prefixexp "call" metamethod is called, +having as first parameter the value of prefixexp, +followed by the original call arguments +(see §2.8). + + +

+The form + +

+	functioncall ::= prefixexp `:´ Name args
+

+can be used to call "methods". +A call v:name(args) +is syntactic sugar for v.name(v,args), +except that v is evaluated only once. + + +

+Arguments have the following syntax: + +

+	args ::= `(´ [explist] `)´
+	args ::= tableconstructor
+	args ::= String
+

+All argument expressions are evaluated before the call. +A call of the form f{fields} is +syntactic sugar for f({fields}); +that is, the argument list is a single new table. +A call of the form f'string' +(or f"string" or f[[string]]) +is syntactic sugar for f('string'); +that is, the argument list is a single literal string. + + +

+As an exception to the free-format syntax of Lua, +you cannot put a line break before the '(' in a function call. +This restriction avoids some ambiguities in the language. +If you write + +

+     a = f
+     (g).x(a)
+

+Lua would see that as a single statement, a = f(g).x(a). +So, if you want two statements, you must add a semi-colon between them. +If you actually want to call f, +you must remove the line break before (g). + + +

+A call of the form return functioncall is called +a tail call. +Lua implements proper tail calls +(or proper tail recursion): +in a tail call, +the called function reuses the stack entry of the calling function. +Therefore, there is no limit on the number of nested tail calls that +a program can execute. +However, a tail call erases any debug information about the +calling function. +Note that a tail call only happens with a particular syntax, +where the return has one single function call as argument; +this syntax makes the calling function return exactly +the returns of the called function. +So, none of the following examples are tail calls: + +

+     return (f(x))        -- results adjusted to 1
+     return 2 * f(x)
+     return x, f(x)       -- additional results
+     f(x); return         -- results discarded
+     return x or f(x)     -- results adjusted to 1
+
+ + + + +

2.5.9 - Function Definitions

+ +

+The syntax for function definition is + +

+	function ::= function funcbody
+	funcbody ::= `(´ [parlist] `)´ block end
+
+ +

+The following syntactic sugar simplifies function definitions: + +

+	stat ::= function funcname funcbody
+	stat ::= local function Name funcbody
+	funcname ::= Name {`.´ Name} [`:´ Name]
+

+The statement + +

+     function f () body end
+

+translates to + +

+     f = function () body end
+

+The statement + +

+     function t.a.b.c.f () body end
+

+translates to + +

+     t.a.b.c.f = function () body end
+

+The statement + +

+     local function f () body end
+

+translates to + +

+     local f; f = function () body end
+

+not to + +

+     local f = function () body end
+

+(This only makes a difference when the body of the function +contains references to f.) + + +

+A function definition is an executable expression, +whose value has type function. +When Lua pre-compiles a chunk, +all its function bodies are pre-compiled too. +Then, whenever Lua executes the function definition, +the function is instantiated (or closed). +This function instance (or closure) +is the final value of the expression. +Different instances of the same function +can refer to different external local variables +and can have different environment tables. + + +

+Parameters act as local variables that are +initialized with the argument values: + +

+	parlist ::= namelist [`,´ `...´] | `...´
+

+When a function is called, +the list of arguments is adjusted to +the length of the list of parameters, +unless the function is a variadic or vararg function, +which is +indicated by three dots ('...') at the end of its parameter list. +A vararg function does not adjust its argument list; +instead, it collects all extra arguments and supplies them +to the function through a vararg expression, +which is also written as three dots. +The value of this expression is a list of all actual extra arguments, +similar to a function with multiple results. +If a vararg expression is used inside another expression +or in the middle of a list of expressions, +then its return list is adjusted to one element. +If the expression is used as the last element of a list of expressions, +then no adjustment is made +(unless that last expression is enclosed in parentheses). + + +

+As an example, consider the following definitions: + +

+     function f(a, b) end
+     function g(a, b, ...) end
+     function r() return 1,2,3 end
+

+Then, we have the following mapping from arguments to parameters and +to the vararg expression: + +

+     CALL            PARAMETERS
+     
+     f(3)             a=3, b=nil
+     f(3, 4)          a=3, b=4
+     f(3, 4, 5)       a=3, b=4
+     f(r(), 10)       a=1, b=10
+     f(r())           a=1, b=2
+     
+     g(3)             a=3, b=nil, ... -->  (nothing)
+     g(3, 4)          a=3, b=4,   ... -->  (nothing)
+     g(3, 4, 5, 8)    a=3, b=4,   ... -->  5  8
+     g(5, r())        a=5, b=1,   ... -->  2  3
+
+ +

+Results are returned using the return statement (see §2.4.4). +If control reaches the end of a function +without encountering a return statement, +then the function returns with no results. + + +

+The colon syntax +is used for defining methods, +that is, functions that have an implicit extra parameter self. +Thus, the statement + +

+     function t.a.b.c:f (params) body end
+

+is syntactic sugar for + +

+     t.a.b.c.f = function (self, params) body end
+
+ + + + + + +

2.6 - Visibility Rules

+ +

+ +Lua is a lexically scoped language. +The scope of variables begins at the first statement after +their declaration and lasts until the end of the innermost block that +includes the declaration. +Consider the following example: + +

+     x = 10                -- global variable
+     do                    -- new block
+       local x = x         -- new 'x', with value 10
+       print(x)            --> 10
+       x = x+1
+       do                  -- another block
+         local x = x+1     -- another 'x'
+         print(x)          --> 12
+       end
+       print(x)            --> 11
+     end
+     print(x)              --> 10  (the global one)
+
+ +

+Notice that, in a declaration like local x = x, +the new x being declared is not in scope yet, +and so the second x refers to the outside variable. + + +

+Because of the lexical scoping rules, +local variables can be freely accessed by functions +defined inside their scope. +A local variable used by an inner function is called +an upvalue, or external local variable, +inside the inner function. + + +

+Notice that each execution of a local statement +defines new local variables. +Consider the following example: + +

+     a = {}
+     local x = 20
+     for i=1,10 do
+       local y = 0
+       a[i] = function () y=y+1; return x+y end
+     end
+

+The loop creates ten closures +(that is, ten instances of the anonymous function). +Each of these closures uses a different y variable, +while all of them share the same x. + + + + + +

2.7 - Error Handling

+ +

+Because Lua is an embedded extension language, +all Lua actions start from C code in the host program +calling a function from the Lua library (see lua_pcall). +Whenever an error occurs during Lua compilation or execution, +control returns to C, +which can take appropriate measures +(such as printing an error message). + + +

+Lua code can explicitly generate an error by calling the +error function. +If you need to catch errors in Lua, +you can use the pcall function. + + + + + +

2.8 - Metatables

+ +

+Every value in Lua can have a metatable. +This metatable is an ordinary Lua table +that defines the behavior of the original value +under certain special operations. +You can change several aspects of the behavior +of operations over a value by setting specific fields in its metatable. +For instance, when a non-numeric value is the operand of an addition, +Lua checks for a function in the field "__add" in its metatable. +If it finds one, +Lua calls this function to perform the addition. + + +

+We call the keys in a metatable events +and the values metamethods. +In the previous example, the event is "add" +and the metamethod is the function that performs the addition. + + +

+You can query the metatable of any value +through the getmetatable function. + + +

+You can replace the metatable of tables +through the setmetatable +function. +You cannot change the metatable of other types from Lua +(except by using the debug library); +you must use the C API for that. + + +

+Tables and full userdata have individual metatables +(although multiple tables and userdata can share their metatables). +Values of all other types share one single metatable per type; +that is, there is one single metatable for all numbers, +one for all strings, etc. + + +

+A metatable controls how an object behaves in arithmetic operations, +order comparisons, concatenation, length operation, and indexing. +A metatable also can define a function to be called when a userdata +is garbage collected. +For each of these operations Lua associates a specific key +called an event. +When Lua performs one of these operations over a value, +it checks whether this value has a metatable with the corresponding event. +If so, the value associated with that key (the metamethod) +controls how Lua will perform the operation. + + +

+Metatables control the operations listed next. +Each operation is identified by its corresponding name. +The key for each operation is a string with its name prefixed by +two underscores, '__'; +for instance, the key for operation "add" is the +string "__add". +The semantics of these operations is better explained by a Lua function +describing how the interpreter executes the operation. + + +

+The code shown here in Lua is only illustrative; +the real behavior is hard coded in the interpreter +and it is much more efficient than this simulation. +All functions used in these descriptions +(rawget, tonumber, etc.) +are described in §5.1. +In particular, to retrieve the metamethod of a given object, +we use the expression + +

+     metatable(obj)[event]
+

+This should be read as + +

+     rawget(getmetatable(obj) or {}, event)
+

+ +That is, the access to a metamethod does not invoke other metamethods, +and the access to objects with no metatables does not fail +(it simply results in nil). + + + +

+ + + + +

2.9 - Environments

+ +

+Besides metatables, +objects of types thread, function, and userdata +have another table associated with them, +called their environment. +Like metatables, environments are regular tables and +multiple objects can share the same environment. + + +

+Threads are created sharing the environment of the creating thread. +Userdata and C functions are created sharing the environment +of the creating C function. +Non-nested Lua functions +(created by loadfile, loadstring or load) +are created sharing the environment of the creating thread. +Nested Lua functions are created sharing the environment of +the creating Lua function. + + +

+Environments associated with userdata have no meaning for Lua. +It is only a convenience feature for programmers to associate a table to +a userdata. + + +

+Environments associated with threads are called +global environments. +They are used as the default environment for threads and +non-nested Lua functions created by the thread +and can be directly accessed by C code (see §3.3). + + +

+The environment associated with a C function can be directly +accessed by C code (see §3.3). +It is used as the default environment for other C functions +and userdata created by the function. + + +

+Environments associated with Lua functions are used to resolve +all accesses to global variables within the function (see §2.3). +They are used as the default environment for nested Lua functions +created by the function. + + +

+You can change the environment of a Lua function or the +running thread by calling setfenv. +You can get the environment of a Lua function or the running thread +by calling getfenv. +To manipulate the environment of other objects +(userdata, C functions, other threads) you must +use the C API. + + + + + +

2.10 - Garbage Collection

+ +

+Lua performs automatic memory management. +This means that +you have to worry neither about allocating memory for new objects +nor about freeing it when the objects are no longer needed. +Lua manages memory automatically by running +a garbage collector from time to time +to collect all dead objects +(that is, objects that are no longer accessible from Lua). +All memory used by Lua is subject to automatic management: +tables, userdata, functions, threads, strings, etc. + + +

+Lua implements an incremental mark-and-sweep collector. +It uses two numbers to control its garbage-collection cycles: +the garbage-collector pause and +the garbage-collector step multiplier. +Both use percentage points as units +(so that a value of 100 means an internal value of 1). + + +

+The garbage-collector pause +controls how long the collector waits before starting a new cycle. +Larger values make the collector less aggressive. +Values smaller than 100 mean the collector will not wait to +start a new cycle. +A value of 200 means that the collector waits for the total memory in use +to double before starting a new cycle. + + +

+The step multiplier +controls the relative speed of the collector relative to +memory allocation. +Larger values make the collector more aggressive but also increase +the size of each incremental step. +Values smaller than 100 make the collector too slow and +can result in the collector never finishing a cycle. +The default, 200, means that the collector runs at "twice" +the speed of memory allocation. + + +

+You can change these numbers by calling lua_gc in C +or collectgarbage in Lua. +With these functions you can also control +the collector directly (e.g., stop and restart it). + + + +

2.10.1 - Garbage-Collection Metamethods

+ +

+Using the C API, +you can set garbage-collector metamethods for userdata (see §2.8). +These metamethods are also called finalizers. +Finalizers allow you to coordinate Lua's garbage collection +with external resource management +(such as closing files, network or database connections, +or freeing your own memory). + + +

+Garbage userdata with a field __gc in their metatables are not +collected immediately by the garbage collector. +Instead, Lua puts them in a list. +After the collection, +Lua does the equivalent of the following function +for each userdata in that list: + +

+     function gc_event (udata)
+       local h = metatable(udata).__gc
+       if h then
+         h(udata)
+       end
+     end
+
+ +

+At the end of each garbage-collection cycle, +the finalizers for userdata are called in reverse +order of their creation, +among those collected in that cycle. +That is, the first finalizer to be called is the one associated +with the userdata created last in the program. +The userdata itself is freed only in the next garbage-collection cycle. + + + + + +

2.10.2 - Weak Tables

+ +

+A weak table is a table whose elements are +weak references. +A weak reference is ignored by the garbage collector. +In other words, +if the only references to an object are weak references, +then the garbage collector will collect this object. + + +

+A weak table can have weak keys, weak values, or both. +A table with weak keys allows the collection of its keys, +but prevents the collection of its values. +A table with both weak keys and weak values allows the collection of +both keys and values. +In any case, if either the key or the value is collected, +the whole pair is removed from the table. +The weakness of a table is controlled by the +__mode field of its metatable. +If the __mode field is a string containing the character 'k', +the keys in the table are weak. +If __mode contains 'v', +the values in the table are weak. + + +

+After you use a table as a metatable, +you should not change the value of its __mode field. +Otherwise, the weak behavior of the tables controlled by this +metatable is undefined. + + + + + + + +

2.11 - Coroutines

+ +

+Lua supports coroutines, +also called collaborative multithreading. +A coroutine in Lua represents an independent thread of execution. +Unlike threads in multithread systems, however, +a coroutine only suspends its execution by explicitly calling +a yield function. + + +

+You create a coroutine with a call to coroutine.create. +Its sole argument is a function +that is the main function of the coroutine. +The create function only creates a new coroutine and +returns a handle to it (an object of type thread); +it does not start the coroutine execution. + + +

+When you first call coroutine.resume, +passing as its first argument +a thread returned by coroutine.create, +the coroutine starts its execution, +at the first line of its main function. +Extra arguments passed to coroutine.resume are passed on +to the coroutine main function. +After the coroutine starts running, +it runs until it terminates or yields. + + +

+A coroutine can terminate its execution in two ways: +normally, when its main function returns +(explicitly or implicitly, after the last instruction); +and abnormally, if there is an unprotected error. +In the first case, coroutine.resume returns true, +plus any values returned by the coroutine main function. +In case of errors, coroutine.resume returns false +plus an error message. + + +

+A coroutine yields by calling coroutine.yield. +When a coroutine yields, +the corresponding coroutine.resume returns immediately, +even if the yield happens inside nested function calls +(that is, not in the main function, +but in a function directly or indirectly called by the main function). +In the case of a yield, coroutine.resume also returns true, +plus any values passed to coroutine.yield. +The next time you resume the same coroutine, +it continues its execution from the point where it yielded, +with the call to coroutine.yield returning any extra +arguments passed to coroutine.resume. + + +

+Like coroutine.create, +the coroutine.wrap function also creates a coroutine, +but instead of returning the coroutine itself, +it returns a function that, when called, resumes the coroutine. +Any arguments passed to this function +go as extra arguments to coroutine.resume. +coroutine.wrap returns all the values returned by coroutine.resume, +except the first one (the boolean error code). +Unlike coroutine.resume, +coroutine.wrap does not catch errors; +any error is propagated to the caller. + + +

+As an example, +consider the following code: + +

+     function foo (a)
+       print("foo", a)
+       return coroutine.yield(2*a)
+     end
+     
+     co = coroutine.create(function (a,b)
+           print("co-body", a, b)
+           local r = foo(a+1)
+           print("co-body", r)
+           local r, s = coroutine.yield(a+b, a-b)
+           print("co-body", r, s)
+           return b, "end"
+     end)
+            
+     print("main", coroutine.resume(co, 1, 10))
+     print("main", coroutine.resume(co, "r"))
+     print("main", coroutine.resume(co, "x", "y"))
+     print("main", coroutine.resume(co, "x", "y"))
+

+When you run it, it produces the following output: + +

+     co-body 1       10
+     foo     2
+     
+     main    true    4
+     co-body r
+     main    true    11      -9
+     co-body x       y
+     main    true    10      end
+     main    false   cannot resume dead coroutine
+
+ + + + +

3 - The Application Program Interface

+ +

+ +This section describes the C API for Lua, that is, +the set of C functions available to the host program to communicate +with Lua. +All API functions and related types and constants +are declared in the header file lua.h. + + +

+Even when we use the term "function", +any facility in the API may be provided as a macro instead. +All such macros use each of their arguments exactly once +(except for the first argument, which is always a Lua state), +and so do not generate any hidden side-effects. + + +

+As in most C libraries, +the Lua API functions do not check their arguments for validity or consistency. +However, you can change this behavior by compiling Lua +with a proper definition for the macro luai_apicheck, +in file luaconf.h. + + + +

3.1 - The Stack

+ +

+Lua uses a virtual stack to pass values to and from C. +Each element in this stack represents a Lua value +(nil, number, string, etc.). + + +

+Whenever Lua calls C, the called function gets a new stack, +which is independent of previous stacks and of stacks of +C functions that are still active. +This stack initially contains any arguments to the C function +and it is where the C function pushes its results +to be returned to the caller (see lua_CFunction). + + +

+For convenience, +most query operations in the API do not follow a strict stack discipline. +Instead, they can refer to any element in the stack +by using an index: +A positive index represents an absolute stack position +(starting at 1); +a negative index represents an offset relative to the top of the stack. +More specifically, if the stack has n elements, +then index 1 represents the first element +(that is, the element that was pushed onto the stack first) +and +index n represents the last element; +index -1 also represents the last element +(that is, the element at the top) +and index -n represents the first element. +We say that an index is valid +if it lies between 1 and the stack top +(that is, if 1 ≤ abs(index) ≤ top). + + + + + + +

3.2 - Stack Size

+ +

+When you interact with Lua API, +you are responsible for ensuring consistency. +In particular, +you are responsible for controlling stack overflow. +You can use the function lua_checkstack +to grow the stack size. + + +

+Whenever Lua calls C, +it ensures that at least LUA_MINSTACK stack positions are available. +LUA_MINSTACK is defined as 20, +so that usually you do not have to worry about stack space +unless your code has loops pushing elements onto the stack. + + +

+Most query functions accept as indices any value inside the +available stack space, that is, indices up to the maximum stack size +you have set through lua_checkstack. +Such indices are called acceptable indices. +More formally, we define an acceptable index +as follows: + +

+     (index < 0 && abs(index) <= top) ||
+     (index > 0 && index <= stackspace)
+

+Note that 0 is never an acceptable index. + + + + + +

3.3 - Pseudo-Indices

+ +

+Unless otherwise noted, +any function that accepts valid indices can also be called with +pseudo-indices, +which represent some Lua values that are accessible to C code +but which are not in the stack. +Pseudo-indices are used to access the thread environment, +the function environment, +the registry, +and the upvalues of a C function (see §3.4). + + +

+The thread environment (where global variables live) is +always at pseudo-index LUA_GLOBALSINDEX. +The environment of the running C function is always +at pseudo-index LUA_ENVIRONINDEX. + + +

+To access and change the value of global variables, +you can use regular table operations over an environment table. +For instance, to access the value of a global variable, do + +

+     lua_getfield(L, LUA_GLOBALSINDEX, varname);
+
+ + + + +

3.4 - C Closures

+ +

+When a C function is created, +it is possible to associate some values with it, +thus creating a C closure; +these values are called upvalues and are +accessible to the function whenever it is called +(see lua_pushcclosure). + + +

+Whenever a C function is called, +its upvalues are located at specific pseudo-indices. +These pseudo-indices are produced by the macro +lua_upvalueindex. +The first value associated with a function is at position +lua_upvalueindex(1), and so on. +Any access to lua_upvalueindex(n), +where n is greater than the number of upvalues of the +current function (but not greater than 256), +produces an acceptable (but invalid) index. + + + + + +

3.5 - Registry

+ +

+Lua provides a registry, +a pre-defined table that can be used by any C code to +store whatever Lua value it needs to store. +This table is always located at pseudo-index +LUA_REGISTRYINDEX. +Any C library can store data into this table, +but it should take care to choose keys different from those used +by other libraries, to avoid collisions. +Typically, you should use as key a string containing your library name +or a light userdata with the address of a C object in your code. + + +

+The integer keys in the registry are used by the reference mechanism, +implemented by the auxiliary library, +and therefore should not be used for other purposes. + + + + + +

3.6 - Error Handling in C

+ +

+Internally, Lua uses the C longjmp facility to handle errors. +(You can also choose to use exceptions if you use C++; +see file luaconf.h.) +When Lua faces any error +(such as memory allocation errors, type errors, syntax errors, +and runtime errors) +it raises an error; +that is, it does a long jump. +A protected environment uses setjmp +to set a recover point; +any error jumps to the most recent active recover point. + + +

+Most functions in the API can throw an error, +for instance due to a memory allocation error. +The documentation for each function indicates whether +it can throw errors. + + +

+Inside a C function you can throw an error by calling lua_error. + + + + + +

3.7 - Functions and Types

+ +

+Here we list all functions and types from the C API in +alphabetical order. +Each function has an indicator like this: +[-o, +p, x] + + +

+The first field, o, +is how many elements the function pops from the stack. +The second field, p, +is how many elements the function pushes onto the stack. +(Any function always pushes its results after popping its arguments.) +A field in the form x|y means the function can push (or pop) +x or y elements, +depending on the situation; +an interrogation mark '?' means that +we cannot know how many elements the function pops/pushes +by looking only at its arguments +(e.g., they may depend on what is on the stack). +The third field, x, +tells whether the function may throw errors: +'-' means the function never throws any error; +'m' means the function may throw an error +only due to not enough memory; +'e' means the function may throw other kinds of errors; +'v' means the function may throw an error on purpose. + + + +


lua_Alloc

+
typedef void * (*lua_Alloc) (void *ud,
+                             void *ptr,
+                             size_t osize,
+                             size_t nsize);
+ +

+The type of the memory-allocation function used by Lua states. +The allocator function must provide a +functionality similar to realloc, +but not exactly the same. +Its arguments are +ud, an opaque pointer passed to lua_newstate; +ptr, a pointer to the block being allocated/reallocated/freed; +osize, the original size of the block; +nsize, the new size of the block. +ptr is NULL if and only if osize is zero. +When nsize is zero, the allocator must return NULL; +if osize is not zero, +it should free the block pointed to by ptr. +When nsize is not zero, the allocator returns NULL +if and only if it cannot fill the request. +When nsize is not zero and osize is zero, +the allocator should behave like malloc. +When nsize and osize are not zero, +the allocator behaves like realloc. +Lua assumes that the allocator never fails when +osize >= nsize. + + +

+Here is a simple implementation for the allocator function. +It is used in the auxiliary library by luaL_newstate. + +

+     static void *l_alloc (void *ud, void *ptr, size_t osize,
+                                                size_t nsize) {
+       (void)ud;  (void)osize;  /* not used */
+       if (nsize == 0) {
+         free(ptr);
+         return NULL;
+       }
+       else
+         return realloc(ptr, nsize);
+     }
+

+This code assumes +that free(NULL) has no effect and that +realloc(NULL, size) is equivalent to malloc(size). +ANSI C ensures both behaviors. + + + + + +


lua_atpanic

+[-0, +0, -] +

lua_CFunction lua_atpanic (lua_State *L, lua_CFunction panicf);
+ +

+Sets a new panic function and returns the old one. + + +

+If an error happens outside any protected environment, +Lua calls a panic function +and then calls exit(EXIT_FAILURE), +thus exiting the host application. +Your panic function can avoid this exit by +never returning (e.g., doing a long jump). + + +

+The panic function can access the error message at the top of the stack. + + + + + +


lua_call

+[-(nargs + 1), +nresults, e] +

void lua_call (lua_State *L, int nargs, int nresults);
+ +

+Calls a function. + + +

+To call a function you must use the following protocol: +first, the function to be called is pushed onto the stack; +then, the arguments to the function are pushed +in direct order; +that is, the first argument is pushed first. +Finally you call lua_call; +nargs is the number of arguments that you pushed onto the stack. +All arguments and the function value are popped from the stack +when the function is called. +The function results are pushed onto the stack when the function returns. +The number of results is adjusted to nresults, +unless nresults is LUA_MULTRET. +In this case, all results from the function are pushed. +Lua takes care that the returned values fit into the stack space. +The function results are pushed onto the stack in direct order +(the first result is pushed first), +so that after the call the last result is on the top of the stack. + + +

+Any error inside the called function is propagated upwards +(with a longjmp). + + +

+The following example shows how the host program can do the +equivalent to this Lua code: + +

+     a = f("how", t.x, 14)
+

+Here it is in C: + +

+     lua_getfield(L, LUA_GLOBALSINDEX, "f"); /* function to be called */
+     lua_pushstring(L, "how");                        /* 1st argument */
+     lua_getfield(L, LUA_GLOBALSINDEX, "t");   /* table to be indexed */
+     lua_getfield(L, -1, "x");        /* push result of t.x (2nd arg) */
+     lua_remove(L, -2);                  /* remove 't' from the stack */
+     lua_pushinteger(L, 14);                          /* 3rd argument */
+     lua_call(L, 3, 1);     /* call 'f' with 3 arguments and 1 result */
+     lua_setfield(L, LUA_GLOBALSINDEX, "a");        /* set global 'a' */
+

+Note that the code above is "balanced": +at its end, the stack is back to its original configuration. +This is considered good programming practice. + + + + + +


lua_CFunction

+
typedef int (*lua_CFunction) (lua_State *L);
+ +

+Type for C functions. + + +

+In order to communicate properly with Lua, +a C function must use the following protocol, +which defines the way parameters and results are passed: +a C function receives its arguments from Lua in its stack +in direct order (the first argument is pushed first). +So, when the function starts, +lua_gettop(L) returns the number of arguments received by the function. +The first argument (if any) is at index 1 +and its last argument is at index lua_gettop(L). +To return values to Lua, a C function just pushes them onto the stack, +in direct order (the first result is pushed first), +and returns the number of results. +Any other value in the stack below the results will be properly +discarded by Lua. +Like a Lua function, a C function called by Lua can also return +many results. + + +

+As an example, the following function receives a variable number +of numerical arguments and returns their average and sum: + +

+     static int foo (lua_State *L) {
+       int n = lua_gettop(L);    /* number of arguments */
+       lua_Number sum = 0;
+       int i;
+       for (i = 1; i <= n; i++) {
+         if (!lua_isnumber(L, i)) {
+           lua_pushstring(L, "incorrect argument");
+           lua_error(L);
+         }
+         sum += lua_tonumber(L, i);
+       }
+       lua_pushnumber(L, sum/n);        /* first result */
+       lua_pushnumber(L, sum);         /* second result */
+       return 2;                   /* number of results */
+     }
+
+ + + + +

lua_checkstack

+[-0, +0, m] +

int lua_checkstack (lua_State *L, int extra);
+ +

+Ensures that there are at least extra free stack slots in the stack. +It returns false if it cannot grow the stack to that size. +This function never shrinks the stack; +if the stack is already larger than the new size, +it is left unchanged. + + + + + +


lua_close

+[-0, +0, -] +

void lua_close (lua_State *L);
+ +

+Destroys all objects in the given Lua state +(calling the corresponding garbage-collection metamethods, if any) +and frees all dynamic memory used by this state. +On several platforms, you may not need to call this function, +because all resources are naturally released when the host program ends. +On the other hand, long-running programs, +such as a daemon or a web server, +might need to release states as soon as they are not needed, +to avoid growing too large. + + + + + +


lua_concat

+[-n, +1, e] +

void lua_concat (lua_State *L, int n);
+ +

+Concatenates the n values at the top of the stack, +pops them, and leaves the result at the top. +If n is 1, the result is the single value on the stack +(that is, the function does nothing); +if n is 0, the result is the empty string. +Concatenation is performed following the usual semantics of Lua +(see §2.5.4). + + + + + +


lua_cpcall

+[-0, +(0|1), -] +

int lua_cpcall (lua_State *L, lua_CFunction func, void *ud);
+ +

+Calls the C function func in protected mode. +func starts with only one element in its stack, +a light userdata containing ud. +In case of errors, +lua_cpcall returns the same error codes as lua_pcall, +plus the error object on the top of the stack; +otherwise, it returns zero, and does not change the stack. +All values returned by func are discarded. + + + + + +


lua_createtable

+[-0, +1, m] +

void lua_createtable (lua_State *L, int narr, int nrec);
+ +

+Creates a new empty table and pushes it onto the stack. +The new table has space pre-allocated +for narr array elements and nrec non-array elements. +This pre-allocation is useful when you know exactly how many elements +the table will have. +Otherwise you can use the function lua_newtable. + + + + + +


lua_dump

+[-0, +0, m] +

int lua_dump (lua_State *L, lua_Writer writer, void *data);
+ +

+Dumps a function as a binary chunk. +Receives a Lua function on the top of the stack +and produces a binary chunk that, +if loaded again, +results in a function equivalent to the one dumped. +As it produces parts of the chunk, +lua_dump calls function writer (see lua_Writer) +with the given data +to write them. + + +

+The value returned is the error code returned by the last +call to the writer; +0 means no errors. + + +

+This function does not pop the Lua function from the stack. + + + + + +


lua_equal

+[-0, +0, e] +

int lua_equal (lua_State *L, int index1, int index2);
+ +

+Returns 1 if the two values in acceptable indices index1 and +index2 are equal, +following the semantics of the Lua == operator +(that is, may call metamethods). +Otherwise returns 0. +Also returns 0 if any of the indices is non valid. + + + + + +


lua_error

+[-1, +0, v] +

int lua_error (lua_State *L);
+ +

+Generates a Lua error. +The error message (which can actually be a Lua value of any type) +must be on the stack top. +This function does a long jump, +and therefore never returns. +(see luaL_error). + + + + + +


lua_gc

+[-0, +0, e] +

int lua_gc (lua_State *L, int what, int data);
+ +

+Controls the garbage collector. + + +

+This function performs several tasks, +according to the value of the parameter what: + +

+ + + + +

lua_getallocf

+[-0, +0, -] +

lua_Alloc lua_getallocf (lua_State *L, void **ud);
+ +

+Returns the memory-allocation function of a given state. +If ud is not NULL, Lua stores in *ud the +opaque pointer passed to lua_newstate. + + + + + +


lua_getfenv

+[-0, +1, -] +

void lua_getfenv (lua_State *L, int index);
+ +

+Pushes onto the stack the environment table of +the value at the given index. + + + + + +


lua_getfield

+[-0, +1, e] +

void lua_getfield (lua_State *L, int index, const char *k);
+ +

+Pushes onto the stack the value t[k], +where t is the value at the given valid index. +As in Lua, this function may trigger a metamethod +for the "index" event (see §2.8). + + + + + +


lua_getglobal

+[-0, +1, e] +

void lua_getglobal (lua_State *L, const char *name);
+ +

+Pushes onto the stack the value of the global name. +It is defined as a macro: + +

+     #define lua_getglobal(L,s)  lua_getfield(L, LUA_GLOBALSINDEX, s)
+
+ + + + +

lua_getmetatable

+[-0, +(0|1), -] +

int lua_getmetatable (lua_State *L, int index);
+ +

+Pushes onto the stack the metatable of the value at the given +acceptable index. +If the index is not valid, +or if the value does not have a metatable, +the function returns 0 and pushes nothing on the stack. + + + + + +


lua_gettable

+[-1, +1, e] +

void lua_gettable (lua_State *L, int index);
+ +

+Pushes onto the stack the value t[k], +where t is the value at the given valid index +and k is the value at the top of the stack. + + +

+This function pops the key from the stack +(putting the resulting value in its place). +As in Lua, this function may trigger a metamethod +for the "index" event (see §2.8). + + + + + +


lua_gettop

+[-0, +0, -] +

int lua_gettop (lua_State *L);
+ +

+Returns the index of the top element in the stack. +Because indices start at 1, +this result is equal to the number of elements in the stack +(and so 0 means an empty stack). + + + + + +


lua_insert

+[-1, +1, -] +

void lua_insert (lua_State *L, int index);
+ +

+Moves the top element into the given valid index, +shifting up the elements above this index to open space. +Cannot be called with a pseudo-index, +because a pseudo-index is not an actual stack position. + + + + + +


lua_Integer

+
typedef ptrdiff_t lua_Integer;
+ +

+The type used by the Lua API to represent integral values. + + +

+By default it is a ptrdiff_t, +which is usually the largest signed integral type the machine handles +"comfortably". + + + + + +


lua_isboolean

+[-0, +0, -] +

int lua_isboolean (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index has type boolean, +and 0 otherwise. + + + + + +


lua_iscfunction

+[-0, +0, -] +

int lua_iscfunction (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a C function, +and 0 otherwise. + + + + + +


lua_isfunction

+[-0, +0, -] +

int lua_isfunction (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a function +(either C or Lua), and 0 otherwise. + + + + + +


lua_islightuserdata

+[-0, +0, -] +

int lua_islightuserdata (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a light userdata, +and 0 otherwise. + + + + + +


lua_isnil

+[-0, +0, -] +

int lua_isnil (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is nil, +and 0 otherwise. + + + + + +


lua_isnone

+[-0, +0, -] +

int lua_isnone (lua_State *L, int index);
+ +

+Returns 1 if the given acceptable index is not valid +(that is, it refers to an element outside the current stack), +and 0 otherwise. + + + + + +


lua_isnoneornil

+[-0, +0, -] +

int lua_isnoneornil (lua_State *L, int index);
+ +

+Returns 1 if the given acceptable index is not valid +(that is, it refers to an element outside the current stack) +or if the value at this index is nil, +and 0 otherwise. + + + + + +


lua_isnumber

+[-0, +0, -] +

int lua_isnumber (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a number +or a string convertible to a number, +and 0 otherwise. + + + + + +


lua_isstring

+[-0, +0, -] +

int lua_isstring (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a string +or a number (which is always convertible to a string), +and 0 otherwise. + + + + + +


lua_istable

+[-0, +0, -] +

int lua_istable (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a table, +and 0 otherwise. + + + + + +


lua_isthread

+[-0, +0, -] +

int lua_isthread (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a thread, +and 0 otherwise. + + + + + +


lua_isuserdata

+[-0, +0, -] +

int lua_isuserdata (lua_State *L, int index);
+ +

+Returns 1 if the value at the given acceptable index is a userdata +(either full or light), and 0 otherwise. + + + + + +


lua_lessthan

+[-0, +0, e] +

int lua_lessthan (lua_State *L, int index1, int index2);
+ +

+Returns 1 if the value at acceptable index index1 is smaller +than the value at acceptable index index2, +following the semantics of the Lua < operator +(that is, may call metamethods). +Otherwise returns 0. +Also returns 0 if any of the indices is non valid. + + + + + +


lua_load

+[-0, +1, -] +

int lua_load (lua_State *L,
+              lua_Reader reader,
+              void *data,
+              const char *chunkname);
+ +

+Loads a Lua chunk. +If there are no errors, +lua_load pushes the compiled chunk as a Lua +function on top of the stack. +Otherwise, it pushes an error message. +The return values of lua_load are: + +

+ +

+This function only loads a chunk; +it does not run it. + + +

+lua_load automatically detects whether the chunk is text or binary, +and loads it accordingly (see program luac). + + +

+The lua_load function uses a user-supplied reader function +to read the chunk (see lua_Reader). +The data argument is an opaque value passed to the reader function. + + +

+The chunkname argument gives a name to the chunk, +which is used for error messages and in debug information (see §3.8). + + + + + +


lua_newstate

+[-0, +0, -] +

lua_State *lua_newstate (lua_Alloc f, void *ud);
+ +

+Creates a new, independent state. +Returns NULL if cannot create the state +(due to lack of memory). +The argument f is the allocator function; +Lua does all memory allocation for this state through this function. +The second argument, ud, is an opaque pointer that Lua +simply passes to the allocator in every call. + + + + + +


lua_newtable

+[-0, +1, m] +

void lua_newtable (lua_State *L);
+ +

+Creates a new empty table and pushes it onto the stack. +It is equivalent to lua_createtable(L, 0, 0). + + + + + +


lua_newthread

+[-0, +1, m] +

lua_State *lua_newthread (lua_State *L);
+ +

+Creates a new thread, pushes it on the stack, +and returns a pointer to a lua_State that represents this new thread. +The new state returned by this function shares with the original state +all global objects (such as tables), +but has an independent execution stack. + + +

+There is no explicit function to close or to destroy a thread. +Threads are subject to garbage collection, +like any Lua object. + + + + + +


lua_newuserdata

+[-0, +1, m] +

void *lua_newuserdata (lua_State *L, size_t size);
+ +

+This function allocates a new block of memory with the given size, +pushes onto the stack a new full userdata with the block address, +and returns this address. + + +

+Userdata represent C values in Lua. +A full userdata represents a block of memory. +It is an object (like a table): +you must create it, it can have its own metatable, +and you can detect when it is being collected. +A full userdata is only equal to itself (under raw equality). + + +

+When Lua collects a full userdata with a gc metamethod, +Lua calls the metamethod and marks the userdata as finalized. +When this userdata is collected again then +Lua frees its corresponding memory. + + + + + +


lua_next

+[-1, +(2|0), e] +

int lua_next (lua_State *L, int index);
+ +

+Pops a key from the stack, +and pushes a key-value pair from the table at the given index +(the "next" pair after the given key). +If there are no more elements in the table, +then lua_next returns 0 (and pushes nothing). + + +

+A typical traversal looks like this: + +

+     /* table is in the stack at index 't' */
+     lua_pushnil(L);  /* first key */
+     while (lua_next(L, t) != 0) {
+       /* uses 'key' (at index -2) and 'value' (at index -1) */
+       printf("%s - %s\n",
+              lua_typename(L, lua_type(L, -2)),
+              lua_typename(L, lua_type(L, -1)));
+       /* removes 'value'; keeps 'key' for next iteration */
+       lua_pop(L, 1);
+     }
+
+ +

+While traversing a table, +do not call lua_tolstring directly on a key, +unless you know that the key is actually a string. +Recall that lua_tolstring changes +the value at the given index; +this confuses the next call to lua_next. + + + + + +


lua_Number

+
typedef double lua_Number;
+ +

+The type of numbers in Lua. +By default, it is double, but that can be changed in luaconf.h. + + +

+Through the configuration file you can change +Lua to operate with another type for numbers (e.g., float or long). + + + + + +


lua_objlen

+[-0, +0, -] +

size_t lua_objlen (lua_State *L, int index);
+ +

+Returns the "length" of the value at the given acceptable index: +for strings, this is the string length; +for tables, this is the result of the length operator ('#'); +for userdata, this is the size of the block of memory allocated +for the userdata; +for other values, it is 0. + + + + + +


lua_pcall

+[-(nargs + 1), +(nresults|1), -] +

int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);
+ +

+Calls a function in protected mode. + + +

+Both nargs and nresults have the same meaning as +in lua_call. +If there are no errors during the call, +lua_pcall behaves exactly like lua_call. +However, if there is any error, +lua_pcall catches it, +pushes a single value on the stack (the error message), +and returns an error code. +Like lua_call, +lua_pcall always removes the function +and its arguments from the stack. + + +

+If errfunc is 0, +then the error message returned on the stack +is exactly the original error message. +Otherwise, errfunc is the stack index of an +error handler function. +(In the current implementation, this index cannot be a pseudo-index.) +In case of runtime errors, +this function will be called with the error message +and its return value will be the message returned on the stack by lua_pcall. + + +

+Typically, the error handler function is used to add more debug +information to the error message, such as a stack traceback. +Such information cannot be gathered after the return of lua_pcall, +since by then the stack has unwound. + + +

+The lua_pcall function returns 0 in case of success +or one of the following error codes +(defined in lua.h): + +

+ + + + +

lua_pop

+[-n, +0, -] +

void lua_pop (lua_State *L, int n);
+ +

+Pops n elements from the stack. + + + + + +


lua_pushboolean

+[-0, +1, -] +

void lua_pushboolean (lua_State *L, int b);
+ +

+Pushes a boolean value with value b onto the stack. + + + + + +


lua_pushcclosure

+[-n, +1, m] +

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);
+ +

+Pushes a new C closure onto the stack. + + +

+When a C function is created, +it is possible to associate some values with it, +thus creating a C closure (see §3.4); +these values are then accessible to the function whenever it is called. +To associate values with a C function, +first these values should be pushed onto the stack +(when there are multiple values, the first value is pushed first). +Then lua_pushcclosure +is called to create and push the C function onto the stack, +with the argument n telling how many values should be +associated with the function. +lua_pushcclosure also pops these values from the stack. + + +

+The maximum value for n is 255. + + + + + +


lua_pushcfunction

+[-0, +1, m] +

void lua_pushcfunction (lua_State *L, lua_CFunction f);
+ +

+Pushes a C function onto the stack. +This function receives a pointer to a C function +and pushes onto the stack a Lua value of type function that, +when called, invokes the corresponding C function. + + +

+Any function to be registered in Lua must +follow the correct protocol to receive its parameters +and return its results (see lua_CFunction). + + +

+lua_pushcfunction is defined as a macro: + +

+     #define lua_pushcfunction(L,f)  lua_pushcclosure(L,f,0)
+
+ + + + +

lua_pushfstring

+[-0, +1, m] +

const char *lua_pushfstring (lua_State *L, const char *fmt, ...);
+ +

+Pushes onto the stack a formatted string +and returns a pointer to this string. +It is similar to the C function sprintf, +but has some important differences: + +

+ + + + +

lua_pushinteger

+[-0, +1, -] +

void lua_pushinteger (lua_State *L, lua_Integer n);
+ +

+Pushes a number with value n onto the stack. + + + + + +


lua_pushlightuserdata

+[-0, +1, -] +

void lua_pushlightuserdata (lua_State *L, void *p);
+ +

+Pushes a light userdata onto the stack. + + +

+Userdata represent C values in Lua. +A light userdata represents a pointer. +It is a value (like a number): +you do not create it, it has no individual metatable, +and it is not collected (as it was never created). +A light userdata is equal to "any" +light userdata with the same C address. + + + + + +


lua_pushliteral

+[-0, +1, m] +

void lua_pushliteral (lua_State *L, const char *s);
+ +

+This macro is equivalent to lua_pushlstring, +but can be used only when s is a literal string. +In these cases, it automatically provides the string length. + + + + + +


lua_pushlstring

+[-0, +1, m] +

void lua_pushlstring (lua_State *L, const char *s, size_t len);
+ +

+Pushes the string pointed to by s with size len +onto the stack. +Lua makes (or reuses) an internal copy of the given string, +so the memory at s can be freed or reused immediately after +the function returns. +The string can contain embedded zeros. + + + + + +


lua_pushnil

+[-0, +1, -] +

void lua_pushnil (lua_State *L);
+ +

+Pushes a nil value onto the stack. + + + + + +


lua_pushnumber

+[-0, +1, -] +

void lua_pushnumber (lua_State *L, lua_Number n);
+ +

+Pushes a number with value n onto the stack. + + + + + +


lua_pushstring

+[-0, +1, m] +

void lua_pushstring (lua_State *L, const char *s);
+ +

+Pushes the zero-terminated string pointed to by s +onto the stack. +Lua makes (or reuses) an internal copy of the given string, +so the memory at s can be freed or reused immediately after +the function returns. +The string cannot contain embedded zeros; +it is assumed to end at the first zero. + + + + + +


lua_pushthread

+[-0, +1, -] +

int lua_pushthread (lua_State *L);
+ +

+Pushes the thread represented by L onto the stack. +Returns 1 if this thread is the main thread of its state. + + + + + +


lua_pushvalue

+[-0, +1, -] +

void lua_pushvalue (lua_State *L, int index);
+ +

+Pushes a copy of the element at the given valid index +onto the stack. + + + + + +


lua_pushvfstring

+[-0, +1, m] +

const char *lua_pushvfstring (lua_State *L,
+                              const char *fmt,
+                              va_list argp);
+ +

+Equivalent to lua_pushfstring, except that it receives a va_list +instead of a variable number of arguments. + + + + + +


lua_rawequal

+[-0, +0, -] +

int lua_rawequal (lua_State *L, int index1, int index2);
+ +

+Returns 1 if the two values in acceptable indices index1 and +index2 are primitively equal +(that is, without calling metamethods). +Otherwise returns 0. +Also returns 0 if any of the indices are non valid. + + + + + +


lua_rawget

+[-1, +1, -] +

void lua_rawget (lua_State *L, int index);
+ +

+Similar to lua_gettable, but does a raw access +(i.e., without metamethods). + + + + + +


lua_rawgeti

+[-0, +1, -] +

void lua_rawgeti (lua_State *L, int index, int n);
+ +

+Pushes onto the stack the value t[n], +where t is the value at the given valid index. +The access is raw; +that is, it does not invoke metamethods. + + + + + +


lua_rawset

+[-2, +0, m] +

void lua_rawset (lua_State *L, int index);
+ +

+Similar to lua_settable, but does a raw assignment +(i.e., without metamethods). + + + + + +


lua_rawseti

+[-1, +0, m] +

void lua_rawseti (lua_State *L, int index, int n);
+ +

+Does the equivalent of t[n] = v, +where t is the value at the given valid index +and v is the value at the top of the stack. + + +

+This function pops the value from the stack. +The assignment is raw; +that is, it does not invoke metamethods. + + + + + +


lua_Reader

+
typedef const char * (*lua_Reader) (lua_State *L,
+                                    void *data,
+                                    size_t *size);
+ +

+The reader function used by lua_load. +Every time it needs another piece of the chunk, +lua_load calls the reader, +passing along its data parameter. +The reader must return a pointer to a block of memory +with a new piece of the chunk +and set size to the block size. +The block must exist until the reader function is called again. +To signal the end of the chunk, +the reader must return NULL or set size to zero. +The reader function may return pieces of any size greater than zero. + + + + + +


lua_register

+[-0, +0, e] +

void lua_register (lua_State *L,
+                   const char *name,
+                   lua_CFunction f);
+ +

+Sets the C function f as the new value of global name. +It is defined as a macro: + +

+     #define lua_register(L,n,f) \
+            (lua_pushcfunction(L, f), lua_setglobal(L, n))
+
+ + + + +

lua_remove

+[-1, +0, -] +

void lua_remove (lua_State *L, int index);
+ +

+Removes the element at the given valid index, +shifting down the elements above this index to fill the gap. +Cannot be called with a pseudo-index, +because a pseudo-index is not an actual stack position. + + + + + +


lua_replace

+[-1, +0, -] +

void lua_replace (lua_State *L, int index);
+ +

+Moves the top element into the given position (and pops it), +without shifting any element +(therefore replacing the value at the given position). + + + + + +


lua_resume

+[-?, +?, -] +

int lua_resume (lua_State *L, int narg);
+ +

+Starts and resumes a coroutine in a given thread. + + +

+To start a coroutine, you first create a new thread +(see lua_newthread); +then you push onto its stack the main function plus any arguments; +then you call lua_resume, +with narg being the number of arguments. +This call returns when the coroutine suspends or finishes its execution. +When it returns, the stack contains all values passed to lua_yield, +or all values returned by the body function. +lua_resume returns +LUA_YIELD if the coroutine yields, +0 if the coroutine finishes its execution +without errors, +or an error code in case of errors (see lua_pcall). +In case of errors, +the stack is not unwound, +so you can use the debug API over it. +The error message is on the top of the stack. +To restart a coroutine, you put on its stack only the values to +be passed as results from yield, +and then call lua_resume. + + + + + +


lua_setallocf

+[-0, +0, -] +

void lua_setallocf (lua_State *L, lua_Alloc f, void *ud);
+ +

+Changes the allocator function of a given state to f +with user data ud. + + + + + +


lua_setfenv

+[-1, +0, -] +

int lua_setfenv (lua_State *L, int index);
+ +

+Pops a table from the stack and sets it as +the new environment for the value at the given index. +If the value at the given index is +neither a function nor a thread nor a userdata, +lua_setfenv returns 0. +Otherwise it returns 1. + + + + + +


lua_setfield

+[-1, +0, e] +

void lua_setfield (lua_State *L, int index, const char *k);
+ +

+Does the equivalent to t[k] = v, +where t is the value at the given valid index +and v is the value at the top of the stack. + + +

+This function pops the value from the stack. +As in Lua, this function may trigger a metamethod +for the "newindex" event (see §2.8). + + + + + +


lua_setglobal

+[-1, +0, e] +

void lua_setglobal (lua_State *L, const char *name);
+ +

+Pops a value from the stack and +sets it as the new value of global name. +It is defined as a macro: + +

+     #define lua_setglobal(L,s)   lua_setfield(L, LUA_GLOBALSINDEX, s)
+
+ + + + +

lua_setmetatable

+[-1, +0, -] +

int lua_setmetatable (lua_State *L, int index);
+ +

+Pops a table from the stack and +sets it as the new metatable for the value at the given +acceptable index. + + + + + +


lua_settable

+[-2, +0, e] +

void lua_settable (lua_State *L, int index);
+ +

+Does the equivalent to t[k] = v, +where t is the value at the given valid index, +v is the value at the top of the stack, +and k is the value just below the top. + + +

+This function pops both the key and the value from the stack. +As in Lua, this function may trigger a metamethod +for the "newindex" event (see §2.8). + + + + + +


lua_settop

+[-?, +?, -] +

void lua_settop (lua_State *L, int index);
+ +

+Accepts any acceptable index, or 0, +and sets the stack top to this index. +If the new top is larger than the old one, +then the new elements are filled with nil. +If index is 0, then all stack elements are removed. + + + + + +


lua_State

+
typedef struct lua_State lua_State;
+ +

+Opaque structure that keeps the whole state of a Lua interpreter. +The Lua library is fully reentrant: +it has no global variables. +All information about a state is kept in this structure. + + +

+A pointer to this state must be passed as the first argument to +every function in the library, except to lua_newstate, +which creates a Lua state from scratch. + + + + + +


lua_status

+[-0, +0, -] +

int lua_status (lua_State *L);
+ +

+Returns the status of the thread L. + + +

+The status can be 0 for a normal thread, +an error code if the thread finished its execution with an error, +or LUA_YIELD if the thread is suspended. + + + + + +


lua_toboolean

+[-0, +0, -] +

int lua_toboolean (lua_State *L, int index);
+ +

+Converts the Lua value at the given acceptable index to a C boolean +value (0 or 1). +Like all tests in Lua, +lua_toboolean returns 1 for any Lua value +different from false and nil; +otherwise it returns 0. +It also returns 0 when called with a non-valid index. +(If you want to accept only actual boolean values, +use lua_isboolean to test the value's type.) + + + + + +


lua_tocfunction

+[-0, +0, -] +

lua_CFunction lua_tocfunction (lua_State *L, int index);
+ +

+Converts a value at the given acceptable index to a C function. +That value must be a C function; +otherwise, returns NULL. + + + + + +


lua_tointeger

+[-0, +0, -] +

lua_Integer lua_tointeger (lua_State *L, int index);
+ +

+Converts the Lua value at the given acceptable index +to the signed integral type lua_Integer. +The Lua value must be a number or a string convertible to a number +(see §2.2.1); +otherwise, lua_tointeger returns 0. + + +

+If the number is not an integer, +it is truncated in some non-specified way. + + + + + +


lua_tolstring

+[-0, +0, m] +

const char *lua_tolstring (lua_State *L, int index, size_t *len);
+ +

+Converts the Lua value at the given acceptable index to a C string. +If len is not NULL, +it also sets *len with the string length. +The Lua value must be a string or a number; +otherwise, the function returns NULL. +If the value is a number, +then lua_tolstring also +changes the actual value in the stack to a string. +(This change confuses lua_next +when lua_tolstring is applied to keys during a table traversal.) + + +

+lua_tolstring returns a fully aligned pointer +to a string inside the Lua state. +This string always has a zero ('\0') +after its last character (as in C), +but can contain other zeros in its body. +Because Lua has garbage collection, +there is no guarantee that the pointer returned by lua_tolstring +will be valid after the corresponding value is removed from the stack. + + + + + +


lua_tonumber

+[-0, +0, -] +

lua_Number lua_tonumber (lua_State *L, int index);
+ +

+Converts the Lua value at the given acceptable index +to the C type lua_Number (see lua_Number). +The Lua value must be a number or a string convertible to a number +(see §2.2.1); +otherwise, lua_tonumber returns 0. + + + + + +


lua_topointer

+[-0, +0, -] +

const void *lua_topointer (lua_State *L, int index);
+ +

+Converts the value at the given acceptable index to a generic +C pointer (void*). +The value can be a userdata, a table, a thread, or a function; +otherwise, lua_topointer returns NULL. +Different objects will give different pointers. +There is no way to convert the pointer back to its original value. + + +

+Typically this function is used only for debug information. + + + + + +


lua_tostring

+[-0, +0, m] +

const char *lua_tostring (lua_State *L, int index);
+ +

+Equivalent to lua_tolstring with len equal to NULL. + + + + + +


lua_tothread

+[-0, +0, -] +

lua_State *lua_tothread (lua_State *L, int index);
+ +

+Converts the value at the given acceptable index to a Lua thread +(represented as lua_State*). +This value must be a thread; +otherwise, the function returns NULL. + + + + + +


lua_touserdata

+[-0, +0, -] +

void *lua_touserdata (lua_State *L, int index);
+ +

+If the value at the given acceptable index is a full userdata, +returns its block address. +If the value is a light userdata, +returns its pointer. +Otherwise, returns NULL. + + + + + +


lua_type

+[-0, +0, -] +

int lua_type (lua_State *L, int index);
+ +

+Returns the type of the value in the given acceptable index, +or LUA_TNONE for a non-valid index +(that is, an index to an "empty" stack position). +The types returned by lua_type are coded by the following constants +defined in lua.h: +LUA_TNIL, +LUA_TNUMBER, +LUA_TBOOLEAN, +LUA_TSTRING, +LUA_TTABLE, +LUA_TFUNCTION, +LUA_TUSERDATA, +LUA_TTHREAD, +and +LUA_TLIGHTUSERDATA. + + + + + +


lua_typename

+[-0, +0, -] +

const char *lua_typename  (lua_State *L, int tp);
+ +

+Returns the name of the type encoded by the value tp, +which must be one the values returned by lua_type. + + + + + +


lua_Writer

+
typedef int (*lua_Writer) (lua_State *L,
+                           const void* p,
+                           size_t sz,
+                           void* ud);
+ +

+The type of the writer function used by lua_dump. +Every time it produces another piece of chunk, +lua_dump calls the writer, +passing along the buffer to be written (p), +its size (sz), +and the data parameter supplied to lua_dump. + + +

+The writer returns an error code: +0 means no errors; +any other value means an error and stops lua_dump from +calling the writer again. + + + + + +


lua_xmove

+[-?, +?, -] +

void lua_xmove (lua_State *from, lua_State *to, int n);
+ +

+Exchange values between different threads of the same global state. + + +

+This function pops n values from the stack from, +and pushes them onto the stack to. + + + + + +


lua_yield

+[-?, +?, -] +

int lua_yield  (lua_State *L, int nresults);
+ +

+Yields a coroutine. + + +

+This function should only be called as the +return expression of a C function, as follows: + +

+     return lua_yield (L, nresults);
+

+When a C function calls lua_yield in that way, +the running coroutine suspends its execution, +and the call to lua_resume that started this coroutine returns. +The parameter nresults is the number of values from the stack +that are passed as results to lua_resume. + + + + + + + +

3.8 - The Debug Interface

+ +

+Lua has no built-in debugging facilities. +Instead, it offers a special interface +by means of functions and hooks. +This interface allows the construction of different +kinds of debuggers, profilers, and other tools +that need "inside information" from the interpreter. + + + +


lua_Debug

+
typedef struct lua_Debug {
+  int event;
+  const char *name;           /* (n) */
+  const char *namewhat;       /* (n) */
+  const char *what;           /* (S) */
+  const char *source;         /* (S) */
+  int currentline;            /* (l) */
+  int nups;                   /* (u) number of upvalues */
+  int linedefined;            /* (S) */
+  int lastlinedefined;        /* (S) */
+  char short_src[LUA_IDSIZE]; /* (S) */
+  /* private part */
+  other fields
+} lua_Debug;
+ +

+A structure used to carry different pieces of +information about an active function. +lua_getstack fills only the private part +of this structure, for later use. +To fill the other fields of lua_Debug with useful information, +call lua_getinfo. + + +

+The fields of lua_Debug have the following meaning: + +

+ + + + +

lua_gethook

+[-0, +0, -] +

lua_Hook lua_gethook (lua_State *L);
+ +

+Returns the current hook function. + + + + + +


lua_gethookcount

+[-0, +0, -] +

int lua_gethookcount (lua_State *L);
+ +

+Returns the current hook count. + + + + + +


lua_gethookmask

+[-0, +0, -] +

int lua_gethookmask (lua_State *L);
+ +

+Returns the current hook mask. + + + + + +


lua_getinfo

+[-(0|1), +(0|1|2), m] +

int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar);
+ +

+Returns information about a specific function or function invocation. + + +

+To get information about a function invocation, +the parameter ar must be a valid activation record that was +filled by a previous call to lua_getstack or +given as argument to a hook (see lua_Hook). + + +

+To get information about a function you push it onto the stack +and start the what string with the character '>'. +(In that case, +lua_getinfo pops the function in the top of the stack.) +For instance, to know in which line a function f was defined, +you can write the following code: + +

+     lua_Debug ar;
+     lua_getfield(L, LUA_GLOBALSINDEX, "f");  /* get global 'f' */
+     lua_getinfo(L, ">S", &ar);
+     printf("%d\n", ar.linedefined);
+
+ +

+Each character in the string what +selects some fields of the structure ar to be filled or +a value to be pushed on the stack: + +

+ +

+This function returns 0 on error +(for instance, an invalid option in what). + + + + + +


lua_getlocal

+[-0, +(0|1), -] +

const char *lua_getlocal (lua_State *L, lua_Debug *ar, int n);
+ +

+Gets information about a local variable of a given activation record. +The parameter ar must be a valid activation record that was +filled by a previous call to lua_getstack or +given as argument to a hook (see lua_Hook). +The index n selects which local variable to inspect +(1 is the first parameter or active local variable, and so on, +until the last active local variable). +lua_getlocal pushes the variable's value onto the stack +and returns its name. + + +

+Variable names starting with '(' (open parentheses) +represent internal variables +(loop control variables, temporaries, and C function locals). + + +

+Returns NULL (and pushes nothing) +when the index is greater than +the number of active local variables. + + + + + +


lua_getstack

+[-0, +0, -] +

int lua_getstack (lua_State *L, int level, lua_Debug *ar);
+ +

+Get information about the interpreter runtime stack. + + +

+This function fills parts of a lua_Debug structure with +an identification of the activation record +of the function executing at a given level. +Level 0 is the current running function, +whereas level n+1 is the function that has called level n. +When there are no errors, lua_getstack returns 1; +when called with a level greater than the stack depth, +it returns 0. + + + + + +


lua_getupvalue

+[-0, +(0|1), -] +

const char *lua_getupvalue (lua_State *L, int funcindex, int n);
+ +

+Gets information about a closure's upvalue. +(For Lua functions, +upvalues are the external local variables that the function uses, +and that are consequently included in its closure.) +lua_getupvalue gets the index n of an upvalue, +pushes the upvalue's value onto the stack, +and returns its name. +funcindex points to the closure in the stack. +(Upvalues have no particular order, +as they are active through the whole function. +So, they are numbered in an arbitrary order.) + + +

+Returns NULL (and pushes nothing) +when the index is greater than the number of upvalues. +For C functions, this function uses the empty string "" +as a name for all upvalues. + + + + + +


lua_Hook

+
typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);
+ +

+Type for debugging hook functions. + + +

+Whenever a hook is called, its ar argument has its field +event set to the specific event that triggered the hook. +Lua identifies these events with the following constants: +LUA_HOOKCALL, LUA_HOOKRET, +LUA_HOOKTAILRET, LUA_HOOKLINE, +and LUA_HOOKCOUNT. +Moreover, for line events, the field currentline is also set. +To get the value of any other field in ar, +the hook must call lua_getinfo. +For return events, event can be LUA_HOOKRET, +the normal value, or LUA_HOOKTAILRET. +In the latter case, Lua is simulating a return from +a function that did a tail call; +in this case, it is useless to call lua_getinfo. + + +

+While Lua is running a hook, it disables other calls to hooks. +Therefore, if a hook calls back Lua to execute a function or a chunk, +this execution occurs without any calls to hooks. + + + + + +


lua_sethook

+[-0, +0, -] +

int lua_sethook (lua_State *L, lua_Hook f, int mask, int count);
+ +

+Sets the debugging hook function. + + +

+Argument f is the hook function. +mask specifies on which events the hook will be called: +it is formed by a bitwise or of the constants +LUA_MASKCALL, +LUA_MASKRET, +LUA_MASKLINE, +and LUA_MASKCOUNT. +The count argument is only meaningful when the mask +includes LUA_MASKCOUNT. +For each event, the hook is called as explained below: + +

+ +

+A hook is disabled by setting mask to zero. + + + + + +


lua_setlocal

+[-(0|1), +0, -] +

const char *lua_setlocal (lua_State *L, lua_Debug *ar, int n);
+ +

+Sets the value of a local variable of a given activation record. +Parameters ar and n are as in lua_getlocal +(see lua_getlocal). +lua_setlocal assigns the value at the top of the stack +to the variable and returns its name. +It also pops the value from the stack. + + +

+Returns NULL (and pops nothing) +when the index is greater than +the number of active local variables. + + + + + +


lua_setupvalue

+[-(0|1), +0, -] +

const char *lua_setupvalue (lua_State *L, int funcindex, int n);
+ +

+Sets the value of a closure's upvalue. +It assigns the value at the top of the stack +to the upvalue and returns its name. +It also pops the value from the stack. +Parameters funcindex and n are as in the lua_getupvalue +(see lua_getupvalue). + + +

+Returns NULL (and pops nothing) +when the index is greater than the number of upvalues. + + + + + + + +

4 - The Auxiliary Library

+ +

+ +The auxiliary library provides several convenient functions +to interface C with Lua. +While the basic API provides the primitive functions for all +interactions between C and Lua, +the auxiliary library provides higher-level functions for some +common tasks. + + +

+All functions from the auxiliary library +are defined in header file lauxlib.h and +have a prefix luaL_. + + +

+All functions in the auxiliary library are built on +top of the basic API, +and so they provide nothing that cannot be done with this API. + + +

+Several functions in the auxiliary library are used to +check C function arguments. +Their names are always luaL_check* or luaL_opt*. +All of these functions throw an error if the check is not satisfied. +Because the error message is formatted for arguments +(e.g., "bad argument #1"), +you should not use these functions for other stack values. + + + +

4.1 - Functions and Types

+ +

+Here we list all functions and types from the auxiliary library +in alphabetical order. + + + +


luaL_addchar

+[-0, +0, m] +

void luaL_addchar (luaL_Buffer *B, char c);
+ +

+Adds the character c to the buffer B +(see luaL_Buffer). + + + + + +


luaL_addlstring

+[-0, +0, m] +

void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l);
+ +

+Adds the string pointed to by s with length l to +the buffer B +(see luaL_Buffer). +The string may contain embedded zeros. + + + + + +


luaL_addsize

+[-0, +0, m] +

void luaL_addsize (luaL_Buffer *B, size_t n);
+ +

+Adds to the buffer B (see luaL_Buffer) +a string of length n previously copied to the +buffer area (see luaL_prepbuffer). + + + + + +


luaL_addstring

+[-0, +0, m] +

void luaL_addstring (luaL_Buffer *B, const char *s);
+ +

+Adds the zero-terminated string pointed to by s +to the buffer B +(see luaL_Buffer). +The string may not contain embedded zeros. + + + + + +


luaL_addvalue

+[-1, +0, m] +

void luaL_addvalue (luaL_Buffer *B);
+ +

+Adds the value at the top of the stack +to the buffer B +(see luaL_Buffer). +Pops the value. + + +

+This is the only function on string buffers that can (and must) +be called with an extra element on the stack, +which is the value to be added to the buffer. + + + + + +


luaL_argcheck

+[-0, +0, v] +

void luaL_argcheck (lua_State *L,
+                    int cond,
+                    int narg,
+                    const char *extramsg);
+ +

+Checks whether cond is true. +If not, raises an error with the following message, +where func is retrieved from the call stack: + +

+     bad argument #<narg> to <func> (<extramsg>)
+
+ + + + +

luaL_argerror

+[-0, +0, v] +

int luaL_argerror (lua_State *L, int narg, const char *extramsg);
+ +

+Raises an error with the following message, +where func is retrieved from the call stack: + +

+     bad argument #<narg> to <func> (<extramsg>)
+
+ +

+This function never returns, +but it is an idiom to use it in C functions +as return luaL_argerror(args). + + + + + +


luaL_Buffer

+
typedef struct luaL_Buffer luaL_Buffer;
+ +

+Type for a string buffer. + + +

+A string buffer allows C code to build Lua strings piecemeal. +Its pattern of use is as follows: + +

+ +

+During its normal operation, +a string buffer uses a variable number of stack slots. +So, while using a buffer, you cannot assume that you know where +the top of the stack is. +You can use the stack between successive calls to buffer operations +as long as that use is balanced; +that is, +when you call a buffer operation, +the stack is at the same level +it was immediately after the previous buffer operation. +(The only exception to this rule is luaL_addvalue.) +After calling luaL_pushresult the stack is back to its +level when the buffer was initialized, +plus the final string on its top. + + + + + +


luaL_buffinit

+[-0, +0, -] +

void luaL_buffinit (lua_State *L, luaL_Buffer *B);
+ +

+Initializes a buffer B. +This function does not allocate any space; +the buffer must be declared as a variable +(see luaL_Buffer). + + + + + +


luaL_callmeta

+[-0, +(0|1), e] +

int luaL_callmeta (lua_State *L, int obj, const char *e);
+ +

+Calls a metamethod. + + +

+If the object at index obj has a metatable and this +metatable has a field e, +this function calls this field and passes the object as its only argument. +In this case this function returns 1 and pushes onto the +stack the value returned by the call. +If there is no metatable or no metamethod, +this function returns 0 (without pushing any value on the stack). + + + + + +


luaL_checkany

+[-0, +0, v] +

void luaL_checkany (lua_State *L, int narg);
+ +

+Checks whether the function has an argument +of any type (including nil) at position narg. + + + + + +


luaL_checkint

+[-0, +0, v] +

int luaL_checkint (lua_State *L, int narg);
+ +

+Checks whether the function argument narg is a number +and returns this number cast to an int. + + + + + +


luaL_checkinteger

+[-0, +0, v] +

lua_Integer luaL_checkinteger (lua_State *L, int narg);
+ +

+Checks whether the function argument narg is a number +and returns this number cast to a lua_Integer. + + + + + +


luaL_checklong

+[-0, +0, v] +

long luaL_checklong (lua_State *L, int narg);
+ +

+Checks whether the function argument narg is a number +and returns this number cast to a long. + + + + + +


luaL_checklstring

+[-0, +0, v] +

const char *luaL_checklstring (lua_State *L, int narg, size_t *l);
+ +

+Checks whether the function argument narg is a string +and returns this string; +if l is not NULL fills *l +with the string's length. + + +

+This function uses lua_tolstring to get its result, +so all conversions and caveats of that function apply here. + + + + + +


luaL_checknumber

+[-0, +0, v] +

lua_Number luaL_checknumber (lua_State *L, int narg);
+ +

+Checks whether the function argument narg is a number +and returns this number. + + + + + +


luaL_checkoption

+[-0, +0, v] +

int luaL_checkoption (lua_State *L,
+                      int narg,
+                      const char *def,
+                      const char *const lst[]);
+ +

+Checks whether the function argument narg is a string and +searches for this string in the array lst +(which must be NULL-terminated). +Returns the index in the array where the string was found. +Raises an error if the argument is not a string or +if the string cannot be found. + + +

+If def is not NULL, +the function uses def as a default value when +there is no argument narg or if this argument is nil. + + +

+This is a useful function for mapping strings to C enums. +(The usual convention in Lua libraries is +to use strings instead of numbers to select options.) + + + + + +


luaL_checkstack

+[-0, +0, v] +

void luaL_checkstack (lua_State *L, int sz, const char *msg);
+ +

+Grows the stack size to top + sz elements, +raising an error if the stack cannot grow to that size. +msg is an additional text to go into the error message. + + + + + +


luaL_checkstring

+[-0, +0, v] +

const char *luaL_checkstring (lua_State *L, int narg);
+ +

+Checks whether the function argument narg is a string +and returns this string. + + +

+This function uses lua_tolstring to get its result, +so all conversions and caveats of that function apply here. + + + + + +


luaL_checktype

+[-0, +0, v] +

void luaL_checktype (lua_State *L, int narg, int t);
+ +

+Checks whether the function argument narg has type t. +See lua_type for the encoding of types for t. + + + + + +


luaL_checkudata

+[-0, +0, v] +

void *luaL_checkudata (lua_State *L, int narg, const char *tname);
+ +

+Checks whether the function argument narg is a userdata +of the type tname (see luaL_newmetatable). + + + + + +


luaL_dofile

+[-0, +?, m] +

int luaL_dofile (lua_State *L, const char *filename);
+ +

+Loads and runs the given file. +It is defined as the following macro: + +

+     (luaL_loadfile(L, filename) || lua_pcall(L, 0, LUA_MULTRET, 0))
+

+It returns 0 if there are no errors +or 1 in case of errors. + + + + + +


luaL_dostring

+[-0, +?, m] +

int luaL_dostring (lua_State *L, const char *str);
+ +

+Loads and runs the given string. +It is defined as the following macro: + +

+     (luaL_loadstring(L, str) || lua_pcall(L, 0, LUA_MULTRET, 0))
+

+It returns 0 if there are no errors +or 1 in case of errors. + + + + + +


luaL_error

+[-0, +0, v] +

int luaL_error (lua_State *L, const char *fmt, ...);
+ +

+Raises an error. +The error message format is given by fmt +plus any extra arguments, +following the same rules of lua_pushfstring. +It also adds at the beginning of the message the file name and +the line number where the error occurred, +if this information is available. + + +

+This function never returns, +but it is an idiom to use it in C functions +as return luaL_error(args). + + + + + +


luaL_getmetafield

+[-0, +(0|1), m] +

int luaL_getmetafield (lua_State *L, int obj, const char *e);
+ +

+Pushes onto the stack the field e from the metatable +of the object at index obj. +If the object does not have a metatable, +or if the metatable does not have this field, +returns 0 and pushes nothing. + + + + + +


luaL_getmetatable

+[-0, +1, -] +

void luaL_getmetatable (lua_State *L, const char *tname);
+ +

+Pushes onto the stack the metatable associated with name tname +in the registry (see luaL_newmetatable). + + + + + +


luaL_gsub

+[-0, +1, m] +

const char *luaL_gsub (lua_State *L,
+                       const char *s,
+                       const char *p,
+                       const char *r);
+ +

+Creates a copy of string s by replacing +any occurrence of the string p +with the string r. +Pushes the resulting string on the stack and returns it. + + + + + +


luaL_loadbuffer

+[-0, +1, m] +

int luaL_loadbuffer (lua_State *L,
+                     const char *buff,
+                     size_t sz,
+                     const char *name);
+ +

+Loads a buffer as a Lua chunk. +This function uses lua_load to load the chunk in the +buffer pointed to by buff with size sz. + + +

+This function returns the same results as lua_load. +name is the chunk name, +used for debug information and error messages. + + + + + +


luaL_loadfile

+[-0, +1, m] +

int luaL_loadfile (lua_State *L, const char *filename);
+ +

+Loads a file as a Lua chunk. +This function uses lua_load to load the chunk in the file +named filename. +If filename is NULL, +then it loads from the standard input. +The first line in the file is ignored if it starts with a #. + + +

+This function returns the same results as lua_load, +but it has an extra error code LUA_ERRFILE +if it cannot open/read the file. + + +

+As lua_load, this function only loads the chunk; +it does not run it. + + + + + +


luaL_loadstring

+[-0, +1, m] +

int luaL_loadstring (lua_State *L, const char *s);
+ +

+Loads a string as a Lua chunk. +This function uses lua_load to load the chunk in +the zero-terminated string s. + + +

+This function returns the same results as lua_load. + + +

+Also as lua_load, this function only loads the chunk; +it does not run it. + + + + + +


luaL_newmetatable

+[-0, +1, m] +

int luaL_newmetatable (lua_State *L, const char *tname);
+ +

+If the registry already has the key tname, +returns 0. +Otherwise, +creates a new table to be used as a metatable for userdata, +adds it to the registry with key tname, +and returns 1. + + +

+In both cases pushes onto the stack the final value associated +with tname in the registry. + + + + + +


luaL_newstate

+[-0, +0, -] +

lua_State *luaL_newstate (void);
+ +

+Creates a new Lua state. +It calls lua_newstate with an +allocator based on the standard C realloc function +and then sets a panic function (see lua_atpanic) that prints +an error message to the standard error output in case of fatal +errors. + + +

+Returns the new state, +or NULL if there is a memory allocation error. + + + + + +


luaL_openlibs

+[-0, +0, m] +

void luaL_openlibs (lua_State *L);
+ +

+Opens all standard Lua libraries into the given state. + + + + + +


luaL_optint

+[-0, +0, v] +

int luaL_optint (lua_State *L, int narg, int d);
+ +

+If the function argument narg is a number, +returns this number cast to an int. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_optinteger

+[-0, +0, v] +

lua_Integer luaL_optinteger (lua_State *L,
+                             int narg,
+                             lua_Integer d);
+ +

+If the function argument narg is a number, +returns this number cast to a lua_Integer. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_optlong

+[-0, +0, v] +

long luaL_optlong (lua_State *L, int narg, long d);
+ +

+If the function argument narg is a number, +returns this number cast to a long. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_optlstring

+[-0, +0, v] +

const char *luaL_optlstring (lua_State *L,
+                             int narg,
+                             const char *d,
+                             size_t *l);
+ +

+If the function argument narg is a string, +returns this string. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + +

+If l is not NULL, +fills the position *l with the results's length. + + + + + +


luaL_optnumber

+[-0, +0, v] +

lua_Number luaL_optnumber (lua_State *L, int narg, lua_Number d);
+ +

+If the function argument narg is a number, +returns this number. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_optstring

+[-0, +0, v] +

const char *luaL_optstring (lua_State *L,
+                            int narg,
+                            const char *d);
+ +

+If the function argument narg is a string, +returns this string. +If this argument is absent or is nil, +returns d. +Otherwise, raises an error. + + + + + +


luaL_prepbuffer

+[-0, +0, -] +

char *luaL_prepbuffer (luaL_Buffer *B);
+ +

+Returns an address to a space of size LUAL_BUFFERSIZE +where you can copy a string to be added to buffer B +(see luaL_Buffer). +After copying the string into this space you must call +luaL_addsize with the size of the string to actually add +it to the buffer. + + + + + +


luaL_pushresult

+[-?, +1, m] +

void luaL_pushresult (luaL_Buffer *B);
+ +

+Finishes the use of buffer B leaving the final string on +the top of the stack. + + + + + +


luaL_ref

+[-1, +0, m] +

int luaL_ref (lua_State *L, int t);
+ +

+Creates and returns a reference, +in the table at index t, +for the object at the top of the stack (and pops the object). + + +

+A reference is a unique integer key. +As long as you do not manually add integer keys into table t, +luaL_ref ensures the uniqueness of the key it returns. +You can retrieve an object referred by reference r +by calling lua_rawgeti(L, t, r). +Function luaL_unref frees a reference and its associated object. + + +

+If the object at the top of the stack is nil, +luaL_ref returns the constant LUA_REFNIL. +The constant LUA_NOREF is guaranteed to be different +from any reference returned by luaL_ref. + + + + + +


luaL_Reg

+
typedef struct luaL_Reg {
+  const char *name;
+  lua_CFunction func;
+} luaL_Reg;
+ +

+Type for arrays of functions to be registered by +luaL_register. +name is the function name and func is a pointer to +the function. +Any array of luaL_Reg must end with an sentinel entry +in which both name and func are NULL. + + + + + +


luaL_register

+[-(0|1), +1, m] +

void luaL_register (lua_State *L,
+                    const char *libname,
+                    const luaL_Reg *l);
+ +

+Opens a library. + + +

+When called with libname equal to NULL, +it simply registers all functions in the list l +(see luaL_Reg) into the table on the top of the stack. + + +

+When called with a non-null libname, +luaL_register creates a new table t, +sets it as the value of the global variable libname, +sets it as the value of package.loaded[libname], +and registers on it all functions in the list l. +If there is a table in package.loaded[libname] or in +variable libname, +reuses this table instead of creating a new one. + + +

+In any case the function leaves the table +on the top of the stack. + + + + + +


luaL_typename

+[-0, +0, -] +

const char *luaL_typename (lua_State *L, int index);
+ +

+Returns the name of the type of the value at the given index. + + + + + +


luaL_typerror

+[-0, +0, v] +

int luaL_typerror (lua_State *L, int narg, const char *tname);
+ +

+Generates an error with a message like the following: + +

+     location: bad argument narg to 'func' (tname expected, got rt)
+

+where location is produced by luaL_where, +func is the name of the current function, +and rt is the type name of the actual argument. + + + + + +


luaL_unref

+[-0, +0, -] +

void luaL_unref (lua_State *L, int t, int ref);
+ +

+Releases reference ref from the table at index t +(see luaL_ref). +The entry is removed from the table, +so that the referred object can be collected. +The reference ref is also freed to be used again. + + +

+If ref is LUA_NOREF or LUA_REFNIL, +luaL_unref does nothing. + + + + + +


luaL_where

+[-0, +1, m] +

void luaL_where (lua_State *L, int lvl);
+ +

+Pushes onto the stack a string identifying the current position +of the control at level lvl in the call stack. +Typically this string has the following format: + +

+     chunkname:currentline:
+

+Level 0 is the running function, +level 1 is the function that called the running function, +etc. + + +

+This function is used to build a prefix for error messages. + + + + + + + +

5 - Standard Libraries

+ +

+The standard Lua libraries provide useful functions +that are implemented directly through the C API. +Some of these functions provide essential services to the language +(e.g., type and getmetatable); +others provide access to "outside" services (e.g., I/O); +and others could be implemented in Lua itself, +but are quite useful or have critical performance requirements that +deserve an implementation in C (e.g., table.sort). + + +

+All libraries are implemented through the official C API +and are provided as separate C modules. +Currently, Lua has the following standard libraries: + +

+Except for the basic and package libraries, +each library provides all its functions as fields of a global table +or as methods of its objects. + + +

+To have access to these libraries, +the C host program should call the luaL_openlibs function, +which opens all standard libraries. +Alternatively, +it can open them individually by calling +luaopen_base (for the basic library), +luaopen_package (for the package library), +luaopen_string (for the string library), +luaopen_table (for the table library), +luaopen_math (for the mathematical library), +luaopen_io (for the I/O library), +luaopen_os (for the Operating System library), +and luaopen_debug (for the debug library). +These functions are declared in lualib.h +and should not be called directly: +you must call them like any other Lua C function, +e.g., by using lua_call. + + + +

5.1 - Basic Functions

+ +

+The basic library provides some core functions to Lua. +If you do not include this library in your application, +you should check carefully whether you need to provide +implementations for some of its facilities. + + +

+


assert (v [, message])

+Issues an error when +the value of its argument v is false (i.e., nil or false); +otherwise, returns all its arguments. +message is an error message; +when absent, it defaults to "assertion failed!" + + + + +

+


collectgarbage ([opt [, arg]])

+ + +

+This function is a generic interface to the garbage collector. +It performs different functions according to its first argument, opt: + +

+ + + +

+


dofile ([filename])

+Opens the named file and executes its contents as a Lua chunk. +When called without arguments, +dofile executes the contents of the standard input (stdin). +Returns all values returned by the chunk. +In case of errors, dofile propagates the error +to its caller (that is, dofile does not run in protected mode). + + + + +

+


error (message [, level])

+Terminates the last protected function called +and returns message as the error message. +Function error never returns. + + +

+Usually, error adds some information about the error position +at the beginning of the message. +The level argument specifies how to get the error position. +With level 1 (the default), the error position is where the +error function was called. +Level 2 points the error to where the function +that called error was called; and so on. +Passing a level 0 avoids the addition of error position information +to the message. + + + + +

+


_G

+A global variable (not a function) that +holds the global environment (that is, _G._G = _G). +Lua itself does not use this variable; +changing its value does not affect any environment, +nor vice-versa. +(Use setfenv to change environments.) + + + + +

+


getfenv ([f])

+Returns the current environment in use by the function. +f can be a Lua function or a number +that specifies the function at that stack level: +Level 1 is the function calling getfenv. +If the given function is not a Lua function, +or if f is 0, +getfenv returns the global environment. +The default for f is 1. + + + + +

+


getmetatable (object)

+ + +

+If object does not have a metatable, returns nil. +Otherwise, +if the object's metatable has a "__metatable" field, +returns the associated value. +Otherwise, returns the metatable of the given object. + + + + +

+


ipairs (t)

+ + +

+Returns three values: an iterator function, the table t, and 0, +so that the construction + +

+     for i,v in ipairs(t) do body end
+

+will iterate over the pairs (1,t[1]), (2,t[2]), ···, +up to the first integer key absent from the table. + + + + +

+


load (func [, chunkname])

+ + +

+Loads a chunk using function func to get its pieces. +Each call to func must return a string that concatenates +with previous results. +A return of an empty string, nil, or no value signals the end of the chunk. + + +

+If there are no errors, +returns the compiled chunk as a function; +otherwise, returns nil plus the error message. +The environment of the returned function is the global environment. + + +

+chunkname is used as the chunk name for error messages +and debug information. +When absent, +it defaults to "=(load)". + + + + +

+


loadfile ([filename])

+ + +

+Similar to load, +but gets the chunk from file filename +or from the standard input, +if no file name is given. + + + + +

+


loadstring (string [, chunkname])

+ + +

+Similar to load, +but gets the chunk from the given string. + + +

+To load and run a given string, use the idiom + +

+     assert(loadstring(s))()
+
+ +

+When absent, +chunkname defaults to the given string. + + + + +

+


next (table [, index])

+ + +

+Allows a program to traverse all fields of a table. +Its first argument is a table and its second argument +is an index in this table. +next returns the next index of the table +and its associated value. +When called with nil as its second argument, +next returns an initial index +and its associated value. +When called with the last index, +or with nil in an empty table, +next returns nil. +If the second argument is absent, then it is interpreted as nil. +In particular, +you can use next(t) to check whether a table is empty. + + +

+The order in which the indices are enumerated is not specified, +even for numeric indices. +(To traverse a table in numeric order, +use a numerical for or the ipairs function.) + + +

+The behavior of next is undefined if, +during the traversal, +you assign any value to a non-existent field in the table. +You may however modify existing fields. +In particular, you may clear existing fields. + + + + +

+


pairs (t)

+ + +

+Returns three values: the next function, the table t, and nil, +so that the construction + +

+     for k,v in pairs(t) do body end
+

+will iterate over all key–value pairs of table t. + + +

+See function next for the caveats of modifying +the table during its traversal. + + + + +

+


pcall (f, arg1, ···)

+ + +

+Calls function f with +the given arguments in protected mode. +This means that any error inside f is not propagated; +instead, pcall catches the error +and returns a status code. +Its first result is the status code (a boolean), +which is true if the call succeeds without errors. +In such case, pcall also returns all results from the call, +after this first result. +In case of any error, pcall returns false plus the error message. + + + + +

+


print (···)

+Receives any number of arguments, +and prints their values to stdout, +using the tostring function to convert them to strings. +print is not intended for formatted output, +but only as a quick way to show a value, +typically for debugging. +For formatted output, use string.format. + + + + +

+


rawequal (v1, v2)

+Checks whether v1 is equal to v2, +without invoking any metamethod. +Returns a boolean. + + + + +

+


rawget (table, index)

+Gets the real value of table[index], +without invoking any metamethod. +table must be a table; +index may be any value. + + + + +

+


rawset (table, index, value)

+Sets the real value of table[index] to value, +without invoking any metamethod. +table must be a table, +index any value different from nil, +and value any Lua value. + + +

+This function returns table. + + + + +

+


select (index, ···)

+ + +

+If index is a number, +returns all arguments after argument number index. +Otherwise, index must be the string "#", +and select returns the total number of extra arguments it received. + + + + +

+


setfenv (f, table)

+ + +

+Sets the environment to be used by the given function. +f can be a Lua function or a number +that specifies the function at that stack level: +Level 1 is the function calling setfenv. +setfenv returns the given function. + + +

+As a special case, when f is 0 setfenv changes +the environment of the running thread. +In this case, setfenv returns no values. + + + + +

+


setmetatable (table, metatable)

+ + +

+Sets the metatable for the given table. +(You cannot change the metatable of other types from Lua, only from C.) +If metatable is nil, +removes the metatable of the given table. +If the original metatable has a "__metatable" field, +raises an error. + + +

+This function returns table. + + + + +

+


tonumber (e [, base])

+Tries to convert its argument to a number. +If the argument is already a number or a string convertible +to a number, then tonumber returns this number; +otherwise, it returns nil. + + +

+An optional argument specifies the base to interpret the numeral. +The base may be any integer between 2 and 36, inclusive. +In bases above 10, the letter 'A' (in either upper or lower case) +represents 10, 'B' represents 11, and so forth, +with 'Z' representing 35. +In base 10 (the default), the number can have a decimal part, +as well as an optional exponent part (see §2.1). +In other bases, only unsigned integers are accepted. + + + + +

+


tostring (e)

+Receives an argument of any type and +converts it to a string in a reasonable format. +For complete control of how numbers are converted, +use string.format. + + +

+If the metatable of e has a "__tostring" field, +then tostring calls the corresponding value +with e as argument, +and uses the result of the call as its result. + + + + +

+


type (v)

+Returns the type of its only argument, coded as a string. +The possible results of this function are +"nil" (a string, not the value nil), +"number", +"string", +"boolean", +"table", +"function", +"thread", +and "userdata". + + + + +

+


unpack (list [, i [, j]])

+Returns the elements from the given table. +This function is equivalent to + +
+     return list[i], list[i+1], ···, list[j]
+

+except that the above code can be written only for a fixed number +of elements. +By default, i is 1 and j is the length of the list, +as defined by the length operator (see §2.5.5). + + + + +

+


_VERSION

+A global variable (not a function) that +holds a string containing the current interpreter version. +The current contents of this variable is "Lua 5.1". + + + + +

+


xpcall (f, err)

+ + +

+This function is similar to pcall, +except that you can set a new error handler. + + +

+xpcall calls function f in protected mode, +using err as the error handler. +Any error inside f is not propagated; +instead, xpcall catches the error, +calls the err function with the original error object, +and returns a status code. +Its first result is the status code (a boolean), +which is true if the call succeeds without errors. +In this case, xpcall also returns all results from the call, +after this first result. +In case of any error, +xpcall returns false plus the result from err. + + + + + + + +

5.2 - Coroutine Manipulation

+ +

+The operations related to coroutines comprise a sub-library of +the basic library and come inside the table coroutine. +See §2.11 for a general description of coroutines. + + +

+


coroutine.create (f)

+ + +

+Creates a new coroutine, with body f. +f must be a Lua function. +Returns this new coroutine, +an object with type "thread". + + + + +

+


coroutine.resume (co [, val1, ···])

+ + +

+Starts or continues the execution of coroutine co. +The first time you resume a coroutine, +it starts running its body. +The values val1, ··· are passed +as the arguments to the body function. +If the coroutine has yielded, +resume restarts it; +the values val1, ··· are passed +as the results from the yield. + + +

+If the coroutine runs without any errors, +resume returns true plus any values passed to yield +(if the coroutine yields) or any values returned by the body function +(if the coroutine terminates). +If there is any error, +resume returns false plus the error message. + + + + +

+


coroutine.running ()

+ + +

+Returns the running coroutine, +or nil when called by the main thread. + + + + +

+


coroutine.status (co)

+ + +

+Returns the status of coroutine co, as a string: +"running", +if the coroutine is running (that is, it called status); +"suspended", if the coroutine is suspended in a call to yield, +or if it has not started running yet; +"normal" if the coroutine is active but not running +(that is, it has resumed another coroutine); +and "dead" if the coroutine has finished its body function, +or if it has stopped with an error. + + + + +

+


coroutine.wrap (f)

+ + +

+Creates a new coroutine, with body f. +f must be a Lua function. +Returns a function that resumes the coroutine each time it is called. +Any arguments passed to the function behave as the +extra arguments to resume. +Returns the same values returned by resume, +except the first boolean. +In case of error, propagates the error. + + + + +

+


coroutine.yield (···)

+ + +

+Suspends the execution of the calling coroutine. +The coroutine cannot be running a C function, +a metamethod, or an iterator. +Any arguments to yield are passed as extra results to resume. + + + + + + + +

5.3 - Modules

+ +

+The package library provides basic +facilities for loading and building modules in Lua. +It exports two of its functions directly in the global environment: +require and module. +Everything else is exported in a table package. + + +

+


module (name [, ···])

+ + +

+Creates a module. +If there is a table in package.loaded[name], +this table is the module. +Otherwise, if there is a global table t with the given name, +this table is the module. +Otherwise creates a new table t and +sets it as the value of the global name and +the value of package.loaded[name]. +This function also initializes t._NAME with the given name, +t._M with the module (t itself), +and t._PACKAGE with the package name +(the full module name minus last component; see below). +Finally, module sets t as the new environment +of the current function and the new value of package.loaded[name], +so that require returns t. + + +

+If name is a compound name +(that is, one with components separated by dots), +module creates (or reuses, if they already exist) +tables for each component. +For instance, if name is a.b.c, +then module stores the module table in field c of +field b of global a. + + +

+This function can receive optional options after +the module name, +where each option is a function to be applied over the module. + + + + +

+


require (modname)

+ + +

+Loads the given module. +The function starts by looking into the package.loaded table +to determine whether modname is already loaded. +If it is, then require returns the value stored +at package.loaded[modname]. +Otherwise, it tries to find a loader for the module. + + +

+To find a loader, +require is guided by the package.loaders array. +By changing this array, +we can change how require looks for a module. +The following explanation is based on the default configuration +for package.loaders. + + +

+First require queries package.preload[modname]. +If it has a value, +this value (which should be a function) is the loader. +Otherwise require searches for a Lua loader using the +path stored in package.path. +If that also fails, it searches for a C loader using the +path stored in package.cpath. +If that also fails, +it tries an all-in-one loader (see package.loaders). + + +

+Once a loader is found, +require calls the loader with a single argument, modname. +If the loader returns any value, +require assigns the returned value to package.loaded[modname]. +If the loader returns no value and +has not assigned any value to package.loaded[modname], +then require assigns true to this entry. +In any case, require returns the +final value of package.loaded[modname]. + + +

+If there is any error loading or running the module, +or if it cannot find any loader for the module, +then require signals an error. + + + + +

+


package.cpath

+ + +

+The path used by require to search for a C loader. + + +

+Lua initializes the C path package.cpath in the same way +it initializes the Lua path package.path, +using the environment variable LUA_CPATH +or a default path defined in luaconf.h. + + + + +

+ +


package.loaded

+ + +

+A table used by require to control which +modules are already loaded. +When you require a module modname and +package.loaded[modname] is not false, +require simply returns the value stored there. + + + + +

+


package.loaders

+ + +

+A table used by require to control how to load modules. + + +

+Each entry in this table is a searcher function. +When looking for a module, +require calls each of these searchers in ascending order, +with the module name (the argument given to require) as its +sole parameter. +The function can return another function (the module loader) +or a string explaining why it did not find that module +(or nil if it has nothing to say). +Lua initializes this table with four functions. + + +

+The first searcher simply looks for a loader in the +package.preload table. + + +

+The second searcher looks for a loader as a Lua library, +using the path stored at package.path. +A path is a sequence of templates separated by semicolons. +For each template, +the searcher will change each interrogation +mark in the template by filename, +which is the module name with each dot replaced by a +"directory separator" (such as "/" in Unix); +then it will try to open the resulting file name. +So, for instance, if the Lua path is the string + +

+     "./?.lua;./?.lc;/usr/local/?/init.lua"
+

+the search for a Lua file for module foo +will try to open the files +./foo.lua, ./foo.lc, and +/usr/local/foo/init.lua, in that order. + + +

+The third searcher looks for a loader as a C library, +using the path given by the variable package.cpath. +For instance, +if the C path is the string + +

+     "./?.so;./?.dll;/usr/local/?/init.so"
+

+the searcher for module foo +will try to open the files ./foo.so, ./foo.dll, +and /usr/local/foo/init.so, in that order. +Once it finds a C library, +this searcher first uses a dynamic link facility to link the +application with the library. +Then it tries to find a C function inside the library to +be used as the loader. +The name of this C function is the string "luaopen_" +concatenated with a copy of the module name where each dot +is replaced by an underscore. +Moreover, if the module name has a hyphen, +its prefix up to (and including) the first hyphen is removed. +For instance, if the module name is a.v1-b.c, +the function name will be luaopen_b_c. + + +

+The fourth searcher tries an all-in-one loader. +It searches the C path for a library for +the root name of the given module. +For instance, when requiring a.b.c, +it will search for a C library for a. +If found, it looks into it for an open function for +the submodule; +in our example, that would be luaopen_a_b_c. +With this facility, a package can pack several C submodules +into one single library, +with each submodule keeping its original open function. + + + + +

+


package.loadlib (libname, funcname)

+ + +

+Dynamically links the host program with the C library libname. +Inside this library, looks for a function funcname +and returns this function as a C function. +(So, funcname must follow the protocol (see lua_CFunction)). + + +

+This is a low-level function. +It completely bypasses the package and module system. +Unlike require, +it does not perform any path searching and +does not automatically adds extensions. +libname must be the complete file name of the C library, +including if necessary a path and extension. +funcname must be the exact name exported by the C library +(which may depend on the C compiler and linker used). + + +

+This function is not supported by ANSI C. +As such, it is only available on some platforms +(Windows, Linux, Mac OS X, Solaris, BSD, +plus other Unix systems that support the dlfcn standard). + + + + +

+


package.path

+ + +

+The path used by require to search for a Lua loader. + + +

+At start-up, Lua initializes this variable with +the value of the environment variable LUA_PATH or +with a default path defined in luaconf.h, +if the environment variable is not defined. +Any ";;" in the value of the environment variable +is replaced by the default path. + + + + +

+


package.preload

+ + +

+A table to store loaders for specific modules +(see require). + + + + +

+


package.seeall (module)

+ + +

+Sets a metatable for module with +its __index field referring to the global environment, +so that this module inherits values +from the global environment. +To be used as an option to function module. + + + + + + + +

5.4 - String Manipulation

+ +

+This library provides generic functions for string manipulation, +such as finding and extracting substrings, and pattern matching. +When indexing a string in Lua, the first character is at position 1 +(not at 0, as in C). +Indices are allowed to be negative and are interpreted as indexing backwards, +from the end of the string. +Thus, the last character is at position -1, and so on. + + +

+The string library provides all its functions inside the table +string. +It also sets a metatable for strings +where the __index field points to the string table. +Therefore, you can use the string functions in object-oriented style. +For instance, string.byte(s, i) +can be written as s:byte(i). + + +

+The string library assumes one-byte character encodings. + + +

+


string.byte (s [, i [, j]])

+Returns the internal numerical codes of the characters s[i], +s[i+1], ···, s[j]. +The default value for i is 1; +the default value for j is i. + + +

+Note that numerical codes are not necessarily portable across platforms. + + + + +

+


string.char (···)

+Receives zero or more integers. +Returns a string with length equal to the number of arguments, +in which each character has the internal numerical code equal +to its corresponding argument. + + +

+Note that numerical codes are not necessarily portable across platforms. + + + + +

+


string.dump (function)

+ + +

+Returns a string containing a binary representation of the given function, +so that a later loadstring on this string returns +a copy of the function. +function must be a Lua function without upvalues. + + + + +

+


string.find (s, pattern [, init [, plain]])

+Looks for the first match of +pattern in the string s. +If it finds a match, then find returns the indices of s +where this occurrence starts and ends; +otherwise, it returns nil. +A third, optional numerical argument init specifies +where to start the search; +its default value is 1 and can be negative. +A value of true as a fourth, optional argument plain +turns off the pattern matching facilities, +so the function does a plain "find substring" operation, +with no characters in pattern being considered "magic". +Note that if plain is given, then init must be given as well. + + +

+If the pattern has captures, +then in a successful match +the captured values are also returned, +after the two indices. + + + + +

+


string.format (formatstring, ···)

+Returns a formatted version of its variable number of arguments +following the description given in its first argument (which must be a string). +The format string follows the same rules as the printf family of +standard C functions. +The only differences are that the options/modifiers +*, l, L, n, p, +and h are not supported +and that there is an extra option, q. +The q option formats a string in a form suitable to be safely read +back by the Lua interpreter: +the string is written between double quotes, +and all double quotes, newlines, embedded zeros, +and backslashes in the string +are correctly escaped when written. +For instance, the call + +
+     string.format('%q', 'a string with "quotes" and \n new line')
+

+will produce the string: + +

+     "a string with \"quotes\" and \
+      new line"
+
+ +

+The options c, d, E, e, f, +g, G, i, o, u, X, and x all +expect a number as argument, +whereas q and s expect a string. + + +

+This function does not accept string values +containing embedded zeros, +except as arguments to the q option. + + + + +

+


string.gmatch (s, pattern)

+Returns an iterator function that, +each time it is called, +returns the next captures from pattern over string s. +If pattern specifies no captures, +then the whole match is produced in each call. + + +

+As an example, the following loop + +

+     s = "hello world from Lua"
+     for w in string.gmatch(s, "%a+") do
+       print(w)
+     end
+

+will iterate over all the words from string s, +printing one per line. +The next example collects all pairs key=value from the +given string into a table: + +

+     t = {}
+     s = "from=world, to=Lua"
+     for k, v in string.gmatch(s, "(%w+)=(%w+)") do
+       t[k] = v
+     end
+
+ +

+For this function, a '^' at the start of a pattern does not +work as an anchor, as this would prevent the iteration. + + + + +

+


string.gsub (s, pattern, repl [, n])

+Returns a copy of s +in which all (or the first n, if given) +occurrences of the pattern have been +replaced by a replacement string specified by repl, +which can be a string, a table, or a function. +gsub also returns, as its second value, +the total number of matches that occurred. + + +

+If repl is a string, then its value is used for replacement. +The character % works as an escape character: +any sequence in repl of the form %n, +with n between 1 and 9, +stands for the value of the n-th captured substring (see below). +The sequence %0 stands for the whole match. +The sequence %% stands for a single %. + + +

+If repl is a table, then the table is queried for every match, +using the first capture as the key; +if the pattern specifies no captures, +then the whole match is used as the key. + + +

+If repl is a function, then this function is called every time a +match occurs, with all captured substrings passed as arguments, +in order; +if the pattern specifies no captures, +then the whole match is passed as a sole argument. + + +

+If the value returned by the table query or by the function call +is a string or a number, +then it is used as the replacement string; +otherwise, if it is false or nil, +then there is no replacement +(that is, the original match is kept in the string). + + +

+Here are some examples: + +

+     x = string.gsub("hello world", "(%w+)", "%1 %1")
+     --> x="hello hello world world"
+     
+     x = string.gsub("hello world", "%w+", "%0 %0", 1)
+     --> x="hello hello world"
+     
+     x = string.gsub("hello world from Lua", "(%w+)%s*(%w+)", "%2 %1")
+     --> x="world hello Lua from"
+     
+     x = string.gsub("home = $HOME, user = $USER", "%$(%w+)", os.getenv)
+     --> x="home = /home/roberto, user = roberto"
+     
+     x = string.gsub("4+5 = $return 4+5$", "%$(.-)%$", function (s)
+           return loadstring(s)()
+         end)
+     --> x="4+5 = 9"
+     
+     local t = {name="lua", version="5.1"}
+     x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t)
+     --> x="lua-5.1.tar.gz"
+
+ + + +

+


string.len (s)

+Receives a string and returns its length. +The empty string "" has length 0. +Embedded zeros are counted, +so "a\000bc\000" has length 5. + + + + +

+


string.lower (s)

+Receives a string and returns a copy of this string with all +uppercase letters changed to lowercase. +All other characters are left unchanged. +The definition of what an uppercase letter is depends on the current locale. + + + + +

+


string.match (s, pattern [, init])

+Looks for the first match of +pattern in the string s. +If it finds one, then match returns +the captures from the pattern; +otherwise it returns nil. +If pattern specifies no captures, +then the whole match is returned. +A third, optional numerical argument init specifies +where to start the search; +its default value is 1 and can be negative. + + + + +

+


string.rep (s, n)

+Returns a string that is the concatenation of n copies of +the string s. + + + + +

+


string.reverse (s)

+Returns a string that is the string s reversed. + + + + +

+


string.sub (s, i [, j])

+Returns the substring of s that +starts at i and continues until j; +i and j can be negative. +If j is absent, then it is assumed to be equal to -1 +(which is the same as the string length). +In particular, +the call string.sub(s,1,j) returns a prefix of s +with length j, +and string.sub(s, -i) returns a suffix of s +with length i. + + + + +

+


string.upper (s)

+Receives a string and returns a copy of this string with all +lowercase letters changed to uppercase. +All other characters are left unchanged. +The definition of what a lowercase letter is depends on the current locale. + + + +

5.4.1 - Patterns

+ + +

Character Class:

+A character class is used to represent a set of characters. +The following combinations are allowed in describing a character class: + +

+For all classes represented by single letters (%a, %c, etc.), +the corresponding uppercase letter represents the complement of the class. +For instance, %S represents all non-space characters. + + +

+The definitions of letter, space, and other character groups +depend on the current locale. +In particular, the class [a-z] may not be equivalent to %l. + + + + + +

Pattern Item:

+A pattern item can be + +

+ + + + +

Pattern:

+A pattern is a sequence of pattern items. +A '^' at the beginning of a pattern anchors the match at the +beginning of the subject string. +A '$' at the end of a pattern anchors the match at the +end of the subject string. +At other positions, +'^' and '$' have no special meaning and represent themselves. + + + + + +

Captures:

+A pattern can contain sub-patterns enclosed in parentheses; +they describe captures. +When a match succeeds, the substrings of the subject string +that match captures are stored (captured) for future use. +Captures are numbered according to their left parentheses. +For instance, in the pattern "(a*(.)%w(%s*))", +the part of the string matching "a*(.)%w(%s*)" is +stored as the first capture (and therefore has number 1); +the character matching "." is captured with number 2, +and the part matching "%s*" has number 3. + + +

+As a special case, the empty capture () captures +the current string position (a number). +For instance, if we apply the pattern "()aa()" on the +string "flaaap", there will be two captures: 3 and 5. + + +

+A pattern cannot contain embedded zeros. Use %z instead. + + + + + + + + + + + +

5.5 - Table Manipulation

+This library provides generic functions for table manipulation. +It provides all its functions inside the table table. + + +

+Most functions in the table library assume that the table +represents an array or a list. +For these functions, when we talk about the "length" of a table +we mean the result of the length operator. + + +

+


table.concat (table [, sep [, i [, j]]])

+Given an array where all elements are strings or numbers, +returns table[i]..sep..table[i+1] ··· sep..table[j]. +The default value for sep is the empty string, +the default for i is 1, +and the default for j is the length of the table. +If i is greater than j, returns the empty string. + + + + +

+


table.insert (table, [pos,] value)

+ + +

+Inserts element value at position pos in table, +shifting up other elements to open space, if necessary. +The default value for pos is n+1, +where n is the length of the table (see §2.5.5), +so that a call table.insert(t,x) inserts x at the end +of table t. + + + + +

+


table.maxn (table)

+ + +

+Returns the largest positive numerical index of the given table, +or zero if the table has no positive numerical indices. +(To do its job this function does a linear traversal of +the whole table.) + + + + +

+


table.remove (table [, pos])

+ + +

+Removes from table the element at position pos, +shifting down other elements to close the space, if necessary. +Returns the value of the removed element. +The default value for pos is n, +where n is the length of the table, +so that a call table.remove(t) removes the last element +of table t. + + + + +

+


table.sort (table [, comp])

+Sorts table elements in a given order, in-place, +from table[1] to table[n], +where n is the length of the table. +If comp is given, +then it must be a function that receives two table elements, +and returns true +when the first is less than the second +(so that not comp(a[i+1],a[i]) will be true after the sort). +If comp is not given, +then the standard Lua operator < is used instead. + + +

+The sort algorithm is not stable; +that is, elements considered equal by the given order +may have their relative positions changed by the sort. + + + + + + + +

5.6 - Mathematical Functions

+ +

+This library is an interface to the standard C math library. +It provides all its functions inside the table math. + + +

+


math.abs (x)

+ + +

+Returns the absolute value of x. + + + + +

+


math.acos (x)

+ + +

+Returns the arc cosine of x (in radians). + + + + +

+


math.asin (x)

+ + +

+Returns the arc sine of x (in radians). + + + + +

+


math.atan (x)

+ + +

+Returns the arc tangent of x (in radians). + + + + +

+


math.atan2 (y, x)

+ + +

+Returns the arc tangent of y/x (in radians), +but uses the signs of both parameters to find the +quadrant of the result. +(It also handles correctly the case of x being zero.) + + + + +

+


math.ceil (x)

+ + +

+Returns the smallest integer larger than or equal to x. + + + + +

+


math.cos (x)

+ + +

+Returns the cosine of x (assumed to be in radians). + + + + +

+


math.cosh (x)

+ + +

+Returns the hyperbolic cosine of x. + + + + +

+


math.deg (x)

+ + +

+Returns the angle x (given in radians) in degrees. + + + + +

+


math.exp (x)

+ + +

+Returns the value ex. + + + + +

+


math.floor (x)

+ + +

+Returns the largest integer smaller than or equal to x. + + + + +

+


math.fmod (x, y)

+ + +

+Returns the remainder of the division of x by y +that rounds the quotient towards zero. + + + + +

+


math.frexp (x)

+ + +

+Returns m and e such that x = m2e, +e is an integer and the absolute value of m is +in the range [0.5, 1) +(or zero when x is zero). + + + + +

+


math.huge

+ + +

+The value HUGE_VAL, +a value larger than or equal to any other numerical value. + + + + +

+


math.ldexp (m, e)

+ + +

+Returns m2e (e should be an integer). + + + + +

+


math.log (x)

+ + +

+Returns the natural logarithm of x. + + + + +

+


math.log10 (x)

+ + +

+Returns the base-10 logarithm of x. + + + + +

+


math.max (x, ···)

+ + +

+Returns the maximum value among its arguments. + + + + +

+


math.min (x, ···)

+ + +

+Returns the minimum value among its arguments. + + + + +

+


math.modf (x)

+ + +

+Returns two numbers, +the integral part of x and the fractional part of x. + + + + +

+


math.pi

+ + +

+The value of pi. + + + + +

+


math.pow (x, y)

+ + +

+Returns xy. +(You can also use the expression x^y to compute this value.) + + + + +

+


math.rad (x)

+ + +

+Returns the angle x (given in degrees) in radians. + + + + +

+


math.random ([m [, n]])

+ + +

+This function is an interface to the simple +pseudo-random generator function rand provided by ANSI C. +(No guarantees can be given for its statistical properties.) + + +

+When called without arguments, +returns a uniform pseudo-random real number +in the range [0,1). +When called with an integer number m, +math.random returns +a uniform pseudo-random integer in the range [1, m]. +When called with two integer numbers m and n, +math.random returns a uniform pseudo-random +integer in the range [m, n]. + + + + +

+


math.randomseed (x)

+ + +

+Sets x as the "seed" +for the pseudo-random generator: +equal seeds produce equal sequences of numbers. + + + + +

+


math.sin (x)

+ + +

+Returns the sine of x (assumed to be in radians). + + + + +

+


math.sinh (x)

+ + +

+Returns the hyperbolic sine of x. + + + + +

+


math.sqrt (x)

+ + +

+Returns the square root of x. +(You can also use the expression x^0.5 to compute this value.) + + + + +

+


math.tan (x)

+ + +

+Returns the tangent of x (assumed to be in radians). + + + + +

+


math.tanh (x)

+ + +

+Returns the hyperbolic tangent of x. + + + + + + + +

5.7 - Input and Output Facilities

+ +

+The I/O library provides two different styles for file manipulation. +The first one uses implicit file descriptors; +that is, there are operations to set a default input file and a +default output file, +and all input/output operations are over these default files. +The second style uses explicit file descriptors. + + +

+When using implicit file descriptors, +all operations are supplied by table io. +When using explicit file descriptors, +the operation io.open returns a file descriptor +and then all operations are supplied as methods of the file descriptor. + + +

+The table io also provides +three predefined file descriptors with their usual meanings from C: +io.stdin, io.stdout, and io.stderr. +The I/O library never closes these files. + + +

+Unless otherwise stated, +all I/O functions return nil on failure +(plus an error message as a second result and +a system-dependent error code as a third result) +and some value different from nil on success. + + +

+


io.close ([file])

+ + +

+Equivalent to file:close(). +Without a file, closes the default output file. + + + + +

+


io.flush ()

+ + +

+Equivalent to file:flush over the default output file. + + + + +

+


io.input ([file])

+ + +

+When called with a file name, it opens the named file (in text mode), +and sets its handle as the default input file. +When called with a file handle, +it simply sets this file handle as the default input file. +When called without parameters, +it returns the current default input file. + + +

+In case of errors this function raises the error, +instead of returning an error code. + + + + +

+


io.lines ([filename])

+ + +

+Opens the given file name in read mode +and returns an iterator function that, +each time it is called, +returns a new line from the file. +Therefore, the construction + +

+     for line in io.lines(filename) do body end
+

+will iterate over all lines of the file. +When the iterator function detects the end of file, +it returns nil (to finish the loop) and automatically closes the file. + + +

+The call io.lines() (with no file name) is equivalent +to io.input():lines(); +that is, it iterates over the lines of the default input file. +In this case it does not close the file when the loop ends. + + + + +

+


io.open (filename [, mode])

+ + +

+This function opens a file, +in the mode specified in the string mode. +It returns a new file handle, +or, in case of errors, nil plus an error message. + + +

+The mode string can be any of the following: + +

+The mode string can also have a 'b' at the end, +which is needed in some systems to open the file in binary mode. +This string is exactly what is used in the +standard C function fopen. + + + + +

+


io.output ([file])

+ + +

+Similar to io.input, but operates over the default output file. + + + + +

+


io.popen (prog [, mode])

+ + +

+Starts program prog in a separated process and returns +a file handle that you can use to read data from this program +(if mode is "r", the default) +or to write data to this program +(if mode is "w"). + + +

+This function is system dependent and is not available +on all platforms. + + + + +

+


io.read (···)

+ + +

+Equivalent to io.input():read. + + + + +

+


io.tmpfile ()

+ + +

+Returns a handle for a temporary file. +This file is opened in update mode +and it is automatically removed when the program ends. + + + + +

+


io.type (obj)

+ + +

+Checks whether obj is a valid file handle. +Returns the string "file" if obj is an open file handle, +"closed file" if obj is a closed file handle, +or nil if obj is not a file handle. + + + + +

+


io.write (···)

+ + +

+Equivalent to io.output():write. + + + + +

+


file:close ()

+ + +

+Closes file. +Note that files are automatically closed when +their handles are garbage collected, +but that takes an unpredictable amount of time to happen. + + + + +

+


file:flush ()

+ + +

+Saves any written data to file. + + + + +

+


file:lines ()

+ + +

+Returns an iterator function that, +each time it is called, +returns a new line from the file. +Therefore, the construction + +

+     for line in file:lines() do body end
+

+will iterate over all lines of the file. +(Unlike io.lines, this function does not close the file +when the loop ends.) + + + + +

+


file:read (···)

+ + +

+Reads the file file, +according to the given formats, which specify what to read. +For each format, +the function returns a string (or a number) with the characters read, +or nil if it cannot read data with the specified format. +When called without formats, +it uses a default format that reads the entire next line +(see below). + + +

+The available formats are + +

+ + + +

+


file:seek ([whence] [, offset])

+ + +

+Sets and gets the file position, +measured from the beginning of the file, +to the position given by offset plus a base +specified by the string whence, as follows: + +

+In case of success, function seek returns the final file position, +measured in bytes from the beginning of the file. +If this function fails, it returns nil, +plus a string describing the error. + + +

+The default value for whence is "cur", +and for offset is 0. +Therefore, the call file:seek() returns the current +file position, without changing it; +the call file:seek("set") sets the position to the +beginning of the file (and returns 0); +and the call file:seek("end") sets the position to the +end of the file, and returns its size. + + + + +

+


file:setvbuf (mode [, size])

+ + +

+Sets the buffering mode for an output file. +There are three available modes: + +

+For the last two cases, size +specifies the size of the buffer, in bytes. +The default is an appropriate size. + + + + +

+


file:write (···)

+ + +

+Writes the value of each of its arguments to +the file. +The arguments must be strings or numbers. +To write other values, +use tostring or string.format before write. + + + + + + + +

5.8 - Operating System Facilities

+ +

+This library is implemented through table os. + + +

+


os.clock ()

+ + +

+Returns an approximation of the amount in seconds of CPU time +used by the program. + + + + +

+


os.date ([format [, time]])

+ + +

+Returns a string or a table containing date and time, +formatted according to the given string format. + + +

+If the time argument is present, +this is the time to be formatted +(see the os.time function for a description of this value). +Otherwise, date formats the current time. + + +

+If format starts with '!', +then the date is formatted in Coordinated Universal Time. +After this optional character, +if format is the string "*t", +then date returns a table with the following fields: +year (four digits), month (1--12), day (1--31), +hour (0--23), min (0--59), sec (0--61), +wday (weekday, Sunday is 1), +yday (day of the year), +and isdst (daylight saving flag, a boolean). + + +

+If format is not "*t", +then date returns the date as a string, +formatted according to the same rules as the C function strftime. + + +

+When called without arguments, +date returns a reasonable date and time representation that depends on +the host system and on the current locale +(that is, os.date() is equivalent to os.date("%c")). + + + + +

+


os.difftime (t2, t1)

+ + +

+Returns the number of seconds from time t1 to time t2. +In POSIX, Windows, and some other systems, +this value is exactly t2-t1. + + + + +

+


os.execute ([command])

+ + +

+This function is equivalent to the C function system. +It passes command to be executed by an operating system shell. +It returns a status code, which is system-dependent. +If command is absent, then it returns nonzero if a shell is available +and zero otherwise. + + + + +

+


os.exit ([code])

+ + +

+Calls the C function exit, +with an optional code, +to terminate the host program. +The default value for code is the success code. + + + + +

+


os.getenv (varname)

+ + +

+Returns the value of the process environment variable varname, +or nil if the variable is not defined. + + + + +

+


os.remove (filename)

+ + +

+Deletes the file or directory with the given name. +Directories must be empty to be removed. +If this function fails, it returns nil, +plus a string describing the error. + + + + +

+


os.rename (oldname, newname)

+ + +

+Renames file or directory named oldname to newname. +If this function fails, it returns nil, +plus a string describing the error. + + + + +

+


os.setlocale (locale [, category])

+ + +

+Sets the current locale of the program. +locale is a string specifying a locale; +category is an optional string describing which category to change: +"all", "collate", "ctype", +"monetary", "numeric", or "time"; +the default category is "all". +The function returns the name of the new locale, +or nil if the request cannot be honored. + + +

+If locale is the empty string, +the current locale is set to an implementation-defined native locale. +If locale is the string "C", +the current locale is set to the standard C locale. + + +

+When called with nil as the first argument, +this function only returns the name of the current locale +for the given category. + + + + +

+


os.time ([table])

+ + +

+Returns the current time when called without arguments, +or a time representing the date and time specified by the given table. +This table must have fields year, month, and day, +and may have fields hour, min, sec, and isdst +(for a description of these fields, see the os.date function). + + +

+The returned value is a number, whose meaning depends on your system. +In POSIX, Windows, and some other systems, this number counts the number +of seconds since some given start time (the "epoch"). +In other systems, the meaning is not specified, +and the number returned by time can be used only as an argument to +date and difftime. + + + + +

+


os.tmpname ()

+ + +

+Returns a string with a file name that can +be used for a temporary file. +The file must be explicitly opened before its use +and explicitly removed when no longer needed. + + +

+On some systems (POSIX), +this function also creates a file with that name, +to avoid security risks. +(Someone else might create the file with wrong permissions +in the time between getting the name and creating the file.) +You still have to open the file to use it +and to remove it (even if you do not use it). + + +

+When possible, +you may prefer to use io.tmpfile, +which automatically removes the file when the program ends. + + + + + + + +

5.9 - The Debug Library

+ +

+This library provides +the functionality of the debug interface to Lua programs. +You should exert care when using this library. +The functions provided here should be used exclusively for debugging +and similar tasks, such as profiling. +Please resist the temptation to use them as a +usual programming tool: +they can be very slow. +Moreover, several of these functions +violate some assumptions about Lua code +(e.g., that variables local to a function +cannot be accessed from outside or +that userdata metatables cannot be changed by Lua code) +and therefore can compromise otherwise secure code. + + +

+All functions in this library are provided +inside the debug table. +All functions that operate over a thread +have an optional first argument which is the +thread to operate over. +The default is always the current thread. + + +

+


debug.debug ()

+ + +

+Enters an interactive mode with the user, +running each string that the user enters. +Using simple commands and other debug facilities, +the user can inspect global and local variables, +change their values, evaluate expressions, and so on. +A line containing only the word cont finishes this function, +so that the caller continues its execution. + + +

+Note that commands for debug.debug are not lexically nested +within any function, and so have no direct access to local variables. + + + + +

+


debug.getfenv (o)

+Returns the environment of object o. + + + + +

+


debug.gethook ([thread])

+ + +

+Returns the current hook settings of the thread, as three values: +the current hook function, the current hook mask, +and the current hook count +(as set by the debug.sethook function). + + + + +

+


debug.getinfo ([thread,] function [, what])

+ + +

+Returns a table with information about a function. +You can give the function directly, +or you can give a number as the value of function, +which means the function running at level function of the call stack +of the given thread: +level 0 is the current function (getinfo itself); +level 1 is the function that called getinfo; +and so on. +If function is a number larger than the number of active functions, +then getinfo returns nil. + + +

+The returned table can contain all the fields returned by lua_getinfo, +with the string what describing which fields to fill in. +The default for what is to get all information available, +except the table of valid lines. +If present, +the option 'f' +adds a field named func with the function itself. +If present, +the option 'L' +adds a field named activelines with the table of +valid lines. + + +

+For instance, the expression debug.getinfo(1,"n").name returns +a table with a name for the current function, +if a reasonable name can be found, +and the expression debug.getinfo(print) +returns a table with all available information +about the print function. + + + + +

+


debug.getlocal ([thread,] level, local)

+ + +

+This function returns the name and the value of the local variable +with index local of the function at level level of the stack. +(The first parameter or local variable has index 1, and so on, +until the last active local variable.) +The function returns nil if there is no local +variable with the given index, +and raises an error when called with a level out of range. +(You can call debug.getinfo to check whether the level is valid.) + + +

+Variable names starting with '(' (open parentheses) +represent internal variables +(loop control variables, temporaries, and C function locals). + + + + +

+


debug.getmetatable (object)

+ + +

+Returns the metatable of the given object +or nil if it does not have a metatable. + + + + +

+


debug.getregistry ()

+ + +

+Returns the registry table (see §3.5). + + + + +

+


debug.getupvalue (func, up)

+ + +

+This function returns the name and the value of the upvalue +with index up of the function func. +The function returns nil if there is no upvalue with the given index. + + + + +

+


debug.setfenv (object, table)

+ + +

+Sets the environment of the given object to the given table. +Returns object. + + + + +

+


debug.sethook ([thread,] hook, mask [, count])

+ + +

+Sets the given function as a hook. +The string mask and the number count describe +when the hook will be called. +The string mask may have the following characters, +with the given meaning: + +

+With a count different from zero, +the hook is called after every count instructions. + + +

+When called without arguments, +debug.sethook turns off the hook. + + +

+When the hook is called, its first parameter is a string +describing the event that has triggered its call: +"call", "return" (or "tail return", +when simulating a return from a tail call), +"line", and "count". +For line events, +the hook also gets the new line number as its second parameter. +Inside a hook, +you can call getinfo with level 2 to get more information about +the running function +(level 0 is the getinfo function, +and level 1 is the hook function), +unless the event is "tail return". +In this case, Lua is only simulating the return, +and a call to getinfo will return invalid data. + + + + +

+


debug.setlocal ([thread,] level, local, value)

+ + +

+This function assigns the value value to the local variable +with index local of the function at level level of the stack. +The function returns nil if there is no local +variable with the given index, +and raises an error when called with a level out of range. +(You can call getinfo to check whether the level is valid.) +Otherwise, it returns the name of the local variable. + + + + +

+


debug.setmetatable (object, table)

+ + +

+Sets the metatable for the given object to the given table +(which can be nil). + + + + +

+


debug.setupvalue (func, up, value)

+ + +

+This function assigns the value value to the upvalue +with index up of the function func. +The function returns nil if there is no upvalue +with the given index. +Otherwise, it returns the name of the upvalue. + + + + +

+


debug.traceback ([thread,] [message [, level]])

+ + +

+Returns a string with a traceback of the call stack. +An optional message string is appended +at the beginning of the traceback. +An optional level number tells at which level +to start the traceback +(default is 1, the function calling traceback). + + + + + + + +

6 - Lua Stand-alone

+ +

+Although Lua has been designed as an extension language, +to be embedded in a host C program, +it is also frequently used as a stand-alone language. +An interpreter for Lua as a stand-alone language, +called simply lua, +is provided with the standard distribution. +The stand-alone interpreter includes +all standard libraries, including the debug library. +Its usage is: + +

+     lua [options] [script [args]]
+

+The options are: + +

+After handling its options, lua runs the given script, +passing to it the given args as string arguments. +When called without arguments, +lua behaves as lua -v -i +when the standard input (stdin) is a terminal, +and as lua - otherwise. + + +

+Before running any argument, +the interpreter checks for an environment variable LUA_INIT. +If its format is @filename, +then lua executes the file. +Otherwise, lua executes the string itself. + + +

+All options are handled in order, except -i. +For instance, an invocation like + +

+     $ lua -e'a=1' -e 'print(a)' script.lua
+

+will first set a to 1, then print the value of a (which is '1'), +and finally run the file script.lua with no arguments. +(Here $ is the shell prompt. Your prompt may be different.) + + +

+Before starting to run the script, +lua collects all arguments in the command line +in a global table called arg. +The script name is stored at index 0, +the first argument after the script name goes to index 1, +and so on. +Any arguments before the script name +(that is, the interpreter name plus the options) +go to negative indices. +For instance, in the call + +

+     $ lua -la b.lua t1 t2
+

+the interpreter first runs the file a.lua, +then creates a table + +

+     arg = { [-2] = "lua", [-1] = "-la",
+             [0] = "b.lua",
+             [1] = "t1", [2] = "t2" }
+

+and finally runs the file b.lua. +The script is called with arg[1], arg[2], ··· +as arguments; +it can also access these arguments with the vararg expression '...'. + + +

+In interactive mode, +if you write an incomplete statement, +the interpreter waits for its completion +by issuing a different prompt. + + +

+If the global variable _PROMPT contains a string, +then its value is used as the prompt. +Similarly, if the global variable _PROMPT2 contains a string, +its value is used as the secondary prompt +(issued during incomplete statements). +Therefore, both prompts can be changed directly on the command line +or in any Lua programs by assigning to _PROMPT. +See the next example: + +

+     $ lua -e"_PROMPT='myprompt> '" -i
+

+(The outer pair of quotes is for the shell, +the inner pair is for Lua.) +Note the use of -i to enter interactive mode; +otherwise, +the program would just end silently +right after the assignment to _PROMPT. + + +

+To allow the use of Lua as a +script interpreter in Unix systems, +the stand-alone interpreter skips +the first line of a chunk if it starts with #. +Therefore, Lua scripts can be made into executable programs +by using chmod +x and the #! form, +as in + +

+     #!/usr/local/bin/lua
+

+(Of course, +the location of the Lua interpreter may be different in your machine. +If lua is in your PATH, +then + +

+     #!/usr/bin/env lua
+

+is a more portable solution.) + + + +

7 - Incompatibilities with the Previous Version

+ +

+Here we list the incompatibilities that you may find when moving a program +from Lua 5.0 to Lua 5.1. +You can avoid most of the incompatibilities compiling Lua with +appropriate options (see file luaconf.h). +However, +all these compatibility options will be removed in the next version of Lua. + + + +

7.1 - Changes in the Language

+ + + + + +

7.2 - Changes in the Libraries

+ + + + + +

7.3 - Changes in the API

+ + + + + +

8 - The Complete Syntax of Lua

+ +

+Here is the complete syntax of Lua in extended BNF. +(It does not describe operator precedences.) + + + + +

+
+	chunk ::= {stat [`;´]} [laststat [`;´]]
+
+	block ::= chunk
+
+	stat ::=  varlist `=´ explist | 
+		 functioncall | 
+		 do block end | 
+		 while exp do block end | 
+		 repeat block until exp | 
+		 if exp then block {elseif exp then block} [else block] end | 
+		 for Name `=´ exp `,´ exp [`,´ exp] do block end | 
+		 for namelist in explist do block end | 
+		 function funcname funcbody | 
+		 local function Name funcbody | 
+		 local namelist [`=´ explist] 
+
+	laststat ::= return [explist] | break
+
+	funcname ::= Name {`.´ Name} [`:´ Name]
+
+	varlist ::= var {`,´ var}
+
+	var ::=  Name | prefixexp `[´ exp `]´ | prefixexp `.´ Name 
+
+	namelist ::= Name {`,´ Name}
+
+	explist ::= {exp `,´} exp
+
+	exp ::=  nil | false | true | Number | String | `...´ | function | 
+		 prefixexp | tableconstructor | exp binop exp | unop exp 
+
+	prefixexp ::= var | functioncall | `(´ exp `)´
+
+	functioncall ::=  prefixexp args | prefixexp `:´ Name args 
+
+	args ::=  `(´ [explist] `)´ | tableconstructor | String 
+
+	function ::= function funcbody
+
+	funcbody ::= `(´ [parlist] `)´ block end
+
+	parlist ::= namelist [`,´ `...´] | `...´
+
+	tableconstructor ::= `{´ [fieldlist] `}´
+
+	fieldlist ::= field {fieldsep field} [fieldsep]
+
+	field ::= `[´ exp `]´ `=´ exp | Name `=´ exp | exp
+
+	fieldsep ::= `,´ | `;´
+
+	binop ::= `+´ | `-´ | `*´ | `/´ | `^´ | `%´ | `..´ | 
+		 `<´ | `<=´ | `>´ | `>=´ | `==´ | `~=´ | 
+		 and | or
+
+	unop ::= `-´ | not | `#´
+
+
+ +

+ + + + + + + +


+ +Last update: +Mon Feb 13 18:54:19 BRST 2012 + + + + + diff --git a/extern/lua-5.1.5/doc/readme.html b/extern/lua-5.1.5/doc/readme.html new file mode 100644 index 00000000..3ed6a818 --- /dev/null +++ b/extern/lua-5.1.5/doc/readme.html @@ -0,0 +1,40 @@ + + +Lua documentation + + + + + +
+

+Lua +Documentation +

+ +This is the documentation included in the source distribution of Lua 5.1.5. + + + +Lua's +official web site +contains updated documentation, +especially the +reference manual. +

+ +


+ +Last update: +Fri Feb 3 09:44:42 BRST 2012 + + + + diff --git a/extern/lua-5.1.5/etc/Makefile b/extern/lua-5.1.5/etc/Makefile new file mode 100644 index 00000000..6d00008d --- /dev/null +++ b/extern/lua-5.1.5/etc/Makefile @@ -0,0 +1,44 @@ +# makefile for Lua etc + +TOP= .. +LIB= $(TOP)/src +INC= $(TOP)/src +BIN= $(TOP)/src +SRC= $(TOP)/src +TST= $(TOP)/test + +CC= gcc +CFLAGS= -O2 -Wall -I$(INC) $(MYCFLAGS) +MYCFLAGS= +MYLDFLAGS= -Wl,-E +MYLIBS= -lm +#MYLIBS= -lm -Wl,-E -ldl -lreadline -lhistory -lncurses +RM= rm -f + +default: + @echo 'Please choose a target: min noparser one strict clean' + +min: min.c + $(CC) $(CFLAGS) $@.c -L$(LIB) -llua $(MYLIBS) + echo 'print"Hello there!"' | ./a.out + +noparser: noparser.o + $(CC) noparser.o $(SRC)/lua.o -L$(LIB) -llua $(MYLIBS) + $(BIN)/luac $(TST)/hello.lua + -./a.out luac.out + -./a.out -e'a=1' + +one: + $(CC) $(CFLAGS) all.c $(MYLIBS) + ./a.out $(TST)/hello.lua + +strict: + -$(BIN)/lua -e 'print(a);b=2' + -$(BIN)/lua -lstrict -e 'print(a)' + -$(BIN)/lua -e 'function f() b=2 end f()' + -$(BIN)/lua -lstrict -e 'function f() b=2 end f()' + +clean: + $(RM) a.out core core.* *.o luac.out + +.PHONY: default min noparser one strict clean diff --git a/extern/lua-5.1.5/etc/README b/extern/lua-5.1.5/etc/README new file mode 100644 index 00000000..5149fc91 --- /dev/null +++ b/extern/lua-5.1.5/etc/README @@ -0,0 +1,37 @@ +This directory contains some useful files and code. +Unlike the code in ../src, everything here is in the public domain. + +If any of the makes fail, you're probably not using the same libraries +used to build Lua. Set MYLIBS in Makefile accordingly. + +all.c + Full Lua interpreter in a single file. + Do "make one" for a demo. + +lua.hpp + Lua header files for C++ using 'extern "C"'. + +lua.ico + A Lua icon for Windows (and web sites: save as favicon.ico). + Drawn by hand by Markus Gritsch . + +lua.pc + pkg-config data for Lua + +luavs.bat + Script to build Lua under "Visual Studio .NET Command Prompt". + Run it from the toplevel as etc\luavs.bat. + +min.c + A minimal Lua interpreter. + Good for learning and for starting your own. + Do "make min" for a demo. + +noparser.c + Linking with noparser.o avoids loading the parsing modules in lualib.a. + Do "make noparser" for a demo. + +strict.lua + Traps uses of undeclared global variables. + Do "make strict" for a demo. + diff --git a/extern/lua-5.1.5/etc/all.c b/extern/lua-5.1.5/etc/all.c new file mode 100644 index 00000000..dab68fac --- /dev/null +++ b/extern/lua-5.1.5/etc/all.c @@ -0,0 +1,38 @@ +/* +* all.c -- Lua core, libraries and interpreter in a single file +*/ + +#define luaall_c + +#include "lapi.c" +#include "lcode.c" +#include "ldebug.c" +#include "ldo.c" +#include "ldump.c" +#include "lfunc.c" +#include "lgc.c" +#include "llex.c" +#include "lmem.c" +#include "lobject.c" +#include "lopcodes.c" +#include "lparser.c" +#include "lstate.c" +#include "lstring.c" +#include "ltable.c" +#include "ltm.c" +#include "lundump.c" +#include "lvm.c" +#include "lzio.c" + +#include "lauxlib.c" +#include "lbaselib.c" +#include "ldblib.c" +#include "liolib.c" +#include "linit.c" +#include "lmathlib.c" +#include "loadlib.c" +#include "loslib.c" +#include "lstrlib.c" +#include "ltablib.c" + +#include "lua.c" diff --git a/extern/lua-5.1.5/etc/lua.hpp b/extern/lua-5.1.5/etc/lua.hpp new file mode 100644 index 00000000..ec417f59 --- /dev/null +++ b/extern/lua-5.1.5/etc/lua.hpp @@ -0,0 +1,9 @@ +// lua.hpp +// Lua header files for C++ +// <> not supplied automatically because Lua also compiles as C++ + +extern "C" { +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" +} diff --git a/extern/lua-5.1.5/etc/lua.ico b/extern/lua-5.1.5/etc/lua.ico new file mode 100644 index 00000000..ccbabc4e Binary files /dev/null and b/extern/lua-5.1.5/etc/lua.ico differ diff --git a/extern/lua-5.1.5/etc/lua.pc b/extern/lua-5.1.5/etc/lua.pc new file mode 100644 index 00000000..07e2852b --- /dev/null +++ b/extern/lua-5.1.5/etc/lua.pc @@ -0,0 +1,31 @@ +# lua.pc -- pkg-config data for Lua + +# vars from install Makefile + +# grep '^V=' ../Makefile +V= 5.1 +# grep '^R=' ../Makefile +R= 5.1.5 + +# grep '^INSTALL_.*=' ../Makefile | sed 's/INSTALL_TOP/prefix/' +prefix= /usr/local +INSTALL_BIN= ${prefix}/bin +INSTALL_INC= ${prefix}/include +INSTALL_LIB= ${prefix}/lib +INSTALL_MAN= ${prefix}/man/man1 +INSTALL_LMOD= ${prefix}/share/lua/${V} +INSTALL_CMOD= ${prefix}/lib/lua/${V} + +# canonical vars +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include + +Name: Lua +Description: An Extensible Extension Language +Version: ${R} +Requires: +Libs: -L${libdir} -llua -lm +Cflags: -I${includedir} + +# (end of lua.pc) diff --git a/extern/lua-5.1.5/etc/luavs.bat b/extern/lua-5.1.5/etc/luavs.bat new file mode 100644 index 00000000..08c2bedd --- /dev/null +++ b/extern/lua-5.1.5/etc/luavs.bat @@ -0,0 +1,28 @@ +@rem Script to build Lua under "Visual Studio .NET Command Prompt". +@rem Do not run from this directory; run it from the toplevel: etc\luavs.bat . +@rem It creates lua51.dll, lua51.lib, lua.exe, and luac.exe in src. +@rem (contributed by David Manura and Mike Pall) + +@setlocal +@set MYCOMPILE=cl /nologo /MD /O2 /W3 /c /D_CRT_SECURE_NO_DEPRECATE +@set MYLINK=link /nologo +@set MYMT=mt /nologo + +cd src +%MYCOMPILE% /DLUA_BUILD_AS_DLL l*.c +del lua.obj luac.obj +%MYLINK% /DLL /out:lua51.dll l*.obj +if exist lua51.dll.manifest^ + %MYMT% -manifest lua51.dll.manifest -outputresource:lua51.dll;2 +%MYCOMPILE% /DLUA_BUILD_AS_DLL lua.c +%MYLINK% /out:lua.exe lua.obj lua51.lib +if exist lua.exe.manifest^ + %MYMT% -manifest lua.exe.manifest -outputresource:lua.exe +%MYCOMPILE% l*.c print.c +del lua.obj linit.obj lbaselib.obj ldblib.obj liolib.obj lmathlib.obj^ + loslib.obj ltablib.obj lstrlib.obj loadlib.obj +%MYLINK% /out:luac.exe *.obj +if exist luac.exe.manifest^ + %MYMT% -manifest luac.exe.manifest -outputresource:luac.exe +del *.obj *.manifest +cd .. diff --git a/extern/lua-5.1.5/etc/min.c b/extern/lua-5.1.5/etc/min.c new file mode 100644 index 00000000..6a85a4d1 --- /dev/null +++ b/extern/lua-5.1.5/etc/min.c @@ -0,0 +1,39 @@ +/* +* min.c -- a minimal Lua interpreter +* loads stdin only with minimal error handling. +* no interaction, and no standard library, only a "print" function. +*/ + +#include + +#include "lua.h" +#include "lauxlib.h" + +static int print(lua_State *L) +{ + int n=lua_gettop(L); + int i; + for (i=1; i<=n; i++) + { + if (i>1) printf("\t"); + if (lua_isstring(L,i)) + printf("%s",lua_tostring(L,i)); + else if (lua_isnil(L,i)) + printf("%s","nil"); + else if (lua_isboolean(L,i)) + printf("%s",lua_toboolean(L,i) ? "true" : "false"); + else + printf("%s:%p",luaL_typename(L,i),lua_topointer(L,i)); + } + printf("\n"); + return 0; +} + +int main(void) +{ + lua_State *L=lua_open(); + lua_register(L,"print",print); + if (luaL_dofile(L,NULL)!=0) fprintf(stderr,"%s\n",lua_tostring(L,-1)); + lua_close(L); + return 0; +} diff --git a/extern/lua-5.1.5/etc/noparser.c b/extern/lua-5.1.5/etc/noparser.c new file mode 100644 index 00000000..13ba5462 --- /dev/null +++ b/extern/lua-5.1.5/etc/noparser.c @@ -0,0 +1,50 @@ +/* +* The code below can be used to make a Lua core that does not contain the +* parsing modules (lcode, llex, lparser), which represent 35% of the total core. +* You'll only be able to load binary files and strings, precompiled with luac. +* (Of course, you'll have to build luac with the original parsing modules!) +* +* To use this module, simply compile it ("make noparser" does that) and list +* its object file before the Lua libraries. The linker should then not load +* the parsing modules. To try it, do "make luab". +* +* If you also want to avoid the dump module (ldump.o), define NODUMP. +* #define NODUMP +*/ + +#define LUA_CORE + +#include "llex.h" +#include "lparser.h" +#include "lzio.h" + +LUAI_FUNC void luaX_init (lua_State *L) { + UNUSED(L); +} + +LUAI_FUNC Proto *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, const char *name) { + UNUSED(z); + UNUSED(buff); + UNUSED(name); + lua_pushliteral(L,"parser not loaded"); + lua_error(L); + return NULL; +} + +#ifdef NODUMP +#include "lundump.h" + +LUAI_FUNC int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip) { + UNUSED(f); + UNUSED(w); + UNUSED(data); + UNUSED(strip); +#if 1 + UNUSED(L); + return 0; +#else + lua_pushliteral(L,"dumper not loaded"); + lua_error(L); +#endif +} +#endif diff --git a/extern/lua-5.1.5/etc/strict.lua b/extern/lua-5.1.5/etc/strict.lua new file mode 100644 index 00000000..604619dd --- /dev/null +++ b/extern/lua-5.1.5/etc/strict.lua @@ -0,0 +1,41 @@ +-- +-- strict.lua +-- checks uses of undeclared global variables +-- All global variables must be 'declared' through a regular assignment +-- (even assigning nil will do) in a main chunk before being used +-- anywhere or assigned to inside a function. +-- + +local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget + +local mt = getmetatable(_G) +if mt == nil then + mt = {} + setmetatable(_G, mt) +end + +mt.__declared = {} + +local function what () + local d = getinfo(3, "S") + return d and d.what or "C" +end + +mt.__newindex = function (t, n, v) + if not mt.__declared[n] then + local w = what() + if w ~= "main" and w ~= "C" then + error("assign to undeclared variable '"..n.."'", 2) + end + mt.__declared[n] = true + end + rawset(t, n, v) +end + +mt.__index = function (t, n) + if not mt.__declared[n] and what() ~= "C" then + error("variable '"..n.."' is not declared", 2) + end + return rawget(t, n) +end + diff --git a/extern/lua-5.1.5/src/Makefile b/extern/lua-5.1.5/src/Makefile new file mode 100644 index 00000000..e0d4c9fa --- /dev/null +++ b/extern/lua-5.1.5/src/Makefile @@ -0,0 +1,182 @@ +# makefile for building Lua +# see ../INSTALL for installation instructions +# see ../Makefile and luaconf.h for further customization + +# == CHANGE THE SETTINGS BELOW TO SUIT YOUR ENVIRONMENT ======================= + +# Your platform. See PLATS for possible values. +PLAT= none + +CC= gcc +CFLAGS= -O2 -Wall $(MYCFLAGS) +AR= ar rcu +RANLIB= ranlib +RM= rm -f +LIBS= -lm $(MYLIBS) + +MYCFLAGS= +MYLDFLAGS= +MYLIBS= + +# == END OF USER SETTINGS. NO NEED TO CHANGE ANYTHING BELOW THIS LINE ========= + +PLATS= aix ansi bsd freebsd generic linux macosx mingw posix solaris + +LUA_A= liblua.a +CORE_O= lapi.o lcode.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o lmem.o \ + lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o ltm.o \ + lundump.o lvm.o lzio.o +LIB_O= lauxlib.o lbaselib.o ldblib.o liolib.o lmathlib.o loslib.o ltablib.o \ + lstrlib.o loadlib.o linit.o + +LUA_T= lua +LUA_O= lua.o + +LUAC_T= luac +LUAC_O= luac.o print.o + +ALL_O= $(CORE_O) $(LIB_O) $(LUA_O) $(LUAC_O) +ALL_T= $(LUA_A) $(LUA_T) $(LUAC_T) +ALL_A= $(LUA_A) + +default: $(PLAT) + +all: $(ALL_T) + +o: $(ALL_O) + +a: $(ALL_A) + +$(LUA_A): $(CORE_O) $(LIB_O) + $(AR) $@ $(CORE_O) $(LIB_O) # DLL needs all object files + $(RANLIB) $@ + +$(LUA_T): $(LUA_O) $(LUA_A) + $(CC) -o $@ $(MYLDFLAGS) $(LUA_O) $(LUA_A) $(LIBS) + +$(LUAC_T): $(LUAC_O) $(LUA_A) + $(CC) -o $@ $(MYLDFLAGS) $(LUAC_O) $(LUA_A) $(LIBS) + +clean: + $(RM) $(ALL_T) $(ALL_O) + +depend: + @$(CC) $(CFLAGS) -MM l*.c print.c + +echo: + @echo "PLAT = $(PLAT)" + @echo "CC = $(CC)" + @echo "CFLAGS = $(CFLAGS)" + @echo "AR = $(AR)" + @echo "RANLIB = $(RANLIB)" + @echo "RM = $(RM)" + @echo "MYCFLAGS = $(MYCFLAGS)" + @echo "MYLDFLAGS = $(MYLDFLAGS)" + @echo "MYLIBS = $(MYLIBS)" + +# convenience targets for popular platforms + +none: + @echo "Please choose a platform:" + @echo " $(PLATS)" + +aix: + $(MAKE) all CC="xlc" CFLAGS="-O2 -DLUA_USE_POSIX -DLUA_USE_DLOPEN" MYLIBS="-ldl" MYLDFLAGS="-brtl -bexpall" + +ansi: + $(MAKE) all MYCFLAGS=-DLUA_ANSI + +bsd: + $(MAKE) all MYCFLAGS="-DLUA_USE_POSIX -DLUA_USE_DLOPEN" MYLIBS="-Wl,-E" + +freebsd: + $(MAKE) all MYCFLAGS="-DLUA_USE_LINUX" MYLIBS="-Wl,-E -lreadline" + +generic: + $(MAKE) all MYCFLAGS= + +linux: + $(MAKE) all MYCFLAGS=-DLUA_USE_LINUX MYLIBS="-Wl,-E -ldl -lreadline -lhistory -lncurses" + +macosx: + $(MAKE) all MYCFLAGS=-DLUA_USE_LINUX MYLIBS="-lreadline" +# use this on Mac OS X 10.3- +# $(MAKE) all MYCFLAGS=-DLUA_USE_MACOSX + +mingw: + $(MAKE) "LUA_A=lua51.dll" "LUA_T=lua.exe" \ + "AR=$(CC) -shared -o" "RANLIB=strip --strip-unneeded" \ + "MYCFLAGS=-DLUA_BUILD_AS_DLL" "MYLIBS=" "MYLDFLAGS=-s" lua.exe + $(MAKE) "LUAC_T=luac.exe" luac.exe + +posix: + $(MAKE) all MYCFLAGS=-DLUA_USE_POSIX + +solaris: + $(MAKE) all MYCFLAGS="-DLUA_USE_POSIX -DLUA_USE_DLOPEN" MYLIBS="-ldl" + +# list targets that do not create files (but not all makes understand .PHONY) +.PHONY: all $(PLATS) default o a clean depend echo none + +# DO NOT DELETE + +lapi.o: lapi.c lua.h luaconf.h lapi.h lobject.h llimits.h ldebug.h \ + lstate.h ltm.h lzio.h lmem.h ldo.h lfunc.h lgc.h lstring.h ltable.h \ + lundump.h lvm.h +lauxlib.o: lauxlib.c lua.h luaconf.h lauxlib.h +lbaselib.o: lbaselib.c lua.h luaconf.h lauxlib.h lualib.h +lcode.o: lcode.c lua.h luaconf.h lcode.h llex.h lobject.h llimits.h \ + lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h ldo.h lgc.h \ + ltable.h +ldblib.o: ldblib.c lua.h luaconf.h lauxlib.h lualib.h +ldebug.o: ldebug.c lua.h luaconf.h lapi.h lobject.h llimits.h lcode.h \ + llex.h lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h ldo.h \ + lfunc.h lstring.h lgc.h ltable.h lvm.h +ldo.o: ldo.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h ltm.h \ + lzio.h lmem.h ldo.h lfunc.h lgc.h lopcodes.h lparser.h lstring.h \ + ltable.h lundump.h lvm.h +ldump.o: ldump.c lua.h luaconf.h lobject.h llimits.h lstate.h ltm.h \ + lzio.h lmem.h lundump.h +lfunc.o: lfunc.c lua.h luaconf.h lfunc.h lobject.h llimits.h lgc.h lmem.h \ + lstate.h ltm.h lzio.h +lgc.o: lgc.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h ltm.h \ + lzio.h lmem.h ldo.h lfunc.h lgc.h lstring.h ltable.h +linit.o: linit.c lua.h luaconf.h lualib.h lauxlib.h +liolib.o: liolib.c lua.h luaconf.h lauxlib.h lualib.h +llex.o: llex.c lua.h luaconf.h ldo.h lobject.h llimits.h lstate.h ltm.h \ + lzio.h lmem.h llex.h lparser.h lstring.h lgc.h ltable.h +lmathlib.o: lmathlib.c lua.h luaconf.h lauxlib.h lualib.h +lmem.o: lmem.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h \ + ltm.h lzio.h lmem.h ldo.h +loadlib.o: loadlib.c lua.h luaconf.h lauxlib.h lualib.h +lobject.o: lobject.c lua.h luaconf.h ldo.h lobject.h llimits.h lstate.h \ + ltm.h lzio.h lmem.h lstring.h lgc.h lvm.h +lopcodes.o: lopcodes.c lopcodes.h llimits.h lua.h luaconf.h +loslib.o: loslib.c lua.h luaconf.h lauxlib.h lualib.h +lparser.o: lparser.c lua.h luaconf.h lcode.h llex.h lobject.h llimits.h \ + lzio.h lmem.h lopcodes.h lparser.h ldebug.h lstate.h ltm.h ldo.h \ + lfunc.h lstring.h lgc.h ltable.h +lstate.o: lstate.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h \ + ltm.h lzio.h lmem.h ldo.h lfunc.h lgc.h llex.h lstring.h ltable.h +lstring.o: lstring.c lua.h luaconf.h lmem.h llimits.h lobject.h lstate.h \ + ltm.h lzio.h lstring.h lgc.h +lstrlib.o: lstrlib.c lua.h luaconf.h lauxlib.h lualib.h +ltable.o: ltable.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h \ + ltm.h lzio.h lmem.h ldo.h lgc.h ltable.h +ltablib.o: ltablib.c lua.h luaconf.h lauxlib.h lualib.h +ltm.o: ltm.c lua.h luaconf.h lobject.h llimits.h lstate.h ltm.h lzio.h \ + lmem.h lstring.h lgc.h ltable.h +lua.o: lua.c lua.h luaconf.h lauxlib.h lualib.h +luac.o: luac.c lua.h luaconf.h lauxlib.h ldo.h lobject.h llimits.h \ + lstate.h ltm.h lzio.h lmem.h lfunc.h lopcodes.h lstring.h lgc.h \ + lundump.h +lundump.o: lundump.c lua.h luaconf.h ldebug.h lstate.h lobject.h \ + llimits.h ltm.h lzio.h lmem.h ldo.h lfunc.h lstring.h lgc.h lundump.h +lvm.o: lvm.c lua.h luaconf.h ldebug.h lstate.h lobject.h llimits.h ltm.h \ + lzio.h lmem.h ldo.h lfunc.h lgc.h lopcodes.h lstring.h ltable.h lvm.h +lzio.o: lzio.c lua.h luaconf.h llimits.h lmem.h lstate.h lobject.h ltm.h \ + lzio.h +print.o: print.c ldebug.h lstate.h lua.h luaconf.h lobject.h llimits.h \ + ltm.h lzio.h lmem.h lopcodes.h lundump.h + +# (end of Makefile) diff --git a/extern/lua-5.1.5/src/lapi.c b/extern/lua-5.1.5/src/lapi.c new file mode 100644 index 00000000..5d5145d2 --- /dev/null +++ b/extern/lua-5.1.5/src/lapi.c @@ -0,0 +1,1087 @@ +/* +** $Id: lapi.c,v 2.55.1.5 2008/07/04 18:41:18 roberto Exp $ +** Lua API +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include +#include + +#define lapi_c +#define LUA_CORE + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lundump.h" +#include "lvm.h" + + + +const char lua_ident[] = + "$Lua: " LUA_RELEASE " " LUA_COPYRIGHT " $\n" + "$Authors: " LUA_AUTHORS " $\n" + "$URL: www.lua.org $\n"; + + + +#define api_checknelems(L, n) api_check(L, (n) <= (L->top - L->base)) + +#define api_checkvalidindex(L, i) api_check(L, (i) != luaO_nilobject) + +#define api_incr_top(L) {api_check(L, L->top < L->ci->top); L->top++;} + + + +static TValue *index2adr (lua_State *L, int idx) { + if (idx > 0) { + TValue *o = L->base + (idx - 1); + api_check(L, idx <= L->ci->top - L->base); + if (o >= L->top) return cast(TValue *, luaO_nilobject); + else return o; + } + else if (idx > LUA_REGISTRYINDEX) { + api_check(L, idx != 0 && -idx <= L->top - L->base); + return L->top + idx; + } + else switch (idx) { /* pseudo-indices */ + case LUA_REGISTRYINDEX: return registry(L); + case LUA_ENVIRONINDEX: { + Closure *func = curr_func(L); + sethvalue(L, &L->env, func->c.env); + return &L->env; + } + case LUA_GLOBALSINDEX: return gt(L); + default: { + Closure *func = curr_func(L); + idx = LUA_GLOBALSINDEX - idx; + return (idx <= func->c.nupvalues) + ? &func->c.upvalue[idx-1] + : cast(TValue *, luaO_nilobject); + } + } +} + + +static Table *getcurrenv (lua_State *L) { + if (L->ci == L->base_ci) /* no enclosing function? */ + return hvalue(gt(L)); /* use global table as environment */ + else { + Closure *func = curr_func(L); + return func->c.env; + } +} + + +void luaA_pushobject (lua_State *L, const TValue *o) { + setobj2s(L, L->top, o); + api_incr_top(L); +} + + +LUA_API int lua_checkstack (lua_State *L, int size) { + int res = 1; + lua_lock(L); + if (size > LUAI_MAXCSTACK || (L->top - L->base + size) > LUAI_MAXCSTACK) + res = 0; /* stack overflow */ + else if (size > 0) { + luaD_checkstack(L, size); + if (L->ci->top < L->top + size) + L->ci->top = L->top + size; + } + lua_unlock(L); + return res; +} + + +LUA_API void lua_xmove (lua_State *from, lua_State *to, int n) { + int i; + if (from == to) return; + lua_lock(to); + api_checknelems(from, n); + api_check(from, G(from) == G(to)); + api_check(from, to->ci->top - to->top >= n); + from->top -= n; + for (i = 0; i < n; i++) { + setobj2s(to, to->top++, from->top + i); + } + lua_unlock(to); +} + + +LUA_API void lua_setlevel (lua_State *from, lua_State *to) { + to->nCcalls = from->nCcalls; +} + + +LUA_API lua_CFunction lua_atpanic (lua_State *L, lua_CFunction panicf) { + lua_CFunction old; + lua_lock(L); + old = G(L)->panic; + G(L)->panic = panicf; + lua_unlock(L); + return old; +} + + +LUA_API lua_State *lua_newthread (lua_State *L) { + lua_State *L1; + lua_lock(L); + luaC_checkGC(L); + L1 = luaE_newthread(L); + setthvalue(L, L->top, L1); + api_incr_top(L); + lua_unlock(L); + luai_userstatethread(L, L1); + return L1; +} + + + +/* +** basic stack manipulation +*/ + + +LUA_API int lua_gettop (lua_State *L) { + return cast_int(L->top - L->base); +} + + +LUA_API void lua_settop (lua_State *L, int idx) { + lua_lock(L); + if (idx >= 0) { + api_check(L, idx <= L->stack_last - L->base); + while (L->top < L->base + idx) + setnilvalue(L->top++); + L->top = L->base + idx; + } + else { + api_check(L, -(idx+1) <= (L->top - L->base)); + L->top += idx+1; /* `subtract' index (index is negative) */ + } + lua_unlock(L); +} + + +LUA_API void lua_remove (lua_State *L, int idx) { + StkId p; + lua_lock(L); + p = index2adr(L, idx); + api_checkvalidindex(L, p); + while (++p < L->top) setobjs2s(L, p-1, p); + L->top--; + lua_unlock(L); +} + + +LUA_API void lua_insert (lua_State *L, int idx) { + StkId p; + StkId q; + lua_lock(L); + p = index2adr(L, idx); + api_checkvalidindex(L, p); + for (q = L->top; q>p; q--) setobjs2s(L, q, q-1); + setobjs2s(L, p, L->top); + lua_unlock(L); +} + + +LUA_API void lua_replace (lua_State *L, int idx) { + StkId o; + lua_lock(L); + /* explicit test for incompatible code */ + if (idx == LUA_ENVIRONINDEX && L->ci == L->base_ci) + luaG_runerror(L, "no calling environment"); + api_checknelems(L, 1); + o = index2adr(L, idx); + api_checkvalidindex(L, o); + if (idx == LUA_ENVIRONINDEX) { + Closure *func = curr_func(L); + api_check(L, ttistable(L->top - 1)); + func->c.env = hvalue(L->top - 1); + luaC_barrier(L, func, L->top - 1); + } + else { + setobj(L, o, L->top - 1); + if (idx < LUA_GLOBALSINDEX) /* function upvalue? */ + luaC_barrier(L, curr_func(L), L->top - 1); + } + L->top--; + lua_unlock(L); +} + + +LUA_API void lua_pushvalue (lua_State *L, int idx) { + lua_lock(L); + setobj2s(L, L->top, index2adr(L, idx)); + api_incr_top(L); + lua_unlock(L); +} + + + +/* +** access functions (stack -> C) +*/ + + +LUA_API int lua_type (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + return (o == luaO_nilobject) ? LUA_TNONE : ttype(o); +} + + +LUA_API const char *lua_typename (lua_State *L, int t) { + UNUSED(L); + return (t == LUA_TNONE) ? "no value" : luaT_typenames[t]; +} + + +LUA_API int lua_iscfunction (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + return iscfunction(o); +} + + +LUA_API int lua_isnumber (lua_State *L, int idx) { + TValue n; + const TValue *o = index2adr(L, idx); + return tonumber(o, &n); +} + + +LUA_API int lua_isstring (lua_State *L, int idx) { + int t = lua_type(L, idx); + return (t == LUA_TSTRING || t == LUA_TNUMBER); +} + + +LUA_API int lua_isuserdata (lua_State *L, int idx) { + const TValue *o = index2adr(L, idx); + return (ttisuserdata(o) || ttislightuserdata(o)); +} + + +LUA_API int lua_rawequal (lua_State *L, int index1, int index2) { + StkId o1 = index2adr(L, index1); + StkId o2 = index2adr(L, index2); + return (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 + : luaO_rawequalObj(o1, o2); +} + + +LUA_API int lua_equal (lua_State *L, int index1, int index2) { + StkId o1, o2; + int i; + lua_lock(L); /* may call tag method */ + o1 = index2adr(L, index1); + o2 = index2adr(L, index2); + i = (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 : equalobj(L, o1, o2); + lua_unlock(L); + return i; +} + + +LUA_API int lua_lessthan (lua_State *L, int index1, int index2) { + StkId o1, o2; + int i; + lua_lock(L); /* may call tag method */ + o1 = index2adr(L, index1); + o2 = index2adr(L, index2); + i = (o1 == luaO_nilobject || o2 == luaO_nilobject) ? 0 + : luaV_lessthan(L, o1, o2); + lua_unlock(L); + return i; +} + + + +LUA_API lua_Number lua_tonumber (lua_State *L, int idx) { + TValue n; + const TValue *o = index2adr(L, idx); + if (tonumber(o, &n)) + return nvalue(o); + else + return 0; +} + + +LUA_API lua_Integer lua_tointeger (lua_State *L, int idx) { + TValue n; + const TValue *o = index2adr(L, idx); + if (tonumber(o, &n)) { + lua_Integer res; + lua_Number num = nvalue(o); + lua_number2integer(res, num); + return res; + } + else + return 0; +} + + +LUA_API int lua_toboolean (lua_State *L, int idx) { + const TValue *o = index2adr(L, idx); + return !l_isfalse(o); +} + + +LUA_API const char *lua_tolstring (lua_State *L, int idx, size_t *len) { + StkId o = index2adr(L, idx); + if (!ttisstring(o)) { + lua_lock(L); /* `luaV_tostring' may create a new string */ + if (!luaV_tostring(L, o)) { /* conversion failed? */ + if (len != NULL) *len = 0; + lua_unlock(L); + return NULL; + } + luaC_checkGC(L); + o = index2adr(L, idx); /* previous call may reallocate the stack */ + lua_unlock(L); + } + if (len != NULL) *len = tsvalue(o)->len; + return svalue(o); +} + + +LUA_API size_t lua_objlen (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + switch (ttype(o)) { + case LUA_TSTRING: return tsvalue(o)->len; + case LUA_TUSERDATA: return uvalue(o)->len; + case LUA_TTABLE: return luaH_getn(hvalue(o)); + case LUA_TNUMBER: { + size_t l; + lua_lock(L); /* `luaV_tostring' may create a new string */ + l = (luaV_tostring(L, o) ? tsvalue(o)->len : 0); + lua_unlock(L); + return l; + } + default: return 0; + } +} + + +LUA_API lua_CFunction lua_tocfunction (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + return (!iscfunction(o)) ? NULL : clvalue(o)->c.f; +} + + +LUA_API void *lua_touserdata (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + switch (ttype(o)) { + case LUA_TUSERDATA: return (rawuvalue(o) + 1); + case LUA_TLIGHTUSERDATA: return pvalue(o); + default: return NULL; + } +} + + +LUA_API lua_State *lua_tothread (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + return (!ttisthread(o)) ? NULL : thvalue(o); +} + + +LUA_API const void *lua_topointer (lua_State *L, int idx) { + StkId o = index2adr(L, idx); + switch (ttype(o)) { + case LUA_TTABLE: return hvalue(o); + case LUA_TFUNCTION: return clvalue(o); + case LUA_TTHREAD: return thvalue(o); + case LUA_TUSERDATA: + case LUA_TLIGHTUSERDATA: + return lua_touserdata(L, idx); + default: return NULL; + } +} + + + +/* +** push functions (C -> stack) +*/ + + +LUA_API void lua_pushnil (lua_State *L) { + lua_lock(L); + setnilvalue(L->top); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushnumber (lua_State *L, lua_Number n) { + lua_lock(L); + setnvalue(L->top, n); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushinteger (lua_State *L, lua_Integer n) { + lua_lock(L); + setnvalue(L->top, cast_num(n)); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushlstring (lua_State *L, const char *s, size_t len) { + lua_lock(L); + luaC_checkGC(L); + setsvalue2s(L, L->top, luaS_newlstr(L, s, len)); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushstring (lua_State *L, const char *s) { + if (s == NULL) + lua_pushnil(L); + else + lua_pushlstring(L, s, strlen(s)); +} + + +LUA_API const char *lua_pushvfstring (lua_State *L, const char *fmt, + va_list argp) { + const char *ret; + lua_lock(L); + luaC_checkGC(L); + ret = luaO_pushvfstring(L, fmt, argp); + lua_unlock(L); + return ret; +} + + +LUA_API const char *lua_pushfstring (lua_State *L, const char *fmt, ...) { + const char *ret; + va_list argp; + lua_lock(L); + luaC_checkGC(L); + va_start(argp, fmt); + ret = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + lua_unlock(L); + return ret; +} + + +LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) { + Closure *cl; + lua_lock(L); + luaC_checkGC(L); + api_checknelems(L, n); + cl = luaF_newCclosure(L, n, getcurrenv(L)); + cl->c.f = fn; + L->top -= n; + while (n--) + setobj2n(L, &cl->c.upvalue[n], L->top+n); + setclvalue(L, L->top, cl); + lua_assert(iswhite(obj2gco(cl))); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushboolean (lua_State *L, int b) { + lua_lock(L); + setbvalue(L->top, (b != 0)); /* ensure that true is 1 */ + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushlightuserdata (lua_State *L, void *p) { + lua_lock(L); + setpvalue(L->top, p); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API int lua_pushthread (lua_State *L) { + lua_lock(L); + setthvalue(L, L->top, L); + api_incr_top(L); + lua_unlock(L); + return (G(L)->mainthread == L); +} + + + +/* +** get functions (Lua -> stack) +*/ + + +LUA_API void lua_gettable (lua_State *L, int idx) { + StkId t; + lua_lock(L); + t = index2adr(L, idx); + api_checkvalidindex(L, t); + luaV_gettable(L, t, L->top - 1, L->top - 1); + lua_unlock(L); +} + + +LUA_API void lua_getfield (lua_State *L, int idx, const char *k) { + StkId t; + TValue key; + lua_lock(L); + t = index2adr(L, idx); + api_checkvalidindex(L, t); + setsvalue(L, &key, luaS_new(L, k)); + luaV_gettable(L, t, &key, L->top); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_rawget (lua_State *L, int idx) { + StkId t; + lua_lock(L); + t = index2adr(L, idx); + api_check(L, ttistable(t)); + setobj2s(L, L->top - 1, luaH_get(hvalue(t), L->top - 1)); + lua_unlock(L); +} + + +LUA_API void lua_rawgeti (lua_State *L, int idx, int n) { + StkId o; + lua_lock(L); + o = index2adr(L, idx); + api_check(L, ttistable(o)); + setobj2s(L, L->top, luaH_getnum(hvalue(o), n)); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_createtable (lua_State *L, int narray, int nrec) { + lua_lock(L); + luaC_checkGC(L); + sethvalue(L, L->top, luaH_new(L, narray, nrec)); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API int lua_getmetatable (lua_State *L, int objindex) { + const TValue *obj; + Table *mt = NULL; + int res; + lua_lock(L); + obj = index2adr(L, objindex); + switch (ttype(obj)) { + case LUA_TTABLE: + mt = hvalue(obj)->metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(obj)->metatable; + break; + default: + mt = G(L)->mt[ttype(obj)]; + break; + } + if (mt == NULL) + res = 0; + else { + sethvalue(L, L->top, mt); + api_incr_top(L); + res = 1; + } + lua_unlock(L); + return res; +} + + +LUA_API void lua_getfenv (lua_State *L, int idx) { + StkId o; + lua_lock(L); + o = index2adr(L, idx); + api_checkvalidindex(L, o); + switch (ttype(o)) { + case LUA_TFUNCTION: + sethvalue(L, L->top, clvalue(o)->c.env); + break; + case LUA_TUSERDATA: + sethvalue(L, L->top, uvalue(o)->env); + break; + case LUA_TTHREAD: + setobj2s(L, L->top, gt(thvalue(o))); + break; + default: + setnilvalue(L->top); + break; + } + api_incr_top(L); + lua_unlock(L); +} + + +/* +** set functions (stack -> Lua) +*/ + + +LUA_API void lua_settable (lua_State *L, int idx) { + StkId t; + lua_lock(L); + api_checknelems(L, 2); + t = index2adr(L, idx); + api_checkvalidindex(L, t); + luaV_settable(L, t, L->top - 2, L->top - 1); + L->top -= 2; /* pop index and value */ + lua_unlock(L); +} + + +LUA_API void lua_setfield (lua_State *L, int idx, const char *k) { + StkId t; + TValue key; + lua_lock(L); + api_checknelems(L, 1); + t = index2adr(L, idx); + api_checkvalidindex(L, t); + setsvalue(L, &key, luaS_new(L, k)); + luaV_settable(L, t, &key, L->top - 1); + L->top--; /* pop value */ + lua_unlock(L); +} + + +LUA_API void lua_rawset (lua_State *L, int idx) { + StkId t; + lua_lock(L); + api_checknelems(L, 2); + t = index2adr(L, idx); + api_check(L, ttistable(t)); + setobj2t(L, luaH_set(L, hvalue(t), L->top-2), L->top-1); + luaC_barriert(L, hvalue(t), L->top-1); + L->top -= 2; + lua_unlock(L); +} + + +LUA_API void lua_rawseti (lua_State *L, int idx, int n) { + StkId o; + lua_lock(L); + api_checknelems(L, 1); + o = index2adr(L, idx); + api_check(L, ttistable(o)); + setobj2t(L, luaH_setnum(L, hvalue(o), n), L->top-1); + luaC_barriert(L, hvalue(o), L->top-1); + L->top--; + lua_unlock(L); +} + + +LUA_API int lua_setmetatable (lua_State *L, int objindex) { + TValue *obj; + Table *mt; + lua_lock(L); + api_checknelems(L, 1); + obj = index2adr(L, objindex); + api_checkvalidindex(L, obj); + if (ttisnil(L->top - 1)) + mt = NULL; + else { + api_check(L, ttistable(L->top - 1)); + mt = hvalue(L->top - 1); + } + switch (ttype(obj)) { + case LUA_TTABLE: { + hvalue(obj)->metatable = mt; + if (mt) + luaC_objbarriert(L, hvalue(obj), mt); + break; + } + case LUA_TUSERDATA: { + uvalue(obj)->metatable = mt; + if (mt) + luaC_objbarrier(L, rawuvalue(obj), mt); + break; + } + default: { + G(L)->mt[ttype(obj)] = mt; + break; + } + } + L->top--; + lua_unlock(L); + return 1; +} + + +LUA_API int lua_setfenv (lua_State *L, int idx) { + StkId o; + int res = 1; + lua_lock(L); + api_checknelems(L, 1); + o = index2adr(L, idx); + api_checkvalidindex(L, o); + api_check(L, ttistable(L->top - 1)); + switch (ttype(o)) { + case LUA_TFUNCTION: + clvalue(o)->c.env = hvalue(L->top - 1); + break; + case LUA_TUSERDATA: + uvalue(o)->env = hvalue(L->top - 1); + break; + case LUA_TTHREAD: + sethvalue(L, gt(thvalue(o)), hvalue(L->top - 1)); + break; + default: + res = 0; + break; + } + if (res) luaC_objbarrier(L, gcvalue(o), hvalue(L->top - 1)); + L->top--; + lua_unlock(L); + return res; +} + + +/* +** `load' and `call' functions (run Lua code) +*/ + + +#define adjustresults(L,nres) \ + { if (nres == LUA_MULTRET && L->top >= L->ci->top) L->ci->top = L->top; } + + +#define checkresults(L,na,nr) \ + api_check(L, (nr) == LUA_MULTRET || (L->ci->top - L->top >= (nr) - (na))) + + +LUA_API void lua_call (lua_State *L, int nargs, int nresults) { + StkId func; + lua_lock(L); + api_checknelems(L, nargs+1); + checkresults(L, nargs, nresults); + func = L->top - (nargs+1); + luaD_call(L, func, nresults); + adjustresults(L, nresults); + lua_unlock(L); +} + + + +/* +** Execute a protected call. +*/ +struct CallS { /* data to `f_call' */ + StkId func; + int nresults; +}; + + +static void f_call (lua_State *L, void *ud) { + struct CallS *c = cast(struct CallS *, ud); + luaD_call(L, c->func, c->nresults); +} + + + +LUA_API int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc) { + struct CallS c; + int status; + ptrdiff_t func; + lua_lock(L); + api_checknelems(L, nargs+1); + checkresults(L, nargs, nresults); + if (errfunc == 0) + func = 0; + else { + StkId o = index2adr(L, errfunc); + api_checkvalidindex(L, o); + func = savestack(L, o); + } + c.func = L->top - (nargs+1); /* function to be called */ + c.nresults = nresults; + status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); + adjustresults(L, nresults); + lua_unlock(L); + return status; +} + + +/* +** Execute a protected C call. +*/ +struct CCallS { /* data to `f_Ccall' */ + lua_CFunction func; + void *ud; +}; + + +static void f_Ccall (lua_State *L, void *ud) { + struct CCallS *c = cast(struct CCallS *, ud); + Closure *cl; + cl = luaF_newCclosure(L, 0, getcurrenv(L)); + cl->c.f = c->func; + setclvalue(L, L->top, cl); /* push function */ + api_incr_top(L); + setpvalue(L->top, c->ud); /* push only argument */ + api_incr_top(L); + luaD_call(L, L->top - 2, 0); +} + + +LUA_API int lua_cpcall (lua_State *L, lua_CFunction func, void *ud) { + struct CCallS c; + int status; + lua_lock(L); + c.func = func; + c.ud = ud; + status = luaD_pcall(L, f_Ccall, &c, savestack(L, L->top), 0); + lua_unlock(L); + return status; +} + + +LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data, + const char *chunkname) { + ZIO z; + int status; + lua_lock(L); + if (!chunkname) chunkname = "?"; + luaZ_init(L, &z, reader, data); + status = luaD_protectedparser(L, &z, chunkname); + lua_unlock(L); + return status; +} + + +LUA_API int lua_dump (lua_State *L, lua_Writer writer, void *data) { + int status; + TValue *o; + lua_lock(L); + api_checknelems(L, 1); + o = L->top - 1; + if (isLfunction(o)) + status = luaU_dump(L, clvalue(o)->l.p, writer, data, 0); + else + status = 1; + lua_unlock(L); + return status; +} + + +LUA_API int lua_status (lua_State *L) { + return L->status; +} + + +/* +** Garbage-collection function +*/ + +LUA_API int lua_gc (lua_State *L, int what, int data) { + int res = 0; + global_State *g; + lua_lock(L); + g = G(L); + switch (what) { + case LUA_GCSTOP: { + g->GCthreshold = MAX_LUMEM; + break; + } + case LUA_GCRESTART: { + g->GCthreshold = g->totalbytes; + break; + } + case LUA_GCCOLLECT: { + luaC_fullgc(L); + break; + } + case LUA_GCCOUNT: { + /* GC values are expressed in Kbytes: #bytes/2^10 */ + res = cast_int(g->totalbytes >> 10); + break; + } + case LUA_GCCOUNTB: { + res = cast_int(g->totalbytes & 0x3ff); + break; + } + case LUA_GCSTEP: { + lu_mem a = (cast(lu_mem, data) << 10); + if (a <= g->totalbytes) + g->GCthreshold = g->totalbytes - a; + else + g->GCthreshold = 0; + while (g->GCthreshold <= g->totalbytes) { + luaC_step(L); + if (g->gcstate == GCSpause) { /* end of cycle? */ + res = 1; /* signal it */ + break; + } + } + break; + } + case LUA_GCSETPAUSE: { + res = g->gcpause; + g->gcpause = data; + break; + } + case LUA_GCSETSTEPMUL: { + res = g->gcstepmul; + g->gcstepmul = data; + break; + } + default: res = -1; /* invalid option */ + } + lua_unlock(L); + return res; +} + + + +/* +** miscellaneous functions +*/ + + +LUA_API int lua_error (lua_State *L) { + lua_lock(L); + api_checknelems(L, 1); + luaG_errormsg(L); + lua_unlock(L); + return 0; /* to avoid warnings */ +} + + +LUA_API int lua_next (lua_State *L, int idx) { + StkId t; + int more; + lua_lock(L); + t = index2adr(L, idx); + api_check(L, ttistable(t)); + more = luaH_next(L, hvalue(t), L->top - 1); + if (more) { + api_incr_top(L); + } + else /* no more elements */ + L->top -= 1; /* remove key */ + lua_unlock(L); + return more; +} + + +LUA_API void lua_concat (lua_State *L, int n) { + lua_lock(L); + api_checknelems(L, n); + if (n >= 2) { + luaC_checkGC(L); + luaV_concat(L, n, cast_int(L->top - L->base) - 1); + L->top -= (n-1); + } + else if (n == 0) { /* push empty string */ + setsvalue2s(L, L->top, luaS_newlstr(L, "", 0)); + api_incr_top(L); + } + /* else n == 1; nothing to do */ + lua_unlock(L); +} + + +LUA_API lua_Alloc lua_getallocf (lua_State *L, void **ud) { + lua_Alloc f; + lua_lock(L); + if (ud) *ud = G(L)->ud; + f = G(L)->frealloc; + lua_unlock(L); + return f; +} + + +LUA_API void lua_setallocf (lua_State *L, lua_Alloc f, void *ud) { + lua_lock(L); + G(L)->ud = ud; + G(L)->frealloc = f; + lua_unlock(L); +} + + +LUA_API void *lua_newuserdata (lua_State *L, size_t size) { + Udata *u; + lua_lock(L); + luaC_checkGC(L); + u = luaS_newudata(L, size, getcurrenv(L)); + setuvalue(L, L->top, u); + api_incr_top(L); + lua_unlock(L); + return u + 1; +} + + + + +static const char *aux_upvalue (StkId fi, int n, TValue **val) { + Closure *f; + if (!ttisfunction(fi)) return NULL; + f = clvalue(fi); + if (f->c.isC) { + if (!(1 <= n && n <= f->c.nupvalues)) return NULL; + *val = &f->c.upvalue[n-1]; + return ""; + } + else { + Proto *p = f->l.p; + if (!(1 <= n && n <= p->sizeupvalues)) return NULL; + *val = f->l.upvals[n-1]->v; + return getstr(p->upvalues[n-1]); + } +} + + +LUA_API const char *lua_getupvalue (lua_State *L, int funcindex, int n) { + const char *name; + TValue *val; + lua_lock(L); + name = aux_upvalue(index2adr(L, funcindex), n, &val); + if (name) { + setobj2s(L, L->top, val); + api_incr_top(L); + } + lua_unlock(L); + return name; +} + + +LUA_API const char *lua_setupvalue (lua_State *L, int funcindex, int n) { + const char *name; + TValue *val; + StkId fi; + lua_lock(L); + fi = index2adr(L, funcindex); + api_checknelems(L, 1); + name = aux_upvalue(fi, n, &val); + if (name) { + L->top--; + setobj(L, val, L->top); + luaC_barrier(L, clvalue(fi), L->top); + } + lua_unlock(L); + return name; +} + diff --git a/extern/lua-5.1.5/src/lapi.h b/extern/lua-5.1.5/src/lapi.h new file mode 100644 index 00000000..2c3fab24 --- /dev/null +++ b/extern/lua-5.1.5/src/lapi.h @@ -0,0 +1,16 @@ +/* +** $Id: lapi.h,v 2.2.1.1 2007/12/27 13:02:25 roberto Exp $ +** Auxiliary functions from Lua API +** See Copyright Notice in lua.h +*/ + +#ifndef lapi_h +#define lapi_h + + +#include "lobject.h" + + +LUAI_FUNC void luaA_pushobject (lua_State *L, const TValue *o); + +#endif diff --git a/extern/lua-5.1.5/src/lauxlib.c b/extern/lua-5.1.5/src/lauxlib.c new file mode 100644 index 00000000..10f14e2c --- /dev/null +++ b/extern/lua-5.1.5/src/lauxlib.c @@ -0,0 +1,652 @@ +/* +** $Id: lauxlib.c,v 1.159.1.3 2008/01/21 13:20:51 roberto Exp $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include +#include +#include +#include + + +/* This file uses only the official API of Lua. +** Any function declared here could be written as an application function. +*/ + +#define lauxlib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" + + +#define FREELIST_REF 0 /* free list of references */ + + +/* convert a stack index to positive */ +#define abs_index(L, i) ((i) > 0 || (i) <= LUA_REGISTRYINDEX ? (i) : \ + lua_gettop(L) + (i) + 1) + + +/* +** {====================================================== +** Error-report functions +** ======================================================= +*/ + + +LUALIB_API int luaL_argerror (lua_State *L, int narg, const char *extramsg) { + lua_Debug ar; + if (!lua_getstack(L, 0, &ar)) /* no stack frame? */ + return luaL_error(L, "bad argument #%d (%s)", narg, extramsg); + lua_getinfo(L, "n", &ar); + if (strcmp(ar.namewhat, "method") == 0) { + narg--; /* do not count `self' */ + if (narg == 0) /* error is in the self argument itself? */ + return luaL_error(L, "calling " LUA_QS " on bad self (%s)", + ar.name, extramsg); + } + if (ar.name == NULL) + ar.name = "?"; + return luaL_error(L, "bad argument #%d to " LUA_QS " (%s)", + narg, ar.name, extramsg); +} + + +LUALIB_API int luaL_typerror (lua_State *L, int narg, const char *tname) { + const char *msg = lua_pushfstring(L, "%s expected, got %s", + tname, luaL_typename(L, narg)); + return luaL_argerror(L, narg, msg); +} + + +static void tag_error (lua_State *L, int narg, int tag) { + luaL_typerror(L, narg, lua_typename(L, tag)); +} + + +LUALIB_API void luaL_where (lua_State *L, int level) { + lua_Debug ar; + if (lua_getstack(L, level, &ar)) { /* check function at level */ + lua_getinfo(L, "Sl", &ar); /* get info about it */ + if (ar.currentline > 0) { /* is there info? */ + lua_pushfstring(L, "%s:%d: ", ar.short_src, ar.currentline); + return; + } + } + lua_pushliteral(L, ""); /* else, no information available... */ +} + + +LUALIB_API int luaL_error (lua_State *L, const char *fmt, ...) { + va_list argp; + va_start(argp, fmt); + luaL_where(L, 1); + lua_pushvfstring(L, fmt, argp); + va_end(argp); + lua_concat(L, 2); + return lua_error(L); +} + +/* }====================================================== */ + + +LUALIB_API int luaL_checkoption (lua_State *L, int narg, const char *def, + const char *const lst[]) { + const char *name = (def) ? luaL_optstring(L, narg, def) : + luaL_checkstring(L, narg); + int i; + for (i=0; lst[i]; i++) + if (strcmp(lst[i], name) == 0) + return i; + return luaL_argerror(L, narg, + lua_pushfstring(L, "invalid option " LUA_QS, name)); +} + + +LUALIB_API int luaL_newmetatable (lua_State *L, const char *tname) { + lua_getfield(L, LUA_REGISTRYINDEX, tname); /* get registry.name */ + if (!lua_isnil(L, -1)) /* name already in use? */ + return 0; /* leave previous value on top, but return 0 */ + lua_pop(L, 1); + lua_newtable(L); /* create metatable */ + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, tname); /* registry.name = metatable */ + return 1; +} + + +LUALIB_API void *luaL_checkudata (lua_State *L, int ud, const char *tname) { + void *p = lua_touserdata(L, ud); + if (p != NULL) { /* value is a userdata? */ + if (lua_getmetatable(L, ud)) { /* does it have a metatable? */ + lua_getfield(L, LUA_REGISTRYINDEX, tname); /* get correct metatable */ + if (lua_rawequal(L, -1, -2)) { /* does it have the correct mt? */ + lua_pop(L, 2); /* remove both metatables */ + return p; + } + } + } + luaL_typerror(L, ud, tname); /* else error */ + return NULL; /* to avoid warnings */ +} + + +LUALIB_API void luaL_checkstack (lua_State *L, int space, const char *mes) { + if (!lua_checkstack(L, space)) + luaL_error(L, "stack overflow (%s)", mes); +} + + +LUALIB_API void luaL_checktype (lua_State *L, int narg, int t) { + if (lua_type(L, narg) != t) + tag_error(L, narg, t); +} + + +LUALIB_API void luaL_checkany (lua_State *L, int narg) { + if (lua_type(L, narg) == LUA_TNONE) + luaL_argerror(L, narg, "value expected"); +} + + +LUALIB_API const char *luaL_checklstring (lua_State *L, int narg, size_t *len) { + const char *s = lua_tolstring(L, narg, len); + if (!s) tag_error(L, narg, LUA_TSTRING); + return s; +} + + +LUALIB_API const char *luaL_optlstring (lua_State *L, int narg, + const char *def, size_t *len) { + if (lua_isnoneornil(L, narg)) { + if (len) + *len = (def ? strlen(def) : 0); + return def; + } + else return luaL_checklstring(L, narg, len); +} + + +LUALIB_API lua_Number luaL_checknumber (lua_State *L, int narg) { + lua_Number d = lua_tonumber(L, narg); + if (d == 0 && !lua_isnumber(L, narg)) /* avoid extra test when d is not 0 */ + tag_error(L, narg, LUA_TNUMBER); + return d; +} + + +LUALIB_API lua_Number luaL_optnumber (lua_State *L, int narg, lua_Number def) { + return luaL_opt(L, luaL_checknumber, narg, def); +} + + +LUALIB_API lua_Integer luaL_checkinteger (lua_State *L, int narg) { + lua_Integer d = lua_tointeger(L, narg); + if (d == 0 && !lua_isnumber(L, narg)) /* avoid extra test when d is not 0 */ + tag_error(L, narg, LUA_TNUMBER); + return d; +} + + +LUALIB_API lua_Integer luaL_optinteger (lua_State *L, int narg, + lua_Integer def) { + return luaL_opt(L, luaL_checkinteger, narg, def); +} + + +LUALIB_API int luaL_getmetafield (lua_State *L, int obj, const char *event) { + if (!lua_getmetatable(L, obj)) /* no metatable? */ + return 0; + lua_pushstring(L, event); + lua_rawget(L, -2); + if (lua_isnil(L, -1)) { + lua_pop(L, 2); /* remove metatable and metafield */ + return 0; + } + else { + lua_remove(L, -2); /* remove only metatable */ + return 1; + } +} + + +LUALIB_API int luaL_callmeta (lua_State *L, int obj, const char *event) { + obj = abs_index(L, obj); + if (!luaL_getmetafield(L, obj, event)) /* no metafield? */ + return 0; + lua_pushvalue(L, obj); + lua_call(L, 1, 1); + return 1; +} + + +LUALIB_API void (luaL_register) (lua_State *L, const char *libname, + const luaL_Reg *l) { + luaI_openlib(L, libname, l, 0); +} + + +static int libsize (const luaL_Reg *l) { + int size = 0; + for (; l->name; l++) size++; + return size; +} + + +LUALIB_API void luaI_openlib (lua_State *L, const char *libname, + const luaL_Reg *l, int nup) { + if (libname) { + int size = libsize(l); + /* check whether lib already exists */ + luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", 1); + lua_getfield(L, -1, libname); /* get _LOADED[libname] */ + if (!lua_istable(L, -1)) { /* not found? */ + lua_pop(L, 1); /* remove previous result */ + /* try global variable (and create one if it does not exist) */ + if (luaL_findtable(L, LUA_GLOBALSINDEX, libname, size) != NULL) + luaL_error(L, "name conflict for module " LUA_QS, libname); + lua_pushvalue(L, -1); + lua_setfield(L, -3, libname); /* _LOADED[libname] = new table */ + } + lua_remove(L, -2); /* remove _LOADED table */ + lua_insert(L, -(nup+1)); /* move library table to below upvalues */ + } + for (; l->name; l++) { + int i; + for (i=0; ifunc, nup); + lua_setfield(L, -(nup+2), l->name); + } + lua_pop(L, nup); /* remove upvalues */ +} + + + +/* +** {====================================================== +** getn-setn: size for arrays +** ======================================================= +*/ + +#if defined(LUA_COMPAT_GETN) + +static int checkint (lua_State *L, int topop) { + int n = (lua_type(L, -1) == LUA_TNUMBER) ? lua_tointeger(L, -1) : -1; + lua_pop(L, topop); + return n; +} + + +static void getsizes (lua_State *L) { + lua_getfield(L, LUA_REGISTRYINDEX, "LUA_SIZES"); + if (lua_isnil(L, -1)) { /* no `size' table? */ + lua_pop(L, 1); /* remove nil */ + lua_newtable(L); /* create it */ + lua_pushvalue(L, -1); /* `size' will be its own metatable */ + lua_setmetatable(L, -2); + lua_pushliteral(L, "kv"); + lua_setfield(L, -2, "__mode"); /* metatable(N).__mode = "kv" */ + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, "LUA_SIZES"); /* store in register */ + } +} + + +LUALIB_API void luaL_setn (lua_State *L, int t, int n) { + t = abs_index(L, t); + lua_pushliteral(L, "n"); + lua_rawget(L, t); + if (checkint(L, 1) >= 0) { /* is there a numeric field `n'? */ + lua_pushliteral(L, "n"); /* use it */ + lua_pushinteger(L, n); + lua_rawset(L, t); + } + else { /* use `sizes' */ + getsizes(L); + lua_pushvalue(L, t); + lua_pushinteger(L, n); + lua_rawset(L, -3); /* sizes[t] = n */ + lua_pop(L, 1); /* remove `sizes' */ + } +} + + +LUALIB_API int luaL_getn (lua_State *L, int t) { + int n; + t = abs_index(L, t); + lua_pushliteral(L, "n"); /* try t.n */ + lua_rawget(L, t); + if ((n = checkint(L, 1)) >= 0) return n; + getsizes(L); /* else try sizes[t] */ + lua_pushvalue(L, t); + lua_rawget(L, -2); + if ((n = checkint(L, 2)) >= 0) return n; + return (int)lua_objlen(L, t); +} + +#endif + +/* }====================================================== */ + + + +LUALIB_API const char *luaL_gsub (lua_State *L, const char *s, const char *p, + const char *r) { + const char *wild; + size_t l = strlen(p); + luaL_Buffer b; + luaL_buffinit(L, &b); + while ((wild = strstr(s, p)) != NULL) { + luaL_addlstring(&b, s, wild - s); /* push prefix */ + luaL_addstring(&b, r); /* push replacement in place of pattern */ + s = wild + l; /* continue after `p' */ + } + luaL_addstring(&b, s); /* push last suffix */ + luaL_pushresult(&b); + return lua_tostring(L, -1); +} + + +LUALIB_API const char *luaL_findtable (lua_State *L, int idx, + const char *fname, int szhint) { + const char *e; + lua_pushvalue(L, idx); + do { + e = strchr(fname, '.'); + if (e == NULL) e = fname + strlen(fname); + lua_pushlstring(L, fname, e - fname); + lua_rawget(L, -2); + if (lua_isnil(L, -1)) { /* no such field? */ + lua_pop(L, 1); /* remove this nil */ + lua_createtable(L, 0, (*e == '.' ? 1 : szhint)); /* new table for field */ + lua_pushlstring(L, fname, e - fname); + lua_pushvalue(L, -2); + lua_settable(L, -4); /* set new table into field */ + } + else if (!lua_istable(L, -1)) { /* field has a non-table value? */ + lua_pop(L, 2); /* remove table and value */ + return fname; /* return problematic part of the name */ + } + lua_remove(L, -2); /* remove previous table */ + fname = e + 1; + } while (*e == '.'); + return NULL; +} + + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + + +#define bufflen(B) ((B)->p - (B)->buffer) +#define bufffree(B) ((size_t)(LUAL_BUFFERSIZE - bufflen(B))) + +#define LIMIT (LUA_MINSTACK/2) + + +static int emptybuffer (luaL_Buffer *B) { + size_t l = bufflen(B); + if (l == 0) return 0; /* put nothing on stack */ + else { + lua_pushlstring(B->L, B->buffer, l); + B->p = B->buffer; + B->lvl++; + return 1; + } +} + + +static void adjuststack (luaL_Buffer *B) { + if (B->lvl > 1) { + lua_State *L = B->L; + int toget = 1; /* number of levels to concat */ + size_t toplen = lua_strlen(L, -1); + do { + size_t l = lua_strlen(L, -(toget+1)); + if (B->lvl - toget + 1 >= LIMIT || toplen > l) { + toplen += l; + toget++; + } + else break; + } while (toget < B->lvl); + lua_concat(L, toget); + B->lvl = B->lvl - toget + 1; + } +} + + +LUALIB_API char *luaL_prepbuffer (luaL_Buffer *B) { + if (emptybuffer(B)) + adjuststack(B); + return B->buffer; +} + + +LUALIB_API void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l) { + while (l--) + luaL_addchar(B, *s++); +} + + +LUALIB_API void luaL_addstring (luaL_Buffer *B, const char *s) { + luaL_addlstring(B, s, strlen(s)); +} + + +LUALIB_API void luaL_pushresult (luaL_Buffer *B) { + emptybuffer(B); + lua_concat(B->L, B->lvl); + B->lvl = 1; +} + + +LUALIB_API void luaL_addvalue (luaL_Buffer *B) { + lua_State *L = B->L; + size_t vl; + const char *s = lua_tolstring(L, -1, &vl); + if (vl <= bufffree(B)) { /* fit into buffer? */ + memcpy(B->p, s, vl); /* put it there */ + B->p += vl; + lua_pop(L, 1); /* remove from stack */ + } + else { + if (emptybuffer(B)) + lua_insert(L, -2); /* put buffer before new value */ + B->lvl++; /* add new value into B stack */ + adjuststack(B); + } +} + + +LUALIB_API void luaL_buffinit (lua_State *L, luaL_Buffer *B) { + B->L = L; + B->p = B->buffer; + B->lvl = 0; +} + +/* }====================================================== */ + + +LUALIB_API int luaL_ref (lua_State *L, int t) { + int ref; + t = abs_index(L, t); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); /* remove from stack */ + return LUA_REFNIL; /* `nil' has a unique fixed reference */ + } + lua_rawgeti(L, t, FREELIST_REF); /* get first free element */ + ref = (int)lua_tointeger(L, -1); /* ref = t[FREELIST_REF] */ + lua_pop(L, 1); /* remove it from stack */ + if (ref != 0) { /* any free element? */ + lua_rawgeti(L, t, ref); /* remove it from list */ + lua_rawseti(L, t, FREELIST_REF); /* (t[FREELIST_REF] = t[ref]) */ + } + else { /* no free elements */ + ref = (int)lua_objlen(L, t); + ref++; /* create new reference */ + } + lua_rawseti(L, t, ref); + return ref; +} + + +LUALIB_API void luaL_unref (lua_State *L, int t, int ref) { + if (ref >= 0) { + t = abs_index(L, t); + lua_rawgeti(L, t, FREELIST_REF); + lua_rawseti(L, t, ref); /* t[ref] = t[FREELIST_REF] */ + lua_pushinteger(L, ref); + lua_rawseti(L, t, FREELIST_REF); /* t[FREELIST_REF] = ref */ + } +} + + + +/* +** {====================================================== +** Load functions +** ======================================================= +*/ + +typedef struct LoadF { + int extraline; + FILE *f; + char buff[LUAL_BUFFERSIZE]; +} LoadF; + + +static const char *getF (lua_State *L, void *ud, size_t *size) { + LoadF *lf = (LoadF *)ud; + (void)L; + if (lf->extraline) { + lf->extraline = 0; + *size = 1; + return "\n"; + } + if (feof(lf->f)) return NULL; + *size = fread(lf->buff, 1, sizeof(lf->buff), lf->f); + return (*size > 0) ? lf->buff : NULL; +} + + +static int errfile (lua_State *L, const char *what, int fnameindex) { + const char *serr = strerror(errno); + const char *filename = lua_tostring(L, fnameindex) + 1; + lua_pushfstring(L, "cannot %s %s: %s", what, filename, serr); + lua_remove(L, fnameindex); + return LUA_ERRFILE; +} + + +LUALIB_API int luaL_loadfile (lua_State *L, const char *filename) { + LoadF lf; + int status, readstatus; + int c; + int fnameindex = lua_gettop(L) + 1; /* index of filename on the stack */ + lf.extraline = 0; + if (filename == NULL) { + lua_pushliteral(L, "=stdin"); + lf.f = stdin; + } + else { + lua_pushfstring(L, "@%s", filename); + lf.f = fopen(filename, "r"); + if (lf.f == NULL) return errfile(L, "open", fnameindex); + } + c = getc(lf.f); + if (c == '#') { /* Unix exec. file? */ + lf.extraline = 1; + while ((c = getc(lf.f)) != EOF && c != '\n') ; /* skip first line */ + if (c == '\n') c = getc(lf.f); + } + if (c == LUA_SIGNATURE[0] && filename) { /* binary file? */ + lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */ + if (lf.f == NULL) return errfile(L, "reopen", fnameindex); + /* skip eventual `#!...' */ + while ((c = getc(lf.f)) != EOF && c != LUA_SIGNATURE[0]) ; + lf.extraline = 0; + } + ungetc(c, lf.f); + status = lua_load(L, getF, &lf, lua_tostring(L, -1)); + readstatus = ferror(lf.f); + if (filename) fclose(lf.f); /* close file (even in case of errors) */ + if (readstatus) { + lua_settop(L, fnameindex); /* ignore results from `lua_load' */ + return errfile(L, "read", fnameindex); + } + lua_remove(L, fnameindex); + return status; +} + + +typedef struct LoadS { + const char *s; + size_t size; +} LoadS; + + +static const char *getS (lua_State *L, void *ud, size_t *size) { + LoadS *ls = (LoadS *)ud; + (void)L; + if (ls->size == 0) return NULL; + *size = ls->size; + ls->size = 0; + return ls->s; +} + + +LUALIB_API int luaL_loadbuffer (lua_State *L, const char *buff, size_t size, + const char *name) { + LoadS ls; + ls.s = buff; + ls.size = size; + return lua_load(L, getS, &ls, name); +} + + +LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s) { + return luaL_loadbuffer(L, s, strlen(s), s); +} + + + +/* }====================================================== */ + + +static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { + (void)ud; + (void)osize; + if (nsize == 0) { + free(ptr); + return NULL; + } + else + return realloc(ptr, nsize); +} + + +static int panic (lua_State *L) { + (void)L; /* to avoid warnings */ + fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n", + lua_tostring(L, -1)); + return 0; +} + + +LUALIB_API lua_State *luaL_newstate (void) { + lua_State *L = lua_newstate(l_alloc, NULL); + if (L) lua_atpanic(L, &panic); + return L; +} + diff --git a/extern/lua-5.1.5/src/lauxlib.h b/extern/lua-5.1.5/src/lauxlib.h new file mode 100644 index 00000000..34258235 --- /dev/null +++ b/extern/lua-5.1.5/src/lauxlib.h @@ -0,0 +1,174 @@ +/* +** $Id: lauxlib.h,v 1.88.1.1 2007/12/27 13:02:25 roberto Exp $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lauxlib_h +#define lauxlib_h + + +#include +#include + +#include "lua.h" + + +#if defined(LUA_COMPAT_GETN) +LUALIB_API int (luaL_getn) (lua_State *L, int t); +LUALIB_API void (luaL_setn) (lua_State *L, int t, int n); +#else +#define luaL_getn(L,i) ((int)lua_objlen(L, i)) +#define luaL_setn(L,i,j) ((void)0) /* no op! */ +#endif + +#if defined(LUA_COMPAT_OPENLIB) +#define luaI_openlib luaL_openlib +#endif + + +/* extra error code for `luaL_load' */ +#define LUA_ERRFILE (LUA_ERRERR+1) + + +typedef struct luaL_Reg { + const char *name; + lua_CFunction func; +} luaL_Reg; + + + +LUALIB_API void (luaI_openlib) (lua_State *L, const char *libname, + const luaL_Reg *l, int nup); +LUALIB_API void (luaL_register) (lua_State *L, const char *libname, + const luaL_Reg *l); +LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e); +LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e); +LUALIB_API int (luaL_typerror) (lua_State *L, int narg, const char *tname); +LUALIB_API int (luaL_argerror) (lua_State *L, int numarg, const char *extramsg); +LUALIB_API const char *(luaL_checklstring) (lua_State *L, int numArg, + size_t *l); +LUALIB_API const char *(luaL_optlstring) (lua_State *L, int numArg, + const char *def, size_t *l); +LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int numArg); +LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int nArg, lua_Number def); + +LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int numArg); +LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int nArg, + lua_Integer def); + +LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg); +LUALIB_API void (luaL_checktype) (lua_State *L, int narg, int t); +LUALIB_API void (luaL_checkany) (lua_State *L, int narg); + +LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname); +LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname); + +LUALIB_API void (luaL_where) (lua_State *L, int lvl); +LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...); + +LUALIB_API int (luaL_checkoption) (lua_State *L, int narg, const char *def, + const char *const lst[]); + +LUALIB_API int (luaL_ref) (lua_State *L, int t); +LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref); + +LUALIB_API int (luaL_loadfile) (lua_State *L, const char *filename); +LUALIB_API int (luaL_loadbuffer) (lua_State *L, const char *buff, size_t sz, + const char *name); +LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s); + +LUALIB_API lua_State *(luaL_newstate) (void); + + +LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s, const char *p, + const char *r); + +LUALIB_API const char *(luaL_findtable) (lua_State *L, int idx, + const char *fname, int szhint); + + + + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ + +#define luaL_argcheck(L, cond,numarg,extramsg) \ + ((void)((cond) || luaL_argerror(L, (numarg), (extramsg)))) +#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL)) +#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL)) +#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n))) +#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d))) +#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n))) +#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d))) + +#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i))) + +#define luaL_dofile(L, fn) \ + (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_dostring(L, s) \ + (luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n))) + +#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n))) + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + + + +typedef struct luaL_Buffer { + char *p; /* current position in buffer */ + int lvl; /* number of strings in the stack (level) */ + lua_State *L; + char buffer[LUAL_BUFFERSIZE]; +} luaL_Buffer; + +#define luaL_addchar(B,c) \ + ((void)((B)->p < ((B)->buffer+LUAL_BUFFERSIZE) || luaL_prepbuffer(B)), \ + (*(B)->p++ = (char)(c))) + +/* compatibility only */ +#define luaL_putchar(B,c) luaL_addchar(B,c) + +#define luaL_addsize(B,n) ((B)->p += (n)) + +LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B); +LUALIB_API char *(luaL_prepbuffer) (luaL_Buffer *B); +LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); +LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s); +LUALIB_API void (luaL_addvalue) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresult) (luaL_Buffer *B); + + +/* }====================================================== */ + + +/* compatibility with ref system */ + +/* pre-defined references */ +#define LUA_NOREF (-2) +#define LUA_REFNIL (-1) + +#define lua_ref(L,lock) ((lock) ? luaL_ref(L, LUA_REGISTRYINDEX) : \ + (lua_pushstring(L, "unlocked references are obsolete"), lua_error(L), 0)) + +#define lua_unref(L,ref) luaL_unref(L, LUA_REGISTRYINDEX, (ref)) + +#define lua_getref(L,ref) lua_rawgeti(L, LUA_REGISTRYINDEX, (ref)) + + +#define luaL_reg luaL_Reg + +#endif + + diff --git a/extern/lua-5.1.5/src/lbaselib.c b/extern/lua-5.1.5/src/lbaselib.c new file mode 100644 index 00000000..2ab550bd --- /dev/null +++ b/extern/lua-5.1.5/src/lbaselib.c @@ -0,0 +1,653 @@ +/* +** $Id: lbaselib.c,v 1.191.1.6 2008/02/14 16:46:22 roberto Exp $ +** Basic library +** See Copyright Notice in lua.h +*/ + + + +#include +#include +#include +#include + +#define lbaselib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + + + +/* +** If your system does not support `stdout', you can just remove this function. +** If you need, you can define your own `print' function, following this +** model but changing `fputs' to put the strings at a proper place +** (a console window or a log file, for instance). +*/ +static int luaB_print (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int i; + lua_getglobal(L, "tostring"); + for (i=1; i<=n; i++) { + const char *s; + lua_pushvalue(L, -1); /* function to be called */ + lua_pushvalue(L, i); /* value to print */ + lua_call(L, 1, 1); + s = lua_tostring(L, -1); /* get result */ + if (s == NULL) + return luaL_error(L, LUA_QL("tostring") " must return a string to " + LUA_QL("print")); + if (i>1) fputs("\t", stdout); + fputs(s, stdout); + lua_pop(L, 1); /* pop result */ + } + fputs("\n", stdout); + return 0; +} + + +static int luaB_tonumber (lua_State *L) { + int base = luaL_optint(L, 2, 10); + if (base == 10) { /* standard conversion */ + luaL_checkany(L, 1); + if (lua_isnumber(L, 1)) { + lua_pushnumber(L, lua_tonumber(L, 1)); + return 1; + } + } + else { + const char *s1 = luaL_checkstring(L, 1); + char *s2; + unsigned long n; + luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range"); + n = strtoul(s1, &s2, base); + if (s1 != s2) { /* at least one valid digit? */ + while (isspace((unsigned char)(*s2))) s2++; /* skip trailing spaces */ + if (*s2 == '\0') { /* no invalid trailing characters? */ + lua_pushnumber(L, (lua_Number)n); + return 1; + } + } + } + lua_pushnil(L); /* else not a number */ + return 1; +} + + +static int luaB_error (lua_State *L) { + int level = luaL_optint(L, 2, 1); + lua_settop(L, 1); + if (lua_isstring(L, 1) && level > 0) { /* add extra information? */ + luaL_where(L, level); + lua_pushvalue(L, 1); + lua_concat(L, 2); + } + return lua_error(L); +} + + +static int luaB_getmetatable (lua_State *L) { + luaL_checkany(L, 1); + if (!lua_getmetatable(L, 1)) { + lua_pushnil(L); + return 1; /* no metatable */ + } + luaL_getmetafield(L, 1, "__metatable"); + return 1; /* returns either __metatable field (if present) or metatable */ +} + + +static int luaB_setmetatable (lua_State *L) { + int t = lua_type(L, 2); + luaL_checktype(L, 1, LUA_TTABLE); + luaL_argcheck(L, t == LUA_TNIL || t == LUA_TTABLE, 2, + "nil or table expected"); + if (luaL_getmetafield(L, 1, "__metatable")) + luaL_error(L, "cannot change a protected metatable"); + lua_settop(L, 2); + lua_setmetatable(L, 1); + return 1; +} + + +static void getfunc (lua_State *L, int opt) { + if (lua_isfunction(L, 1)) lua_pushvalue(L, 1); + else { + lua_Debug ar; + int level = opt ? luaL_optint(L, 1, 1) : luaL_checkint(L, 1); + luaL_argcheck(L, level >= 0, 1, "level must be non-negative"); + if (lua_getstack(L, level, &ar) == 0) + luaL_argerror(L, 1, "invalid level"); + lua_getinfo(L, "f", &ar); + if (lua_isnil(L, -1)) + luaL_error(L, "no function environment for tail call at level %d", + level); + } +} + + +static int luaB_getfenv (lua_State *L) { + getfunc(L, 1); + if (lua_iscfunction(L, -1)) /* is a C function? */ + lua_pushvalue(L, LUA_GLOBALSINDEX); /* return the thread's global env. */ + else + lua_getfenv(L, -1); + return 1; +} + + +static int luaB_setfenv (lua_State *L) { + luaL_checktype(L, 2, LUA_TTABLE); + getfunc(L, 0); + lua_pushvalue(L, 2); + if (lua_isnumber(L, 1) && lua_tonumber(L, 1) == 0) { + /* change environment of current thread */ + lua_pushthread(L); + lua_insert(L, -2); + lua_setfenv(L, -2); + return 0; + } + else if (lua_iscfunction(L, -2) || lua_setfenv(L, -2) == 0) + luaL_error(L, + LUA_QL("setfenv") " cannot change environment of given object"); + return 1; +} + + +static int luaB_rawequal (lua_State *L) { + luaL_checkany(L, 1); + luaL_checkany(L, 2); + lua_pushboolean(L, lua_rawequal(L, 1, 2)); + return 1; +} + + +static int luaB_rawget (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + lua_settop(L, 2); + lua_rawget(L, 1); + return 1; +} + +static int luaB_rawset (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + luaL_checkany(L, 3); + lua_settop(L, 3); + lua_rawset(L, 1); + return 1; +} + + +static int luaB_gcinfo (lua_State *L) { + lua_pushinteger(L, lua_getgccount(L)); + return 1; +} + + +static int luaB_collectgarbage (lua_State *L) { + static const char *const opts[] = {"stop", "restart", "collect", + "count", "step", "setpause", "setstepmul", NULL}; + static const int optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, LUA_GCCOLLECT, + LUA_GCCOUNT, LUA_GCSTEP, LUA_GCSETPAUSE, LUA_GCSETSTEPMUL}; + int o = luaL_checkoption(L, 1, "collect", opts); + int ex = luaL_optint(L, 2, 0); + int res = lua_gc(L, optsnum[o], ex); + switch (optsnum[o]) { + case LUA_GCCOUNT: { + int b = lua_gc(L, LUA_GCCOUNTB, 0); + lua_pushnumber(L, res + ((lua_Number)b/1024)); + return 1; + } + case LUA_GCSTEP: { + lua_pushboolean(L, res); + return 1; + } + default: { + lua_pushnumber(L, res); + return 1; + } + } +} + + +static int luaB_type (lua_State *L) { + luaL_checkany(L, 1); + lua_pushstring(L, luaL_typename(L, 1)); + return 1; +} + + +static int luaB_next (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 2); /* create a 2nd argument if there isn't one */ + if (lua_next(L, 1)) + return 2; + else { + lua_pushnil(L); + return 1; + } +} + + +static int luaB_pairs (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushvalue(L, lua_upvalueindex(1)); /* return generator, */ + lua_pushvalue(L, 1); /* state, */ + lua_pushnil(L); /* and initial value */ + return 3; +} + + +static int ipairsaux (lua_State *L) { + int i = luaL_checkint(L, 2); + luaL_checktype(L, 1, LUA_TTABLE); + i++; /* next value */ + lua_pushinteger(L, i); + lua_rawgeti(L, 1, i); + return (lua_isnil(L, -1)) ? 0 : 2; +} + + +static int luaB_ipairs (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushvalue(L, lua_upvalueindex(1)); /* return generator, */ + lua_pushvalue(L, 1); /* state, */ + lua_pushinteger(L, 0); /* and initial value */ + return 3; +} + + +static int load_aux (lua_State *L, int status) { + if (status == 0) /* OK? */ + return 1; + else { + lua_pushnil(L); + lua_insert(L, -2); /* put before error message */ + return 2; /* return nil plus error message */ + } +} + + +static int luaB_loadstring (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + const char *chunkname = luaL_optstring(L, 2, s); + return load_aux(L, luaL_loadbuffer(L, s, l, chunkname)); +} + + +static int luaB_loadfile (lua_State *L) { + const char *fname = luaL_optstring(L, 1, NULL); + return load_aux(L, luaL_loadfile(L, fname)); +} + + +/* +** Reader for generic `load' function: `lua_load' uses the +** stack for internal stuff, so the reader cannot change the +** stack top. Instead, it keeps its resulting string in a +** reserved slot inside the stack. +*/ +static const char *generic_reader (lua_State *L, void *ud, size_t *size) { + (void)ud; /* to avoid warnings */ + luaL_checkstack(L, 2, "too many nested functions"); + lua_pushvalue(L, 1); /* get function */ + lua_call(L, 0, 1); /* call it */ + if (lua_isnil(L, -1)) { + *size = 0; + return NULL; + } + else if (lua_isstring(L, -1)) { + lua_replace(L, 3); /* save string in a reserved stack slot */ + return lua_tolstring(L, 3, size); + } + else luaL_error(L, "reader function must return a string"); + return NULL; /* to avoid warnings */ +} + + +static int luaB_load (lua_State *L) { + int status; + const char *cname = luaL_optstring(L, 2, "=(load)"); + luaL_checktype(L, 1, LUA_TFUNCTION); + lua_settop(L, 3); /* function, eventual name, plus one reserved slot */ + status = lua_load(L, generic_reader, NULL, cname); + return load_aux(L, status); +} + + +static int luaB_dofile (lua_State *L) { + const char *fname = luaL_optstring(L, 1, NULL); + int n = lua_gettop(L); + if (luaL_loadfile(L, fname) != 0) lua_error(L); + lua_call(L, 0, LUA_MULTRET); + return lua_gettop(L) - n; +} + + +static int luaB_assert (lua_State *L) { + luaL_checkany(L, 1); + if (!lua_toboolean(L, 1)) + return luaL_error(L, "%s", luaL_optstring(L, 2, "assertion failed!")); + return lua_gettop(L); +} + + +static int luaB_unpack (lua_State *L) { + int i, e, n; + luaL_checktype(L, 1, LUA_TTABLE); + i = luaL_optint(L, 2, 1); + e = luaL_opt(L, luaL_checkint, 3, luaL_getn(L, 1)); + if (i > e) return 0; /* empty range */ + n = e - i + 1; /* number of elements */ + if (n <= 0 || !lua_checkstack(L, n)) /* n <= 0 means arith. overflow */ + return luaL_error(L, "too many results to unpack"); + lua_rawgeti(L, 1, i); /* push arg[i] (avoiding overflow problems) */ + while (i++ < e) /* push arg[i + 1...e] */ + lua_rawgeti(L, 1, i); + return n; +} + + +static int luaB_select (lua_State *L) { + int n = lua_gettop(L); + if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#') { + lua_pushinteger(L, n-1); + return 1; + } + else { + int i = luaL_checkint(L, 1); + if (i < 0) i = n + i; + else if (i > n) i = n; + luaL_argcheck(L, 1 <= i, 1, "index out of range"); + return n - i; + } +} + + +static int luaB_pcall (lua_State *L) { + int status; + luaL_checkany(L, 1); + status = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0); + lua_pushboolean(L, (status == 0)); + lua_insert(L, 1); + return lua_gettop(L); /* return status + all results */ +} + + +static int luaB_xpcall (lua_State *L) { + int status; + luaL_checkany(L, 2); + lua_settop(L, 2); + lua_insert(L, 1); /* put error function under function to be called */ + status = lua_pcall(L, 0, LUA_MULTRET, 1); + lua_pushboolean(L, (status == 0)); + lua_replace(L, 1); + return lua_gettop(L); /* return status + all results */ +} + + +static int luaB_tostring (lua_State *L) { + luaL_checkany(L, 1); + if (luaL_callmeta(L, 1, "__tostring")) /* is there a metafield? */ + return 1; /* use its value */ + switch (lua_type(L, 1)) { + case LUA_TNUMBER: + lua_pushstring(L, lua_tostring(L, 1)); + break; + case LUA_TSTRING: + lua_pushvalue(L, 1); + break; + case LUA_TBOOLEAN: + lua_pushstring(L, (lua_toboolean(L, 1) ? "true" : "false")); + break; + case LUA_TNIL: + lua_pushliteral(L, "nil"); + break; + default: + lua_pushfstring(L, "%s: %p", luaL_typename(L, 1), lua_topointer(L, 1)); + break; + } + return 1; +} + + +static int luaB_newproxy (lua_State *L) { + lua_settop(L, 1); + lua_newuserdata(L, 0); /* create proxy */ + if (lua_toboolean(L, 1) == 0) + return 1; /* no metatable */ + else if (lua_isboolean(L, 1)) { + lua_newtable(L); /* create a new metatable `m' ... */ + lua_pushvalue(L, -1); /* ... and mark `m' as a valid metatable */ + lua_pushboolean(L, 1); + lua_rawset(L, lua_upvalueindex(1)); /* weaktable[m] = true */ + } + else { + int validproxy = 0; /* to check if weaktable[metatable(u)] == true */ + if (lua_getmetatable(L, 1)) { + lua_rawget(L, lua_upvalueindex(1)); + validproxy = lua_toboolean(L, -1); + lua_pop(L, 1); /* remove value */ + } + luaL_argcheck(L, validproxy, 1, "boolean or proxy expected"); + lua_getmetatable(L, 1); /* metatable is valid; get it */ + } + lua_setmetatable(L, 2); + return 1; +} + + +static const luaL_Reg base_funcs[] = { + {"assert", luaB_assert}, + {"collectgarbage", luaB_collectgarbage}, + {"dofile", luaB_dofile}, + {"error", luaB_error}, + {"gcinfo", luaB_gcinfo}, + {"getfenv", luaB_getfenv}, + {"getmetatable", luaB_getmetatable}, + {"loadfile", luaB_loadfile}, + {"load", luaB_load}, + {"loadstring", luaB_loadstring}, + {"next", luaB_next}, + {"pcall", luaB_pcall}, + {"print", luaB_print}, + {"rawequal", luaB_rawequal}, + {"rawget", luaB_rawget}, + {"rawset", luaB_rawset}, + {"select", luaB_select}, + {"setfenv", luaB_setfenv}, + {"setmetatable", luaB_setmetatable}, + {"tonumber", luaB_tonumber}, + {"tostring", luaB_tostring}, + {"type", luaB_type}, + {"unpack", luaB_unpack}, + {"xpcall", luaB_xpcall}, + {NULL, NULL} +}; + + +/* +** {====================================================== +** Coroutine library +** ======================================================= +*/ + +#define CO_RUN 0 /* running */ +#define CO_SUS 1 /* suspended */ +#define CO_NOR 2 /* 'normal' (it resumed another coroutine) */ +#define CO_DEAD 3 + +static const char *const statnames[] = + {"running", "suspended", "normal", "dead"}; + +static int costatus (lua_State *L, lua_State *co) { + if (L == co) return CO_RUN; + switch (lua_status(co)) { + case LUA_YIELD: + return CO_SUS; + case 0: { + lua_Debug ar; + if (lua_getstack(co, 0, &ar) > 0) /* does it have frames? */ + return CO_NOR; /* it is running */ + else if (lua_gettop(co) == 0) + return CO_DEAD; + else + return CO_SUS; /* initial state */ + } + default: /* some error occured */ + return CO_DEAD; + } +} + + +static int luaB_costatus (lua_State *L) { + lua_State *co = lua_tothread(L, 1); + luaL_argcheck(L, co, 1, "coroutine expected"); + lua_pushstring(L, statnames[costatus(L, co)]); + return 1; +} + + +static int auxresume (lua_State *L, lua_State *co, int narg) { + int status = costatus(L, co); + if (!lua_checkstack(co, narg)) + luaL_error(L, "too many arguments to resume"); + if (status != CO_SUS) { + lua_pushfstring(L, "cannot resume %s coroutine", statnames[status]); + return -1; /* error flag */ + } + lua_xmove(L, co, narg); + lua_setlevel(L, co); + status = lua_resume(co, narg); + if (status == 0 || status == LUA_YIELD) { + int nres = lua_gettop(co); + if (!lua_checkstack(L, nres + 1)) + luaL_error(L, "too many results to resume"); + lua_xmove(co, L, nres); /* move yielded values */ + return nres; + } + else { + lua_xmove(co, L, 1); /* move error message */ + return -1; /* error flag */ + } +} + + +static int luaB_coresume (lua_State *L) { + lua_State *co = lua_tothread(L, 1); + int r; + luaL_argcheck(L, co, 1, "coroutine expected"); + r = auxresume(L, co, lua_gettop(L) - 1); + if (r < 0) { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; /* return false + error message */ + } + else { + lua_pushboolean(L, 1); + lua_insert(L, -(r + 1)); + return r + 1; /* return true + `resume' returns */ + } +} + + +static int luaB_auxwrap (lua_State *L) { + lua_State *co = lua_tothread(L, lua_upvalueindex(1)); + int r = auxresume(L, co, lua_gettop(L)); + if (r < 0) { + if (lua_isstring(L, -1)) { /* error object is a string? */ + luaL_where(L, 1); /* add extra info */ + lua_insert(L, -2); + lua_concat(L, 2); + } + lua_error(L); /* propagate error */ + } + return r; +} + + +static int luaB_cocreate (lua_State *L) { + lua_State *NL = lua_newthread(L); + luaL_argcheck(L, lua_isfunction(L, 1) && !lua_iscfunction(L, 1), 1, + "Lua function expected"); + lua_pushvalue(L, 1); /* move function to top */ + lua_xmove(L, NL, 1); /* move function from L to NL */ + return 1; +} + + +static int luaB_cowrap (lua_State *L) { + luaB_cocreate(L); + lua_pushcclosure(L, luaB_auxwrap, 1); + return 1; +} + + +static int luaB_yield (lua_State *L) { + return lua_yield(L, lua_gettop(L)); +} + + +static int luaB_corunning (lua_State *L) { + if (lua_pushthread(L)) + lua_pushnil(L); /* main thread is not a coroutine */ + return 1; +} + + +static const luaL_Reg co_funcs[] = { + {"create", luaB_cocreate}, + {"resume", luaB_coresume}, + {"running", luaB_corunning}, + {"status", luaB_costatus}, + {"wrap", luaB_cowrap}, + {"yield", luaB_yield}, + {NULL, NULL} +}; + +/* }====================================================== */ + + +static void auxopen (lua_State *L, const char *name, + lua_CFunction f, lua_CFunction u) { + lua_pushcfunction(L, u); + lua_pushcclosure(L, f, 1); + lua_setfield(L, -2, name); +} + + +static void base_open (lua_State *L) { + /* set global _G */ + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_setglobal(L, "_G"); + /* open lib into global table */ + luaL_register(L, "_G", base_funcs); + lua_pushliteral(L, LUA_VERSION); + lua_setglobal(L, "_VERSION"); /* set global _VERSION */ + /* `ipairs' and `pairs' need auxiliary functions as upvalues */ + auxopen(L, "ipairs", luaB_ipairs, ipairsaux); + auxopen(L, "pairs", luaB_pairs, luaB_next); + /* `newproxy' needs a weaktable as upvalue */ + lua_createtable(L, 0, 1); /* new table `w' */ + lua_pushvalue(L, -1); /* `w' will be its own metatable */ + lua_setmetatable(L, -2); + lua_pushliteral(L, "kv"); + lua_setfield(L, -2, "__mode"); /* metatable(w).__mode = "kv" */ + lua_pushcclosure(L, luaB_newproxy, 1); + lua_setglobal(L, "newproxy"); /* set global `newproxy' */ +} + + +LUALIB_API int luaopen_base (lua_State *L) { + base_open(L); + luaL_register(L, LUA_COLIBNAME, co_funcs); + return 2; +} + diff --git a/extern/lua-5.1.5/src/lcode.c b/extern/lua-5.1.5/src/lcode.c new file mode 100644 index 00000000..679cb9cf --- /dev/null +++ b/extern/lua-5.1.5/src/lcode.c @@ -0,0 +1,831 @@ +/* +** $Id: lcode.c,v 2.25.1.5 2011/01/31 14:53:16 roberto Exp $ +** Code generator for Lua +** See Copyright Notice in lua.h +*/ + + +#include + +#define lcode_c +#define LUA_CORE + +#include "lua.h" + +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "llex.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "ltable.h" + + +#define hasjumps(e) ((e)->t != (e)->f) + + +static int isnumeral(expdesc *e) { + return (e->k == VKNUM && e->t == NO_JUMP && e->f == NO_JUMP); +} + + +void luaK_nil (FuncState *fs, int from, int n) { + Instruction *previous; + if (fs->pc > fs->lasttarget) { /* no jumps to current position? */ + if (fs->pc == 0) { /* function start? */ + if (from >= fs->nactvar) + return; /* positions are already clean */ + } + else { + previous = &fs->f->code[fs->pc-1]; + if (GET_OPCODE(*previous) == OP_LOADNIL) { + int pfrom = GETARG_A(*previous); + int pto = GETARG_B(*previous); + if (pfrom <= from && from <= pto+1) { /* can connect both? */ + if (from+n-1 > pto) + SETARG_B(*previous, from+n-1); + return; + } + } + } + } + luaK_codeABC(fs, OP_LOADNIL, from, from+n-1, 0); /* else no optimization */ +} + + +int luaK_jump (FuncState *fs) { + int jpc = fs->jpc; /* save list of jumps to here */ + int j; + fs->jpc = NO_JUMP; + j = luaK_codeAsBx(fs, OP_JMP, 0, NO_JUMP); + luaK_concat(fs, &j, jpc); /* keep them on hold */ + return j; +} + + +void luaK_ret (FuncState *fs, int first, int nret) { + luaK_codeABC(fs, OP_RETURN, first, nret+1, 0); +} + + +static int condjump (FuncState *fs, OpCode op, int A, int B, int C) { + luaK_codeABC(fs, op, A, B, C); + return luaK_jump(fs); +} + + +static void fixjump (FuncState *fs, int pc, int dest) { + Instruction *jmp = &fs->f->code[pc]; + int offset = dest-(pc+1); + lua_assert(dest != NO_JUMP); + if (abs(offset) > MAXARG_sBx) + luaX_syntaxerror(fs->ls, "control structure too long"); + SETARG_sBx(*jmp, offset); +} + + +/* +** returns current `pc' and marks it as a jump target (to avoid wrong +** optimizations with consecutive instructions not in the same basic block). +*/ +int luaK_getlabel (FuncState *fs) { + fs->lasttarget = fs->pc; + return fs->pc; +} + + +static int getjump (FuncState *fs, int pc) { + int offset = GETARG_sBx(fs->f->code[pc]); + if (offset == NO_JUMP) /* point to itself represents end of list */ + return NO_JUMP; /* end of list */ + else + return (pc+1)+offset; /* turn offset into absolute position */ +} + + +static Instruction *getjumpcontrol (FuncState *fs, int pc) { + Instruction *pi = &fs->f->code[pc]; + if (pc >= 1 && testTMode(GET_OPCODE(*(pi-1)))) + return pi-1; + else + return pi; +} + + +/* +** check whether list has any jump that do not produce a value +** (or produce an inverted value) +*/ +static int need_value (FuncState *fs, int list) { + for (; list != NO_JUMP; list = getjump(fs, list)) { + Instruction i = *getjumpcontrol(fs, list); + if (GET_OPCODE(i) != OP_TESTSET) return 1; + } + return 0; /* not found */ +} + + +static int patchtestreg (FuncState *fs, int node, int reg) { + Instruction *i = getjumpcontrol(fs, node); + if (GET_OPCODE(*i) != OP_TESTSET) + return 0; /* cannot patch other instructions */ + if (reg != NO_REG && reg != GETARG_B(*i)) + SETARG_A(*i, reg); + else /* no register to put value or register already has the value */ + *i = CREATE_ABC(OP_TEST, GETARG_B(*i), 0, GETARG_C(*i)); + + return 1; +} + + +static void removevalues (FuncState *fs, int list) { + for (; list != NO_JUMP; list = getjump(fs, list)) + patchtestreg(fs, list, NO_REG); +} + + +static void patchlistaux (FuncState *fs, int list, int vtarget, int reg, + int dtarget) { + while (list != NO_JUMP) { + int next = getjump(fs, list); + if (patchtestreg(fs, list, reg)) + fixjump(fs, list, vtarget); + else + fixjump(fs, list, dtarget); /* jump to default target */ + list = next; + } +} + + +static void dischargejpc (FuncState *fs) { + patchlistaux(fs, fs->jpc, fs->pc, NO_REG, fs->pc); + fs->jpc = NO_JUMP; +} + + +void luaK_patchlist (FuncState *fs, int list, int target) { + if (target == fs->pc) + luaK_patchtohere(fs, list); + else { + lua_assert(target < fs->pc); + patchlistaux(fs, list, target, NO_REG, target); + } +} + + +void luaK_patchtohere (FuncState *fs, int list) { + luaK_getlabel(fs); + luaK_concat(fs, &fs->jpc, list); +} + + +void luaK_concat (FuncState *fs, int *l1, int l2) { + if (l2 == NO_JUMP) return; + else if (*l1 == NO_JUMP) + *l1 = l2; + else { + int list = *l1; + int next; + while ((next = getjump(fs, list)) != NO_JUMP) /* find last element */ + list = next; + fixjump(fs, list, l2); + } +} + + +void luaK_checkstack (FuncState *fs, int n) { + int newstack = fs->freereg + n; + if (newstack > fs->f->maxstacksize) { + if (newstack >= MAXSTACK) + luaX_syntaxerror(fs->ls, "function or expression too complex"); + fs->f->maxstacksize = cast_byte(newstack); + } +} + + +void luaK_reserveregs (FuncState *fs, int n) { + luaK_checkstack(fs, n); + fs->freereg += n; +} + + +static void freereg (FuncState *fs, int reg) { + if (!ISK(reg) && reg >= fs->nactvar) { + fs->freereg--; + lua_assert(reg == fs->freereg); + } +} + + +static void freeexp (FuncState *fs, expdesc *e) { + if (e->k == VNONRELOC) + freereg(fs, e->u.s.info); +} + + +static int addk (FuncState *fs, TValue *k, TValue *v) { + lua_State *L = fs->L; + TValue *idx = luaH_set(L, fs->h, k); + Proto *f = fs->f; + int oldsize = f->sizek; + if (ttisnumber(idx)) { + lua_assert(luaO_rawequalObj(&fs->f->k[cast_int(nvalue(idx))], v)); + return cast_int(nvalue(idx)); + } + else { /* constant not found; create a new entry */ + setnvalue(idx, cast_num(fs->nk)); + luaM_growvector(L, f->k, fs->nk, f->sizek, TValue, + MAXARG_Bx, "constant table overflow"); + while (oldsize < f->sizek) setnilvalue(&f->k[oldsize++]); + setobj(L, &f->k[fs->nk], v); + luaC_barrier(L, f, v); + return fs->nk++; + } +} + + +int luaK_stringK (FuncState *fs, TString *s) { + TValue o; + setsvalue(fs->L, &o, s); + return addk(fs, &o, &o); +} + + +int luaK_numberK (FuncState *fs, lua_Number r) { + TValue o; + setnvalue(&o, r); + return addk(fs, &o, &o); +} + + +static int boolK (FuncState *fs, int b) { + TValue o; + setbvalue(&o, b); + return addk(fs, &o, &o); +} + + +static int nilK (FuncState *fs) { + TValue k, v; + setnilvalue(&v); + /* cannot use nil as key; instead use table itself to represent nil */ + sethvalue(fs->L, &k, fs->h); + return addk(fs, &k, &v); +} + + +void luaK_setreturns (FuncState *fs, expdesc *e, int nresults) { + if (e->k == VCALL) { /* expression is an open function call? */ + SETARG_C(getcode(fs, e), nresults+1); + } + else if (e->k == VVARARG) { + SETARG_B(getcode(fs, e), nresults+1); + SETARG_A(getcode(fs, e), fs->freereg); + luaK_reserveregs(fs, 1); + } +} + + +void luaK_setoneret (FuncState *fs, expdesc *e) { + if (e->k == VCALL) { /* expression is an open function call? */ + e->k = VNONRELOC; + e->u.s.info = GETARG_A(getcode(fs, e)); + } + else if (e->k == VVARARG) { + SETARG_B(getcode(fs, e), 2); + e->k = VRELOCABLE; /* can relocate its simple result */ + } +} + + +void luaK_dischargevars (FuncState *fs, expdesc *e) { + switch (e->k) { + case VLOCAL: { + e->k = VNONRELOC; + break; + } + case VUPVAL: { + e->u.s.info = luaK_codeABC(fs, OP_GETUPVAL, 0, e->u.s.info, 0); + e->k = VRELOCABLE; + break; + } + case VGLOBAL: { + e->u.s.info = luaK_codeABx(fs, OP_GETGLOBAL, 0, e->u.s.info); + e->k = VRELOCABLE; + break; + } + case VINDEXED: { + freereg(fs, e->u.s.aux); + freereg(fs, e->u.s.info); + e->u.s.info = luaK_codeABC(fs, OP_GETTABLE, 0, e->u.s.info, e->u.s.aux); + e->k = VRELOCABLE; + break; + } + case VVARARG: + case VCALL: { + luaK_setoneret(fs, e); + break; + } + default: break; /* there is one value available (somewhere) */ + } +} + + +static int code_label (FuncState *fs, int A, int b, int jump) { + luaK_getlabel(fs); /* those instructions may be jump targets */ + return luaK_codeABC(fs, OP_LOADBOOL, A, b, jump); +} + + +static void discharge2reg (FuncState *fs, expdesc *e, int reg) { + luaK_dischargevars(fs, e); + switch (e->k) { + case VNIL: { + luaK_nil(fs, reg, 1); + break; + } + case VFALSE: case VTRUE: { + luaK_codeABC(fs, OP_LOADBOOL, reg, e->k == VTRUE, 0); + break; + } + case VK: { + luaK_codeABx(fs, OP_LOADK, reg, e->u.s.info); + break; + } + case VKNUM: { + luaK_codeABx(fs, OP_LOADK, reg, luaK_numberK(fs, e->u.nval)); + break; + } + case VRELOCABLE: { + Instruction *pc = &getcode(fs, e); + SETARG_A(*pc, reg); + break; + } + case VNONRELOC: { + if (reg != e->u.s.info) + luaK_codeABC(fs, OP_MOVE, reg, e->u.s.info, 0); + break; + } + default: { + lua_assert(e->k == VVOID || e->k == VJMP); + return; /* nothing to do... */ + } + } + e->u.s.info = reg; + e->k = VNONRELOC; +} + + +static void discharge2anyreg (FuncState *fs, expdesc *e) { + if (e->k != VNONRELOC) { + luaK_reserveregs(fs, 1); + discharge2reg(fs, e, fs->freereg-1); + } +} + + +static void exp2reg (FuncState *fs, expdesc *e, int reg) { + discharge2reg(fs, e, reg); + if (e->k == VJMP) + luaK_concat(fs, &e->t, e->u.s.info); /* put this jump in `t' list */ + if (hasjumps(e)) { + int final; /* position after whole expression */ + int p_f = NO_JUMP; /* position of an eventual LOAD false */ + int p_t = NO_JUMP; /* position of an eventual LOAD true */ + if (need_value(fs, e->t) || need_value(fs, e->f)) { + int fj = (e->k == VJMP) ? NO_JUMP : luaK_jump(fs); + p_f = code_label(fs, reg, 0, 1); + p_t = code_label(fs, reg, 1, 0); + luaK_patchtohere(fs, fj); + } + final = luaK_getlabel(fs); + patchlistaux(fs, e->f, final, reg, p_f); + patchlistaux(fs, e->t, final, reg, p_t); + } + e->f = e->t = NO_JUMP; + e->u.s.info = reg; + e->k = VNONRELOC; +} + + +void luaK_exp2nextreg (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + freeexp(fs, e); + luaK_reserveregs(fs, 1); + exp2reg(fs, e, fs->freereg - 1); +} + + +int luaK_exp2anyreg (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + if (e->k == VNONRELOC) { + if (!hasjumps(e)) return e->u.s.info; /* exp is already in a register */ + if (e->u.s.info >= fs->nactvar) { /* reg. is not a local? */ + exp2reg(fs, e, e->u.s.info); /* put value on it */ + return e->u.s.info; + } + } + luaK_exp2nextreg(fs, e); /* default */ + return e->u.s.info; +} + + +void luaK_exp2val (FuncState *fs, expdesc *e) { + if (hasjumps(e)) + luaK_exp2anyreg(fs, e); + else + luaK_dischargevars(fs, e); +} + + +int luaK_exp2RK (FuncState *fs, expdesc *e) { + luaK_exp2val(fs, e); + switch (e->k) { + case VKNUM: + case VTRUE: + case VFALSE: + case VNIL: { + if (fs->nk <= MAXINDEXRK) { /* constant fit in RK operand? */ + e->u.s.info = (e->k == VNIL) ? nilK(fs) : + (e->k == VKNUM) ? luaK_numberK(fs, e->u.nval) : + boolK(fs, (e->k == VTRUE)); + e->k = VK; + return RKASK(e->u.s.info); + } + else break; + } + case VK: { + if (e->u.s.info <= MAXINDEXRK) /* constant fit in argC? */ + return RKASK(e->u.s.info); + else break; + } + default: break; + } + /* not a constant in the right range: put it in a register */ + return luaK_exp2anyreg(fs, e); +} + + +void luaK_storevar (FuncState *fs, expdesc *var, expdesc *ex) { + switch (var->k) { + case VLOCAL: { + freeexp(fs, ex); + exp2reg(fs, ex, var->u.s.info); + return; + } + case VUPVAL: { + int e = luaK_exp2anyreg(fs, ex); + luaK_codeABC(fs, OP_SETUPVAL, e, var->u.s.info, 0); + break; + } + case VGLOBAL: { + int e = luaK_exp2anyreg(fs, ex); + luaK_codeABx(fs, OP_SETGLOBAL, e, var->u.s.info); + break; + } + case VINDEXED: { + int e = luaK_exp2RK(fs, ex); + luaK_codeABC(fs, OP_SETTABLE, var->u.s.info, var->u.s.aux, e); + break; + } + default: { + lua_assert(0); /* invalid var kind to store */ + break; + } + } + freeexp(fs, ex); +} + + +void luaK_self (FuncState *fs, expdesc *e, expdesc *key) { + int func; + luaK_exp2anyreg(fs, e); + freeexp(fs, e); + func = fs->freereg; + luaK_reserveregs(fs, 2); + luaK_codeABC(fs, OP_SELF, func, e->u.s.info, luaK_exp2RK(fs, key)); + freeexp(fs, key); + e->u.s.info = func; + e->k = VNONRELOC; +} + + +static void invertjump (FuncState *fs, expdesc *e) { + Instruction *pc = getjumpcontrol(fs, e->u.s.info); + lua_assert(testTMode(GET_OPCODE(*pc)) && GET_OPCODE(*pc) != OP_TESTSET && + GET_OPCODE(*pc) != OP_TEST); + SETARG_A(*pc, !(GETARG_A(*pc))); +} + + +static int jumponcond (FuncState *fs, expdesc *e, int cond) { + if (e->k == VRELOCABLE) { + Instruction ie = getcode(fs, e); + if (GET_OPCODE(ie) == OP_NOT) { + fs->pc--; /* remove previous OP_NOT */ + return condjump(fs, OP_TEST, GETARG_B(ie), 0, !cond); + } + /* else go through */ + } + discharge2anyreg(fs, e); + freeexp(fs, e); + return condjump(fs, OP_TESTSET, NO_REG, e->u.s.info, cond); +} + + +void luaK_goiftrue (FuncState *fs, expdesc *e) { + int pc; /* pc of last jump */ + luaK_dischargevars(fs, e); + switch (e->k) { + case VK: case VKNUM: case VTRUE: { + pc = NO_JUMP; /* always true; do nothing */ + break; + } + case VJMP: { + invertjump(fs, e); + pc = e->u.s.info; + break; + } + default: { + pc = jumponcond(fs, e, 0); + break; + } + } + luaK_concat(fs, &e->f, pc); /* insert last jump in `f' list */ + luaK_patchtohere(fs, e->t); + e->t = NO_JUMP; +} + + +static void luaK_goiffalse (FuncState *fs, expdesc *e) { + int pc; /* pc of last jump */ + luaK_dischargevars(fs, e); + switch (e->k) { + case VNIL: case VFALSE: { + pc = NO_JUMP; /* always false; do nothing */ + break; + } + case VJMP: { + pc = e->u.s.info; + break; + } + default: { + pc = jumponcond(fs, e, 1); + break; + } + } + luaK_concat(fs, &e->t, pc); /* insert last jump in `t' list */ + luaK_patchtohere(fs, e->f); + e->f = NO_JUMP; +} + + +static void codenot (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + switch (e->k) { + case VNIL: case VFALSE: { + e->k = VTRUE; + break; + } + case VK: case VKNUM: case VTRUE: { + e->k = VFALSE; + break; + } + case VJMP: { + invertjump(fs, e); + break; + } + case VRELOCABLE: + case VNONRELOC: { + discharge2anyreg(fs, e); + freeexp(fs, e); + e->u.s.info = luaK_codeABC(fs, OP_NOT, 0, e->u.s.info, 0); + e->k = VRELOCABLE; + break; + } + default: { + lua_assert(0); /* cannot happen */ + break; + } + } + /* interchange true and false lists */ + { int temp = e->f; e->f = e->t; e->t = temp; } + removevalues(fs, e->f); + removevalues(fs, e->t); +} + + +void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k) { + t->u.s.aux = luaK_exp2RK(fs, k); + t->k = VINDEXED; +} + + +static int constfolding (OpCode op, expdesc *e1, expdesc *e2) { + lua_Number v1, v2, r; + if (!isnumeral(e1) || !isnumeral(e2)) return 0; + v1 = e1->u.nval; + v2 = e2->u.nval; + switch (op) { + case OP_ADD: r = luai_numadd(v1, v2); break; + case OP_SUB: r = luai_numsub(v1, v2); break; + case OP_MUL: r = luai_nummul(v1, v2); break; + case OP_DIV: + if (v2 == 0) return 0; /* do not attempt to divide by 0 */ + r = luai_numdiv(v1, v2); break; + case OP_MOD: + if (v2 == 0) return 0; /* do not attempt to divide by 0 */ + r = luai_nummod(v1, v2); break; + case OP_POW: r = luai_numpow(v1, v2); break; + case OP_UNM: r = luai_numunm(v1); break; + case OP_LEN: return 0; /* no constant folding for 'len' */ + default: lua_assert(0); r = 0; break; + } + if (luai_numisnan(r)) return 0; /* do not attempt to produce NaN */ + e1->u.nval = r; + return 1; +} + + +static void codearith (FuncState *fs, OpCode op, expdesc *e1, expdesc *e2) { + if (constfolding(op, e1, e2)) + return; + else { + int o2 = (op != OP_UNM && op != OP_LEN) ? luaK_exp2RK(fs, e2) : 0; + int o1 = luaK_exp2RK(fs, e1); + if (o1 > o2) { + freeexp(fs, e1); + freeexp(fs, e2); + } + else { + freeexp(fs, e2); + freeexp(fs, e1); + } + e1->u.s.info = luaK_codeABC(fs, op, 0, o1, o2); + e1->k = VRELOCABLE; + } +} + + +static void codecomp (FuncState *fs, OpCode op, int cond, expdesc *e1, + expdesc *e2) { + int o1 = luaK_exp2RK(fs, e1); + int o2 = luaK_exp2RK(fs, e2); + freeexp(fs, e2); + freeexp(fs, e1); + if (cond == 0 && op != OP_EQ) { + int temp; /* exchange args to replace by `<' or `<=' */ + temp = o1; o1 = o2; o2 = temp; /* o1 <==> o2 */ + cond = 1; + } + e1->u.s.info = condjump(fs, op, cond, o1, o2); + e1->k = VJMP; +} + + +void luaK_prefix (FuncState *fs, UnOpr op, expdesc *e) { + expdesc e2; + e2.t = e2.f = NO_JUMP; e2.k = VKNUM; e2.u.nval = 0; + switch (op) { + case OPR_MINUS: { + if (!isnumeral(e)) + luaK_exp2anyreg(fs, e); /* cannot operate on non-numeric constants */ + codearith(fs, OP_UNM, e, &e2); + break; + } + case OPR_NOT: codenot(fs, e); break; + case OPR_LEN: { + luaK_exp2anyreg(fs, e); /* cannot operate on constants */ + codearith(fs, OP_LEN, e, &e2); + break; + } + default: lua_assert(0); + } +} + + +void luaK_infix (FuncState *fs, BinOpr op, expdesc *v) { + switch (op) { + case OPR_AND: { + luaK_goiftrue(fs, v); + break; + } + case OPR_OR: { + luaK_goiffalse(fs, v); + break; + } + case OPR_CONCAT: { + luaK_exp2nextreg(fs, v); /* operand must be on the `stack' */ + break; + } + case OPR_ADD: case OPR_SUB: case OPR_MUL: case OPR_DIV: + case OPR_MOD: case OPR_POW: { + if (!isnumeral(v)) luaK_exp2RK(fs, v); + break; + } + default: { + luaK_exp2RK(fs, v); + break; + } + } +} + + +void luaK_posfix (FuncState *fs, BinOpr op, expdesc *e1, expdesc *e2) { + switch (op) { + case OPR_AND: { + lua_assert(e1->t == NO_JUMP); /* list must be closed */ + luaK_dischargevars(fs, e2); + luaK_concat(fs, &e2->f, e1->f); + *e1 = *e2; + break; + } + case OPR_OR: { + lua_assert(e1->f == NO_JUMP); /* list must be closed */ + luaK_dischargevars(fs, e2); + luaK_concat(fs, &e2->t, e1->t); + *e1 = *e2; + break; + } + case OPR_CONCAT: { + luaK_exp2val(fs, e2); + if (e2->k == VRELOCABLE && GET_OPCODE(getcode(fs, e2)) == OP_CONCAT) { + lua_assert(e1->u.s.info == GETARG_B(getcode(fs, e2))-1); + freeexp(fs, e1); + SETARG_B(getcode(fs, e2), e1->u.s.info); + e1->k = VRELOCABLE; e1->u.s.info = e2->u.s.info; + } + else { + luaK_exp2nextreg(fs, e2); /* operand must be on the 'stack' */ + codearith(fs, OP_CONCAT, e1, e2); + } + break; + } + case OPR_ADD: codearith(fs, OP_ADD, e1, e2); break; + case OPR_SUB: codearith(fs, OP_SUB, e1, e2); break; + case OPR_MUL: codearith(fs, OP_MUL, e1, e2); break; + case OPR_DIV: codearith(fs, OP_DIV, e1, e2); break; + case OPR_MOD: codearith(fs, OP_MOD, e1, e2); break; + case OPR_POW: codearith(fs, OP_POW, e1, e2); break; + case OPR_EQ: codecomp(fs, OP_EQ, 1, e1, e2); break; + case OPR_NE: codecomp(fs, OP_EQ, 0, e1, e2); break; + case OPR_LT: codecomp(fs, OP_LT, 1, e1, e2); break; + case OPR_LE: codecomp(fs, OP_LE, 1, e1, e2); break; + case OPR_GT: codecomp(fs, OP_LT, 0, e1, e2); break; + case OPR_GE: codecomp(fs, OP_LE, 0, e1, e2); break; + default: lua_assert(0); + } +} + + +void luaK_fixline (FuncState *fs, int line) { + fs->f->lineinfo[fs->pc - 1] = line; +} + + +static int luaK_code (FuncState *fs, Instruction i, int line) { + Proto *f = fs->f; + dischargejpc(fs); /* `pc' will change */ + /* put new instruction in code array */ + luaM_growvector(fs->L, f->code, fs->pc, f->sizecode, Instruction, + MAX_INT, "code size overflow"); + f->code[fs->pc] = i; + /* save corresponding line information */ + luaM_growvector(fs->L, f->lineinfo, fs->pc, f->sizelineinfo, int, + MAX_INT, "code size overflow"); + f->lineinfo[fs->pc] = line; + return fs->pc++; +} + + +int luaK_codeABC (FuncState *fs, OpCode o, int a, int b, int c) { + lua_assert(getOpMode(o) == iABC); + lua_assert(getBMode(o) != OpArgN || b == 0); + lua_assert(getCMode(o) != OpArgN || c == 0); + return luaK_code(fs, CREATE_ABC(o, a, b, c), fs->ls->lastline); +} + + +int luaK_codeABx (FuncState *fs, OpCode o, int a, unsigned int bc) { + lua_assert(getOpMode(o) == iABx || getOpMode(o) == iAsBx); + lua_assert(getCMode(o) == OpArgN); + return luaK_code(fs, CREATE_ABx(o, a, bc), fs->ls->lastline); +} + + +void luaK_setlist (FuncState *fs, int base, int nelems, int tostore) { + int c = (nelems - 1)/LFIELDS_PER_FLUSH + 1; + int b = (tostore == LUA_MULTRET) ? 0 : tostore; + lua_assert(tostore != 0); + if (c <= MAXARG_C) + luaK_codeABC(fs, OP_SETLIST, base, b, c); + else { + luaK_codeABC(fs, OP_SETLIST, base, b, 0); + luaK_code(fs, cast(Instruction, c), fs->ls->lastline); + } + fs->freereg = base + 1; /* free registers with list values */ +} + diff --git a/extern/lua-5.1.5/src/lcode.h b/extern/lua-5.1.5/src/lcode.h new file mode 100644 index 00000000..b941c607 --- /dev/null +++ b/extern/lua-5.1.5/src/lcode.h @@ -0,0 +1,76 @@ +/* +** $Id: lcode.h,v 1.48.1.1 2007/12/27 13:02:25 roberto Exp $ +** Code generator for Lua +** See Copyright Notice in lua.h +*/ + +#ifndef lcode_h +#define lcode_h + +#include "llex.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" + + +/* +** Marks the end of a patch list. It is an invalid value both as an absolute +** address, and as a list link (would link an element to itself). +*/ +#define NO_JUMP (-1) + + +/* +** grep "ORDER OPR" if you change these enums +*/ +typedef enum BinOpr { + OPR_ADD, OPR_SUB, OPR_MUL, OPR_DIV, OPR_MOD, OPR_POW, + OPR_CONCAT, + OPR_NE, OPR_EQ, + OPR_LT, OPR_LE, OPR_GT, OPR_GE, + OPR_AND, OPR_OR, + OPR_NOBINOPR +} BinOpr; + + +typedef enum UnOpr { OPR_MINUS, OPR_NOT, OPR_LEN, OPR_NOUNOPR } UnOpr; + + +#define getcode(fs,e) ((fs)->f->code[(e)->u.s.info]) + +#define luaK_codeAsBx(fs,o,A,sBx) luaK_codeABx(fs,o,A,(sBx)+MAXARG_sBx) + +#define luaK_setmultret(fs,e) luaK_setreturns(fs, e, LUA_MULTRET) + +LUAI_FUNC int luaK_codeABx (FuncState *fs, OpCode o, int A, unsigned int Bx); +LUAI_FUNC int luaK_codeABC (FuncState *fs, OpCode o, int A, int B, int C); +LUAI_FUNC void luaK_fixline (FuncState *fs, int line); +LUAI_FUNC void luaK_nil (FuncState *fs, int from, int n); +LUAI_FUNC void luaK_reserveregs (FuncState *fs, int n); +LUAI_FUNC void luaK_checkstack (FuncState *fs, int n); +LUAI_FUNC int luaK_stringK (FuncState *fs, TString *s); +LUAI_FUNC int luaK_numberK (FuncState *fs, lua_Number r); +LUAI_FUNC void luaK_dischargevars (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_exp2anyreg (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2nextreg (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2val (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_exp2RK (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_self (FuncState *fs, expdesc *e, expdesc *key); +LUAI_FUNC void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k); +LUAI_FUNC void luaK_goiftrue (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_storevar (FuncState *fs, expdesc *var, expdesc *e); +LUAI_FUNC void luaK_setreturns (FuncState *fs, expdesc *e, int nresults); +LUAI_FUNC void luaK_setoneret (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_jump (FuncState *fs); +LUAI_FUNC void luaK_ret (FuncState *fs, int first, int nret); +LUAI_FUNC void luaK_patchlist (FuncState *fs, int list, int target); +LUAI_FUNC void luaK_patchtohere (FuncState *fs, int list); +LUAI_FUNC void luaK_concat (FuncState *fs, int *l1, int l2); +LUAI_FUNC int luaK_getlabel (FuncState *fs); +LUAI_FUNC void luaK_prefix (FuncState *fs, UnOpr op, expdesc *v); +LUAI_FUNC void luaK_infix (FuncState *fs, BinOpr op, expdesc *v); +LUAI_FUNC void luaK_posfix (FuncState *fs, BinOpr op, expdesc *v1, expdesc *v2); +LUAI_FUNC void luaK_setlist (FuncState *fs, int base, int nelems, int tostore); + + +#endif diff --git a/extern/lua-5.1.5/src/ldblib.c b/extern/lua-5.1.5/src/ldblib.c new file mode 100644 index 00000000..2027eda5 --- /dev/null +++ b/extern/lua-5.1.5/src/ldblib.c @@ -0,0 +1,398 @@ +/* +** $Id: ldblib.c,v 1.104.1.4 2009/08/04 18:50:18 roberto Exp $ +** Interface from Lua to its debug API +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include + +#define ldblib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + + +static int db_getregistry (lua_State *L) { + lua_pushvalue(L, LUA_REGISTRYINDEX); + return 1; +} + + +static int db_getmetatable (lua_State *L) { + luaL_checkany(L, 1); + if (!lua_getmetatable(L, 1)) { + lua_pushnil(L); /* no metatable */ + } + return 1; +} + + +static int db_setmetatable (lua_State *L) { + int t = lua_type(L, 2); + luaL_argcheck(L, t == LUA_TNIL || t == LUA_TTABLE, 2, + "nil or table expected"); + lua_settop(L, 2); + lua_pushboolean(L, lua_setmetatable(L, 1)); + return 1; +} + + +static int db_getfenv (lua_State *L) { + luaL_checkany(L, 1); + lua_getfenv(L, 1); + return 1; +} + + +static int db_setfenv (lua_State *L) { + luaL_checktype(L, 2, LUA_TTABLE); + lua_settop(L, 2); + if (lua_setfenv(L, 1) == 0) + luaL_error(L, LUA_QL("setfenv") + " cannot change environment of given object"); + return 1; +} + + +static void settabss (lua_State *L, const char *i, const char *v) { + lua_pushstring(L, v); + lua_setfield(L, -2, i); +} + + +static void settabsi (lua_State *L, const char *i, int v) { + lua_pushinteger(L, v); + lua_setfield(L, -2, i); +} + + +static lua_State *getthread (lua_State *L, int *arg) { + if (lua_isthread(L, 1)) { + *arg = 1; + return lua_tothread(L, 1); + } + else { + *arg = 0; + return L; + } +} + + +static void treatstackoption (lua_State *L, lua_State *L1, const char *fname) { + if (L == L1) { + lua_pushvalue(L, -2); + lua_remove(L, -3); + } + else + lua_xmove(L1, L, 1); + lua_setfield(L, -2, fname); +} + + +static int db_getinfo (lua_State *L) { + lua_Debug ar; + int arg; + lua_State *L1 = getthread(L, &arg); + const char *options = luaL_optstring(L, arg+2, "flnSu"); + if (lua_isnumber(L, arg+1)) { + if (!lua_getstack(L1, (int)lua_tointeger(L, arg+1), &ar)) { + lua_pushnil(L); /* level out of range */ + return 1; + } + } + else if (lua_isfunction(L, arg+1)) { + lua_pushfstring(L, ">%s", options); + options = lua_tostring(L, -1); + lua_pushvalue(L, arg+1); + lua_xmove(L, L1, 1); + } + else + return luaL_argerror(L, arg+1, "function or level expected"); + if (!lua_getinfo(L1, options, &ar)) + return luaL_argerror(L, arg+2, "invalid option"); + lua_createtable(L, 0, 2); + if (strchr(options, 'S')) { + settabss(L, "source", ar.source); + settabss(L, "short_src", ar.short_src); + settabsi(L, "linedefined", ar.linedefined); + settabsi(L, "lastlinedefined", ar.lastlinedefined); + settabss(L, "what", ar.what); + } + if (strchr(options, 'l')) + settabsi(L, "currentline", ar.currentline); + if (strchr(options, 'u')) + settabsi(L, "nups", ar.nups); + if (strchr(options, 'n')) { + settabss(L, "name", ar.name); + settabss(L, "namewhat", ar.namewhat); + } + if (strchr(options, 'L')) + treatstackoption(L, L1, "activelines"); + if (strchr(options, 'f')) + treatstackoption(L, L1, "func"); + return 1; /* return table */ +} + + +static int db_getlocal (lua_State *L) { + int arg; + lua_State *L1 = getthread(L, &arg); + lua_Debug ar; + const char *name; + if (!lua_getstack(L1, luaL_checkint(L, arg+1), &ar)) /* out of range? */ + return luaL_argerror(L, arg+1, "level out of range"); + name = lua_getlocal(L1, &ar, luaL_checkint(L, arg+2)); + if (name) { + lua_xmove(L1, L, 1); + lua_pushstring(L, name); + lua_pushvalue(L, -2); + return 2; + } + else { + lua_pushnil(L); + return 1; + } +} + + +static int db_setlocal (lua_State *L) { + int arg; + lua_State *L1 = getthread(L, &arg); + lua_Debug ar; + if (!lua_getstack(L1, luaL_checkint(L, arg+1), &ar)) /* out of range? */ + return luaL_argerror(L, arg+1, "level out of range"); + luaL_checkany(L, arg+3); + lua_settop(L, arg+3); + lua_xmove(L, L1, 1); + lua_pushstring(L, lua_setlocal(L1, &ar, luaL_checkint(L, arg+2))); + return 1; +} + + +static int auxupvalue (lua_State *L, int get) { + const char *name; + int n = luaL_checkint(L, 2); + luaL_checktype(L, 1, LUA_TFUNCTION); + if (lua_iscfunction(L, 1)) return 0; /* cannot touch C upvalues from Lua */ + name = get ? lua_getupvalue(L, 1, n) : lua_setupvalue(L, 1, n); + if (name == NULL) return 0; + lua_pushstring(L, name); + lua_insert(L, -(get+1)); + return get + 1; +} + + +static int db_getupvalue (lua_State *L) { + return auxupvalue(L, 1); +} + + +static int db_setupvalue (lua_State *L) { + luaL_checkany(L, 3); + return auxupvalue(L, 0); +} + + + +static const char KEY_HOOK = 'h'; + + +static void hookf (lua_State *L, lua_Debug *ar) { + static const char *const hooknames[] = + {"call", "return", "line", "count", "tail return"}; + lua_pushlightuserdata(L, (void *)&KEY_HOOK); + lua_rawget(L, LUA_REGISTRYINDEX); + lua_pushlightuserdata(L, L); + lua_rawget(L, -2); + if (lua_isfunction(L, -1)) { + lua_pushstring(L, hooknames[(int)ar->event]); + if (ar->currentline >= 0) + lua_pushinteger(L, ar->currentline); + else lua_pushnil(L); + lua_assert(lua_getinfo(L, "lS", ar)); + lua_call(L, 2, 0); + } +} + + +static int makemask (const char *smask, int count) { + int mask = 0; + if (strchr(smask, 'c')) mask |= LUA_MASKCALL; + if (strchr(smask, 'r')) mask |= LUA_MASKRET; + if (strchr(smask, 'l')) mask |= LUA_MASKLINE; + if (count > 0) mask |= LUA_MASKCOUNT; + return mask; +} + + +static char *unmakemask (int mask, char *smask) { + int i = 0; + if (mask & LUA_MASKCALL) smask[i++] = 'c'; + if (mask & LUA_MASKRET) smask[i++] = 'r'; + if (mask & LUA_MASKLINE) smask[i++] = 'l'; + smask[i] = '\0'; + return smask; +} + + +static void gethooktable (lua_State *L) { + lua_pushlightuserdata(L, (void *)&KEY_HOOK); + lua_rawget(L, LUA_REGISTRYINDEX); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_createtable(L, 0, 1); + lua_pushlightuserdata(L, (void *)&KEY_HOOK); + lua_pushvalue(L, -2); + lua_rawset(L, LUA_REGISTRYINDEX); + } +} + + +static int db_sethook (lua_State *L) { + int arg, mask, count; + lua_Hook func; + lua_State *L1 = getthread(L, &arg); + if (lua_isnoneornil(L, arg+1)) { + lua_settop(L, arg+1); + func = NULL; mask = 0; count = 0; /* turn off hooks */ + } + else { + const char *smask = luaL_checkstring(L, arg+2); + luaL_checktype(L, arg+1, LUA_TFUNCTION); + count = luaL_optint(L, arg+3, 0); + func = hookf; mask = makemask(smask, count); + } + gethooktable(L); + lua_pushlightuserdata(L, L1); + lua_pushvalue(L, arg+1); + lua_rawset(L, -3); /* set new hook */ + lua_pop(L, 1); /* remove hook table */ + lua_sethook(L1, func, mask, count); /* set hooks */ + return 0; +} + + +static int db_gethook (lua_State *L) { + int arg; + lua_State *L1 = getthread(L, &arg); + char buff[5]; + int mask = lua_gethookmask(L1); + lua_Hook hook = lua_gethook(L1); + if (hook != NULL && hook != hookf) /* external hook? */ + lua_pushliteral(L, "external hook"); + else { + gethooktable(L); + lua_pushlightuserdata(L, L1); + lua_rawget(L, -2); /* get hook */ + lua_remove(L, -2); /* remove hook table */ + } + lua_pushstring(L, unmakemask(mask, buff)); + lua_pushinteger(L, lua_gethookcount(L1)); + return 3; +} + + +static int db_debug (lua_State *L) { + for (;;) { + char buffer[250]; + fputs("lua_debug> ", stderr); + if (fgets(buffer, sizeof(buffer), stdin) == 0 || + strcmp(buffer, "cont\n") == 0) + return 0; + if (luaL_loadbuffer(L, buffer, strlen(buffer), "=(debug command)") || + lua_pcall(L, 0, 0, 0)) { + fputs(lua_tostring(L, -1), stderr); + fputs("\n", stderr); + } + lua_settop(L, 0); /* remove eventual returns */ + } +} + + +#define LEVELS1 12 /* size of the first part of the stack */ +#define LEVELS2 10 /* size of the second part of the stack */ + +static int db_errorfb (lua_State *L) { + int level; + int firstpart = 1; /* still before eventual `...' */ + int arg; + lua_State *L1 = getthread(L, &arg); + lua_Debug ar; + if (lua_isnumber(L, arg+2)) { + level = (int)lua_tointeger(L, arg+2); + lua_pop(L, 1); + } + else + level = (L == L1) ? 1 : 0; /* level 0 may be this own function */ + if (lua_gettop(L) == arg) + lua_pushliteral(L, ""); + else if (!lua_isstring(L, arg+1)) return 1; /* message is not a string */ + else lua_pushliteral(L, "\n"); + lua_pushliteral(L, "stack traceback:"); + while (lua_getstack(L1, level++, &ar)) { + if (level > LEVELS1 && firstpart) { + /* no more than `LEVELS2' more levels? */ + if (!lua_getstack(L1, level+LEVELS2, &ar)) + level--; /* keep going */ + else { + lua_pushliteral(L, "\n\t..."); /* too many levels */ + while (lua_getstack(L1, level+LEVELS2, &ar)) /* find last levels */ + level++; + } + firstpart = 0; + continue; + } + lua_pushliteral(L, "\n\t"); + lua_getinfo(L1, "Snl", &ar); + lua_pushfstring(L, "%s:", ar.short_src); + if (ar.currentline > 0) + lua_pushfstring(L, "%d:", ar.currentline); + if (*ar.namewhat != '\0') /* is there a name? */ + lua_pushfstring(L, " in function " LUA_QS, ar.name); + else { + if (*ar.what == 'm') /* main? */ + lua_pushfstring(L, " in main chunk"); + else if (*ar.what == 'C' || *ar.what == 't') + lua_pushliteral(L, " ?"); /* C function or tail call */ + else + lua_pushfstring(L, " in function <%s:%d>", + ar.short_src, ar.linedefined); + } + lua_concat(L, lua_gettop(L) - arg); + } + lua_concat(L, lua_gettop(L) - arg); + return 1; +} + + +static const luaL_Reg dblib[] = { + {"debug", db_debug}, + {"getfenv", db_getfenv}, + {"gethook", db_gethook}, + {"getinfo", db_getinfo}, + {"getlocal", db_getlocal}, + {"getregistry", db_getregistry}, + {"getmetatable", db_getmetatable}, + {"getupvalue", db_getupvalue}, + {"setfenv", db_setfenv}, + {"sethook", db_sethook}, + {"setlocal", db_setlocal}, + {"setmetatable", db_setmetatable}, + {"setupvalue", db_setupvalue}, + {"traceback", db_errorfb}, + {NULL, NULL} +}; + + +LUALIB_API int luaopen_debug (lua_State *L) { + luaL_register(L, LUA_DBLIBNAME, dblib); + return 1; +} + diff --git a/extern/lua-5.1.5/src/ldebug.c b/extern/lua-5.1.5/src/ldebug.c new file mode 100644 index 00000000..50ad3d38 --- /dev/null +++ b/extern/lua-5.1.5/src/ldebug.c @@ -0,0 +1,638 @@ +/* +** $Id: ldebug.c,v 2.29.1.6 2008/05/08 16:56:26 roberto Exp $ +** Debug Interface +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include + + +#define ldebug_c +#define LUA_CORE + +#include "lua.h" + +#include "lapi.h" +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + + +static const char *getfuncname (lua_State *L, CallInfo *ci, const char **name); + + +static int currentpc (lua_State *L, CallInfo *ci) { + if (!isLua(ci)) return -1; /* function is not a Lua function? */ + if (ci == L->ci) + ci->savedpc = L->savedpc; + return pcRel(ci->savedpc, ci_func(ci)->l.p); +} + + +static int currentline (lua_State *L, CallInfo *ci) { + int pc = currentpc(L, ci); + if (pc < 0) + return -1; /* only active lua functions have current-line information */ + else + return getline(ci_func(ci)->l.p, pc); +} + + +/* +** this function can be called asynchronous (e.g. during a signal) +*/ +LUA_API int lua_sethook (lua_State *L, lua_Hook func, int mask, int count) { + if (func == NULL || mask == 0) { /* turn off hooks? */ + mask = 0; + func = NULL; + } + L->hook = func; + L->basehookcount = count; + resethookcount(L); + L->hookmask = cast_byte(mask); + return 1; +} + + +LUA_API lua_Hook lua_gethook (lua_State *L) { + return L->hook; +} + + +LUA_API int lua_gethookmask (lua_State *L) { + return L->hookmask; +} + + +LUA_API int lua_gethookcount (lua_State *L) { + return L->basehookcount; +} + + +LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar) { + int status; + CallInfo *ci; + lua_lock(L); + for (ci = L->ci; level > 0 && ci > L->base_ci; ci--) { + level--; + if (f_isLua(ci)) /* Lua function? */ + level -= ci->tailcalls; /* skip lost tail calls */ + } + if (level == 0 && ci > L->base_ci) { /* level found? */ + status = 1; + ar->i_ci = cast_int(ci - L->base_ci); + } + else if (level < 0) { /* level is of a lost tail call? */ + status = 1; + ar->i_ci = 0; + } + else status = 0; /* no such level */ + lua_unlock(L); + return status; +} + + +static Proto *getluaproto (CallInfo *ci) { + return (isLua(ci) ? ci_func(ci)->l.p : NULL); +} + + +static const char *findlocal (lua_State *L, CallInfo *ci, int n) { + const char *name; + Proto *fp = getluaproto(ci); + if (fp && (name = luaF_getlocalname(fp, n, currentpc(L, ci))) != NULL) + return name; /* is a local variable in a Lua function */ + else { + StkId limit = (ci == L->ci) ? L->top : (ci+1)->func; + if (limit - ci->base >= n && n > 0) /* is 'n' inside 'ci' stack? */ + return "(*temporary)"; + else + return NULL; + } +} + + +LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n) { + CallInfo *ci = L->base_ci + ar->i_ci; + const char *name = findlocal(L, ci, n); + lua_lock(L); + if (name) + luaA_pushobject(L, ci->base + (n - 1)); + lua_unlock(L); + return name; +} + + +LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n) { + CallInfo *ci = L->base_ci + ar->i_ci; + const char *name = findlocal(L, ci, n); + lua_lock(L); + if (name) + setobjs2s(L, ci->base + (n - 1), L->top - 1); + L->top--; /* pop value */ + lua_unlock(L); + return name; +} + + +static void funcinfo (lua_Debug *ar, Closure *cl) { + if (cl->c.isC) { + ar->source = "=[C]"; + ar->linedefined = -1; + ar->lastlinedefined = -1; + ar->what = "C"; + } + else { + ar->source = getstr(cl->l.p->source); + ar->linedefined = cl->l.p->linedefined; + ar->lastlinedefined = cl->l.p->lastlinedefined; + ar->what = (ar->linedefined == 0) ? "main" : "Lua"; + } + luaO_chunkid(ar->short_src, ar->source, LUA_IDSIZE); +} + + +static void info_tailcall (lua_Debug *ar) { + ar->name = ar->namewhat = ""; + ar->what = "tail"; + ar->lastlinedefined = ar->linedefined = ar->currentline = -1; + ar->source = "=(tail call)"; + luaO_chunkid(ar->short_src, ar->source, LUA_IDSIZE); + ar->nups = 0; +} + + +static void collectvalidlines (lua_State *L, Closure *f) { + if (f == NULL || f->c.isC) { + setnilvalue(L->top); + } + else { + Table *t = luaH_new(L, 0, 0); + int *lineinfo = f->l.p->lineinfo; + int i; + for (i=0; il.p->sizelineinfo; i++) + setbvalue(luaH_setnum(L, t, lineinfo[i]), 1); + sethvalue(L, L->top, t); + } + incr_top(L); +} + + +static int auxgetinfo (lua_State *L, const char *what, lua_Debug *ar, + Closure *f, CallInfo *ci) { + int status = 1; + if (f == NULL) { + info_tailcall(ar); + return status; + } + for (; *what; what++) { + switch (*what) { + case 'S': { + funcinfo(ar, f); + break; + } + case 'l': { + ar->currentline = (ci) ? currentline(L, ci) : -1; + break; + } + case 'u': { + ar->nups = f->c.nupvalues; + break; + } + case 'n': { + ar->namewhat = (ci) ? getfuncname(L, ci, &ar->name) : NULL; + if (ar->namewhat == NULL) { + ar->namewhat = ""; /* not found */ + ar->name = NULL; + } + break; + } + case 'L': + case 'f': /* handled by lua_getinfo */ + break; + default: status = 0; /* invalid option */ + } + } + return status; +} + + +LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar) { + int status; + Closure *f = NULL; + CallInfo *ci = NULL; + lua_lock(L); + if (*what == '>') { + StkId func = L->top - 1; + luai_apicheck(L, ttisfunction(func)); + what++; /* skip the '>' */ + f = clvalue(func); + L->top--; /* pop function */ + } + else if (ar->i_ci != 0) { /* no tail call? */ + ci = L->base_ci + ar->i_ci; + lua_assert(ttisfunction(ci->func)); + f = clvalue(ci->func); + } + status = auxgetinfo(L, what, ar, f, ci); + if (strchr(what, 'f')) { + if (f == NULL) setnilvalue(L->top); + else setclvalue(L, L->top, f); + incr_top(L); + } + if (strchr(what, 'L')) + collectvalidlines(L, f); + lua_unlock(L); + return status; +} + + +/* +** {====================================================== +** Symbolic Execution and code checker +** ======================================================= +*/ + +#define check(x) if (!(x)) return 0; + +#define checkjump(pt,pc) check(0 <= pc && pc < pt->sizecode) + +#define checkreg(pt,reg) check((reg) < (pt)->maxstacksize) + + + +static int precheck (const Proto *pt) { + check(pt->maxstacksize <= MAXSTACK); + check(pt->numparams+(pt->is_vararg & VARARG_HASARG) <= pt->maxstacksize); + check(!(pt->is_vararg & VARARG_NEEDSARG) || + (pt->is_vararg & VARARG_HASARG)); + check(pt->sizeupvalues <= pt->nups); + check(pt->sizelineinfo == pt->sizecode || pt->sizelineinfo == 0); + check(pt->sizecode > 0 && GET_OPCODE(pt->code[pt->sizecode-1]) == OP_RETURN); + return 1; +} + + +#define checkopenop(pt,pc) luaG_checkopenop((pt)->code[(pc)+1]) + +int luaG_checkopenop (Instruction i) { + switch (GET_OPCODE(i)) { + case OP_CALL: + case OP_TAILCALL: + case OP_RETURN: + case OP_SETLIST: { + check(GETARG_B(i) == 0); + return 1; + } + default: return 0; /* invalid instruction after an open call */ + } +} + + +static int checkArgMode (const Proto *pt, int r, enum OpArgMask mode) { + switch (mode) { + case OpArgN: check(r == 0); break; + case OpArgU: break; + case OpArgR: checkreg(pt, r); break; + case OpArgK: + check(ISK(r) ? INDEXK(r) < pt->sizek : r < pt->maxstacksize); + break; + } + return 1; +} + + +static Instruction symbexec (const Proto *pt, int lastpc, int reg) { + int pc; + int last; /* stores position of last instruction that changed `reg' */ + last = pt->sizecode-1; /* points to final return (a `neutral' instruction) */ + check(precheck(pt)); + for (pc = 0; pc < lastpc; pc++) { + Instruction i = pt->code[pc]; + OpCode op = GET_OPCODE(i); + int a = GETARG_A(i); + int b = 0; + int c = 0; + check(op < NUM_OPCODES); + checkreg(pt, a); + switch (getOpMode(op)) { + case iABC: { + b = GETARG_B(i); + c = GETARG_C(i); + check(checkArgMode(pt, b, getBMode(op))); + check(checkArgMode(pt, c, getCMode(op))); + break; + } + case iABx: { + b = GETARG_Bx(i); + if (getBMode(op) == OpArgK) check(b < pt->sizek); + break; + } + case iAsBx: { + b = GETARG_sBx(i); + if (getBMode(op) == OpArgR) { + int dest = pc+1+b; + check(0 <= dest && dest < pt->sizecode); + if (dest > 0) { + int j; + /* check that it does not jump to a setlist count; this + is tricky, because the count from a previous setlist may + have the same value of an invalid setlist; so, we must + go all the way back to the first of them (if any) */ + for (j = 0; j < dest; j++) { + Instruction d = pt->code[dest-1-j]; + if (!(GET_OPCODE(d) == OP_SETLIST && GETARG_C(d) == 0)) break; + } + /* if 'j' is even, previous value is not a setlist (even if + it looks like one) */ + check((j&1) == 0); + } + } + break; + } + } + if (testAMode(op)) { + if (a == reg) last = pc; /* change register `a' */ + } + if (testTMode(op)) { + check(pc+2 < pt->sizecode); /* check skip */ + check(GET_OPCODE(pt->code[pc+1]) == OP_JMP); + } + switch (op) { + case OP_LOADBOOL: { + if (c == 1) { /* does it jump? */ + check(pc+2 < pt->sizecode); /* check its jump */ + check(GET_OPCODE(pt->code[pc+1]) != OP_SETLIST || + GETARG_C(pt->code[pc+1]) != 0); + } + break; + } + case OP_LOADNIL: { + if (a <= reg && reg <= b) + last = pc; /* set registers from `a' to `b' */ + break; + } + case OP_GETUPVAL: + case OP_SETUPVAL: { + check(b < pt->nups); + break; + } + case OP_GETGLOBAL: + case OP_SETGLOBAL: { + check(ttisstring(&pt->k[b])); + break; + } + case OP_SELF: { + checkreg(pt, a+1); + if (reg == a+1) last = pc; + break; + } + case OP_CONCAT: { + check(b < c); /* at least two operands */ + break; + } + case OP_TFORLOOP: { + check(c >= 1); /* at least one result (control variable) */ + checkreg(pt, a+2+c); /* space for results */ + if (reg >= a+2) last = pc; /* affect all regs above its base */ + break; + } + case OP_FORLOOP: + case OP_FORPREP: + checkreg(pt, a+3); + /* go through */ + case OP_JMP: { + int dest = pc+1+b; + /* not full check and jump is forward and do not skip `lastpc'? */ + if (reg != NO_REG && pc < dest && dest <= lastpc) + pc += b; /* do the jump */ + break; + } + case OP_CALL: + case OP_TAILCALL: { + if (b != 0) { + checkreg(pt, a+b-1); + } + c--; /* c = num. returns */ + if (c == LUA_MULTRET) { + check(checkopenop(pt, pc)); + } + else if (c != 0) + checkreg(pt, a+c-1); + if (reg >= a) last = pc; /* affect all registers above base */ + break; + } + case OP_RETURN: { + b--; /* b = num. returns */ + if (b > 0) checkreg(pt, a+b-1); + break; + } + case OP_SETLIST: { + if (b > 0) checkreg(pt, a + b); + if (c == 0) { + pc++; + check(pc < pt->sizecode - 1); + } + break; + } + case OP_CLOSURE: { + int nup, j; + check(b < pt->sizep); + nup = pt->p[b]->nups; + check(pc + nup < pt->sizecode); + for (j = 1; j <= nup; j++) { + OpCode op1 = GET_OPCODE(pt->code[pc + j]); + check(op1 == OP_GETUPVAL || op1 == OP_MOVE); + } + if (reg != NO_REG) /* tracing? */ + pc += nup; /* do not 'execute' these pseudo-instructions */ + break; + } + case OP_VARARG: { + check((pt->is_vararg & VARARG_ISVARARG) && + !(pt->is_vararg & VARARG_NEEDSARG)); + b--; + if (b == LUA_MULTRET) check(checkopenop(pt, pc)); + checkreg(pt, a+b-1); + break; + } + default: break; + } + } + return pt->code[last]; +} + +#undef check +#undef checkjump +#undef checkreg + +/* }====================================================== */ + + +int luaG_checkcode (const Proto *pt) { + return (symbexec(pt, pt->sizecode, NO_REG) != 0); +} + + +static const char *kname (Proto *p, int c) { + if (ISK(c) && ttisstring(&p->k[INDEXK(c)])) + return svalue(&p->k[INDEXK(c)]); + else + return "?"; +} + + +static const char *getobjname (lua_State *L, CallInfo *ci, int stackpos, + const char **name) { + if (isLua(ci)) { /* a Lua function? */ + Proto *p = ci_func(ci)->l.p; + int pc = currentpc(L, ci); + Instruction i; + *name = luaF_getlocalname(p, stackpos+1, pc); + if (*name) /* is a local? */ + return "local"; + i = symbexec(p, pc, stackpos); /* try symbolic execution */ + lua_assert(pc != -1); + switch (GET_OPCODE(i)) { + case OP_GETGLOBAL: { + int g = GETARG_Bx(i); /* global index */ + lua_assert(ttisstring(&p->k[g])); + *name = svalue(&p->k[g]); + return "global"; + } + case OP_MOVE: { + int a = GETARG_A(i); + int b = GETARG_B(i); /* move from `b' to `a' */ + if (b < a) + return getobjname(L, ci, b, name); /* get name for `b' */ + break; + } + case OP_GETTABLE: { + int k = GETARG_C(i); /* key index */ + *name = kname(p, k); + return "field"; + } + case OP_GETUPVAL: { + int u = GETARG_B(i); /* upvalue index */ + *name = p->upvalues ? getstr(p->upvalues[u]) : "?"; + return "upvalue"; + } + case OP_SELF: { + int k = GETARG_C(i); /* key index */ + *name = kname(p, k); + return "method"; + } + default: break; + } + } + return NULL; /* no useful name found */ +} + + +static const char *getfuncname (lua_State *L, CallInfo *ci, const char **name) { + Instruction i; + if ((isLua(ci) && ci->tailcalls > 0) || !isLua(ci - 1)) + return NULL; /* calling function is not Lua (or is unknown) */ + ci--; /* calling function */ + i = ci_func(ci)->l.p->code[currentpc(L, ci)]; + if (GET_OPCODE(i) == OP_CALL || GET_OPCODE(i) == OP_TAILCALL || + GET_OPCODE(i) == OP_TFORLOOP) + return getobjname(L, ci, GETARG_A(i), name); + else + return NULL; /* no useful name can be found */ +} + + +/* only ANSI way to check whether a pointer points to an array */ +static int isinstack (CallInfo *ci, const TValue *o) { + StkId p; + for (p = ci->base; p < ci->top; p++) + if (o == p) return 1; + return 0; +} + + +void luaG_typeerror (lua_State *L, const TValue *o, const char *op) { + const char *name = NULL; + const char *t = luaT_typenames[ttype(o)]; + const char *kind = (isinstack(L->ci, o)) ? + getobjname(L, L->ci, cast_int(o - L->base), &name) : + NULL; + if (kind) + luaG_runerror(L, "attempt to %s %s " LUA_QS " (a %s value)", + op, kind, name, t); + else + luaG_runerror(L, "attempt to %s a %s value", op, t); +} + + +void luaG_concaterror (lua_State *L, StkId p1, StkId p2) { + if (ttisstring(p1) || ttisnumber(p1)) p1 = p2; + lua_assert(!ttisstring(p1) && !ttisnumber(p1)); + luaG_typeerror(L, p1, "concatenate"); +} + + +void luaG_aritherror (lua_State *L, const TValue *p1, const TValue *p2) { + TValue temp; + if (luaV_tonumber(p1, &temp) == NULL) + p2 = p1; /* first operand is wrong */ + luaG_typeerror(L, p2, "perform arithmetic on"); +} + + +int luaG_ordererror (lua_State *L, const TValue *p1, const TValue *p2) { + const char *t1 = luaT_typenames[ttype(p1)]; + const char *t2 = luaT_typenames[ttype(p2)]; + if (t1[2] == t2[2]) + luaG_runerror(L, "attempt to compare two %s values", t1); + else + luaG_runerror(L, "attempt to compare %s with %s", t1, t2); + return 0; +} + + +static void addinfo (lua_State *L, const char *msg) { + CallInfo *ci = L->ci; + if (isLua(ci)) { /* is Lua code? */ + char buff[LUA_IDSIZE]; /* add file:line information */ + int line = currentline(L, ci); + luaO_chunkid(buff, getstr(getluaproto(ci)->source), LUA_IDSIZE); + luaO_pushfstring(L, "%s:%d: %s", buff, line, msg); + } +} + + +void luaG_errormsg (lua_State *L) { + if (L->errfunc != 0) { /* is there an error handling function? */ + StkId errfunc = restorestack(L, L->errfunc); + if (!ttisfunction(errfunc)) luaD_throw(L, LUA_ERRERR); + setobjs2s(L, L->top, L->top - 1); /* move argument */ + setobjs2s(L, L->top - 1, errfunc); /* push function */ + incr_top(L); + luaD_call(L, L->top - 2, 1); /* call it */ + } + luaD_throw(L, LUA_ERRRUN); +} + + +void luaG_runerror (lua_State *L, const char *fmt, ...) { + va_list argp; + va_start(argp, fmt); + addinfo(L, luaO_pushvfstring(L, fmt, argp)); + va_end(argp); + luaG_errormsg(L); +} + diff --git a/extern/lua-5.1.5/src/ldebug.h b/extern/lua-5.1.5/src/ldebug.h new file mode 100644 index 00000000..ba28a972 --- /dev/null +++ b/extern/lua-5.1.5/src/ldebug.h @@ -0,0 +1,33 @@ +/* +** $Id: ldebug.h,v 2.3.1.1 2007/12/27 13:02:25 roberto Exp $ +** Auxiliary functions from Debug Interface module +** See Copyright Notice in lua.h +*/ + +#ifndef ldebug_h +#define ldebug_h + + +#include "lstate.h" + + +#define pcRel(pc, p) (cast(int, (pc) - (p)->code) - 1) + +#define getline(f,pc) (((f)->lineinfo) ? (f)->lineinfo[pc] : 0) + +#define resethookcount(L) (L->hookcount = L->basehookcount) + + +LUAI_FUNC void luaG_typeerror (lua_State *L, const TValue *o, + const char *opname); +LUAI_FUNC void luaG_concaterror (lua_State *L, StkId p1, StkId p2); +LUAI_FUNC void luaG_aritherror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC int luaG_ordererror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC void luaG_runerror (lua_State *L, const char *fmt, ...); +LUAI_FUNC void luaG_errormsg (lua_State *L); +LUAI_FUNC int luaG_checkcode (const Proto *pt); +LUAI_FUNC int luaG_checkopenop (Instruction i); + +#endif diff --git a/extern/lua-5.1.5/src/ldo.c b/extern/lua-5.1.5/src/ldo.c new file mode 100644 index 00000000..d1bf786c --- /dev/null +++ b/extern/lua-5.1.5/src/ldo.c @@ -0,0 +1,519 @@ +/* +** $Id: ldo.c,v 2.38.1.4 2012/01/18 02:27:10 roberto Exp $ +** Stack and Call structure of Lua +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include + +#define ldo_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lundump.h" +#include "lvm.h" +#include "lzio.h" + + + + +/* +** {====================================================== +** Error-recovery functions +** ======================================================= +*/ + + +/* chain list of long jump buffers */ +struct lua_longjmp { + struct lua_longjmp *previous; + luai_jmpbuf b; + volatile int status; /* error code */ +}; + + +void luaD_seterrorobj (lua_State *L, int errcode, StkId oldtop) { + switch (errcode) { + case LUA_ERRMEM: { + setsvalue2s(L, oldtop, luaS_newliteral(L, MEMERRMSG)); + break; + } + case LUA_ERRERR: { + setsvalue2s(L, oldtop, luaS_newliteral(L, "error in error handling")); + break; + } + case LUA_ERRSYNTAX: + case LUA_ERRRUN: { + setobjs2s(L, oldtop, L->top - 1); /* error message on current top */ + break; + } + } + L->top = oldtop + 1; +} + + +static void restore_stack_limit (lua_State *L) { + lua_assert(L->stack_last - L->stack == L->stacksize - EXTRA_STACK - 1); + if (L->size_ci > LUAI_MAXCALLS) { /* there was an overflow? */ + int inuse = cast_int(L->ci - L->base_ci); + if (inuse + 1 < LUAI_MAXCALLS) /* can `undo' overflow? */ + luaD_reallocCI(L, LUAI_MAXCALLS); + } +} + + +static void resetstack (lua_State *L, int status) { + L->ci = L->base_ci; + L->base = L->ci->base; + luaF_close(L, L->base); /* close eventual pending closures */ + luaD_seterrorobj(L, status, L->base); + L->nCcalls = L->baseCcalls; + L->allowhook = 1; + restore_stack_limit(L); + L->errfunc = 0; + L->errorJmp = NULL; +} + + +void luaD_throw (lua_State *L, int errcode) { + if (L->errorJmp) { + L->errorJmp->status = errcode; + LUAI_THROW(L, L->errorJmp); + } + else { + L->status = cast_byte(errcode); + if (G(L)->panic) { + resetstack(L, errcode); + lua_unlock(L); + G(L)->panic(L); + } + exit(EXIT_FAILURE); + } +} + + +int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) { + struct lua_longjmp lj; + lj.status = 0; + lj.previous = L->errorJmp; /* chain new error handler */ + L->errorJmp = &lj; + LUAI_TRY(L, &lj, + (*f)(L, ud); + ); + L->errorJmp = lj.previous; /* restore old error handler */ + return lj.status; +} + +/* }====================================================== */ + + +static void correctstack (lua_State *L, TValue *oldstack) { + CallInfo *ci; + GCObject *up; + L->top = (L->top - oldstack) + L->stack; + for (up = L->openupval; up != NULL; up = up->gch.next) + gco2uv(up)->v = (gco2uv(up)->v - oldstack) + L->stack; + for (ci = L->base_ci; ci <= L->ci; ci++) { + ci->top = (ci->top - oldstack) + L->stack; + ci->base = (ci->base - oldstack) + L->stack; + ci->func = (ci->func - oldstack) + L->stack; + } + L->base = (L->base - oldstack) + L->stack; +} + + +void luaD_reallocstack (lua_State *L, int newsize) { + TValue *oldstack = L->stack; + int realsize = newsize + 1 + EXTRA_STACK; + lua_assert(L->stack_last - L->stack == L->stacksize - EXTRA_STACK - 1); + luaM_reallocvector(L, L->stack, L->stacksize, realsize, TValue); + L->stacksize = realsize; + L->stack_last = L->stack+newsize; + correctstack(L, oldstack); +} + + +void luaD_reallocCI (lua_State *L, int newsize) { + CallInfo *oldci = L->base_ci; + luaM_reallocvector(L, L->base_ci, L->size_ci, newsize, CallInfo); + L->size_ci = newsize; + L->ci = (L->ci - oldci) + L->base_ci; + L->end_ci = L->base_ci + L->size_ci - 1; +} + + +void luaD_growstack (lua_State *L, int n) { + if (n <= L->stacksize) /* double size is enough? */ + luaD_reallocstack(L, 2*L->stacksize); + else + luaD_reallocstack(L, L->stacksize + n); +} + + +static CallInfo *growCI (lua_State *L) { + if (L->size_ci > LUAI_MAXCALLS) /* overflow while handling overflow? */ + luaD_throw(L, LUA_ERRERR); + else { + luaD_reallocCI(L, 2*L->size_ci); + if (L->size_ci > LUAI_MAXCALLS) + luaG_runerror(L, "stack overflow"); + } + return ++L->ci; +} + + +void luaD_callhook (lua_State *L, int event, int line) { + lua_Hook hook = L->hook; + if (hook && L->allowhook) { + ptrdiff_t top = savestack(L, L->top); + ptrdiff_t ci_top = savestack(L, L->ci->top); + lua_Debug ar; + ar.event = event; + ar.currentline = line; + if (event == LUA_HOOKTAILRET) + ar.i_ci = 0; /* tail call; no debug information about it */ + else + ar.i_ci = cast_int(L->ci - L->base_ci); + luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */ + L->ci->top = L->top + LUA_MINSTACK; + lua_assert(L->ci->top <= L->stack_last); + L->allowhook = 0; /* cannot call hooks inside a hook */ + lua_unlock(L); + (*hook)(L, &ar); + lua_lock(L); + lua_assert(!L->allowhook); + L->allowhook = 1; + L->ci->top = restorestack(L, ci_top); + L->top = restorestack(L, top); + } +} + + +static StkId adjust_varargs (lua_State *L, Proto *p, int actual) { + int i; + int nfixargs = p->numparams; + Table *htab = NULL; + StkId base, fixed; + for (; actual < nfixargs; ++actual) + setnilvalue(L->top++); +#if defined(LUA_COMPAT_VARARG) + if (p->is_vararg & VARARG_NEEDSARG) { /* compat. with old-style vararg? */ + int nvar = actual - nfixargs; /* number of extra arguments */ + lua_assert(p->is_vararg & VARARG_HASARG); + luaC_checkGC(L); + luaD_checkstack(L, p->maxstacksize); + htab = luaH_new(L, nvar, 1); /* create `arg' table */ + for (i=0; itop - nvar + i); + /* store counter in field `n' */ + setnvalue(luaH_setstr(L, htab, luaS_newliteral(L, "n")), cast_num(nvar)); + } +#endif + /* move fixed parameters to final position */ + fixed = L->top - actual; /* first fixed argument */ + base = L->top; /* final position of first argument */ + for (i=0; itop++, fixed+i); + setnilvalue(fixed+i); + } + /* add `arg' parameter */ + if (htab) { + sethvalue(L, L->top++, htab); + lua_assert(iswhite(obj2gco(htab))); + } + return base; +} + + +static StkId tryfuncTM (lua_State *L, StkId func) { + const TValue *tm = luaT_gettmbyobj(L, func, TM_CALL); + StkId p; + ptrdiff_t funcr = savestack(L, func); + if (!ttisfunction(tm)) + luaG_typeerror(L, func, "call"); + /* Open a hole inside the stack at `func' */ + for (p = L->top; p > func; p--) setobjs2s(L, p, p-1); + incr_top(L); + func = restorestack(L, funcr); /* previous call may change stack */ + setobj2s(L, func, tm); /* tag method is the new function to be called */ + return func; +} + + + +#define inc_ci(L) \ + ((L->ci == L->end_ci) ? growCI(L) : \ + (condhardstacktests(luaD_reallocCI(L, L->size_ci)), ++L->ci)) + + +int luaD_precall (lua_State *L, StkId func, int nresults) { + LClosure *cl; + ptrdiff_t funcr; + if (!ttisfunction(func)) /* `func' is not a function? */ + func = tryfuncTM(L, func); /* check the `function' tag method */ + funcr = savestack(L, func); + cl = &clvalue(func)->l; + L->ci->savedpc = L->savedpc; + if (!cl->isC) { /* Lua function? prepare its call */ + CallInfo *ci; + StkId st, base; + Proto *p = cl->p; + luaD_checkstack(L, p->maxstacksize); + func = restorestack(L, funcr); + if (!p->is_vararg) { /* no varargs? */ + base = func + 1; + if (L->top > base + p->numparams) + L->top = base + p->numparams; + } + else { /* vararg function */ + int nargs = cast_int(L->top - func) - 1; + base = adjust_varargs(L, p, nargs); + func = restorestack(L, funcr); /* previous call may change the stack */ + } + ci = inc_ci(L); /* now `enter' new function */ + ci->func = func; + L->base = ci->base = base; + ci->top = L->base + p->maxstacksize; + lua_assert(ci->top <= L->stack_last); + L->savedpc = p->code; /* starting point */ + ci->tailcalls = 0; + ci->nresults = nresults; + for (st = L->top; st < ci->top; st++) + setnilvalue(st); + L->top = ci->top; + if (L->hookmask & LUA_MASKCALL) { + L->savedpc++; /* hooks assume 'pc' is already incremented */ + luaD_callhook(L, LUA_HOOKCALL, -1); + L->savedpc--; /* correct 'pc' */ + } + return PCRLUA; + } + else { /* if is a C function, call it */ + CallInfo *ci; + int n; + luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */ + ci = inc_ci(L); /* now `enter' new function */ + ci->func = restorestack(L, funcr); + L->base = ci->base = ci->func + 1; + ci->top = L->top + LUA_MINSTACK; + lua_assert(ci->top <= L->stack_last); + ci->nresults = nresults; + if (L->hookmask & LUA_MASKCALL) + luaD_callhook(L, LUA_HOOKCALL, -1); + lua_unlock(L); + n = (*curr_func(L)->c.f)(L); /* do the actual call */ + lua_lock(L); + if (n < 0) /* yielding? */ + return PCRYIELD; + else { + luaD_poscall(L, L->top - n); + return PCRC; + } + } +} + + +static StkId callrethooks (lua_State *L, StkId firstResult) { + ptrdiff_t fr = savestack(L, firstResult); /* next call may change stack */ + luaD_callhook(L, LUA_HOOKRET, -1); + if (f_isLua(L->ci)) { /* Lua function? */ + while ((L->hookmask & LUA_MASKRET) && L->ci->tailcalls--) /* tail calls */ + luaD_callhook(L, LUA_HOOKTAILRET, -1); + } + return restorestack(L, fr); +} + + +int luaD_poscall (lua_State *L, StkId firstResult) { + StkId res; + int wanted, i; + CallInfo *ci; + if (L->hookmask & LUA_MASKRET) + firstResult = callrethooks(L, firstResult); + ci = L->ci--; + res = ci->func; /* res == final position of 1st result */ + wanted = ci->nresults; + L->base = (ci - 1)->base; /* restore base */ + L->savedpc = (ci - 1)->savedpc; /* restore savedpc */ + /* move results to correct place */ + for (i = wanted; i != 0 && firstResult < L->top; i--) + setobjs2s(L, res++, firstResult++); + while (i-- > 0) + setnilvalue(res++); + L->top = res; + return (wanted - LUA_MULTRET); /* 0 iff wanted == LUA_MULTRET */ +} + + +/* +** Call a function (C or Lua). The function to be called is at *func. +** The arguments are on the stack, right after the function. +** When returns, all the results are on the stack, starting at the original +** function position. +*/ +void luaD_call (lua_State *L, StkId func, int nResults) { + if (++L->nCcalls >= LUAI_MAXCCALLS) { + if (L->nCcalls == LUAI_MAXCCALLS) + luaG_runerror(L, "C stack overflow"); + else if (L->nCcalls >= (LUAI_MAXCCALLS + (LUAI_MAXCCALLS>>3))) + luaD_throw(L, LUA_ERRERR); /* error while handing stack error */ + } + if (luaD_precall(L, func, nResults) == PCRLUA) /* is a Lua function? */ + luaV_execute(L, 1); /* call it */ + L->nCcalls--; + luaC_checkGC(L); +} + + +static void resume (lua_State *L, void *ud) { + StkId firstArg = cast(StkId, ud); + CallInfo *ci = L->ci; + if (L->status == 0) { /* start coroutine? */ + lua_assert(ci == L->base_ci && firstArg > L->base); + if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA) + return; + } + else { /* resuming from previous yield */ + lua_assert(L->status == LUA_YIELD); + L->status = 0; + if (!f_isLua(ci)) { /* `common' yield? */ + /* finish interrupted execution of `OP_CALL' */ + lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL || + GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL); + if (luaD_poscall(L, firstArg)) /* complete it... */ + L->top = L->ci->top; /* and correct top if not multiple results */ + } + else /* yielded inside a hook: just continue its execution */ + L->base = L->ci->base; + } + luaV_execute(L, cast_int(L->ci - L->base_ci)); +} + + +static int resume_error (lua_State *L, const char *msg) { + L->top = L->ci->base; + setsvalue2s(L, L->top, luaS_new(L, msg)); + incr_top(L); + lua_unlock(L); + return LUA_ERRRUN; +} + + +LUA_API int lua_resume (lua_State *L, int nargs) { + int status; + lua_lock(L); + if (L->status != LUA_YIELD && (L->status != 0 || L->ci != L->base_ci)) + return resume_error(L, "cannot resume non-suspended coroutine"); + if (L->nCcalls >= LUAI_MAXCCALLS) + return resume_error(L, "C stack overflow"); + luai_userstateresume(L, nargs); + lua_assert(L->errfunc == 0); + L->baseCcalls = ++L->nCcalls; + status = luaD_rawrunprotected(L, resume, L->top - nargs); + if (status != 0) { /* error? */ + L->status = cast_byte(status); /* mark thread as `dead' */ + luaD_seterrorobj(L, status, L->top); + L->ci->top = L->top; + } + else { + lua_assert(L->nCcalls == L->baseCcalls); + status = L->status; + } + --L->nCcalls; + lua_unlock(L); + return status; +} + + +LUA_API int lua_yield (lua_State *L, int nresults) { + luai_userstateyield(L, nresults); + lua_lock(L); + if (L->nCcalls > L->baseCcalls) + luaG_runerror(L, "attempt to yield across metamethod/C-call boundary"); + L->base = L->top - nresults; /* protect stack slots below */ + L->status = LUA_YIELD; + lua_unlock(L); + return -1; +} + + +int luaD_pcall (lua_State *L, Pfunc func, void *u, + ptrdiff_t old_top, ptrdiff_t ef) { + int status; + unsigned short oldnCcalls = L->nCcalls; + ptrdiff_t old_ci = saveci(L, L->ci); + lu_byte old_allowhooks = L->allowhook; + ptrdiff_t old_errfunc = L->errfunc; + L->errfunc = ef; + status = luaD_rawrunprotected(L, func, u); + if (status != 0) { /* an error occurred? */ + StkId oldtop = restorestack(L, old_top); + luaF_close(L, oldtop); /* close eventual pending closures */ + luaD_seterrorobj(L, status, oldtop); + L->nCcalls = oldnCcalls; + L->ci = restoreci(L, old_ci); + L->base = L->ci->base; + L->savedpc = L->ci->savedpc; + L->allowhook = old_allowhooks; + restore_stack_limit(L); + } + L->errfunc = old_errfunc; + return status; +} + + + +/* +** Execute a protected parser. +*/ +struct SParser { /* data to `f_parser' */ + ZIO *z; + Mbuffer buff; /* buffer to be used by the scanner */ + const char *name; +}; + +static void f_parser (lua_State *L, void *ud) { + int i; + Proto *tf; + Closure *cl; + struct SParser *p = cast(struct SParser *, ud); + int c = luaZ_lookahead(p->z); + luaC_checkGC(L); + tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z, + &p->buff, p->name); + cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L))); + cl->l.p = tf; + for (i = 0; i < tf->nups; i++) /* initialize eventual upvalues */ + cl->l.upvals[i] = luaF_newupval(L); + setclvalue(L, L->top, cl); + incr_top(L); +} + + +int luaD_protectedparser (lua_State *L, ZIO *z, const char *name) { + struct SParser p; + int status; + p.z = z; p.name = name; + luaZ_initbuffer(L, &p.buff); + status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc); + luaZ_freebuffer(L, &p.buff); + return status; +} + + diff --git a/extern/lua-5.1.5/src/ldo.h b/extern/lua-5.1.5/src/ldo.h new file mode 100644 index 00000000..98fddac5 --- /dev/null +++ b/extern/lua-5.1.5/src/ldo.h @@ -0,0 +1,57 @@ +/* +** $Id: ldo.h,v 2.7.1.1 2007/12/27 13:02:25 roberto Exp $ +** Stack and Call structure of Lua +** See Copyright Notice in lua.h +*/ + +#ifndef ldo_h +#define ldo_h + + +#include "lobject.h" +#include "lstate.h" +#include "lzio.h" + + +#define luaD_checkstack(L,n) \ + if ((char *)L->stack_last - (char *)L->top <= (n)*(int)sizeof(TValue)) \ + luaD_growstack(L, n); \ + else condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK - 1)); + + +#define incr_top(L) {luaD_checkstack(L,1); L->top++;} + +#define savestack(L,p) ((char *)(p) - (char *)L->stack) +#define restorestack(L,n) ((TValue *)((char *)L->stack + (n))) + +#define saveci(L,p) ((char *)(p) - (char *)L->base_ci) +#define restoreci(L,n) ((CallInfo *)((char *)L->base_ci + (n))) + + +/* results from luaD_precall */ +#define PCRLUA 0 /* initiated a call to a Lua function */ +#define PCRC 1 /* did a call to a C function */ +#define PCRYIELD 2 /* C funtion yielded */ + + +/* type of protected functions, to be ran by `runprotected' */ +typedef void (*Pfunc) (lua_State *L, void *ud); + +LUAI_FUNC int luaD_protectedparser (lua_State *L, ZIO *z, const char *name); +LUAI_FUNC void luaD_callhook (lua_State *L, int event, int line); +LUAI_FUNC int luaD_precall (lua_State *L, StkId func, int nresults); +LUAI_FUNC void luaD_call (lua_State *L, StkId func, int nResults); +LUAI_FUNC int luaD_pcall (lua_State *L, Pfunc func, void *u, + ptrdiff_t oldtop, ptrdiff_t ef); +LUAI_FUNC int luaD_poscall (lua_State *L, StkId firstResult); +LUAI_FUNC void luaD_reallocCI (lua_State *L, int newsize); +LUAI_FUNC void luaD_reallocstack (lua_State *L, int newsize); +LUAI_FUNC void luaD_growstack (lua_State *L, int n); + +LUAI_FUNC void luaD_throw (lua_State *L, int errcode); +LUAI_FUNC int luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud); + +LUAI_FUNC void luaD_seterrorobj (lua_State *L, int errcode, StkId oldtop); + +#endif + diff --git a/extern/lua-5.1.5/src/ldump.c b/extern/lua-5.1.5/src/ldump.c new file mode 100644 index 00000000..c9d3d487 --- /dev/null +++ b/extern/lua-5.1.5/src/ldump.c @@ -0,0 +1,164 @@ +/* +** $Id: ldump.c,v 2.8.1.1 2007/12/27 13:02:25 roberto Exp $ +** save precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#include + +#define ldump_c +#define LUA_CORE + +#include "lua.h" + +#include "lobject.h" +#include "lstate.h" +#include "lundump.h" + +typedef struct { + lua_State* L; + lua_Writer writer; + void* data; + int strip; + int status; +} DumpState; + +#define DumpMem(b,n,size,D) DumpBlock(b,(n)*(size),D) +#define DumpVar(x,D) DumpMem(&x,1,sizeof(x),D) + +static void DumpBlock(const void* b, size_t size, DumpState* D) +{ + if (D->status==0) + { + lua_unlock(D->L); + D->status=(*D->writer)(D->L,b,size,D->data); + lua_lock(D->L); + } +} + +static void DumpChar(int y, DumpState* D) +{ + char x=(char)y; + DumpVar(x,D); +} + +static void DumpInt(int x, DumpState* D) +{ + DumpVar(x,D); +} + +static void DumpNumber(lua_Number x, DumpState* D) +{ + DumpVar(x,D); +} + +static void DumpVector(const void* b, int n, size_t size, DumpState* D) +{ + DumpInt(n,D); + DumpMem(b,n,size,D); +} + +static void DumpString(const TString* s, DumpState* D) +{ + if (s==NULL || getstr(s)==NULL) + { + size_t size=0; + DumpVar(size,D); + } + else + { + size_t size=s->tsv.len+1; /* include trailing '\0' */ + DumpVar(size,D); + DumpBlock(getstr(s),size,D); + } +} + +#define DumpCode(f,D) DumpVector(f->code,f->sizecode,sizeof(Instruction),D) + +static void DumpFunction(const Proto* f, const TString* p, DumpState* D); + +static void DumpConstants(const Proto* f, DumpState* D) +{ + int i,n=f->sizek; + DumpInt(n,D); + for (i=0; ik[i]; + DumpChar(ttype(o),D); + switch (ttype(o)) + { + case LUA_TNIL: + break; + case LUA_TBOOLEAN: + DumpChar(bvalue(o),D); + break; + case LUA_TNUMBER: + DumpNumber(nvalue(o),D); + break; + case LUA_TSTRING: + DumpString(rawtsvalue(o),D); + break; + default: + lua_assert(0); /* cannot happen */ + break; + } + } + n=f->sizep; + DumpInt(n,D); + for (i=0; ip[i],f->source,D); +} + +static void DumpDebug(const Proto* f, DumpState* D) +{ + int i,n; + n= (D->strip) ? 0 : f->sizelineinfo; + DumpVector(f->lineinfo,n,sizeof(int),D); + n= (D->strip) ? 0 : f->sizelocvars; + DumpInt(n,D); + for (i=0; ilocvars[i].varname,D); + DumpInt(f->locvars[i].startpc,D); + DumpInt(f->locvars[i].endpc,D); + } + n= (D->strip) ? 0 : f->sizeupvalues; + DumpInt(n,D); + for (i=0; iupvalues[i],D); +} + +static void DumpFunction(const Proto* f, const TString* p, DumpState* D) +{ + DumpString((f->source==p || D->strip) ? NULL : f->source,D); + DumpInt(f->linedefined,D); + DumpInt(f->lastlinedefined,D); + DumpChar(f->nups,D); + DumpChar(f->numparams,D); + DumpChar(f->is_vararg,D); + DumpChar(f->maxstacksize,D); + DumpCode(f,D); + DumpConstants(f,D); + DumpDebug(f,D); +} + +static void DumpHeader(DumpState* D) +{ + char h[LUAC_HEADERSIZE]; + luaU_header(h); + DumpBlock(h,LUAC_HEADERSIZE,D); +} + +/* +** dump Lua function as precompiled chunk +*/ +int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip) +{ + DumpState D; + D.L=L; + D.writer=w; + D.data=data; + D.strip=strip; + D.status=0; + DumpHeader(&D); + DumpFunction(f,NULL,&D); + return D.status; +} diff --git a/extern/lua-5.1.5/src/lfunc.c b/extern/lua-5.1.5/src/lfunc.c new file mode 100644 index 00000000..813e88f5 --- /dev/null +++ b/extern/lua-5.1.5/src/lfunc.c @@ -0,0 +1,174 @@ +/* +** $Id: lfunc.c,v 2.12.1.2 2007/12/28 14:58:43 roberto Exp $ +** Auxiliary functions to manipulate prototypes and closures +** See Copyright Notice in lua.h +*/ + + +#include + +#define lfunc_c +#define LUA_CORE + +#include "lua.h" + +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" + + + +Closure *luaF_newCclosure (lua_State *L, int nelems, Table *e) { + Closure *c = cast(Closure *, luaM_malloc(L, sizeCclosure(nelems))); + luaC_link(L, obj2gco(c), LUA_TFUNCTION); + c->c.isC = 1; + c->c.env = e; + c->c.nupvalues = cast_byte(nelems); + return c; +} + + +Closure *luaF_newLclosure (lua_State *L, int nelems, Table *e) { + Closure *c = cast(Closure *, luaM_malloc(L, sizeLclosure(nelems))); + luaC_link(L, obj2gco(c), LUA_TFUNCTION); + c->l.isC = 0; + c->l.env = e; + c->l.nupvalues = cast_byte(nelems); + while (nelems--) c->l.upvals[nelems] = NULL; + return c; +} + + +UpVal *luaF_newupval (lua_State *L) { + UpVal *uv = luaM_new(L, UpVal); + luaC_link(L, obj2gco(uv), LUA_TUPVAL); + uv->v = &uv->u.value; + setnilvalue(uv->v); + return uv; +} + + +UpVal *luaF_findupval (lua_State *L, StkId level) { + global_State *g = G(L); + GCObject **pp = &L->openupval; + UpVal *p; + UpVal *uv; + while (*pp != NULL && (p = ngcotouv(*pp))->v >= level) { + lua_assert(p->v != &p->u.value); + if (p->v == level) { /* found a corresponding upvalue? */ + if (isdead(g, obj2gco(p))) /* is it dead? */ + changewhite(obj2gco(p)); /* ressurect it */ + return p; + } + pp = &p->next; + } + uv = luaM_new(L, UpVal); /* not found: create a new one */ + uv->tt = LUA_TUPVAL; + uv->marked = luaC_white(g); + uv->v = level; /* current value lives in the stack */ + uv->next = *pp; /* chain it in the proper position */ + *pp = obj2gco(uv); + uv->u.l.prev = &g->uvhead; /* double link it in `uvhead' list */ + uv->u.l.next = g->uvhead.u.l.next; + uv->u.l.next->u.l.prev = uv; + g->uvhead.u.l.next = uv; + lua_assert(uv->u.l.next->u.l.prev == uv && uv->u.l.prev->u.l.next == uv); + return uv; +} + + +static void unlinkupval (UpVal *uv) { + lua_assert(uv->u.l.next->u.l.prev == uv && uv->u.l.prev->u.l.next == uv); + uv->u.l.next->u.l.prev = uv->u.l.prev; /* remove from `uvhead' list */ + uv->u.l.prev->u.l.next = uv->u.l.next; +} + + +void luaF_freeupval (lua_State *L, UpVal *uv) { + if (uv->v != &uv->u.value) /* is it open? */ + unlinkupval(uv); /* remove from open list */ + luaM_free(L, uv); /* free upvalue */ +} + + +void luaF_close (lua_State *L, StkId level) { + UpVal *uv; + global_State *g = G(L); + while (L->openupval != NULL && (uv = ngcotouv(L->openupval))->v >= level) { + GCObject *o = obj2gco(uv); + lua_assert(!isblack(o) && uv->v != &uv->u.value); + L->openupval = uv->next; /* remove from `open' list */ + if (isdead(g, o)) + luaF_freeupval(L, uv); /* free upvalue */ + else { + unlinkupval(uv); + setobj(L, &uv->u.value, uv->v); + uv->v = &uv->u.value; /* now current value lives here */ + luaC_linkupval(L, uv); /* link upvalue into `gcroot' list */ + } + } +} + + +Proto *luaF_newproto (lua_State *L) { + Proto *f = luaM_new(L, Proto); + luaC_link(L, obj2gco(f), LUA_TPROTO); + f->k = NULL; + f->sizek = 0; + f->p = NULL; + f->sizep = 0; + f->code = NULL; + f->sizecode = 0; + f->sizelineinfo = 0; + f->sizeupvalues = 0; + f->nups = 0; + f->upvalues = NULL; + f->numparams = 0; + f->is_vararg = 0; + f->maxstacksize = 0; + f->lineinfo = NULL; + f->sizelocvars = 0; + f->locvars = NULL; + f->linedefined = 0; + f->lastlinedefined = 0; + f->source = NULL; + return f; +} + + +void luaF_freeproto (lua_State *L, Proto *f) { + luaM_freearray(L, f->code, f->sizecode, Instruction); + luaM_freearray(L, f->p, f->sizep, Proto *); + luaM_freearray(L, f->k, f->sizek, TValue); + luaM_freearray(L, f->lineinfo, f->sizelineinfo, int); + luaM_freearray(L, f->locvars, f->sizelocvars, struct LocVar); + luaM_freearray(L, f->upvalues, f->sizeupvalues, TString *); + luaM_free(L, f); +} + + +void luaF_freeclosure (lua_State *L, Closure *c) { + int size = (c->c.isC) ? sizeCclosure(c->c.nupvalues) : + sizeLclosure(c->l.nupvalues); + luaM_freemem(L, c, size); +} + + +/* +** Look for n-th local variable at line `line' in function `func'. +** Returns NULL if not found. +*/ +const char *luaF_getlocalname (const Proto *f, int local_number, int pc) { + int i; + for (i = 0; isizelocvars && f->locvars[i].startpc <= pc; i++) { + if (pc < f->locvars[i].endpc) { /* is variable active? */ + local_number--; + if (local_number == 0) + return getstr(f->locvars[i].varname); + } + } + return NULL; /* not found */ +} + diff --git a/extern/lua-5.1.5/src/lfunc.h b/extern/lua-5.1.5/src/lfunc.h new file mode 100644 index 00000000..a68cf515 --- /dev/null +++ b/extern/lua-5.1.5/src/lfunc.h @@ -0,0 +1,34 @@ +/* +** $Id: lfunc.h,v 2.4.1.1 2007/12/27 13:02:25 roberto Exp $ +** Auxiliary functions to manipulate prototypes and closures +** See Copyright Notice in lua.h +*/ + +#ifndef lfunc_h +#define lfunc_h + + +#include "lobject.h" + + +#define sizeCclosure(n) (cast(int, sizeof(CClosure)) + \ + cast(int, sizeof(TValue)*((n)-1))) + +#define sizeLclosure(n) (cast(int, sizeof(LClosure)) + \ + cast(int, sizeof(TValue *)*((n)-1))) + + +LUAI_FUNC Proto *luaF_newproto (lua_State *L); +LUAI_FUNC Closure *luaF_newCclosure (lua_State *L, int nelems, Table *e); +LUAI_FUNC Closure *luaF_newLclosure (lua_State *L, int nelems, Table *e); +LUAI_FUNC UpVal *luaF_newupval (lua_State *L); +LUAI_FUNC UpVal *luaF_findupval (lua_State *L, StkId level); +LUAI_FUNC void luaF_close (lua_State *L, StkId level); +LUAI_FUNC void luaF_freeproto (lua_State *L, Proto *f); +LUAI_FUNC void luaF_freeclosure (lua_State *L, Closure *c); +LUAI_FUNC void luaF_freeupval (lua_State *L, UpVal *uv); +LUAI_FUNC const char *luaF_getlocalname (const Proto *func, int local_number, + int pc); + + +#endif diff --git a/extern/lua-5.1.5/src/lgc.c b/extern/lua-5.1.5/src/lgc.c new file mode 100644 index 00000000..e909c79a --- /dev/null +++ b/extern/lua-5.1.5/src/lgc.c @@ -0,0 +1,710 @@ +/* +** $Id: lgc.c,v 2.38.1.2 2011/03/18 18:05:38 roberto Exp $ +** Garbage Collector +** See Copyright Notice in lua.h +*/ + +#include + +#define lgc_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + +#define GCSTEPSIZE 1024u +#define GCSWEEPMAX 40 +#define GCSWEEPCOST 10 +#define GCFINALIZECOST 100 + + +#define maskmarks cast_byte(~(bitmask(BLACKBIT)|WHITEBITS)) + +#define makewhite(g,x) \ + ((x)->gch.marked = cast_byte(((x)->gch.marked & maskmarks) | luaC_white(g))) + +#define white2gray(x) reset2bits((x)->gch.marked, WHITE0BIT, WHITE1BIT) +#define black2gray(x) resetbit((x)->gch.marked, BLACKBIT) + +#define stringmark(s) reset2bits((s)->tsv.marked, WHITE0BIT, WHITE1BIT) + + +#define isfinalized(u) testbit((u)->marked, FINALIZEDBIT) +#define markfinalized(u) l_setbit((u)->marked, FINALIZEDBIT) + + +#define KEYWEAK bitmask(KEYWEAKBIT) +#define VALUEWEAK bitmask(VALUEWEAKBIT) + + + +#define markvalue(g,o) { checkconsistency(o); \ + if (iscollectable(o) && iswhite(gcvalue(o))) reallymarkobject(g,gcvalue(o)); } + +#define markobject(g,t) { if (iswhite(obj2gco(t))) \ + reallymarkobject(g, obj2gco(t)); } + + +#define setthreshold(g) (g->GCthreshold = (g->estimate/100) * g->gcpause) + + +static void removeentry (Node *n) { + lua_assert(ttisnil(gval(n))); + if (iscollectable(gkey(n))) + setttype(gkey(n), LUA_TDEADKEY); /* dead key; remove it */ +} + + +static void reallymarkobject (global_State *g, GCObject *o) { + lua_assert(iswhite(o) && !isdead(g, o)); + white2gray(o); + switch (o->gch.tt) { + case LUA_TSTRING: { + return; + } + case LUA_TUSERDATA: { + Table *mt = gco2u(o)->metatable; + gray2black(o); /* udata are never gray */ + if (mt) markobject(g, mt); + markobject(g, gco2u(o)->env); + return; + } + case LUA_TUPVAL: { + UpVal *uv = gco2uv(o); + markvalue(g, uv->v); + if (uv->v == &uv->u.value) /* closed? */ + gray2black(o); /* open upvalues are never black */ + return; + } + case LUA_TFUNCTION: { + gco2cl(o)->c.gclist = g->gray; + g->gray = o; + break; + } + case LUA_TTABLE: { + gco2h(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TTHREAD: { + gco2th(o)->gclist = g->gray; + g->gray = o; + break; + } + case LUA_TPROTO: { + gco2p(o)->gclist = g->gray; + g->gray = o; + break; + } + default: lua_assert(0); + } +} + + +static void marktmu (global_State *g) { + GCObject *u = g->tmudata; + if (u) { + do { + u = u->gch.next; + makewhite(g, u); /* may be marked, if left from previous GC */ + reallymarkobject(g, u); + } while (u != g->tmudata); + } +} + + +/* move `dead' udata that need finalization to list `tmudata' */ +size_t luaC_separateudata (lua_State *L, int all) { + global_State *g = G(L); + size_t deadmem = 0; + GCObject **p = &g->mainthread->next; + GCObject *curr; + while ((curr = *p) != NULL) { + if (!(iswhite(curr) || all) || isfinalized(gco2u(curr))) + p = &curr->gch.next; /* don't bother with them */ + else if (fasttm(L, gco2u(curr)->metatable, TM_GC) == NULL) { + markfinalized(gco2u(curr)); /* don't need finalization */ + p = &curr->gch.next; + } + else { /* must call its gc method */ + deadmem += sizeudata(gco2u(curr)); + markfinalized(gco2u(curr)); + *p = curr->gch.next; + /* link `curr' at the end of `tmudata' list */ + if (g->tmudata == NULL) /* list is empty? */ + g->tmudata = curr->gch.next = curr; /* creates a circular list */ + else { + curr->gch.next = g->tmudata->gch.next; + g->tmudata->gch.next = curr; + g->tmudata = curr; + } + } + } + return deadmem; +} + + +static int traversetable (global_State *g, Table *h) { + int i; + int weakkey = 0; + int weakvalue = 0; + const TValue *mode; + if (h->metatable) + markobject(g, h->metatable); + mode = gfasttm(g, h->metatable, TM_MODE); + if (mode && ttisstring(mode)) { /* is there a weak mode? */ + weakkey = (strchr(svalue(mode), 'k') != NULL); + weakvalue = (strchr(svalue(mode), 'v') != NULL); + if (weakkey || weakvalue) { /* is really weak? */ + h->marked &= ~(KEYWEAK | VALUEWEAK); /* clear bits */ + h->marked |= cast_byte((weakkey << KEYWEAKBIT) | + (weakvalue << VALUEWEAKBIT)); + h->gclist = g->weak; /* must be cleared after GC, ... */ + g->weak = obj2gco(h); /* ... so put in the appropriate list */ + } + } + if (weakkey && weakvalue) return 1; + if (!weakvalue) { + i = h->sizearray; + while (i--) + markvalue(g, &h->array[i]); + } + i = sizenode(h); + while (i--) { + Node *n = gnode(h, i); + lua_assert(ttype(gkey(n)) != LUA_TDEADKEY || ttisnil(gval(n))); + if (ttisnil(gval(n))) + removeentry(n); /* remove empty entries */ + else { + lua_assert(!ttisnil(gkey(n))); + if (!weakkey) markvalue(g, gkey(n)); + if (!weakvalue) markvalue(g, gval(n)); + } + } + return weakkey || weakvalue; +} + + +/* +** All marks are conditional because a GC may happen while the +** prototype is still being created +*/ +static void traverseproto (global_State *g, Proto *f) { + int i; + if (f->source) stringmark(f->source); + for (i=0; isizek; i++) /* mark literals */ + markvalue(g, &f->k[i]); + for (i=0; isizeupvalues; i++) { /* mark upvalue names */ + if (f->upvalues[i]) + stringmark(f->upvalues[i]); + } + for (i=0; isizep; i++) { /* mark nested protos */ + if (f->p[i]) + markobject(g, f->p[i]); + } + for (i=0; isizelocvars; i++) { /* mark local-variable names */ + if (f->locvars[i].varname) + stringmark(f->locvars[i].varname); + } +} + + + +static void traverseclosure (global_State *g, Closure *cl) { + markobject(g, cl->c.env); + if (cl->c.isC) { + int i; + for (i=0; ic.nupvalues; i++) /* mark its upvalues */ + markvalue(g, &cl->c.upvalue[i]); + } + else { + int i; + lua_assert(cl->l.nupvalues == cl->l.p->nups); + markobject(g, cl->l.p); + for (i=0; il.nupvalues; i++) /* mark its upvalues */ + markobject(g, cl->l.upvals[i]); + } +} + + +static void checkstacksizes (lua_State *L, StkId max) { + int ci_used = cast_int(L->ci - L->base_ci); /* number of `ci' in use */ + int s_used = cast_int(max - L->stack); /* part of stack in use */ + if (L->size_ci > LUAI_MAXCALLS) /* handling overflow? */ + return; /* do not touch the stacks */ + if (4*ci_used < L->size_ci && 2*BASIC_CI_SIZE < L->size_ci) + luaD_reallocCI(L, L->size_ci/2); /* still big enough... */ + condhardstacktests(luaD_reallocCI(L, ci_used + 1)); + if (4*s_used < L->stacksize && + 2*(BASIC_STACK_SIZE+EXTRA_STACK) < L->stacksize) + luaD_reallocstack(L, L->stacksize/2); /* still big enough... */ + condhardstacktests(luaD_reallocstack(L, s_used)); +} + + +static void traversestack (global_State *g, lua_State *l) { + StkId o, lim; + CallInfo *ci; + markvalue(g, gt(l)); + lim = l->top; + for (ci = l->base_ci; ci <= l->ci; ci++) { + lua_assert(ci->top <= l->stack_last); + if (lim < ci->top) lim = ci->top; + } + for (o = l->stack; o < l->top; o++) + markvalue(g, o); + for (; o <= lim; o++) + setnilvalue(o); + checkstacksizes(l, lim); +} + + +/* +** traverse one gray object, turning it to black. +** Returns `quantity' traversed. +*/ +static l_mem propagatemark (global_State *g) { + GCObject *o = g->gray; + lua_assert(isgray(o)); + gray2black(o); + switch (o->gch.tt) { + case LUA_TTABLE: { + Table *h = gco2h(o); + g->gray = h->gclist; + if (traversetable(g, h)) /* table is weak? */ + black2gray(o); /* keep it gray */ + return sizeof(Table) + sizeof(TValue) * h->sizearray + + sizeof(Node) * sizenode(h); + } + case LUA_TFUNCTION: { + Closure *cl = gco2cl(o); + g->gray = cl->c.gclist; + traverseclosure(g, cl); + return (cl->c.isC) ? sizeCclosure(cl->c.nupvalues) : + sizeLclosure(cl->l.nupvalues); + } + case LUA_TTHREAD: { + lua_State *th = gco2th(o); + g->gray = th->gclist; + th->gclist = g->grayagain; + g->grayagain = o; + black2gray(o); + traversestack(g, th); + return sizeof(lua_State) + sizeof(TValue) * th->stacksize + + sizeof(CallInfo) * th->size_ci; + } + case LUA_TPROTO: { + Proto *p = gco2p(o); + g->gray = p->gclist; + traverseproto(g, p); + return sizeof(Proto) + sizeof(Instruction) * p->sizecode + + sizeof(Proto *) * p->sizep + + sizeof(TValue) * p->sizek + + sizeof(int) * p->sizelineinfo + + sizeof(LocVar) * p->sizelocvars + + sizeof(TString *) * p->sizeupvalues; + } + default: lua_assert(0); return 0; + } +} + + +static size_t propagateall (global_State *g) { + size_t m = 0; + while (g->gray) m += propagatemark(g); + return m; +} + + +/* +** The next function tells whether a key or value can be cleared from +** a weak table. Non-collectable objects are never removed from weak +** tables. Strings behave as `values', so are never removed too. for +** other objects: if really collected, cannot keep them; for userdata +** being finalized, keep them in keys, but not in values +*/ +static int iscleared (const TValue *o, int iskey) { + if (!iscollectable(o)) return 0; + if (ttisstring(o)) { + stringmark(rawtsvalue(o)); /* strings are `values', so are never weak */ + return 0; + } + return iswhite(gcvalue(o)) || + (ttisuserdata(o) && (!iskey && isfinalized(uvalue(o)))); +} + + +/* +** clear collected entries from weaktables +*/ +static void cleartable (GCObject *l) { + while (l) { + Table *h = gco2h(l); + int i = h->sizearray; + lua_assert(testbit(h->marked, VALUEWEAKBIT) || + testbit(h->marked, KEYWEAKBIT)); + if (testbit(h->marked, VALUEWEAKBIT)) { + while (i--) { + TValue *o = &h->array[i]; + if (iscleared(o, 0)) /* value was collected? */ + setnilvalue(o); /* remove value */ + } + } + i = sizenode(h); + while (i--) { + Node *n = gnode(h, i); + if (!ttisnil(gval(n)) && /* non-empty entry? */ + (iscleared(key2tval(n), 1) || iscleared(gval(n), 0))) { + setnilvalue(gval(n)); /* remove value ... */ + removeentry(n); /* remove entry from table */ + } + } + l = h->gclist; + } +} + + +static void freeobj (lua_State *L, GCObject *o) { + switch (o->gch.tt) { + case LUA_TPROTO: luaF_freeproto(L, gco2p(o)); break; + case LUA_TFUNCTION: luaF_freeclosure(L, gco2cl(o)); break; + case LUA_TUPVAL: luaF_freeupval(L, gco2uv(o)); break; + case LUA_TTABLE: luaH_free(L, gco2h(o)); break; + case LUA_TTHREAD: { + lua_assert(gco2th(o) != L && gco2th(o) != G(L)->mainthread); + luaE_freethread(L, gco2th(o)); + break; + } + case LUA_TSTRING: { + G(L)->strt.nuse--; + luaM_freemem(L, o, sizestring(gco2ts(o))); + break; + } + case LUA_TUSERDATA: { + luaM_freemem(L, o, sizeudata(gco2u(o))); + break; + } + default: lua_assert(0); + } +} + + + +#define sweepwholelist(L,p) sweeplist(L,p,MAX_LUMEM) + + +static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) { + GCObject *curr; + global_State *g = G(L); + int deadmask = otherwhite(g); + while ((curr = *p) != NULL && count-- > 0) { + if (curr->gch.tt == LUA_TTHREAD) /* sweep open upvalues of each thread */ + sweepwholelist(L, &gco2th(curr)->openupval); + if ((curr->gch.marked ^ WHITEBITS) & deadmask) { /* not dead? */ + lua_assert(!isdead(g, curr) || testbit(curr->gch.marked, FIXEDBIT)); + makewhite(g, curr); /* make it white (for next cycle) */ + p = &curr->gch.next; + } + else { /* must erase `curr' */ + lua_assert(isdead(g, curr) || deadmask == bitmask(SFIXEDBIT)); + *p = curr->gch.next; + if (curr == g->rootgc) /* is the first element of the list? */ + g->rootgc = curr->gch.next; /* adjust first */ + freeobj(L, curr); + } + } + return p; +} + + +static void checkSizes (lua_State *L) { + global_State *g = G(L); + /* check size of string hash */ + if (g->strt.nuse < cast(lu_int32, g->strt.size/4) && + g->strt.size > MINSTRTABSIZE*2) + luaS_resize(L, g->strt.size/2); /* table is too big */ + /* check size of buffer */ + if (luaZ_sizebuffer(&g->buff) > LUA_MINBUFFER*2) { /* buffer too big? */ + size_t newsize = luaZ_sizebuffer(&g->buff) / 2; + luaZ_resizebuffer(L, &g->buff, newsize); + } +} + + +static void GCTM (lua_State *L) { + global_State *g = G(L); + GCObject *o = g->tmudata->gch.next; /* get first element */ + Udata *udata = rawgco2u(o); + const TValue *tm; + /* remove udata from `tmudata' */ + if (o == g->tmudata) /* last element? */ + g->tmudata = NULL; + else + g->tmudata->gch.next = udata->uv.next; + udata->uv.next = g->mainthread->next; /* return it to `root' list */ + g->mainthread->next = o; + makewhite(g, o); + tm = fasttm(L, udata->uv.metatable, TM_GC); + if (tm != NULL) { + lu_byte oldah = L->allowhook; + lu_mem oldt = g->GCthreshold; + L->allowhook = 0; /* stop debug hooks during GC tag method */ + g->GCthreshold = 2*g->totalbytes; /* avoid GC steps */ + setobj2s(L, L->top, tm); + setuvalue(L, L->top+1, udata); + L->top += 2; + luaD_call(L, L->top - 2, 0); + L->allowhook = oldah; /* restore hooks */ + g->GCthreshold = oldt; /* restore threshold */ + } +} + + +/* +** Call all GC tag methods +*/ +void luaC_callGCTM (lua_State *L) { + while (G(L)->tmudata) + GCTM(L); +} + + +void luaC_freeall (lua_State *L) { + global_State *g = G(L); + int i; + g->currentwhite = WHITEBITS | bitmask(SFIXEDBIT); /* mask to collect all elements */ + sweepwholelist(L, &g->rootgc); + for (i = 0; i < g->strt.size; i++) /* free all string lists */ + sweepwholelist(L, &g->strt.hash[i]); +} + + +static void markmt (global_State *g) { + int i; + for (i=0; imt[i]) markobject(g, g->mt[i]); +} + + +/* mark root set */ +static void markroot (lua_State *L) { + global_State *g = G(L); + g->gray = NULL; + g->grayagain = NULL; + g->weak = NULL; + markobject(g, g->mainthread); + /* make global table be traversed before main stack */ + markvalue(g, gt(g->mainthread)); + markvalue(g, registry(L)); + markmt(g); + g->gcstate = GCSpropagate; +} + + +static void remarkupvals (global_State *g) { + UpVal *uv; + for (uv = g->uvhead.u.l.next; uv != &g->uvhead; uv = uv->u.l.next) { + lua_assert(uv->u.l.next->u.l.prev == uv && uv->u.l.prev->u.l.next == uv); + if (isgray(obj2gco(uv))) + markvalue(g, uv->v); + } +} + + +static void atomic (lua_State *L) { + global_State *g = G(L); + size_t udsize; /* total size of userdata to be finalized */ + /* remark occasional upvalues of (maybe) dead threads */ + remarkupvals(g); + /* traverse objects cautch by write barrier and by 'remarkupvals' */ + propagateall(g); + /* remark weak tables */ + g->gray = g->weak; + g->weak = NULL; + lua_assert(!iswhite(obj2gco(g->mainthread))); + markobject(g, L); /* mark running thread */ + markmt(g); /* mark basic metatables (again) */ + propagateall(g); + /* remark gray again */ + g->gray = g->grayagain; + g->grayagain = NULL; + propagateall(g); + udsize = luaC_separateudata(L, 0); /* separate userdata to be finalized */ + marktmu(g); /* mark `preserved' userdata */ + udsize += propagateall(g); /* remark, to propagate `preserveness' */ + cleartable(g->weak); /* remove collected objects from weak tables */ + /* flip current white */ + g->currentwhite = cast_byte(otherwhite(g)); + g->sweepstrgc = 0; + g->sweepgc = &g->rootgc; + g->gcstate = GCSsweepstring; + g->estimate = g->totalbytes - udsize; /* first estimate */ +} + + +static l_mem singlestep (lua_State *L) { + global_State *g = G(L); + /*lua_checkmemory(L);*/ + switch (g->gcstate) { + case GCSpause: { + markroot(L); /* start a new collection */ + return 0; + } + case GCSpropagate: { + if (g->gray) + return propagatemark(g); + else { /* no more `gray' objects */ + atomic(L); /* finish mark phase */ + return 0; + } + } + case GCSsweepstring: { + lu_mem old = g->totalbytes; + sweepwholelist(L, &g->strt.hash[g->sweepstrgc++]); + if (g->sweepstrgc >= g->strt.size) /* nothing more to sweep? */ + g->gcstate = GCSsweep; /* end sweep-string phase */ + lua_assert(old >= g->totalbytes); + g->estimate -= old - g->totalbytes; + return GCSWEEPCOST; + } + case GCSsweep: { + lu_mem old = g->totalbytes; + g->sweepgc = sweeplist(L, g->sweepgc, GCSWEEPMAX); + if (*g->sweepgc == NULL) { /* nothing more to sweep? */ + checkSizes(L); + g->gcstate = GCSfinalize; /* end sweep phase */ + } + lua_assert(old >= g->totalbytes); + g->estimate -= old - g->totalbytes; + return GCSWEEPMAX*GCSWEEPCOST; + } + case GCSfinalize: { + if (g->tmudata) { + GCTM(L); + if (g->estimate > GCFINALIZECOST) + g->estimate -= GCFINALIZECOST; + return GCFINALIZECOST; + } + else { + g->gcstate = GCSpause; /* end collection */ + g->gcdept = 0; + return 0; + } + } + default: lua_assert(0); return 0; + } +} + + +void luaC_step (lua_State *L) { + global_State *g = G(L); + l_mem lim = (GCSTEPSIZE/100) * g->gcstepmul; + if (lim == 0) + lim = (MAX_LUMEM-1)/2; /* no limit */ + g->gcdept += g->totalbytes - g->GCthreshold; + do { + lim -= singlestep(L); + if (g->gcstate == GCSpause) + break; + } while (lim > 0); + if (g->gcstate != GCSpause) { + if (g->gcdept < GCSTEPSIZE) + g->GCthreshold = g->totalbytes + GCSTEPSIZE; /* - lim/g->gcstepmul;*/ + else { + g->gcdept -= GCSTEPSIZE; + g->GCthreshold = g->totalbytes; + } + } + else { + setthreshold(g); + } +} + + +void luaC_fullgc (lua_State *L) { + global_State *g = G(L); + if (g->gcstate <= GCSpropagate) { + /* reset sweep marks to sweep all elements (returning them to white) */ + g->sweepstrgc = 0; + g->sweepgc = &g->rootgc; + /* reset other collector lists */ + g->gray = NULL; + g->grayagain = NULL; + g->weak = NULL; + g->gcstate = GCSsweepstring; + } + lua_assert(g->gcstate != GCSpause && g->gcstate != GCSpropagate); + /* finish any pending sweep phase */ + while (g->gcstate != GCSfinalize) { + lua_assert(g->gcstate == GCSsweepstring || g->gcstate == GCSsweep); + singlestep(L); + } + markroot(L); + while (g->gcstate != GCSpause) { + singlestep(L); + } + setthreshold(g); +} + + +void luaC_barrierf (lua_State *L, GCObject *o, GCObject *v) { + global_State *g = G(L); + lua_assert(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o)); + lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); + lua_assert(ttype(&o->gch) != LUA_TTABLE); + /* must keep invariant? */ + if (g->gcstate == GCSpropagate) + reallymarkobject(g, v); /* restore invariant */ + else /* don't mind */ + makewhite(g, o); /* mark as white just to avoid other barriers */ +} + + +void luaC_barrierback (lua_State *L, Table *t) { + global_State *g = G(L); + GCObject *o = obj2gco(t); + lua_assert(isblack(o) && !isdead(g, o)); + lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); + black2gray(o); /* make table gray (again) */ + t->gclist = g->grayagain; + g->grayagain = o; +} + + +void luaC_link (lua_State *L, GCObject *o, lu_byte tt) { + global_State *g = G(L); + o->gch.next = g->rootgc; + g->rootgc = o; + o->gch.marked = luaC_white(g); + o->gch.tt = tt; +} + + +void luaC_linkupval (lua_State *L, UpVal *uv) { + global_State *g = G(L); + GCObject *o = obj2gco(uv); + o->gch.next = g->rootgc; /* link upvalue into `rootgc' list */ + g->rootgc = o; + if (isgray(o)) { + if (g->gcstate == GCSpropagate) { + gray2black(o); /* closed upvalues need barrier */ + luaC_barrier(L, uv, uv->v); + } + else { /* sweep phase: sweep it (turning it into white) */ + makewhite(g, o); + lua_assert(g->gcstate != GCSfinalize && g->gcstate != GCSpause); + } + } +} + diff --git a/extern/lua-5.1.5/src/lgc.h b/extern/lua-5.1.5/src/lgc.h new file mode 100644 index 00000000..5a8dc605 --- /dev/null +++ b/extern/lua-5.1.5/src/lgc.h @@ -0,0 +1,110 @@ +/* +** $Id: lgc.h,v 2.15.1.1 2007/12/27 13:02:25 roberto Exp $ +** Garbage Collector +** See Copyright Notice in lua.h +*/ + +#ifndef lgc_h +#define lgc_h + + +#include "lobject.h" + + +/* +** Possible states of the Garbage Collector +*/ +#define GCSpause 0 +#define GCSpropagate 1 +#define GCSsweepstring 2 +#define GCSsweep 3 +#define GCSfinalize 4 + + +/* +** some userful bit tricks +*/ +#define resetbits(x,m) ((x) &= cast(lu_byte, ~(m))) +#define setbits(x,m) ((x) |= (m)) +#define testbits(x,m) ((x) & (m)) +#define bitmask(b) (1<<(b)) +#define bit2mask(b1,b2) (bitmask(b1) | bitmask(b2)) +#define l_setbit(x,b) setbits(x, bitmask(b)) +#define resetbit(x,b) resetbits(x, bitmask(b)) +#define testbit(x,b) testbits(x, bitmask(b)) +#define set2bits(x,b1,b2) setbits(x, (bit2mask(b1, b2))) +#define reset2bits(x,b1,b2) resetbits(x, (bit2mask(b1, b2))) +#define test2bits(x,b1,b2) testbits(x, (bit2mask(b1, b2))) + + + +/* +** Layout for bit use in `marked' field: +** bit 0 - object is white (type 0) +** bit 1 - object is white (type 1) +** bit 2 - object is black +** bit 3 - for userdata: has been finalized +** bit 3 - for tables: has weak keys +** bit 4 - for tables: has weak values +** bit 5 - object is fixed (should not be collected) +** bit 6 - object is "super" fixed (only the main thread) +*/ + + +#define WHITE0BIT 0 +#define WHITE1BIT 1 +#define BLACKBIT 2 +#define FINALIZEDBIT 3 +#define KEYWEAKBIT 3 +#define VALUEWEAKBIT 4 +#define FIXEDBIT 5 +#define SFIXEDBIT 6 +#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT) + + +#define iswhite(x) test2bits((x)->gch.marked, WHITE0BIT, WHITE1BIT) +#define isblack(x) testbit((x)->gch.marked, BLACKBIT) +#define isgray(x) (!isblack(x) && !iswhite(x)) + +#define otherwhite(g) (g->currentwhite ^ WHITEBITS) +#define isdead(g,v) ((v)->gch.marked & otherwhite(g) & WHITEBITS) + +#define changewhite(x) ((x)->gch.marked ^= WHITEBITS) +#define gray2black(x) l_setbit((x)->gch.marked, BLACKBIT) + +#define valiswhite(x) (iscollectable(x) && iswhite(gcvalue(x))) + +#define luaC_white(g) cast(lu_byte, (g)->currentwhite & WHITEBITS) + + +#define luaC_checkGC(L) { \ + condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK - 1)); \ + if (G(L)->totalbytes >= G(L)->GCthreshold) \ + luaC_step(L); } + + +#define luaC_barrier(L,p,v) { if (valiswhite(v) && isblack(obj2gco(p))) \ + luaC_barrierf(L,obj2gco(p),gcvalue(v)); } + +#define luaC_barriert(L,t,v) { if (valiswhite(v) && isblack(obj2gco(t))) \ + luaC_barrierback(L,t); } + +#define luaC_objbarrier(L,p,o) \ + { if (iswhite(obj2gco(o)) && isblack(obj2gco(p))) \ + luaC_barrierf(L,obj2gco(p),obj2gco(o)); } + +#define luaC_objbarriert(L,t,o) \ + { if (iswhite(obj2gco(o)) && isblack(obj2gco(t))) luaC_barrierback(L,t); } + +LUAI_FUNC size_t luaC_separateudata (lua_State *L, int all); +LUAI_FUNC void luaC_callGCTM (lua_State *L); +LUAI_FUNC void luaC_freeall (lua_State *L); +LUAI_FUNC void luaC_step (lua_State *L); +LUAI_FUNC void luaC_fullgc (lua_State *L); +LUAI_FUNC void luaC_link (lua_State *L, GCObject *o, lu_byte tt); +LUAI_FUNC void luaC_linkupval (lua_State *L, UpVal *uv); +LUAI_FUNC void luaC_barrierf (lua_State *L, GCObject *o, GCObject *v); +LUAI_FUNC void luaC_barrierback (lua_State *L, Table *t); + + +#endif diff --git a/extern/lua-5.1.5/src/linit.c b/extern/lua-5.1.5/src/linit.c new file mode 100644 index 00000000..c1f90dfa --- /dev/null +++ b/extern/lua-5.1.5/src/linit.c @@ -0,0 +1,38 @@ +/* +** $Id: linit.c,v 1.14.1.1 2007/12/27 13:02:25 roberto Exp $ +** Initialization of libraries for lua.c +** See Copyright Notice in lua.h +*/ + + +#define linit_c +#define LUA_LIB + +#include "lua.h" + +#include "lualib.h" +#include "lauxlib.h" + + +static const luaL_Reg lualibs[] = { + {"", luaopen_base}, + {LUA_LOADLIBNAME, luaopen_package}, + {LUA_TABLIBNAME, luaopen_table}, + {LUA_IOLIBNAME, luaopen_io}, + {LUA_OSLIBNAME, luaopen_os}, + {LUA_STRLIBNAME, luaopen_string}, + {LUA_MATHLIBNAME, luaopen_math}, + {LUA_DBLIBNAME, luaopen_debug}, + {NULL, NULL} +}; + + +LUALIB_API void luaL_openlibs (lua_State *L) { + const luaL_Reg *lib = lualibs; + for (; lib->func; lib++) { + lua_pushcfunction(L, lib->func); + lua_pushstring(L, lib->name); + lua_call(L, 1, 0); + } +} + diff --git a/extern/lua-5.1.5/src/liolib.c b/extern/lua-5.1.5/src/liolib.c new file mode 100644 index 00000000..649f9a59 --- /dev/null +++ b/extern/lua-5.1.5/src/liolib.c @@ -0,0 +1,556 @@ +/* +** $Id: liolib.c,v 2.73.1.4 2010/05/14 15:33:51 roberto Exp $ +** Standard I/O (and system) library +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include +#include + +#define liolib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + + +#define IO_INPUT 1 +#define IO_OUTPUT 2 + + +static const char *const fnames[] = {"input", "output"}; + + +static int pushresult (lua_State *L, int i, const char *filename) { + int en = errno; /* calls to Lua API may change this value */ + if (i) { + lua_pushboolean(L, 1); + return 1; + } + else { + lua_pushnil(L); + if (filename) + lua_pushfstring(L, "%s: %s", filename, strerror(en)); + else + lua_pushfstring(L, "%s", strerror(en)); + lua_pushinteger(L, en); + return 3; + } +} + + +static void fileerror (lua_State *L, int arg, const char *filename) { + lua_pushfstring(L, "%s: %s", filename, strerror(errno)); + luaL_argerror(L, arg, lua_tostring(L, -1)); +} + + +#define tofilep(L) ((FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE)) + + +static int io_type (lua_State *L) { + void *ud; + luaL_checkany(L, 1); + ud = lua_touserdata(L, 1); + lua_getfield(L, LUA_REGISTRYINDEX, LUA_FILEHANDLE); + if (ud == NULL || !lua_getmetatable(L, 1) || !lua_rawequal(L, -2, -1)) + lua_pushnil(L); /* not a file */ + else if (*((FILE **)ud) == NULL) + lua_pushliteral(L, "closed file"); + else + lua_pushliteral(L, "file"); + return 1; +} + + +static FILE *tofile (lua_State *L) { + FILE **f = tofilep(L); + if (*f == NULL) + luaL_error(L, "attempt to use a closed file"); + return *f; +} + + + +/* +** When creating file handles, always creates a `closed' file handle +** before opening the actual file; so, if there is a memory error, the +** file is not left opened. +*/ +static FILE **newfile (lua_State *L) { + FILE **pf = (FILE **)lua_newuserdata(L, sizeof(FILE *)); + *pf = NULL; /* file handle is currently `closed' */ + luaL_getmetatable(L, LUA_FILEHANDLE); + lua_setmetatable(L, -2); + return pf; +} + + +/* +** function to (not) close the standard files stdin, stdout, and stderr +*/ +static int io_noclose (lua_State *L) { + lua_pushnil(L); + lua_pushliteral(L, "cannot close standard file"); + return 2; +} + + +/* +** function to close 'popen' files +*/ +static int io_pclose (lua_State *L) { + FILE **p = tofilep(L); + int ok = lua_pclose(L, *p); + *p = NULL; + return pushresult(L, ok, NULL); +} + + +/* +** function to close regular files +*/ +static int io_fclose (lua_State *L) { + FILE **p = tofilep(L); + int ok = (fclose(*p) == 0); + *p = NULL; + return pushresult(L, ok, NULL); +} + + +static int aux_close (lua_State *L) { + lua_getfenv(L, 1); + lua_getfield(L, -1, "__close"); + return (lua_tocfunction(L, -1))(L); +} + + +static int io_close (lua_State *L) { + if (lua_isnone(L, 1)) + lua_rawgeti(L, LUA_ENVIRONINDEX, IO_OUTPUT); + tofile(L); /* make sure argument is a file */ + return aux_close(L); +} + + +static int io_gc (lua_State *L) { + FILE *f = *tofilep(L); + /* ignore closed files */ + if (f != NULL) + aux_close(L); + return 0; +} + + +static int io_tostring (lua_State *L) { + FILE *f = *tofilep(L); + if (f == NULL) + lua_pushliteral(L, "file (closed)"); + else + lua_pushfstring(L, "file (%p)", f); + return 1; +} + + +static int io_open (lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + const char *mode = luaL_optstring(L, 2, "r"); + FILE **pf = newfile(L); + *pf = fopen(filename, mode); + return (*pf == NULL) ? pushresult(L, 0, filename) : 1; +} + + +/* +** this function has a separated environment, which defines the +** correct __close for 'popen' files +*/ +static int io_popen (lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + const char *mode = luaL_optstring(L, 2, "r"); + FILE **pf = newfile(L); + *pf = lua_popen(L, filename, mode); + return (*pf == NULL) ? pushresult(L, 0, filename) : 1; +} + + +static int io_tmpfile (lua_State *L) { + FILE **pf = newfile(L); + *pf = tmpfile(); + return (*pf == NULL) ? pushresult(L, 0, NULL) : 1; +} + + +static FILE *getiofile (lua_State *L, int findex) { + FILE *f; + lua_rawgeti(L, LUA_ENVIRONINDEX, findex); + f = *(FILE **)lua_touserdata(L, -1); + if (f == NULL) + luaL_error(L, "standard %s file is closed", fnames[findex - 1]); + return f; +} + + +static int g_iofile (lua_State *L, int f, const char *mode) { + if (!lua_isnoneornil(L, 1)) { + const char *filename = lua_tostring(L, 1); + if (filename) { + FILE **pf = newfile(L); + *pf = fopen(filename, mode); + if (*pf == NULL) + fileerror(L, 1, filename); + } + else { + tofile(L); /* check that it's a valid file handle */ + lua_pushvalue(L, 1); + } + lua_rawseti(L, LUA_ENVIRONINDEX, f); + } + /* return current value */ + lua_rawgeti(L, LUA_ENVIRONINDEX, f); + return 1; +} + + +static int io_input (lua_State *L) { + return g_iofile(L, IO_INPUT, "r"); +} + + +static int io_output (lua_State *L) { + return g_iofile(L, IO_OUTPUT, "w"); +} + + +static int io_readline (lua_State *L); + + +static void aux_lines (lua_State *L, int idx, int toclose) { + lua_pushvalue(L, idx); + lua_pushboolean(L, toclose); /* close/not close file when finished */ + lua_pushcclosure(L, io_readline, 2); +} + + +static int f_lines (lua_State *L) { + tofile(L); /* check that it's a valid file handle */ + aux_lines(L, 1, 0); + return 1; +} + + +static int io_lines (lua_State *L) { + if (lua_isnoneornil(L, 1)) { /* no arguments? */ + /* will iterate over default input */ + lua_rawgeti(L, LUA_ENVIRONINDEX, IO_INPUT); + return f_lines(L); + } + else { + const char *filename = luaL_checkstring(L, 1); + FILE **pf = newfile(L); + *pf = fopen(filename, "r"); + if (*pf == NULL) + fileerror(L, 1, filename); + aux_lines(L, lua_gettop(L), 1); + return 1; + } +} + + +/* +** {====================================================== +** READ +** ======================================================= +*/ + + +static int read_number (lua_State *L, FILE *f) { + lua_Number d; + if (fscanf(f, LUA_NUMBER_SCAN, &d) == 1) { + lua_pushnumber(L, d); + return 1; + } + else { + lua_pushnil(L); /* "result" to be removed */ + return 0; /* read fails */ + } +} + + +static int test_eof (lua_State *L, FILE *f) { + int c = getc(f); + ungetc(c, f); + lua_pushlstring(L, NULL, 0); + return (c != EOF); +} + + +static int read_line (lua_State *L, FILE *f) { + luaL_Buffer b; + luaL_buffinit(L, &b); + for (;;) { + size_t l; + char *p = luaL_prepbuffer(&b); + if (fgets(p, LUAL_BUFFERSIZE, f) == NULL) { /* eof? */ + luaL_pushresult(&b); /* close buffer */ + return (lua_objlen(L, -1) > 0); /* check whether read something */ + } + l = strlen(p); + if (l == 0 || p[l-1] != '\n') + luaL_addsize(&b, l); + else { + luaL_addsize(&b, l - 1); /* do not include `eol' */ + luaL_pushresult(&b); /* close buffer */ + return 1; /* read at least an `eol' */ + } + } +} + + +static int read_chars (lua_State *L, FILE *f, size_t n) { + size_t rlen; /* how much to read */ + size_t nr; /* number of chars actually read */ + luaL_Buffer b; + luaL_buffinit(L, &b); + rlen = LUAL_BUFFERSIZE; /* try to read that much each time */ + do { + char *p = luaL_prepbuffer(&b); + if (rlen > n) rlen = n; /* cannot read more than asked */ + nr = fread(p, sizeof(char), rlen, f); + luaL_addsize(&b, nr); + n -= nr; /* still have to read `n' chars */ + } while (n > 0 && nr == rlen); /* until end of count or eof */ + luaL_pushresult(&b); /* close buffer */ + return (n == 0 || lua_objlen(L, -1) > 0); +} + + +static int g_read (lua_State *L, FILE *f, int first) { + int nargs = lua_gettop(L) - 1; + int success; + int n; + clearerr(f); + if (nargs == 0) { /* no arguments? */ + success = read_line(L, f); + n = first+1; /* to return 1 result */ + } + else { /* ensure stack space for all results and for auxlib's buffer */ + luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments"); + success = 1; + for (n = first; nargs-- && success; n++) { + if (lua_type(L, n) == LUA_TNUMBER) { + size_t l = (size_t)lua_tointeger(L, n); + success = (l == 0) ? test_eof(L, f) : read_chars(L, f, l); + } + else { + const char *p = lua_tostring(L, n); + luaL_argcheck(L, p && p[0] == '*', n, "invalid option"); + switch (p[1]) { + case 'n': /* number */ + success = read_number(L, f); + break; + case 'l': /* line */ + success = read_line(L, f); + break; + case 'a': /* file */ + read_chars(L, f, ~((size_t)0)); /* read MAX_SIZE_T chars */ + success = 1; /* always success */ + break; + default: + return luaL_argerror(L, n, "invalid format"); + } + } + } + } + if (ferror(f)) + return pushresult(L, 0, NULL); + if (!success) { + lua_pop(L, 1); /* remove last result */ + lua_pushnil(L); /* push nil instead */ + } + return n - first; +} + + +static int io_read (lua_State *L) { + return g_read(L, getiofile(L, IO_INPUT), 1); +} + + +static int f_read (lua_State *L) { + return g_read(L, tofile(L), 2); +} + + +static int io_readline (lua_State *L) { + FILE *f = *(FILE **)lua_touserdata(L, lua_upvalueindex(1)); + int sucess; + if (f == NULL) /* file is already closed? */ + luaL_error(L, "file is already closed"); + sucess = read_line(L, f); + if (ferror(f)) + return luaL_error(L, "%s", strerror(errno)); + if (sucess) return 1; + else { /* EOF */ + if (lua_toboolean(L, lua_upvalueindex(2))) { /* generator created file? */ + lua_settop(L, 0); + lua_pushvalue(L, lua_upvalueindex(1)); + aux_close(L); /* close it */ + } + return 0; + } +} + +/* }====================================================== */ + + +static int g_write (lua_State *L, FILE *f, int arg) { + int nargs = lua_gettop(L) - 1; + int status = 1; + for (; nargs--; arg++) { + if (lua_type(L, arg) == LUA_TNUMBER) { + /* optimization: could be done exactly as for strings */ + status = status && + fprintf(f, LUA_NUMBER_FMT, lua_tonumber(L, arg)) > 0; + } + else { + size_t l; + const char *s = luaL_checklstring(L, arg, &l); + status = status && (fwrite(s, sizeof(char), l, f) == l); + } + } + return pushresult(L, status, NULL); +} + + +static int io_write (lua_State *L) { + return g_write(L, getiofile(L, IO_OUTPUT), 1); +} + + +static int f_write (lua_State *L) { + return g_write(L, tofile(L), 2); +} + + +static int f_seek (lua_State *L) { + static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END}; + static const char *const modenames[] = {"set", "cur", "end", NULL}; + FILE *f = tofile(L); + int op = luaL_checkoption(L, 2, "cur", modenames); + long offset = luaL_optlong(L, 3, 0); + op = fseek(f, offset, mode[op]); + if (op) + return pushresult(L, 0, NULL); /* error */ + else { + lua_pushinteger(L, ftell(f)); + return 1; + } +} + + +static int f_setvbuf (lua_State *L) { + static const int mode[] = {_IONBF, _IOFBF, _IOLBF}; + static const char *const modenames[] = {"no", "full", "line", NULL}; + FILE *f = tofile(L); + int op = luaL_checkoption(L, 2, NULL, modenames); + lua_Integer sz = luaL_optinteger(L, 3, LUAL_BUFFERSIZE); + int res = setvbuf(f, NULL, mode[op], sz); + return pushresult(L, res == 0, NULL); +} + + + +static int io_flush (lua_State *L) { + return pushresult(L, fflush(getiofile(L, IO_OUTPUT)) == 0, NULL); +} + + +static int f_flush (lua_State *L) { + return pushresult(L, fflush(tofile(L)) == 0, NULL); +} + + +static const luaL_Reg iolib[] = { + {"close", io_close}, + {"flush", io_flush}, + {"input", io_input}, + {"lines", io_lines}, + {"open", io_open}, + {"output", io_output}, + {"popen", io_popen}, + {"read", io_read}, + {"tmpfile", io_tmpfile}, + {"type", io_type}, + {"write", io_write}, + {NULL, NULL} +}; + + +static const luaL_Reg flib[] = { + {"close", io_close}, + {"flush", f_flush}, + {"lines", f_lines}, + {"read", f_read}, + {"seek", f_seek}, + {"setvbuf", f_setvbuf}, + {"write", f_write}, + {"__gc", io_gc}, + {"__tostring", io_tostring}, + {NULL, NULL} +}; + + +static void createmeta (lua_State *L) { + luaL_newmetatable(L, LUA_FILEHANDLE); /* create metatable for file handles */ + lua_pushvalue(L, -1); /* push metatable */ + lua_setfield(L, -2, "__index"); /* metatable.__index = metatable */ + luaL_register(L, NULL, flib); /* file methods */ +} + + +static void createstdfile (lua_State *L, FILE *f, int k, const char *fname) { + *newfile(L) = f; + if (k > 0) { + lua_pushvalue(L, -1); + lua_rawseti(L, LUA_ENVIRONINDEX, k); + } + lua_pushvalue(L, -2); /* copy environment */ + lua_setfenv(L, -2); /* set it */ + lua_setfield(L, -3, fname); +} + + +static void newfenv (lua_State *L, lua_CFunction cls) { + lua_createtable(L, 0, 1); + lua_pushcfunction(L, cls); + lua_setfield(L, -2, "__close"); +} + + +LUALIB_API int luaopen_io (lua_State *L) { + createmeta(L); + /* create (private) environment (with fields IO_INPUT, IO_OUTPUT, __close) */ + newfenv(L, io_fclose); + lua_replace(L, LUA_ENVIRONINDEX); + /* open library */ + luaL_register(L, LUA_IOLIBNAME, iolib); + /* create (and set) default files */ + newfenv(L, io_noclose); /* close function for default files */ + createstdfile(L, stdin, IO_INPUT, "stdin"); + createstdfile(L, stdout, IO_OUTPUT, "stdout"); + createstdfile(L, stderr, 0, "stderr"); + lua_pop(L, 1); /* pop environment for default files */ + lua_getfield(L, -1, "popen"); + newfenv(L, io_pclose); /* create environment for 'popen' */ + lua_setfenv(L, -2); /* set fenv for 'popen' */ + lua_pop(L, 1); /* pop 'popen' */ + return 1; +} + diff --git a/extern/lua-5.1.5/src/llex.c b/extern/lua-5.1.5/src/llex.c new file mode 100644 index 00000000..88c6790c --- /dev/null +++ b/extern/lua-5.1.5/src/llex.c @@ -0,0 +1,463 @@ +/* +** $Id: llex.c,v 2.20.1.2 2009/11/23 14:58:22 roberto Exp $ +** Lexical Analyzer +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include + +#define llex_c +#define LUA_CORE + +#include "lua.h" + +#include "ldo.h" +#include "llex.h" +#include "lobject.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lzio.h" + + + +#define next(ls) (ls->current = zgetc(ls->z)) + + + + +#define currIsNewline(ls) (ls->current == '\n' || ls->current == '\r') + + +/* ORDER RESERVED */ +const char *const luaX_tokens [] = { + "and", "break", "do", "else", "elseif", + "end", "false", "for", "function", "if", + "in", "local", "nil", "not", "or", "repeat", + "return", "then", "true", "until", "while", + "..", "...", "==", ">=", "<=", "~=", + "", "", "", "", + NULL +}; + + +#define save_and_next(ls) (save(ls, ls->current), next(ls)) + + +static void save (LexState *ls, int c) { + Mbuffer *b = ls->buff; + if (b->n + 1 > b->buffsize) { + size_t newsize; + if (b->buffsize >= MAX_SIZET/2) + luaX_lexerror(ls, "lexical element too long", 0); + newsize = b->buffsize * 2; + luaZ_resizebuffer(ls->L, b, newsize); + } + b->buffer[b->n++] = cast(char, c); +} + + +void luaX_init (lua_State *L) { + int i; + for (i=0; itsv.reserved = cast_byte(i+1); /* reserved word */ + } +} + + +#define MAXSRC 80 + + +const char *luaX_token2str (LexState *ls, int token) { + if (token < FIRST_RESERVED) { + lua_assert(token == cast(unsigned char, token)); + return (iscntrl(token)) ? luaO_pushfstring(ls->L, "char(%d)", token) : + luaO_pushfstring(ls->L, "%c", token); + } + else + return luaX_tokens[token-FIRST_RESERVED]; +} + + +static const char *txtToken (LexState *ls, int token) { + switch (token) { + case TK_NAME: + case TK_STRING: + case TK_NUMBER: + save(ls, '\0'); + return luaZ_buffer(ls->buff); + default: + return luaX_token2str(ls, token); + } +} + + +void luaX_lexerror (LexState *ls, const char *msg, int token) { + char buff[MAXSRC]; + luaO_chunkid(buff, getstr(ls->source), MAXSRC); + msg = luaO_pushfstring(ls->L, "%s:%d: %s", buff, ls->linenumber, msg); + if (token) + luaO_pushfstring(ls->L, "%s near " LUA_QS, msg, txtToken(ls, token)); + luaD_throw(ls->L, LUA_ERRSYNTAX); +} + + +void luaX_syntaxerror (LexState *ls, const char *msg) { + luaX_lexerror(ls, msg, ls->t.token); +} + + +TString *luaX_newstring (LexState *ls, const char *str, size_t l) { + lua_State *L = ls->L; + TString *ts = luaS_newlstr(L, str, l); + TValue *o = luaH_setstr(L, ls->fs->h, ts); /* entry for `str' */ + if (ttisnil(o)) { + setbvalue(o, 1); /* make sure `str' will not be collected */ + luaC_checkGC(L); + } + return ts; +} + + +static void inclinenumber (LexState *ls) { + int old = ls->current; + lua_assert(currIsNewline(ls)); + next(ls); /* skip `\n' or `\r' */ + if (currIsNewline(ls) && ls->current != old) + next(ls); /* skip `\n\r' or `\r\n' */ + if (++ls->linenumber >= MAX_INT) + luaX_syntaxerror(ls, "chunk has too many lines"); +} + + +void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source) { + ls->decpoint = '.'; + ls->L = L; + ls->lookahead.token = TK_EOS; /* no look-ahead token */ + ls->z = z; + ls->fs = NULL; + ls->linenumber = 1; + ls->lastline = 1; + ls->source = source; + luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER); /* initialize buffer */ + next(ls); /* read first char */ +} + + + +/* +** ======================================================= +** LEXICAL ANALYZER +** ======================================================= +*/ + + + +static int check_next (LexState *ls, const char *set) { + if (!strchr(set, ls->current)) + return 0; + save_and_next(ls); + return 1; +} + + +static void buffreplace (LexState *ls, char from, char to) { + size_t n = luaZ_bufflen(ls->buff); + char *p = luaZ_buffer(ls->buff); + while (n--) + if (p[n] == from) p[n] = to; +} + + +static void trydecpoint (LexState *ls, SemInfo *seminfo) { + /* format error: try to update decimal point separator */ + struct lconv *cv = localeconv(); + char old = ls->decpoint; + ls->decpoint = (cv ? cv->decimal_point[0] : '.'); + buffreplace(ls, old, ls->decpoint); /* try updated decimal separator */ + if (!luaO_str2d(luaZ_buffer(ls->buff), &seminfo->r)) { + /* format error with correct decimal point: no more options */ + buffreplace(ls, ls->decpoint, '.'); /* undo change (for error message) */ + luaX_lexerror(ls, "malformed number", TK_NUMBER); + } +} + + +/* LUA_NUMBER */ +static void read_numeral (LexState *ls, SemInfo *seminfo) { + lua_assert(isdigit(ls->current)); + do { + save_and_next(ls); + } while (isdigit(ls->current) || ls->current == '.'); + if (check_next(ls, "Ee")) /* `E'? */ + check_next(ls, "+-"); /* optional exponent sign */ + while (isalnum(ls->current) || ls->current == '_') + save_and_next(ls); + save(ls, '\0'); + buffreplace(ls, '.', ls->decpoint); /* follow locale for decimal point */ + if (!luaO_str2d(luaZ_buffer(ls->buff), &seminfo->r)) /* format error? */ + trydecpoint(ls, seminfo); /* try to update decimal point separator */ +} + + +static int skip_sep (LexState *ls) { + int count = 0; + int s = ls->current; + lua_assert(s == '[' || s == ']'); + save_and_next(ls); + while (ls->current == '=') { + save_and_next(ls); + count++; + } + return (ls->current == s) ? count : (-count) - 1; +} + + +static void read_long_string (LexState *ls, SemInfo *seminfo, int sep) { + int cont = 0; + (void)(cont); /* avoid warnings when `cont' is not used */ + save_and_next(ls); /* skip 2nd `[' */ + if (currIsNewline(ls)) /* string starts with a newline? */ + inclinenumber(ls); /* skip it */ + for (;;) { + switch (ls->current) { + case EOZ: + luaX_lexerror(ls, (seminfo) ? "unfinished long string" : + "unfinished long comment", TK_EOS); + break; /* to avoid warnings */ +#if defined(LUA_COMPAT_LSTR) + case '[': { + if (skip_sep(ls) == sep) { + save_and_next(ls); /* skip 2nd `[' */ + cont++; +#if LUA_COMPAT_LSTR == 1 + if (sep == 0) + luaX_lexerror(ls, "nesting of [[...]] is deprecated", '['); +#endif + } + break; + } +#endif + case ']': { + if (skip_sep(ls) == sep) { + save_and_next(ls); /* skip 2nd `]' */ +#if defined(LUA_COMPAT_LSTR) && LUA_COMPAT_LSTR == 2 + cont--; + if (sep == 0 && cont >= 0) break; +#endif + goto endloop; + } + break; + } + case '\n': + case '\r': { + save(ls, '\n'); + inclinenumber(ls); + if (!seminfo) luaZ_resetbuffer(ls->buff); /* avoid wasting space */ + break; + } + default: { + if (seminfo) save_and_next(ls); + else next(ls); + } + } + } endloop: + if (seminfo) + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + (2 + sep), + luaZ_bufflen(ls->buff) - 2*(2 + sep)); +} + + +static void read_string (LexState *ls, int del, SemInfo *seminfo) { + save_and_next(ls); + while (ls->current != del) { + switch (ls->current) { + case EOZ: + luaX_lexerror(ls, "unfinished string", TK_EOS); + continue; /* to avoid warnings */ + case '\n': + case '\r': + luaX_lexerror(ls, "unfinished string", TK_STRING); + continue; /* to avoid warnings */ + case '\\': { + int c; + next(ls); /* do not save the `\' */ + switch (ls->current) { + case 'a': c = '\a'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'v': c = '\v'; break; + case '\n': /* go through */ + case '\r': save(ls, '\n'); inclinenumber(ls); continue; + case EOZ: continue; /* will raise an error next loop */ + default: { + if (!isdigit(ls->current)) + save_and_next(ls); /* handles \\, \", \', and \? */ + else { /* \xxx */ + int i = 0; + c = 0; + do { + c = 10*c + (ls->current-'0'); + next(ls); + } while (++i<3 && isdigit(ls->current)); + if (c > UCHAR_MAX) + luaX_lexerror(ls, "escape sequence too large", TK_STRING); + save(ls, c); + } + continue; + } + } + save(ls, c); + next(ls); + continue; + } + default: + save_and_next(ls); + } + } + save_and_next(ls); /* skip delimiter */ + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1, + luaZ_bufflen(ls->buff) - 2); +} + + +static int llex (LexState *ls, SemInfo *seminfo) { + luaZ_resetbuffer(ls->buff); + for (;;) { + switch (ls->current) { + case '\n': + case '\r': { + inclinenumber(ls); + continue; + } + case '-': { + next(ls); + if (ls->current != '-') return '-'; + /* else is a comment */ + next(ls); + if (ls->current == '[') { + int sep = skip_sep(ls); + luaZ_resetbuffer(ls->buff); /* `skip_sep' may dirty the buffer */ + if (sep >= 0) { + read_long_string(ls, NULL, sep); /* long comment */ + luaZ_resetbuffer(ls->buff); + continue; + } + } + /* else short comment */ + while (!currIsNewline(ls) && ls->current != EOZ) + next(ls); + continue; + } + case '[': { + int sep = skip_sep(ls); + if (sep >= 0) { + read_long_string(ls, seminfo, sep); + return TK_STRING; + } + else if (sep == -1) return '['; + else luaX_lexerror(ls, "invalid long string delimiter", TK_STRING); + } + case '=': { + next(ls); + if (ls->current != '=') return '='; + else { next(ls); return TK_EQ; } + } + case '<': { + next(ls); + if (ls->current != '=') return '<'; + else { next(ls); return TK_LE; } + } + case '>': { + next(ls); + if (ls->current != '=') return '>'; + else { next(ls); return TK_GE; } + } + case '~': { + next(ls); + if (ls->current != '=') return '~'; + else { next(ls); return TK_NE; } + } + case '"': + case '\'': { + read_string(ls, ls->current, seminfo); + return TK_STRING; + } + case '.': { + save_and_next(ls); + if (check_next(ls, ".")) { + if (check_next(ls, ".")) + return TK_DOTS; /* ... */ + else return TK_CONCAT; /* .. */ + } + else if (!isdigit(ls->current)) return '.'; + else { + read_numeral(ls, seminfo); + return TK_NUMBER; + } + } + case EOZ: { + return TK_EOS; + } + default: { + if (isspace(ls->current)) { + lua_assert(!currIsNewline(ls)); + next(ls); + continue; + } + else if (isdigit(ls->current)) { + read_numeral(ls, seminfo); + return TK_NUMBER; + } + else if (isalpha(ls->current) || ls->current == '_') { + /* identifier or reserved word */ + TString *ts; + do { + save_and_next(ls); + } while (isalnum(ls->current) || ls->current == '_'); + ts = luaX_newstring(ls, luaZ_buffer(ls->buff), + luaZ_bufflen(ls->buff)); + if (ts->tsv.reserved > 0) /* reserved word? */ + return ts->tsv.reserved - 1 + FIRST_RESERVED; + else { + seminfo->ts = ts; + return TK_NAME; + } + } + else { + int c = ls->current; + next(ls); + return c; /* single-char tokens (+ - / ...) */ + } + } + } + } +} + + +void luaX_next (LexState *ls) { + ls->lastline = ls->linenumber; + if (ls->lookahead.token != TK_EOS) { /* is there a look-ahead token? */ + ls->t = ls->lookahead; /* use this one */ + ls->lookahead.token = TK_EOS; /* and discharge it */ + } + else + ls->t.token = llex(ls, &ls->t.seminfo); /* read next token */ +} + + +void luaX_lookahead (LexState *ls) { + lua_assert(ls->lookahead.token == TK_EOS); + ls->lookahead.token = llex(ls, &ls->lookahead.seminfo); +} + diff --git a/extern/lua-5.1.5/src/llex.h b/extern/lua-5.1.5/src/llex.h new file mode 100644 index 00000000..a9201cee --- /dev/null +++ b/extern/lua-5.1.5/src/llex.h @@ -0,0 +1,81 @@ +/* +** $Id: llex.h,v 1.58.1.1 2007/12/27 13:02:25 roberto Exp $ +** Lexical Analyzer +** See Copyright Notice in lua.h +*/ + +#ifndef llex_h +#define llex_h + +#include "lobject.h" +#include "lzio.h" + + +#define FIRST_RESERVED 257 + +/* maximum length of a reserved word */ +#define TOKEN_LEN (sizeof("function")/sizeof(char)) + + +/* +* WARNING: if you change the order of this enumeration, +* grep "ORDER RESERVED" +*/ +enum RESERVED { + /* terminal symbols denoted by reserved words */ + TK_AND = FIRST_RESERVED, TK_BREAK, + TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION, + TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, TK_REPEAT, + TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE, + /* other terminal symbols */ + TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE, TK_NUMBER, + TK_NAME, TK_STRING, TK_EOS +}; + +/* number of reserved words */ +#define NUM_RESERVED (cast(int, TK_WHILE-FIRST_RESERVED+1)) + + +/* array with token `names' */ +LUAI_DATA const char *const luaX_tokens []; + + +typedef union { + lua_Number r; + TString *ts; +} SemInfo; /* semantics information */ + + +typedef struct Token { + int token; + SemInfo seminfo; +} Token; + + +typedef struct LexState { + int current; /* current character (charint) */ + int linenumber; /* input line counter */ + int lastline; /* line of last token `consumed' */ + Token t; /* current token */ + Token lookahead; /* look ahead token */ + struct FuncState *fs; /* `FuncState' is private to the parser */ + struct lua_State *L; + ZIO *z; /* input stream */ + Mbuffer *buff; /* buffer for tokens */ + TString *source; /* current source name */ + char decpoint; /* locale decimal point */ +} LexState; + + +LUAI_FUNC void luaX_init (lua_State *L); +LUAI_FUNC void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, + TString *source); +LUAI_FUNC TString *luaX_newstring (LexState *ls, const char *str, size_t l); +LUAI_FUNC void luaX_next (LexState *ls); +LUAI_FUNC void luaX_lookahead (LexState *ls); +LUAI_FUNC void luaX_lexerror (LexState *ls, const char *msg, int token); +LUAI_FUNC void luaX_syntaxerror (LexState *ls, const char *s); +LUAI_FUNC const char *luaX_token2str (LexState *ls, int token); + + +#endif diff --git a/extern/lua-5.1.5/src/llimits.h b/extern/lua-5.1.5/src/llimits.h new file mode 100644 index 00000000..ca8dcb72 --- /dev/null +++ b/extern/lua-5.1.5/src/llimits.h @@ -0,0 +1,128 @@ +/* +** $Id: llimits.h,v 1.69.1.1 2007/12/27 13:02:25 roberto Exp $ +** Limits, basic types, and some other `installation-dependent' definitions +** See Copyright Notice in lua.h +*/ + +#ifndef llimits_h +#define llimits_h + + +#include +#include + + +#include "lua.h" + + +typedef LUAI_UINT32 lu_int32; + +typedef LUAI_UMEM lu_mem; + +typedef LUAI_MEM l_mem; + + + +/* chars used as small naturals (so that `char' is reserved for characters) */ +typedef unsigned char lu_byte; + + +#define MAX_SIZET ((size_t)(~(size_t)0)-2) + +#define MAX_LUMEM ((lu_mem)(~(lu_mem)0)-2) + + +#define MAX_INT (INT_MAX-2) /* maximum value of an int (-2 for safety) */ + +/* +** conversion of pointer to integer +** this is for hashing only; there is no problem if the integer +** cannot hold the whole pointer value +*/ +#define IntPoint(p) ((unsigned int)(lu_mem)(p)) + + + +/* type to ensure maximum alignment */ +typedef LUAI_USER_ALIGNMENT_T L_Umaxalign; + + +/* result of a `usual argument conversion' over lua_Number */ +typedef LUAI_UACNUMBER l_uacNumber; + + +/* internal assertions for in-house debugging */ +#ifdef lua_assert + +#define check_exp(c,e) (lua_assert(c), (e)) +#define api_check(l,e) lua_assert(e) + +#else + +#define lua_assert(c) ((void)0) +#define check_exp(c,e) (e) +#define api_check luai_apicheck + +#endif + + +#ifndef UNUSED +#define UNUSED(x) ((void)(x)) /* to avoid warnings */ +#endif + + +#ifndef cast +#define cast(t, exp) ((t)(exp)) +#endif + +#define cast_byte(i) cast(lu_byte, (i)) +#define cast_num(i) cast(lua_Number, (i)) +#define cast_int(i) cast(int, (i)) + + + +/* +** type for virtual-machine instructions +** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h) +*/ +typedef lu_int32 Instruction; + + + +/* maximum stack for a Lua function */ +#define MAXSTACK 250 + + + +/* minimum size for the string table (must be power of 2) */ +#ifndef MINSTRTABSIZE +#define MINSTRTABSIZE 32 +#endif + + +/* minimum size for string buffer */ +#ifndef LUA_MINBUFFER +#define LUA_MINBUFFER 32 +#endif + + +#ifndef lua_lock +#define lua_lock(L) ((void) 0) +#define lua_unlock(L) ((void) 0) +#endif + +#ifndef luai_threadyield +#define luai_threadyield(L) {lua_unlock(L); lua_lock(L);} +#endif + + +/* +** macro to control inclusion of some hard tests on stack reallocation +*/ +#ifndef HARDSTACKTESTS +#define condhardstacktests(x) ((void)0) +#else +#define condhardstacktests(x) x +#endif + +#endif diff --git a/extern/lua-5.1.5/src/lmathlib.c b/extern/lua-5.1.5/src/lmathlib.c new file mode 100644 index 00000000..441fbf73 --- /dev/null +++ b/extern/lua-5.1.5/src/lmathlib.c @@ -0,0 +1,263 @@ +/* +** $Id: lmathlib.c,v 1.67.1.1 2007/12/27 13:02:25 roberto Exp $ +** Standard mathematical library +** See Copyright Notice in lua.h +*/ + + +#include +#include + +#define lmathlib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +#undef PI +#define PI (3.14159265358979323846) +#define RADIANS_PER_DEGREE (PI/180.0) + + + +static int math_abs (lua_State *L) { + lua_pushnumber(L, fabs(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sin (lua_State *L) { + lua_pushnumber(L, sin(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sinh (lua_State *L) { + lua_pushnumber(L, sinh(luaL_checknumber(L, 1))); + return 1; +} + +static int math_cos (lua_State *L) { + lua_pushnumber(L, cos(luaL_checknumber(L, 1))); + return 1; +} + +static int math_cosh (lua_State *L) { + lua_pushnumber(L, cosh(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tan (lua_State *L) { + lua_pushnumber(L, tan(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tanh (lua_State *L) { + lua_pushnumber(L, tanh(luaL_checknumber(L, 1))); + return 1; +} + +static int math_asin (lua_State *L) { + lua_pushnumber(L, asin(luaL_checknumber(L, 1))); + return 1; +} + +static int math_acos (lua_State *L) { + lua_pushnumber(L, acos(luaL_checknumber(L, 1))); + return 1; +} + +static int math_atan (lua_State *L) { + lua_pushnumber(L, atan(luaL_checknumber(L, 1))); + return 1; +} + +static int math_atan2 (lua_State *L) { + lua_pushnumber(L, atan2(luaL_checknumber(L, 1), luaL_checknumber(L, 2))); + return 1; +} + +static int math_ceil (lua_State *L) { + lua_pushnumber(L, ceil(luaL_checknumber(L, 1))); + return 1; +} + +static int math_floor (lua_State *L) { + lua_pushnumber(L, floor(luaL_checknumber(L, 1))); + return 1; +} + +static int math_fmod (lua_State *L) { + lua_pushnumber(L, fmod(luaL_checknumber(L, 1), luaL_checknumber(L, 2))); + return 1; +} + +static int math_modf (lua_State *L) { + double ip; + double fp = modf(luaL_checknumber(L, 1), &ip); + lua_pushnumber(L, ip); + lua_pushnumber(L, fp); + return 2; +} + +static int math_sqrt (lua_State *L) { + lua_pushnumber(L, sqrt(luaL_checknumber(L, 1))); + return 1; +} + +static int math_pow (lua_State *L) { + lua_pushnumber(L, pow(luaL_checknumber(L, 1), luaL_checknumber(L, 2))); + return 1; +} + +static int math_log (lua_State *L) { + lua_pushnumber(L, log(luaL_checknumber(L, 1))); + return 1; +} + +static int math_log10 (lua_State *L) { + lua_pushnumber(L, log10(luaL_checknumber(L, 1))); + return 1; +} + +static int math_exp (lua_State *L) { + lua_pushnumber(L, exp(luaL_checknumber(L, 1))); + return 1; +} + +static int math_deg (lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1)/RADIANS_PER_DEGREE); + return 1; +} + +static int math_rad (lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1)*RADIANS_PER_DEGREE); + return 1; +} + +static int math_frexp (lua_State *L) { + int e; + lua_pushnumber(L, frexp(luaL_checknumber(L, 1), &e)); + lua_pushinteger(L, e); + return 2; +} + +static int math_ldexp (lua_State *L) { + lua_pushnumber(L, ldexp(luaL_checknumber(L, 1), luaL_checkint(L, 2))); + return 1; +} + + + +static int math_min (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + lua_Number dmin = luaL_checknumber(L, 1); + int i; + for (i=2; i<=n; i++) { + lua_Number d = luaL_checknumber(L, i); + if (d < dmin) + dmin = d; + } + lua_pushnumber(L, dmin); + return 1; +} + + +static int math_max (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + lua_Number dmax = luaL_checknumber(L, 1); + int i; + for (i=2; i<=n; i++) { + lua_Number d = luaL_checknumber(L, i); + if (d > dmax) + dmax = d; + } + lua_pushnumber(L, dmax); + return 1; +} + + +static int math_random (lua_State *L) { + /* the `%' avoids the (rare) case of r==1, and is needed also because on + some systems (SunOS!) `rand()' may return a value larger than RAND_MAX */ + lua_Number r = (lua_Number)(rand()%RAND_MAX) / (lua_Number)RAND_MAX; + switch (lua_gettop(L)) { /* check number of arguments */ + case 0: { /* no arguments */ + lua_pushnumber(L, r); /* Number between 0 and 1 */ + break; + } + case 1: { /* only upper limit */ + int u = luaL_checkint(L, 1); + luaL_argcheck(L, 1<=u, 1, "interval is empty"); + lua_pushnumber(L, floor(r*u)+1); /* int between 1 and `u' */ + break; + } + case 2: { /* lower and upper limits */ + int l = luaL_checkint(L, 1); + int u = luaL_checkint(L, 2); + luaL_argcheck(L, l<=u, 2, "interval is empty"); + lua_pushnumber(L, floor(r*(u-l+1))+l); /* int between `l' and `u' */ + break; + } + default: return luaL_error(L, "wrong number of arguments"); + } + return 1; +} + + +static int math_randomseed (lua_State *L) { + srand(luaL_checkint(L, 1)); + return 0; +} + + +static const luaL_Reg mathlib[] = { + {"abs", math_abs}, + {"acos", math_acos}, + {"asin", math_asin}, + {"atan2", math_atan2}, + {"atan", math_atan}, + {"ceil", math_ceil}, + {"cosh", math_cosh}, + {"cos", math_cos}, + {"deg", math_deg}, + {"exp", math_exp}, + {"floor", math_floor}, + {"fmod", math_fmod}, + {"frexp", math_frexp}, + {"ldexp", math_ldexp}, + {"log10", math_log10}, + {"log", math_log}, + {"max", math_max}, + {"min", math_min}, + {"modf", math_modf}, + {"pow", math_pow}, + {"rad", math_rad}, + {"random", math_random}, + {"randomseed", math_randomseed}, + {"sinh", math_sinh}, + {"sin", math_sin}, + {"sqrt", math_sqrt}, + {"tanh", math_tanh}, + {"tan", math_tan}, + {NULL, NULL} +}; + + +/* +** Open math library +*/ +LUALIB_API int luaopen_math (lua_State *L) { + luaL_register(L, LUA_MATHLIBNAME, mathlib); + lua_pushnumber(L, PI); + lua_setfield(L, -2, "pi"); + lua_pushnumber(L, HUGE_VAL); + lua_setfield(L, -2, "huge"); +#if defined(LUA_COMPAT_MOD) + lua_getfield(L, -1, "fmod"); + lua_setfield(L, -2, "mod"); +#endif + return 1; +} + diff --git a/extern/lua-5.1.5/src/lmem.c b/extern/lua-5.1.5/src/lmem.c new file mode 100644 index 00000000..ae7d8c96 --- /dev/null +++ b/extern/lua-5.1.5/src/lmem.c @@ -0,0 +1,86 @@ +/* +** $Id: lmem.c,v 1.70.1.1 2007/12/27 13:02:25 roberto Exp $ +** Interface to Memory Manager +** See Copyright Notice in lua.h +*/ + + +#include + +#define lmem_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" + + + +/* +** About the realloc function: +** void * frealloc (void *ud, void *ptr, size_t osize, size_t nsize); +** (`osize' is the old size, `nsize' is the new size) +** +** Lua ensures that (ptr == NULL) iff (osize == 0). +** +** * frealloc(ud, NULL, 0, x) creates a new block of size `x' +** +** * frealloc(ud, p, x, 0) frees the block `p' +** (in this specific case, frealloc must return NULL). +** particularly, frealloc(ud, NULL, 0, 0) does nothing +** (which is equivalent to free(NULL) in ANSI C) +** +** frealloc returns NULL if it cannot create or reallocate the area +** (any reallocation to an equal or smaller size cannot fail!) +*/ + + + +#define MINSIZEARRAY 4 + + +void *luaM_growaux_ (lua_State *L, void *block, int *size, size_t size_elems, + int limit, const char *errormsg) { + void *newblock; + int newsize; + if (*size >= limit/2) { /* cannot double it? */ + if (*size >= limit) /* cannot grow even a little? */ + luaG_runerror(L, errormsg); + newsize = limit; /* still have at least one free place */ + } + else { + newsize = (*size)*2; + if (newsize < MINSIZEARRAY) + newsize = MINSIZEARRAY; /* minimum size */ + } + newblock = luaM_reallocv(L, block, *size, newsize, size_elems); + *size = newsize; /* update only when everything else is OK */ + return newblock; +} + + +void *luaM_toobig (lua_State *L) { + luaG_runerror(L, "memory allocation error: block too big"); + return NULL; /* to avoid warnings */ +} + + + +/* +** generic allocation routine. +*/ +void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) { + global_State *g = G(L); + lua_assert((osize == 0) == (block == NULL)); + block = (*g->frealloc)(g->ud, block, osize, nsize); + if (block == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + lua_assert((nsize == 0) == (block == NULL)); + g->totalbytes = (g->totalbytes - osize) + nsize; + return block; +} + diff --git a/extern/lua-5.1.5/src/lmem.h b/extern/lua-5.1.5/src/lmem.h new file mode 100644 index 00000000..7c2dcb32 --- /dev/null +++ b/extern/lua-5.1.5/src/lmem.h @@ -0,0 +1,49 @@ +/* +** $Id: lmem.h,v 1.31.1.1 2007/12/27 13:02:25 roberto Exp $ +** Interface to Memory Manager +** See Copyright Notice in lua.h +*/ + +#ifndef lmem_h +#define lmem_h + + +#include + +#include "llimits.h" +#include "lua.h" + +#define MEMERRMSG "not enough memory" + + +#define luaM_reallocv(L,b,on,n,e) \ + ((cast(size_t, (n)+1) <= MAX_SIZET/(e)) ? /* +1 to avoid warnings */ \ + luaM_realloc_(L, (b), (on)*(e), (n)*(e)) : \ + luaM_toobig(L)) + +#define luaM_freemem(L, b, s) luaM_realloc_(L, (b), (s), 0) +#define luaM_free(L, b) luaM_realloc_(L, (b), sizeof(*(b)), 0) +#define luaM_freearray(L, b, n, t) luaM_reallocv(L, (b), n, 0, sizeof(t)) + +#define luaM_malloc(L,t) luaM_realloc_(L, NULL, 0, (t)) +#define luaM_new(L,t) cast(t *, luaM_malloc(L, sizeof(t))) +#define luaM_newvector(L,n,t) \ + cast(t *, luaM_reallocv(L, NULL, 0, n, sizeof(t))) + +#define luaM_growvector(L,v,nelems,size,t,limit,e) \ + if ((nelems)+1 > (size)) \ + ((v)=cast(t *, luaM_growaux_(L,v,&(size),sizeof(t),limit,e))) + +#define luaM_reallocvector(L, v,oldn,n,t) \ + ((v)=cast(t *, luaM_reallocv(L, v, oldn, n, sizeof(t)))) + + +LUAI_FUNC void *luaM_realloc_ (lua_State *L, void *block, size_t oldsize, + size_t size); +LUAI_FUNC void *luaM_toobig (lua_State *L); +LUAI_FUNC void *luaM_growaux_ (lua_State *L, void *block, int *size, + size_t size_elem, int limit, + const char *errormsg); + +#endif + diff --git a/extern/lua-5.1.5/src/loadlib.c b/extern/lua-5.1.5/src/loadlib.c new file mode 100644 index 00000000..6158c535 --- /dev/null +++ b/extern/lua-5.1.5/src/loadlib.c @@ -0,0 +1,666 @@ +/* +** $Id: loadlib.c,v 1.52.1.4 2009/09/09 13:17:16 roberto Exp $ +** Dynamic library loader for Lua +** See Copyright Notice in lua.h +** +** This module contains an implementation of loadlib for Unix systems +** that have dlfcn, an implementation for Darwin (Mac OS X), an +** implementation for Windows, and a stub for other systems. +*/ + + +#include +#include + + +#define loadlib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* prefix for open functions in C libraries */ +#define LUA_POF "luaopen_" + +/* separator for open functions in C libraries */ +#define LUA_OFSEP "_" + + +#define LIBPREFIX "LOADLIB: " + +#define POF LUA_POF +#define LIB_FAIL "open" + + +/* error codes for ll_loadfunc */ +#define ERRLIB 1 +#define ERRFUNC 2 + +#define setprogdir(L) ((void)0) + + +static void ll_unloadlib (void *lib); +static void *ll_load (lua_State *L, const char *path); +static lua_CFunction ll_sym (lua_State *L, void *lib, const char *sym); + + + +#if defined(LUA_DL_DLOPEN) +/* +** {======================================================================== +** This is an implementation of loadlib based on the dlfcn interface. +** The dlfcn interface is available in Linux, SunOS, Solaris, IRIX, FreeBSD, +** NetBSD, AIX 4.2, HPUX 11, and probably most other Unix flavors, at least +** as an emulation layer on top of native functions. +** ========================================================================= +*/ + +#include + +static void ll_unloadlib (void *lib) { + dlclose(lib); +} + + +static void *ll_load (lua_State *L, const char *path) { + void *lib = dlopen(path, RTLD_NOW); + if (lib == NULL) lua_pushstring(L, dlerror()); + return lib; +} + + +static lua_CFunction ll_sym (lua_State *L, void *lib, const char *sym) { + lua_CFunction f = (lua_CFunction)dlsym(lib, sym); + if (f == NULL) lua_pushstring(L, dlerror()); + return f; +} + +/* }====================================================== */ + + + +#elif defined(LUA_DL_DLL) +/* +** {====================================================================== +** This is an implementation of loadlib for Windows using native functions. +** ======================================================================= +*/ + +#include + + +#undef setprogdir + +static void setprogdir (lua_State *L) { + char buff[MAX_PATH + 1]; + char *lb; + DWORD nsize = sizeof(buff)/sizeof(char); + DWORD n = GetModuleFileNameA(NULL, buff, nsize); + if (n == 0 || n == nsize || (lb = strrchr(buff, '\\')) == NULL) + luaL_error(L, "unable to get ModuleFileName"); + else { + *lb = '\0'; + luaL_gsub(L, lua_tostring(L, -1), LUA_EXECDIR, buff); + lua_remove(L, -2); /* remove original string */ + } +} + + +static void pusherror (lua_State *L) { + int error = GetLastError(); + char buffer[128]; + if (FormatMessageA(FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, + NULL, error, 0, buffer, sizeof(buffer), NULL)) + lua_pushstring(L, buffer); + else + lua_pushfstring(L, "system error %d\n", error); +} + +static void ll_unloadlib (void *lib) { + FreeLibrary((HINSTANCE)lib); +} + + +static void *ll_load (lua_State *L, const char *path) { + HINSTANCE lib = LoadLibraryA(path); + if (lib == NULL) pusherror(L); + return lib; +} + + +static lua_CFunction ll_sym (lua_State *L, void *lib, const char *sym) { + lua_CFunction f = (lua_CFunction)GetProcAddress((HINSTANCE)lib, sym); + if (f == NULL) pusherror(L); + return f; +} + +/* }====================================================== */ + + + +#elif defined(LUA_DL_DYLD) +/* +** {====================================================================== +** Native Mac OS X / Darwin Implementation +** ======================================================================= +*/ + +#include + + +/* Mac appends a `_' before C function names */ +#undef POF +#define POF "_" LUA_POF + + +static void pusherror (lua_State *L) { + const char *err_str; + const char *err_file; + NSLinkEditErrors err; + int err_num; + NSLinkEditError(&err, &err_num, &err_file, &err_str); + lua_pushstring(L, err_str); +} + + +static const char *errorfromcode (NSObjectFileImageReturnCode ret) { + switch (ret) { + case NSObjectFileImageInappropriateFile: + return "file is not a bundle"; + case NSObjectFileImageArch: + return "library is for wrong CPU type"; + case NSObjectFileImageFormat: + return "bad format"; + case NSObjectFileImageAccess: + return "cannot access file"; + case NSObjectFileImageFailure: + default: + return "unable to load library"; + } +} + + +static void ll_unloadlib (void *lib) { + NSUnLinkModule((NSModule)lib, NSUNLINKMODULE_OPTION_RESET_LAZY_REFERENCES); +} + + +static void *ll_load (lua_State *L, const char *path) { + NSObjectFileImage img; + NSObjectFileImageReturnCode ret; + /* this would be a rare case, but prevents crashing if it happens */ + if(!_dyld_present()) { + lua_pushliteral(L, "dyld not present"); + return NULL; + } + ret = NSCreateObjectFileImageFromFile(path, &img); + if (ret == NSObjectFileImageSuccess) { + NSModule mod = NSLinkModule(img, path, NSLINKMODULE_OPTION_PRIVATE | + NSLINKMODULE_OPTION_RETURN_ON_ERROR); + NSDestroyObjectFileImage(img); + if (mod == NULL) pusherror(L); + return mod; + } + lua_pushstring(L, errorfromcode(ret)); + return NULL; +} + + +static lua_CFunction ll_sym (lua_State *L, void *lib, const char *sym) { + NSSymbol nss = NSLookupSymbolInModule((NSModule)lib, sym); + if (nss == NULL) { + lua_pushfstring(L, "symbol " LUA_QS " not found", sym); + return NULL; + } + return (lua_CFunction)NSAddressOfSymbol(nss); +} + +/* }====================================================== */ + + + +#else +/* +** {====================================================== +** Fallback for other systems +** ======================================================= +*/ + +#undef LIB_FAIL +#define LIB_FAIL "absent" + + +#define DLMSG "dynamic libraries not enabled; check your Lua installation" + + +static void ll_unloadlib (void *lib) { + (void)lib; /* to avoid warnings */ +} + + +static void *ll_load (lua_State *L, const char *path) { + (void)path; /* to avoid warnings */ + lua_pushliteral(L, DLMSG); + return NULL; +} + + +static lua_CFunction ll_sym (lua_State *L, void *lib, const char *sym) { + (void)lib; (void)sym; /* to avoid warnings */ + lua_pushliteral(L, DLMSG); + return NULL; +} + +/* }====================================================== */ +#endif + + + +static void **ll_register (lua_State *L, const char *path) { + void **plib; + lua_pushfstring(L, "%s%s", LIBPREFIX, path); + lua_gettable(L, LUA_REGISTRYINDEX); /* check library in registry? */ + if (!lua_isnil(L, -1)) /* is there an entry? */ + plib = (void **)lua_touserdata(L, -1); + else { /* no entry yet; create one */ + lua_pop(L, 1); + plib = (void **)lua_newuserdata(L, sizeof(const void *)); + *plib = NULL; + luaL_getmetatable(L, "_LOADLIB"); + lua_setmetatable(L, -2); + lua_pushfstring(L, "%s%s", LIBPREFIX, path); + lua_pushvalue(L, -2); + lua_settable(L, LUA_REGISTRYINDEX); + } + return plib; +} + + +/* +** __gc tag method: calls library's `ll_unloadlib' function with the lib +** handle +*/ +static int gctm (lua_State *L) { + void **lib = (void **)luaL_checkudata(L, 1, "_LOADLIB"); + if (*lib) ll_unloadlib(*lib); + *lib = NULL; /* mark library as closed */ + return 0; +} + + +static int ll_loadfunc (lua_State *L, const char *path, const char *sym) { + void **reg = ll_register(L, path); + if (*reg == NULL) *reg = ll_load(L, path); + if (*reg == NULL) + return ERRLIB; /* unable to load library */ + else { + lua_CFunction f = ll_sym(L, *reg, sym); + if (f == NULL) + return ERRFUNC; /* unable to find function */ + lua_pushcfunction(L, f); + return 0; /* return function */ + } +} + + +static int ll_loadlib (lua_State *L) { + const char *path = luaL_checkstring(L, 1); + const char *init = luaL_checkstring(L, 2); + int stat = ll_loadfunc(L, path, init); + if (stat == 0) /* no errors? */ + return 1; /* return the loaded function */ + else { /* error; error message is on stack top */ + lua_pushnil(L); + lua_insert(L, -2); + lua_pushstring(L, (stat == ERRLIB) ? LIB_FAIL : "init"); + return 3; /* return nil, error message, and where */ + } +} + + + +/* +** {====================================================== +** 'require' function +** ======================================================= +*/ + + +static int readable (const char *filename) { + FILE *f = fopen(filename, "r"); /* try to open file */ + if (f == NULL) return 0; /* open failed */ + fclose(f); + return 1; +} + + +static const char *pushnexttemplate (lua_State *L, const char *path) { + const char *l; + while (*path == *LUA_PATHSEP) path++; /* skip separators */ + if (*path == '\0') return NULL; /* no more templates */ + l = strchr(path, *LUA_PATHSEP); /* find next separator */ + if (l == NULL) l = path + strlen(path); + lua_pushlstring(L, path, l - path); /* template */ + return l; +} + + +static const char *findfile (lua_State *L, const char *name, + const char *pname) { + const char *path; + name = luaL_gsub(L, name, ".", LUA_DIRSEP); + lua_getfield(L, LUA_ENVIRONINDEX, pname); + path = lua_tostring(L, -1); + if (path == NULL) + luaL_error(L, LUA_QL("package.%s") " must be a string", pname); + lua_pushliteral(L, ""); /* error accumulator */ + while ((path = pushnexttemplate(L, path)) != NULL) { + const char *filename; + filename = luaL_gsub(L, lua_tostring(L, -1), LUA_PATH_MARK, name); + lua_remove(L, -2); /* remove path template */ + if (readable(filename)) /* does file exist and is readable? */ + return filename; /* return that file name */ + lua_pushfstring(L, "\n\tno file " LUA_QS, filename); + lua_remove(L, -2); /* remove file name */ + lua_concat(L, 2); /* add entry to possible error message */ + } + return NULL; /* not found */ +} + + +static void loaderror (lua_State *L, const char *filename) { + luaL_error(L, "error loading module " LUA_QS " from file " LUA_QS ":\n\t%s", + lua_tostring(L, 1), filename, lua_tostring(L, -1)); +} + + +static int loader_Lua (lua_State *L) { + const char *filename; + const char *name = luaL_checkstring(L, 1); + filename = findfile(L, name, "path"); + if (filename == NULL) return 1; /* library not found in this path */ + if (luaL_loadfile(L, filename) != 0) + loaderror(L, filename); + return 1; /* library loaded successfully */ +} + + +static const char *mkfuncname (lua_State *L, const char *modname) { + const char *funcname; + const char *mark = strchr(modname, *LUA_IGMARK); + if (mark) modname = mark + 1; + funcname = luaL_gsub(L, modname, ".", LUA_OFSEP); + funcname = lua_pushfstring(L, POF"%s", funcname); + lua_remove(L, -2); /* remove 'gsub' result */ + return funcname; +} + + +static int loader_C (lua_State *L) { + const char *funcname; + const char *name = luaL_checkstring(L, 1); + const char *filename = findfile(L, name, "cpath"); + if (filename == NULL) return 1; /* library not found in this path */ + funcname = mkfuncname(L, name); + if (ll_loadfunc(L, filename, funcname) != 0) + loaderror(L, filename); + return 1; /* library loaded successfully */ +} + + +static int loader_Croot (lua_State *L) { + const char *funcname; + const char *filename; + const char *name = luaL_checkstring(L, 1); + const char *p = strchr(name, '.'); + int stat; + if (p == NULL) return 0; /* is root */ + lua_pushlstring(L, name, p - name); + filename = findfile(L, lua_tostring(L, -1), "cpath"); + if (filename == NULL) return 1; /* root not found */ + funcname = mkfuncname(L, name); + if ((stat = ll_loadfunc(L, filename, funcname)) != 0) { + if (stat != ERRFUNC) loaderror(L, filename); /* real error */ + lua_pushfstring(L, "\n\tno module " LUA_QS " in file " LUA_QS, + name, filename); + return 1; /* function not found */ + } + return 1; +} + + +static int loader_preload (lua_State *L) { + const char *name = luaL_checkstring(L, 1); + lua_getfield(L, LUA_ENVIRONINDEX, "preload"); + if (!lua_istable(L, -1)) + luaL_error(L, LUA_QL("package.preload") " must be a table"); + lua_getfield(L, -1, name); + if (lua_isnil(L, -1)) /* not found? */ + lua_pushfstring(L, "\n\tno field package.preload['%s']", name); + return 1; +} + + +static const int sentinel_ = 0; +#define sentinel ((void *)&sentinel_) + + +static int ll_require (lua_State *L) { + const char *name = luaL_checkstring(L, 1); + int i; + lua_settop(L, 1); /* _LOADED table will be at index 2 */ + lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED"); + lua_getfield(L, 2, name); + if (lua_toboolean(L, -1)) { /* is it there? */ + if (lua_touserdata(L, -1) == sentinel) /* check loops */ + luaL_error(L, "loop or previous error loading module " LUA_QS, name); + return 1; /* package is already loaded */ + } + /* else must load it; iterate over available loaders */ + lua_getfield(L, LUA_ENVIRONINDEX, "loaders"); + if (!lua_istable(L, -1)) + luaL_error(L, LUA_QL("package.loaders") " must be a table"); + lua_pushliteral(L, ""); /* error message accumulator */ + for (i=1; ; i++) { + lua_rawgeti(L, -2, i); /* get a loader */ + if (lua_isnil(L, -1)) + luaL_error(L, "module " LUA_QS " not found:%s", + name, lua_tostring(L, -2)); + lua_pushstring(L, name); + lua_call(L, 1, 1); /* call it */ + if (lua_isfunction(L, -1)) /* did it find module? */ + break; /* module loaded successfully */ + else if (lua_isstring(L, -1)) /* loader returned error message? */ + lua_concat(L, 2); /* accumulate it */ + else + lua_pop(L, 1); + } + lua_pushlightuserdata(L, sentinel); + lua_setfield(L, 2, name); /* _LOADED[name] = sentinel */ + lua_pushstring(L, name); /* pass name as argument to module */ + lua_call(L, 1, 1); /* run loaded module */ + if (!lua_isnil(L, -1)) /* non-nil return? */ + lua_setfield(L, 2, name); /* _LOADED[name] = returned value */ + lua_getfield(L, 2, name); + if (lua_touserdata(L, -1) == sentinel) { /* module did not set a value? */ + lua_pushboolean(L, 1); /* use true as result */ + lua_pushvalue(L, -1); /* extra copy to be returned */ + lua_setfield(L, 2, name); /* _LOADED[name] = true */ + } + return 1; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** 'module' function +** ======================================================= +*/ + + +static void setfenv (lua_State *L) { + lua_Debug ar; + if (lua_getstack(L, 1, &ar) == 0 || + lua_getinfo(L, "f", &ar) == 0 || /* get calling function */ + lua_iscfunction(L, -1)) + luaL_error(L, LUA_QL("module") " not called from a Lua function"); + lua_pushvalue(L, -2); + lua_setfenv(L, -2); + lua_pop(L, 1); +} + + +static void dooptions (lua_State *L, int n) { + int i; + for (i = 2; i <= n; i++) { + lua_pushvalue(L, i); /* get option (a function) */ + lua_pushvalue(L, -2); /* module */ + lua_call(L, 1, 0); + } +} + + +static void modinit (lua_State *L, const char *modname) { + const char *dot; + lua_pushvalue(L, -1); + lua_setfield(L, -2, "_M"); /* module._M = module */ + lua_pushstring(L, modname); + lua_setfield(L, -2, "_NAME"); + dot = strrchr(modname, '.'); /* look for last dot in module name */ + if (dot == NULL) dot = modname; + else dot++; + /* set _PACKAGE as package name (full module name minus last part) */ + lua_pushlstring(L, modname, dot - modname); + lua_setfield(L, -2, "_PACKAGE"); +} + + +static int ll_module (lua_State *L) { + const char *modname = luaL_checkstring(L, 1); + int loaded = lua_gettop(L) + 1; /* index of _LOADED table */ + lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED"); + lua_getfield(L, loaded, modname); /* get _LOADED[modname] */ + if (!lua_istable(L, -1)) { /* not found? */ + lua_pop(L, 1); /* remove previous result */ + /* try global variable (and create one if it does not exist) */ + if (luaL_findtable(L, LUA_GLOBALSINDEX, modname, 1) != NULL) + return luaL_error(L, "name conflict for module " LUA_QS, modname); + lua_pushvalue(L, -1); + lua_setfield(L, loaded, modname); /* _LOADED[modname] = new table */ + } + /* check whether table already has a _NAME field */ + lua_getfield(L, -1, "_NAME"); + if (!lua_isnil(L, -1)) /* is table an initialized module? */ + lua_pop(L, 1); + else { /* no; initialize it */ + lua_pop(L, 1); + modinit(L, modname); + } + lua_pushvalue(L, -1); + setfenv(L); + dooptions(L, loaded - 1); + return 0; +} + + +static int ll_seeall (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (!lua_getmetatable(L, 1)) { + lua_createtable(L, 0, 1); /* create new metatable */ + lua_pushvalue(L, -1); + lua_setmetatable(L, 1); + } + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_setfield(L, -2, "__index"); /* mt.__index = _G */ + return 0; +} + + +/* }====================================================== */ + + + +/* auxiliary mark (for internal use) */ +#define AUXMARK "\1" + +static void setpath (lua_State *L, const char *fieldname, const char *envname, + const char *def) { + const char *path = getenv(envname); + if (path == NULL) /* no environment variable? */ + lua_pushstring(L, def); /* use default */ + else { + /* replace ";;" by ";AUXMARK;" and then AUXMARK by default path */ + path = luaL_gsub(L, path, LUA_PATHSEP LUA_PATHSEP, + LUA_PATHSEP AUXMARK LUA_PATHSEP); + luaL_gsub(L, path, AUXMARK, def); + lua_remove(L, -2); + } + setprogdir(L); + lua_setfield(L, -2, fieldname); +} + + +static const luaL_Reg pk_funcs[] = { + {"loadlib", ll_loadlib}, + {"seeall", ll_seeall}, + {NULL, NULL} +}; + + +static const luaL_Reg ll_funcs[] = { + {"module", ll_module}, + {"require", ll_require}, + {NULL, NULL} +}; + + +static const lua_CFunction loaders[] = + {loader_preload, loader_Lua, loader_C, loader_Croot, NULL}; + + +LUALIB_API int luaopen_package (lua_State *L) { + int i; + /* create new type _LOADLIB */ + luaL_newmetatable(L, "_LOADLIB"); + lua_pushcfunction(L, gctm); + lua_setfield(L, -2, "__gc"); + /* create `package' table */ + luaL_register(L, LUA_LOADLIBNAME, pk_funcs); +#if defined(LUA_COMPAT_LOADLIB) + lua_getfield(L, -1, "loadlib"); + lua_setfield(L, LUA_GLOBALSINDEX, "loadlib"); +#endif + lua_pushvalue(L, -1); + lua_replace(L, LUA_ENVIRONINDEX); + /* create `loaders' table */ + lua_createtable(L, sizeof(loaders)/sizeof(loaders[0]) - 1, 0); + /* fill it with pre-defined loaders */ + for (i=0; loaders[i] != NULL; i++) { + lua_pushcfunction(L, loaders[i]); + lua_rawseti(L, -2, i+1); + } + lua_setfield(L, -2, "loaders"); /* put it in field `loaders' */ + setpath(L, "path", LUA_PATH, LUA_PATH_DEFAULT); /* set field `path' */ + setpath(L, "cpath", LUA_CPATH, LUA_CPATH_DEFAULT); /* set field `cpath' */ + /* store config information */ + lua_pushliteral(L, LUA_DIRSEP "\n" LUA_PATHSEP "\n" LUA_PATH_MARK "\n" + LUA_EXECDIR "\n" LUA_IGMARK); + lua_setfield(L, -2, "config"); + /* set field `loaded' */ + luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", 2); + lua_setfield(L, -2, "loaded"); + /* set field `preload' */ + lua_newtable(L); + lua_setfield(L, -2, "preload"); + lua_pushvalue(L, LUA_GLOBALSINDEX); + luaL_register(L, NULL, ll_funcs); /* open lib into global table */ + lua_pop(L, 1); + return 1; /* return 'package' table */ +} + diff --git a/extern/lua-5.1.5/src/lobject.c b/extern/lua-5.1.5/src/lobject.c new file mode 100644 index 00000000..4ff50732 --- /dev/null +++ b/extern/lua-5.1.5/src/lobject.c @@ -0,0 +1,214 @@ +/* +** $Id: lobject.c,v 2.22.1.1 2007/12/27 13:02:25 roberto Exp $ +** Some generic functions over Lua objects +** See Copyright Notice in lua.h +*/ + +#include +#include +#include +#include +#include + +#define lobject_c +#define LUA_CORE + +#include "lua.h" + +#include "ldo.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "lvm.h" + + + +const TValue luaO_nilobject_ = {{NULL}, LUA_TNIL}; + + +/* +** converts an integer to a "floating point byte", represented as +** (eeeeexxx), where the real value is (1xxx) * 2^(eeeee - 1) if +** eeeee != 0 and (xxx) otherwise. +*/ +int luaO_int2fb (unsigned int x) { + int e = 0; /* expoent */ + while (x >= 16) { + x = (x+1) >> 1; + e++; + } + if (x < 8) return x; + else return ((e+1) << 3) | (cast_int(x) - 8); +} + + +/* converts back */ +int luaO_fb2int (int x) { + int e = (x >> 3) & 31; + if (e == 0) return x; + else return ((x & 7)+8) << (e - 1); +} + + +int luaO_log2 (unsigned int x) { + static const lu_byte log_2[256] = { + 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8 + }; + int l = -1; + while (x >= 256) { l += 8; x >>= 8; } + return l + log_2[x]; + +} + + +int luaO_rawequalObj (const TValue *t1, const TValue *t2) { + if (ttype(t1) != ttype(t2)) return 0; + else switch (ttype(t1)) { + case LUA_TNIL: + return 1; + case LUA_TNUMBER: + return luai_numeq(nvalue(t1), nvalue(t2)); + case LUA_TBOOLEAN: + return bvalue(t1) == bvalue(t2); /* boolean true must be 1 !! */ + case LUA_TLIGHTUSERDATA: + return pvalue(t1) == pvalue(t2); + default: + lua_assert(iscollectable(t1)); + return gcvalue(t1) == gcvalue(t2); + } +} + + +int luaO_str2d (const char *s, lua_Number *result) { + char *endptr; + *result = lua_str2number(s, &endptr); + if (endptr == s) return 0; /* conversion failed */ + if (*endptr == 'x' || *endptr == 'X') /* maybe an hexadecimal constant? */ + *result = cast_num(strtoul(s, &endptr, 16)); + if (*endptr == '\0') return 1; /* most common case */ + while (isspace(cast(unsigned char, *endptr))) endptr++; + if (*endptr != '\0') return 0; /* invalid trailing characters? */ + return 1; +} + + + +static void pushstr (lua_State *L, const char *str) { + setsvalue2s(L, L->top, luaS_new(L, str)); + incr_top(L); +} + + +/* this function handles only `%d', `%c', %f, %p, and `%s' formats */ +const char *luaO_pushvfstring (lua_State *L, const char *fmt, va_list argp) { + int n = 1; + pushstr(L, ""); + for (;;) { + const char *e = strchr(fmt, '%'); + if (e == NULL) break; + setsvalue2s(L, L->top, luaS_newlstr(L, fmt, e-fmt)); + incr_top(L); + switch (*(e+1)) { + case 's': { + const char *s = va_arg(argp, char *); + if (s == NULL) s = "(null)"; + pushstr(L, s); + break; + } + case 'c': { + char buff[2]; + buff[0] = cast(char, va_arg(argp, int)); + buff[1] = '\0'; + pushstr(L, buff); + break; + } + case 'd': { + setnvalue(L->top, cast_num(va_arg(argp, int))); + incr_top(L); + break; + } + case 'f': { + setnvalue(L->top, cast_num(va_arg(argp, l_uacNumber))); + incr_top(L); + break; + } + case 'p': { + char buff[4*sizeof(void *) + 8]; /* should be enough space for a `%p' */ + sprintf(buff, "%p", va_arg(argp, void *)); + pushstr(L, buff); + break; + } + case '%': { + pushstr(L, "%"); + break; + } + default: { + char buff[3]; + buff[0] = '%'; + buff[1] = *(e+1); + buff[2] = '\0'; + pushstr(L, buff); + break; + } + } + n += 2; + fmt = e+2; + } + pushstr(L, fmt); + luaV_concat(L, n+1, cast_int(L->top - L->base) - 1); + L->top -= n; + return svalue(L->top - 1); +} + + +const char *luaO_pushfstring (lua_State *L, const char *fmt, ...) { + const char *msg; + va_list argp; + va_start(argp, fmt); + msg = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + return msg; +} + + +void luaO_chunkid (char *out, const char *source, size_t bufflen) { + if (*source == '=') { + strncpy(out, source+1, bufflen); /* remove first char */ + out[bufflen-1] = '\0'; /* ensures null termination */ + } + else { /* out = "source", or "...source" */ + if (*source == '@') { + size_t l; + source++; /* skip the `@' */ + bufflen -= sizeof(" '...' "); + l = strlen(source); + strcpy(out, ""); + if (l > bufflen) { + source += (l-bufflen); /* get last part of file name */ + strcat(out, "..."); + } + strcat(out, source); + } + else { /* out = [string "string"] */ + size_t len = strcspn(source, "\n\r"); /* stop at first newline */ + bufflen -= sizeof(" [string \"...\"] "); + if (len > bufflen) len = bufflen; + strcpy(out, "[string \""); + if (source[len] != '\0') { /* must truncate? */ + strncat(out, source, len); + strcat(out, "..."); + } + else + strcat(out, source); + strcat(out, "\"]"); + } + } +} diff --git a/extern/lua-5.1.5/src/lobject.h b/extern/lua-5.1.5/src/lobject.h new file mode 100644 index 00000000..f1e447ef --- /dev/null +++ b/extern/lua-5.1.5/src/lobject.h @@ -0,0 +1,381 @@ +/* +** $Id: lobject.h,v 2.20.1.2 2008/08/06 13:29:48 roberto Exp $ +** Type definitions for Lua objects +** See Copyright Notice in lua.h +*/ + + +#ifndef lobject_h +#define lobject_h + + +#include + + +#include "llimits.h" +#include "lua.h" + + +/* tags for values visible from Lua */ +#define LAST_TAG LUA_TTHREAD + +#define NUM_TAGS (LAST_TAG+1) + + +/* +** Extra tags for non-values +*/ +#define LUA_TPROTO (LAST_TAG+1) +#define LUA_TUPVAL (LAST_TAG+2) +#define LUA_TDEADKEY (LAST_TAG+3) + + +/* +** Union of all collectable objects +*/ +typedef union GCObject GCObject; + + +/* +** Common Header for all collectable objects (in macro form, to be +** included in other objects) +*/ +#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked + + +/* +** Common header in struct form +*/ +typedef struct GCheader { + CommonHeader; +} GCheader; + + + + +/* +** Union of all Lua values +*/ +typedef union { + GCObject *gc; + void *p; + lua_Number n; + int b; +} Value; + + +/* +** Tagged Values +*/ + +#define TValuefields Value value; int tt + +typedef struct lua_TValue { + TValuefields; +} TValue; + + +/* Macros to test type */ +#define ttisnil(o) (ttype(o) == LUA_TNIL) +#define ttisnumber(o) (ttype(o) == LUA_TNUMBER) +#define ttisstring(o) (ttype(o) == LUA_TSTRING) +#define ttistable(o) (ttype(o) == LUA_TTABLE) +#define ttisfunction(o) (ttype(o) == LUA_TFUNCTION) +#define ttisboolean(o) (ttype(o) == LUA_TBOOLEAN) +#define ttisuserdata(o) (ttype(o) == LUA_TUSERDATA) +#define ttisthread(o) (ttype(o) == LUA_TTHREAD) +#define ttislightuserdata(o) (ttype(o) == LUA_TLIGHTUSERDATA) + +/* Macros to access values */ +#define ttype(o) ((o)->tt) +#define gcvalue(o) check_exp(iscollectable(o), (o)->value.gc) +#define pvalue(o) check_exp(ttislightuserdata(o), (o)->value.p) +#define nvalue(o) check_exp(ttisnumber(o), (o)->value.n) +#define rawtsvalue(o) check_exp(ttisstring(o), &(o)->value.gc->ts) +#define tsvalue(o) (&rawtsvalue(o)->tsv) +#define rawuvalue(o) check_exp(ttisuserdata(o), &(o)->value.gc->u) +#define uvalue(o) (&rawuvalue(o)->uv) +#define clvalue(o) check_exp(ttisfunction(o), &(o)->value.gc->cl) +#define hvalue(o) check_exp(ttistable(o), &(o)->value.gc->h) +#define bvalue(o) check_exp(ttisboolean(o), (o)->value.b) +#define thvalue(o) check_exp(ttisthread(o), &(o)->value.gc->th) + +#define l_isfalse(o) (ttisnil(o) || (ttisboolean(o) && bvalue(o) == 0)) + +/* +** for internal debug only +*/ +#define checkconsistency(obj) \ + lua_assert(!iscollectable(obj) || (ttype(obj) == (obj)->value.gc->gch.tt)) + +#define checkliveness(g,obj) \ + lua_assert(!iscollectable(obj) || \ + ((ttype(obj) == (obj)->value.gc->gch.tt) && !isdead(g, (obj)->value.gc))) + + +/* Macros to set values */ +#define setnilvalue(obj) ((obj)->tt=LUA_TNIL) + +#define setnvalue(obj,x) \ + { TValue *i_o=(obj); i_o->value.n=(x); i_o->tt=LUA_TNUMBER; } + +#define setpvalue(obj,x) \ + { TValue *i_o=(obj); i_o->value.p=(x); i_o->tt=LUA_TLIGHTUSERDATA; } + +#define setbvalue(obj,x) \ + { TValue *i_o=(obj); i_o->value.b=(x); i_o->tt=LUA_TBOOLEAN; } + +#define setsvalue(L,obj,x) \ + { TValue *i_o=(obj); \ + i_o->value.gc=cast(GCObject *, (x)); i_o->tt=LUA_TSTRING; \ + checkliveness(G(L),i_o); } + +#define setuvalue(L,obj,x) \ + { TValue *i_o=(obj); \ + i_o->value.gc=cast(GCObject *, (x)); i_o->tt=LUA_TUSERDATA; \ + checkliveness(G(L),i_o); } + +#define setthvalue(L,obj,x) \ + { TValue *i_o=(obj); \ + i_o->value.gc=cast(GCObject *, (x)); i_o->tt=LUA_TTHREAD; \ + checkliveness(G(L),i_o); } + +#define setclvalue(L,obj,x) \ + { TValue *i_o=(obj); \ + i_o->value.gc=cast(GCObject *, (x)); i_o->tt=LUA_TFUNCTION; \ + checkliveness(G(L),i_o); } + +#define sethvalue(L,obj,x) \ + { TValue *i_o=(obj); \ + i_o->value.gc=cast(GCObject *, (x)); i_o->tt=LUA_TTABLE; \ + checkliveness(G(L),i_o); } + +#define setptvalue(L,obj,x) \ + { TValue *i_o=(obj); \ + i_o->value.gc=cast(GCObject *, (x)); i_o->tt=LUA_TPROTO; \ + checkliveness(G(L),i_o); } + + + + +#define setobj(L,obj1,obj2) \ + { const TValue *o2=(obj2); TValue *o1=(obj1); \ + o1->value = o2->value; o1->tt=o2->tt; \ + checkliveness(G(L),o1); } + + +/* +** different types of sets, according to destination +*/ + +/* from stack to (same) stack */ +#define setobjs2s setobj +/* to stack (not from same stack) */ +#define setobj2s setobj +#define setsvalue2s setsvalue +#define sethvalue2s sethvalue +#define setptvalue2s setptvalue +/* from table to same table */ +#define setobjt2t setobj +/* to table */ +#define setobj2t setobj +/* to new object */ +#define setobj2n setobj +#define setsvalue2n setsvalue + +#define setttype(obj, tt) (ttype(obj) = (tt)) + + +#define iscollectable(o) (ttype(o) >= LUA_TSTRING) + + + +typedef TValue *StkId; /* index to stack elements */ + + +/* +** String headers for string table +*/ +typedef union TString { + L_Umaxalign dummy; /* ensures maximum alignment for strings */ + struct { + CommonHeader; + lu_byte reserved; + unsigned int hash; + size_t len; + } tsv; +} TString; + + +#define getstr(ts) cast(const char *, (ts) + 1) +#define svalue(o) getstr(rawtsvalue(o)) + + + +typedef union Udata { + L_Umaxalign dummy; /* ensures maximum alignment for `local' udata */ + struct { + CommonHeader; + struct Table *metatable; + struct Table *env; + size_t len; + } uv; +} Udata; + + + + +/* +** Function Prototypes +*/ +typedef struct Proto { + CommonHeader; + TValue *k; /* constants used by the function */ + Instruction *code; + struct Proto **p; /* functions defined inside the function */ + int *lineinfo; /* map from opcodes to source lines */ + struct LocVar *locvars; /* information about local variables */ + TString **upvalues; /* upvalue names */ + TString *source; + int sizeupvalues; + int sizek; /* size of `k' */ + int sizecode; + int sizelineinfo; + int sizep; /* size of `p' */ + int sizelocvars; + int linedefined; + int lastlinedefined; + GCObject *gclist; + lu_byte nups; /* number of upvalues */ + lu_byte numparams; + lu_byte is_vararg; + lu_byte maxstacksize; +} Proto; + + +/* masks for new-style vararg */ +#define VARARG_HASARG 1 +#define VARARG_ISVARARG 2 +#define VARARG_NEEDSARG 4 + + +typedef struct LocVar { + TString *varname; + int startpc; /* first point where variable is active */ + int endpc; /* first point where variable is dead */ +} LocVar; + + + +/* +** Upvalues +*/ + +typedef struct UpVal { + CommonHeader; + TValue *v; /* points to stack or to its own value */ + union { + TValue value; /* the value (when closed) */ + struct { /* double linked list (when open) */ + struct UpVal *prev; + struct UpVal *next; + } l; + } u; +} UpVal; + + +/* +** Closures +*/ + +#define ClosureHeader \ + CommonHeader; lu_byte isC; lu_byte nupvalues; GCObject *gclist; \ + struct Table *env + +typedef struct CClosure { + ClosureHeader; + lua_CFunction f; + TValue upvalue[1]; +} CClosure; + + +typedef struct LClosure { + ClosureHeader; + struct Proto *p; + UpVal *upvals[1]; +} LClosure; + + +typedef union Closure { + CClosure c; + LClosure l; +} Closure; + + +#define iscfunction(o) (ttype(o) == LUA_TFUNCTION && clvalue(o)->c.isC) +#define isLfunction(o) (ttype(o) == LUA_TFUNCTION && !clvalue(o)->c.isC) + + +/* +** Tables +*/ + +typedef union TKey { + struct { + TValuefields; + struct Node *next; /* for chaining */ + } nk; + TValue tvk; +} TKey; + + +typedef struct Node { + TValue i_val; + TKey i_key; +} Node; + + +typedef struct Table { + CommonHeader; + lu_byte flags; /* 1<

lsizenode)) + + +#define luaO_nilobject (&luaO_nilobject_) + +LUAI_DATA const TValue luaO_nilobject_; + +#define ceillog2(x) (luaO_log2((x)-1) + 1) + +LUAI_FUNC int luaO_log2 (unsigned int x); +LUAI_FUNC int luaO_int2fb (unsigned int x); +LUAI_FUNC int luaO_fb2int (int x); +LUAI_FUNC int luaO_rawequalObj (const TValue *t1, const TValue *t2); +LUAI_FUNC int luaO_str2d (const char *s, lua_Number *result); +LUAI_FUNC const char *luaO_pushvfstring (lua_State *L, const char *fmt, + va_list argp); +LUAI_FUNC const char *luaO_pushfstring (lua_State *L, const char *fmt, ...); +LUAI_FUNC void luaO_chunkid (char *out, const char *source, size_t len); + + +#endif + diff --git a/extern/lua-5.1.5/src/lopcodes.c b/extern/lua-5.1.5/src/lopcodes.c new file mode 100644 index 00000000..4cc74523 --- /dev/null +++ b/extern/lua-5.1.5/src/lopcodes.c @@ -0,0 +1,102 @@ +/* +** $Id: lopcodes.c,v 1.37.1.1 2007/12/27 13:02:25 roberto Exp $ +** See Copyright Notice in lua.h +*/ + + +#define lopcodes_c +#define LUA_CORE + + +#include "lopcodes.h" + + +/* ORDER OP */ + +const char *const luaP_opnames[NUM_OPCODES+1] = { + "MOVE", + "LOADK", + "LOADBOOL", + "LOADNIL", + "GETUPVAL", + "GETGLOBAL", + "GETTABLE", + "SETGLOBAL", + "SETUPVAL", + "SETTABLE", + "NEWTABLE", + "SELF", + "ADD", + "SUB", + "MUL", + "DIV", + "MOD", + "POW", + "UNM", + "NOT", + "LEN", + "CONCAT", + "JMP", + "EQ", + "LT", + "LE", + "TEST", + "TESTSET", + "CALL", + "TAILCALL", + "RETURN", + "FORLOOP", + "FORPREP", + "TFORLOOP", + "SETLIST", + "CLOSE", + "CLOSURE", + "VARARG", + NULL +}; + + +#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m)) + +const lu_byte luaP_opmodes[NUM_OPCODES] = { +/* T A B C mode opcode */ + opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_MOVE */ + ,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_LOADK */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_LOADBOOL */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LOADNIL */ + ,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_GETUPVAL */ + ,opmode(0, 1, OpArgK, OpArgN, iABx) /* OP_GETGLOBAL */ + ,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_GETTABLE */ + ,opmode(0, 0, OpArgK, OpArgN, iABx) /* OP_SETGLOBAL */ + ,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_SETUPVAL */ + ,opmode(0, 0, OpArgK, OpArgK, iABC) /* OP_SETTABLE */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_NEWTABLE */ + ,opmode(0, 1, OpArgR, OpArgK, iABC) /* OP_SELF */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_ADD */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_SUB */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MUL */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_DIV */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_MOD */ + ,opmode(0, 1, OpArgK, OpArgK, iABC) /* OP_POW */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_UNM */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_NOT */ + ,opmode(0, 1, OpArgR, OpArgN, iABC) /* OP_LEN */ + ,opmode(0, 1, OpArgR, OpArgR, iABC) /* OP_CONCAT */ + ,opmode(0, 0, OpArgR, OpArgN, iAsBx) /* OP_JMP */ + ,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_EQ */ + ,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LT */ + ,opmode(1, 0, OpArgK, OpArgK, iABC) /* OP_LE */ + ,opmode(1, 1, OpArgR, OpArgU, iABC) /* OP_TEST */ + ,opmode(1, 1, OpArgR, OpArgU, iABC) /* OP_TESTSET */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_CALL */ + ,opmode(0, 1, OpArgU, OpArgU, iABC) /* OP_TAILCALL */ + ,opmode(0, 0, OpArgU, OpArgN, iABC) /* OP_RETURN */ + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORLOOP */ + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) /* OP_FORPREP */ + ,opmode(1, 0, OpArgN, OpArgU, iABC) /* OP_TFORLOOP */ + ,opmode(0, 0, OpArgU, OpArgU, iABC) /* OP_SETLIST */ + ,opmode(0, 0, OpArgN, OpArgN, iABC) /* OP_CLOSE */ + ,opmode(0, 1, OpArgU, OpArgN, iABx) /* OP_CLOSURE */ + ,opmode(0, 1, OpArgU, OpArgN, iABC) /* OP_VARARG */ +}; + diff --git a/extern/lua-5.1.5/src/lopcodes.h b/extern/lua-5.1.5/src/lopcodes.h new file mode 100644 index 00000000..41224d6e --- /dev/null +++ b/extern/lua-5.1.5/src/lopcodes.h @@ -0,0 +1,268 @@ +/* +** $Id: lopcodes.h,v 1.125.1.1 2007/12/27 13:02:25 roberto Exp $ +** Opcodes for Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#ifndef lopcodes_h +#define lopcodes_h + +#include "llimits.h" + + +/*=========================================================================== + We assume that instructions are unsigned numbers. + All instructions have an opcode in the first 6 bits. + Instructions can have the following fields: + `A' : 8 bits + `B' : 9 bits + `C' : 9 bits + `Bx' : 18 bits (`B' and `C' together) + `sBx' : signed Bx + + A signed argument is represented in excess K; that is, the number + value is the unsigned value minus K. K is exactly the maximum value + for that argument (so that -max is represented by 0, and +max is + represented by 2*max), which is half the maximum for the corresponding + unsigned argument. +===========================================================================*/ + + +enum OpMode {iABC, iABx, iAsBx}; /* basic instruction format */ + + +/* +** size and position of opcode arguments. +*/ +#define SIZE_C 9 +#define SIZE_B 9 +#define SIZE_Bx (SIZE_C + SIZE_B) +#define SIZE_A 8 + +#define SIZE_OP 6 + +#define POS_OP 0 +#define POS_A (POS_OP + SIZE_OP) +#define POS_C (POS_A + SIZE_A) +#define POS_B (POS_C + SIZE_C) +#define POS_Bx POS_C + + +/* +** limits for opcode arguments. +** we use (signed) int to manipulate most arguments, +** so they must fit in LUAI_BITSINT-1 bits (-1 for sign) +*/ +#if SIZE_Bx < LUAI_BITSINT-1 +#define MAXARG_Bx ((1<>1) /* `sBx' is signed */ +#else +#define MAXARG_Bx MAX_INT +#define MAXARG_sBx MAX_INT +#endif + + +#define MAXARG_A ((1<>POS_OP) & MASK1(SIZE_OP,0))) +#define SET_OPCODE(i,o) ((i) = (((i)&MASK0(SIZE_OP,POS_OP)) | \ + ((cast(Instruction, o)<>POS_A) & MASK1(SIZE_A,0))) +#define SETARG_A(i,u) ((i) = (((i)&MASK0(SIZE_A,POS_A)) | \ + ((cast(Instruction, u)<>POS_B) & MASK1(SIZE_B,0))) +#define SETARG_B(i,b) ((i) = (((i)&MASK0(SIZE_B,POS_B)) | \ + ((cast(Instruction, b)<>POS_C) & MASK1(SIZE_C,0))) +#define SETARG_C(i,b) ((i) = (((i)&MASK0(SIZE_C,POS_C)) | \ + ((cast(Instruction, b)<>POS_Bx) & MASK1(SIZE_Bx,0))) +#define SETARG_Bx(i,b) ((i) = (((i)&MASK0(SIZE_Bx,POS_Bx)) | \ + ((cast(Instruction, b)< C) then pc++ */ +OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */ + +OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */ +OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */ +OP_RETURN,/* A B return R(A), ... ,R(A+B-2) (see note) */ + +OP_FORLOOP,/* A sBx R(A)+=R(A+2); + if R(A) =) R(A)*/ +OP_CLOSURE,/* A Bx R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n)) */ + +OP_VARARG/* A B R(A), R(A+1), ..., R(A+B-1) = vararg */ +} OpCode; + + +#define NUM_OPCODES (cast(int, OP_VARARG) + 1) + + + +/*=========================================================================== + Notes: + (*) In OP_CALL, if (B == 0) then B = top. C is the number of returns - 1, + and can be 0: OP_CALL then sets `top' to last_result+1, so + next open instruction (OP_CALL, OP_RETURN, OP_SETLIST) may use `top'. + + (*) In OP_VARARG, if (B == 0) then use actual number of varargs and + set top (like in OP_CALL with C == 0). + + (*) In OP_RETURN, if (B == 0) then return up to `top' + + (*) In OP_SETLIST, if (B == 0) then B = `top'; + if (C == 0) then next `instruction' is real C + + (*) For comparisons, A specifies what condition the test should accept + (true or false). + + (*) All `skips' (pc++) assume that next instruction is a jump +===========================================================================*/ + + +/* +** masks for instruction properties. The format is: +** bits 0-1: op mode +** bits 2-3: C arg mode +** bits 4-5: B arg mode +** bit 6: instruction set register A +** bit 7: operator is a test +*/ + +enum OpArgMask { + OpArgN, /* argument is not used */ + OpArgU, /* argument is used */ + OpArgR, /* argument is a register or a jump offset */ + OpArgK /* argument is a constant or register/constant */ +}; + +LUAI_DATA const lu_byte luaP_opmodes[NUM_OPCODES]; + +#define getOpMode(m) (cast(enum OpMode, luaP_opmodes[m] & 3)) +#define getBMode(m) (cast(enum OpArgMask, (luaP_opmodes[m] >> 4) & 3)) +#define getCMode(m) (cast(enum OpArgMask, (luaP_opmodes[m] >> 2) & 3)) +#define testAMode(m) (luaP_opmodes[m] & (1 << 6)) +#define testTMode(m) (luaP_opmodes[m] & (1 << 7)) + + +LUAI_DATA const char *const luaP_opnames[NUM_OPCODES+1]; /* opcode names */ + + +/* number of list items to accumulate before a SETLIST instruction */ +#define LFIELDS_PER_FLUSH 50 + + +#endif diff --git a/extern/lua-5.1.5/src/loslib.c b/extern/lua-5.1.5/src/loslib.c new file mode 100644 index 00000000..da06a572 --- /dev/null +++ b/extern/lua-5.1.5/src/loslib.c @@ -0,0 +1,243 @@ +/* +** $Id: loslib.c,v 1.19.1.3 2008/01/18 16:38:18 roberto Exp $ +** Standard Operating System library +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include +#include +#include + +#define loslib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +static int os_pushresult (lua_State *L, int i, const char *filename) { + int en = errno; /* calls to Lua API may change this value */ + if (i) { + lua_pushboolean(L, 1); + return 1; + } + else { + lua_pushnil(L); + lua_pushfstring(L, "%s: %s", filename, strerror(en)); + lua_pushinteger(L, en); + return 3; + } +} + + +static int os_execute (lua_State *L) { + lua_pushinteger(L, system(luaL_optstring(L, 1, NULL))); + return 1; +} + + +static int os_remove (lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + return os_pushresult(L, remove(filename) == 0, filename); +} + + +static int os_rename (lua_State *L) { + const char *fromname = luaL_checkstring(L, 1); + const char *toname = luaL_checkstring(L, 2); + return os_pushresult(L, rename(fromname, toname) == 0, fromname); +} + + +static int os_tmpname (lua_State *L) { + char buff[LUA_TMPNAMBUFSIZE]; + int err; + lua_tmpnam(buff, err); + if (err) + return luaL_error(L, "unable to generate a unique filename"); + lua_pushstring(L, buff); + return 1; +} + + +static int os_getenv (lua_State *L) { + lua_pushstring(L, getenv(luaL_checkstring(L, 1))); /* if NULL push nil */ + return 1; +} + + +static int os_clock (lua_State *L) { + lua_pushnumber(L, ((lua_Number)clock())/(lua_Number)CLOCKS_PER_SEC); + return 1; +} + + +/* +** {====================================================== +** Time/Date operations +** { year=%Y, month=%m, day=%d, hour=%H, min=%M, sec=%S, +** wday=%w+1, yday=%j, isdst=? } +** ======================================================= +*/ + +static void setfield (lua_State *L, const char *key, int value) { + lua_pushinteger(L, value); + lua_setfield(L, -2, key); +} + +static void setboolfield (lua_State *L, const char *key, int value) { + if (value < 0) /* undefined? */ + return; /* does not set field */ + lua_pushboolean(L, value); + lua_setfield(L, -2, key); +} + +static int getboolfield (lua_State *L, const char *key) { + int res; + lua_getfield(L, -1, key); + res = lua_isnil(L, -1) ? -1 : lua_toboolean(L, -1); + lua_pop(L, 1); + return res; +} + + +static int getfield (lua_State *L, const char *key, int d) { + int res; + lua_getfield(L, -1, key); + if (lua_isnumber(L, -1)) + res = (int)lua_tointeger(L, -1); + else { + if (d < 0) + return luaL_error(L, "field " LUA_QS " missing in date table", key); + res = d; + } + lua_pop(L, 1); + return res; +} + + +static int os_date (lua_State *L) { + const char *s = luaL_optstring(L, 1, "%c"); + time_t t = luaL_opt(L, (time_t)luaL_checknumber, 2, time(NULL)); + struct tm *stm; + if (*s == '!') { /* UTC? */ + stm = gmtime(&t); + s++; /* skip `!' */ + } + else + stm = localtime(&t); + if (stm == NULL) /* invalid date? */ + lua_pushnil(L); + else if (strcmp(s, "*t") == 0) { + lua_createtable(L, 0, 9); /* 9 = number of fields */ + setfield(L, "sec", stm->tm_sec); + setfield(L, "min", stm->tm_min); + setfield(L, "hour", stm->tm_hour); + setfield(L, "day", stm->tm_mday); + setfield(L, "month", stm->tm_mon+1); + setfield(L, "year", stm->tm_year+1900); + setfield(L, "wday", stm->tm_wday+1); + setfield(L, "yday", stm->tm_yday+1); + setboolfield(L, "isdst", stm->tm_isdst); + } + else { + char cc[3]; + luaL_Buffer b; + cc[0] = '%'; cc[2] = '\0'; + luaL_buffinit(L, &b); + for (; *s; s++) { + if (*s != '%' || *(s + 1) == '\0') /* no conversion specifier? */ + luaL_addchar(&b, *s); + else { + size_t reslen; + char buff[200]; /* should be big enough for any conversion result */ + cc[1] = *(++s); + reslen = strftime(buff, sizeof(buff), cc, stm); + luaL_addlstring(&b, buff, reslen); + } + } + luaL_pushresult(&b); + } + return 1; +} + + +static int os_time (lua_State *L) { + time_t t; + if (lua_isnoneornil(L, 1)) /* called without args? */ + t = time(NULL); /* get current time */ + else { + struct tm ts; + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 1); /* make sure table is at the top */ + ts.tm_sec = getfield(L, "sec", 0); + ts.tm_min = getfield(L, "min", 0); + ts.tm_hour = getfield(L, "hour", 12); + ts.tm_mday = getfield(L, "day", -1); + ts.tm_mon = getfield(L, "month", -1) - 1; + ts.tm_year = getfield(L, "year", -1) - 1900; + ts.tm_isdst = getboolfield(L, "isdst"); + t = mktime(&ts); + } + if (t == (time_t)(-1)) + lua_pushnil(L); + else + lua_pushnumber(L, (lua_Number)t); + return 1; +} + + +static int os_difftime (lua_State *L) { + lua_pushnumber(L, difftime((time_t)(luaL_checknumber(L, 1)), + (time_t)(luaL_optnumber(L, 2, 0)))); + return 1; +} + +/* }====================================================== */ + + +static int os_setlocale (lua_State *L) { + static const int cat[] = {LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, + LC_NUMERIC, LC_TIME}; + static const char *const catnames[] = {"all", "collate", "ctype", "monetary", + "numeric", "time", NULL}; + const char *l = luaL_optstring(L, 1, NULL); + int op = luaL_checkoption(L, 2, "all", catnames); + lua_pushstring(L, setlocale(cat[op], l)); + return 1; +} + + +static int os_exit (lua_State *L) { + exit(luaL_optint(L, 1, EXIT_SUCCESS)); +} + +static const luaL_Reg syslib[] = { + {"clock", os_clock}, + {"date", os_date}, + {"difftime", os_difftime}, + {"execute", os_execute}, + {"exit", os_exit}, + {"getenv", os_getenv}, + {"remove", os_remove}, + {"rename", os_rename}, + {"setlocale", os_setlocale}, + {"time", os_time}, + {"tmpname", os_tmpname}, + {NULL, NULL} +}; + +/* }====================================================== */ + + + +LUALIB_API int luaopen_os (lua_State *L) { + luaL_register(L, LUA_OSLIBNAME, syslib); + return 1; +} + diff --git a/extern/lua-5.1.5/src/lparser.c b/extern/lua-5.1.5/src/lparser.c new file mode 100644 index 00000000..dda7488d --- /dev/null +++ b/extern/lua-5.1.5/src/lparser.c @@ -0,0 +1,1339 @@ +/* +** $Id: lparser.c,v 2.42.1.4 2011/10/21 19:31:42 roberto Exp $ +** Lua Parser +** See Copyright Notice in lua.h +*/ + + +#include + +#define lparser_c +#define LUA_CORE + +#include "lua.h" + +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "llex.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" + + + +#define hasmultret(k) ((k) == VCALL || (k) == VVARARG) + +#define getlocvar(fs, i) ((fs)->f->locvars[(fs)->actvar[i]]) + +#define luaY_checklimit(fs,v,l,m) if ((v)>(l)) errorlimit(fs,l,m) + + +/* +** nodes for block list (list of active blocks) +*/ +typedef struct BlockCnt { + struct BlockCnt *previous; /* chain */ + int breaklist; /* list of jumps out of this loop */ + lu_byte nactvar; /* # active locals outside the breakable structure */ + lu_byte upval; /* true if some variable in the block is an upvalue */ + lu_byte isbreakable; /* true if `block' is a loop */ +} BlockCnt; + + + +/* +** prototypes for recursive non-terminal functions +*/ +static void chunk (LexState *ls); +static void expr (LexState *ls, expdesc *v); + + +static void anchor_token (LexState *ls) { + if (ls->t.token == TK_NAME || ls->t.token == TK_STRING) { + TString *ts = ls->t.seminfo.ts; + luaX_newstring(ls, getstr(ts), ts->tsv.len); + } +} + + +static void error_expected (LexState *ls, int token) { + luaX_syntaxerror(ls, + luaO_pushfstring(ls->L, LUA_QS " expected", luaX_token2str(ls, token))); +} + + +static void errorlimit (FuncState *fs, int limit, const char *what) { + const char *msg = (fs->f->linedefined == 0) ? + luaO_pushfstring(fs->L, "main function has more than %d %s", limit, what) : + luaO_pushfstring(fs->L, "function at line %d has more than %d %s", + fs->f->linedefined, limit, what); + luaX_lexerror(fs->ls, msg, 0); +} + + +static int testnext (LexState *ls, int c) { + if (ls->t.token == c) { + luaX_next(ls); + return 1; + } + else return 0; +} + + +static void check (LexState *ls, int c) { + if (ls->t.token != c) + error_expected(ls, c); +} + +static void checknext (LexState *ls, int c) { + check(ls, c); + luaX_next(ls); +} + + +#define check_condition(ls,c,msg) { if (!(c)) luaX_syntaxerror(ls, msg); } + + + +static void check_match (LexState *ls, int what, int who, int where) { + if (!testnext(ls, what)) { + if (where == ls->linenumber) + error_expected(ls, what); + else { + luaX_syntaxerror(ls, luaO_pushfstring(ls->L, + LUA_QS " expected (to close " LUA_QS " at line %d)", + luaX_token2str(ls, what), luaX_token2str(ls, who), where)); + } + } +} + + +static TString *str_checkname (LexState *ls) { + TString *ts; + check(ls, TK_NAME); + ts = ls->t.seminfo.ts; + luaX_next(ls); + return ts; +} + + +static void init_exp (expdesc *e, expkind k, int i) { + e->f = e->t = NO_JUMP; + e->k = k; + e->u.s.info = i; +} + + +static void codestring (LexState *ls, expdesc *e, TString *s) { + init_exp(e, VK, luaK_stringK(ls->fs, s)); +} + + +static void checkname(LexState *ls, expdesc *e) { + codestring(ls, e, str_checkname(ls)); +} + + +static int registerlocalvar (LexState *ls, TString *varname) { + FuncState *fs = ls->fs; + Proto *f = fs->f; + int oldsize = f->sizelocvars; + luaM_growvector(ls->L, f->locvars, fs->nlocvars, f->sizelocvars, + LocVar, SHRT_MAX, "too many local variables"); + while (oldsize < f->sizelocvars) f->locvars[oldsize++].varname = NULL; + f->locvars[fs->nlocvars].varname = varname; + luaC_objbarrier(ls->L, f, varname); + return fs->nlocvars++; +} + + +#define new_localvarliteral(ls,v,n) \ + new_localvar(ls, luaX_newstring(ls, "" v, (sizeof(v)/sizeof(char))-1), n) + + +static void new_localvar (LexState *ls, TString *name, int n) { + FuncState *fs = ls->fs; + luaY_checklimit(fs, fs->nactvar+n+1, LUAI_MAXVARS, "local variables"); + fs->actvar[fs->nactvar+n] = cast(unsigned short, registerlocalvar(ls, name)); +} + + +static void adjustlocalvars (LexState *ls, int nvars) { + FuncState *fs = ls->fs; + fs->nactvar = cast_byte(fs->nactvar + nvars); + for (; nvars; nvars--) { + getlocvar(fs, fs->nactvar - nvars).startpc = fs->pc; + } +} + + +static void removevars (LexState *ls, int tolevel) { + FuncState *fs = ls->fs; + while (fs->nactvar > tolevel) + getlocvar(fs, --fs->nactvar).endpc = fs->pc; +} + + +static int indexupvalue (FuncState *fs, TString *name, expdesc *v) { + int i; + Proto *f = fs->f; + int oldsize = f->sizeupvalues; + for (i=0; inups; i++) { + if (fs->upvalues[i].k == v->k && fs->upvalues[i].info == v->u.s.info) { + lua_assert(f->upvalues[i] == name); + return i; + } + } + /* new one */ + luaY_checklimit(fs, f->nups + 1, LUAI_MAXUPVALUES, "upvalues"); + luaM_growvector(fs->L, f->upvalues, f->nups, f->sizeupvalues, + TString *, MAX_INT, ""); + while (oldsize < f->sizeupvalues) f->upvalues[oldsize++] = NULL; + f->upvalues[f->nups] = name; + luaC_objbarrier(fs->L, f, name); + lua_assert(v->k == VLOCAL || v->k == VUPVAL); + fs->upvalues[f->nups].k = cast_byte(v->k); + fs->upvalues[f->nups].info = cast_byte(v->u.s.info); + return f->nups++; +} + + +static int searchvar (FuncState *fs, TString *n) { + int i; + for (i=fs->nactvar-1; i >= 0; i--) { + if (n == getlocvar(fs, i).varname) + return i; + } + return -1; /* not found */ +} + + +static void markupval (FuncState *fs, int level) { + BlockCnt *bl = fs->bl; + while (bl && bl->nactvar > level) bl = bl->previous; + if (bl) bl->upval = 1; +} + + +static int singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) { + if (fs == NULL) { /* no more levels? */ + init_exp(var, VGLOBAL, NO_REG); /* default is global variable */ + return VGLOBAL; + } + else { + int v = searchvar(fs, n); /* look up at current level */ + if (v >= 0) { + init_exp(var, VLOCAL, v); + if (!base) + markupval(fs, v); /* local will be used as an upval */ + return VLOCAL; + } + else { /* not found at current level; try upper one */ + if (singlevaraux(fs->prev, n, var, 0) == VGLOBAL) + return VGLOBAL; + var->u.s.info = indexupvalue(fs, n, var); /* else was LOCAL or UPVAL */ + var->k = VUPVAL; /* upvalue in this level */ + return VUPVAL; + } + } +} + + +static void singlevar (LexState *ls, expdesc *var) { + TString *varname = str_checkname(ls); + FuncState *fs = ls->fs; + if (singlevaraux(fs, varname, var, 1) == VGLOBAL) + var->u.s.info = luaK_stringK(fs, varname); /* info points to global name */ +} + + +static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) { + FuncState *fs = ls->fs; + int extra = nvars - nexps; + if (hasmultret(e->k)) { + extra++; /* includes call itself */ + if (extra < 0) extra = 0; + luaK_setreturns(fs, e, extra); /* last exp. provides the difference */ + if (extra > 1) luaK_reserveregs(fs, extra-1); + } + else { + if (e->k != VVOID) luaK_exp2nextreg(fs, e); /* close last expression */ + if (extra > 0) { + int reg = fs->freereg; + luaK_reserveregs(fs, extra); + luaK_nil(fs, reg, extra); + } + } +} + + +static void enterlevel (LexState *ls) { + if (++ls->L->nCcalls > LUAI_MAXCCALLS) + luaX_lexerror(ls, "chunk has too many syntax levels", 0); +} + + +#define leavelevel(ls) ((ls)->L->nCcalls--) + + +static void enterblock (FuncState *fs, BlockCnt *bl, lu_byte isbreakable) { + bl->breaklist = NO_JUMP; + bl->isbreakable = isbreakable; + bl->nactvar = fs->nactvar; + bl->upval = 0; + bl->previous = fs->bl; + fs->bl = bl; + lua_assert(fs->freereg == fs->nactvar); +} + + +static void leaveblock (FuncState *fs) { + BlockCnt *bl = fs->bl; + fs->bl = bl->previous; + removevars(fs->ls, bl->nactvar); + if (bl->upval) + luaK_codeABC(fs, OP_CLOSE, bl->nactvar, 0, 0); + /* a block either controls scope or breaks (never both) */ + lua_assert(!bl->isbreakable || !bl->upval); + lua_assert(bl->nactvar == fs->nactvar); + fs->freereg = fs->nactvar; /* free registers */ + luaK_patchtohere(fs, bl->breaklist); +} + + +static void pushclosure (LexState *ls, FuncState *func, expdesc *v) { + FuncState *fs = ls->fs; + Proto *f = fs->f; + int oldsize = f->sizep; + int i; + luaM_growvector(ls->L, f->p, fs->np, f->sizep, Proto *, + MAXARG_Bx, "constant table overflow"); + while (oldsize < f->sizep) f->p[oldsize++] = NULL; + f->p[fs->np++] = func->f; + luaC_objbarrier(ls->L, f, func->f); + init_exp(v, VRELOCABLE, luaK_codeABx(fs, OP_CLOSURE, 0, fs->np-1)); + for (i=0; if->nups; i++) { + OpCode o = (func->upvalues[i].k == VLOCAL) ? OP_MOVE : OP_GETUPVAL; + luaK_codeABC(fs, o, 0, func->upvalues[i].info, 0); + } +} + + +static void open_func (LexState *ls, FuncState *fs) { + lua_State *L = ls->L; + Proto *f = luaF_newproto(L); + fs->f = f; + fs->prev = ls->fs; /* linked list of funcstates */ + fs->ls = ls; + fs->L = L; + ls->fs = fs; + fs->pc = 0; + fs->lasttarget = -1; + fs->jpc = NO_JUMP; + fs->freereg = 0; + fs->nk = 0; + fs->np = 0; + fs->nlocvars = 0; + fs->nactvar = 0; + fs->bl = NULL; + f->source = ls->source; + f->maxstacksize = 2; /* registers 0/1 are always valid */ + fs->h = luaH_new(L, 0, 0); + /* anchor table of constants and prototype (to avoid being collected) */ + sethvalue2s(L, L->top, fs->h); + incr_top(L); + setptvalue2s(L, L->top, f); + incr_top(L); +} + + +static void close_func (LexState *ls) { + lua_State *L = ls->L; + FuncState *fs = ls->fs; + Proto *f = fs->f; + removevars(ls, 0); + luaK_ret(fs, 0, 0); /* final return */ + luaM_reallocvector(L, f->code, f->sizecode, fs->pc, Instruction); + f->sizecode = fs->pc; + luaM_reallocvector(L, f->lineinfo, f->sizelineinfo, fs->pc, int); + f->sizelineinfo = fs->pc; + luaM_reallocvector(L, f->k, f->sizek, fs->nk, TValue); + f->sizek = fs->nk; + luaM_reallocvector(L, f->p, f->sizep, fs->np, Proto *); + f->sizep = fs->np; + luaM_reallocvector(L, f->locvars, f->sizelocvars, fs->nlocvars, LocVar); + f->sizelocvars = fs->nlocvars; + luaM_reallocvector(L, f->upvalues, f->sizeupvalues, f->nups, TString *); + f->sizeupvalues = f->nups; + lua_assert(luaG_checkcode(f)); + lua_assert(fs->bl == NULL); + ls->fs = fs->prev; + /* last token read was anchored in defunct function; must reanchor it */ + if (fs) anchor_token(ls); + L->top -= 2; /* remove table and prototype from the stack */ +} + + +Proto *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, const char *name) { + struct LexState lexstate; + struct FuncState funcstate; + lexstate.buff = buff; + luaX_setinput(L, &lexstate, z, luaS_new(L, name)); + open_func(&lexstate, &funcstate); + funcstate.f->is_vararg = VARARG_ISVARARG; /* main func. is always vararg */ + luaX_next(&lexstate); /* read first token */ + chunk(&lexstate); + check(&lexstate, TK_EOS); + close_func(&lexstate); + lua_assert(funcstate.prev == NULL); + lua_assert(funcstate.f->nups == 0); + lua_assert(lexstate.fs == NULL); + return funcstate.f; +} + + + +/*============================================================*/ +/* GRAMMAR RULES */ +/*============================================================*/ + + +static void field (LexState *ls, expdesc *v) { + /* field -> ['.' | ':'] NAME */ + FuncState *fs = ls->fs; + expdesc key; + luaK_exp2anyreg(fs, v); + luaX_next(ls); /* skip the dot or colon */ + checkname(ls, &key); + luaK_indexed(fs, v, &key); +} + + +static void yindex (LexState *ls, expdesc *v) { + /* index -> '[' expr ']' */ + luaX_next(ls); /* skip the '[' */ + expr(ls, v); + luaK_exp2val(ls->fs, v); + checknext(ls, ']'); +} + + +/* +** {====================================================================== +** Rules for Constructors +** ======================================================================= +*/ + + +struct ConsControl { + expdesc v; /* last list item read */ + expdesc *t; /* table descriptor */ + int nh; /* total number of `record' elements */ + int na; /* total number of array elements */ + int tostore; /* number of array elements pending to be stored */ +}; + + +static void recfield (LexState *ls, struct ConsControl *cc) { + /* recfield -> (NAME | `['exp1`]') = exp1 */ + FuncState *fs = ls->fs; + int reg = ls->fs->freereg; + expdesc key, val; + int rkkey; + if (ls->t.token == TK_NAME) { + luaY_checklimit(fs, cc->nh, MAX_INT, "items in a constructor"); + checkname(ls, &key); + } + else /* ls->t.token == '[' */ + yindex(ls, &key); + cc->nh++; + checknext(ls, '='); + rkkey = luaK_exp2RK(fs, &key); + expr(ls, &val); + luaK_codeABC(fs, OP_SETTABLE, cc->t->u.s.info, rkkey, luaK_exp2RK(fs, &val)); + fs->freereg = reg; /* free registers */ +} + + +static void closelistfield (FuncState *fs, struct ConsControl *cc) { + if (cc->v.k == VVOID) return; /* there is no list item */ + luaK_exp2nextreg(fs, &cc->v); + cc->v.k = VVOID; + if (cc->tostore == LFIELDS_PER_FLUSH) { + luaK_setlist(fs, cc->t->u.s.info, cc->na, cc->tostore); /* flush */ + cc->tostore = 0; /* no more items pending */ + } +} + + +static void lastlistfield (FuncState *fs, struct ConsControl *cc) { + if (cc->tostore == 0) return; + if (hasmultret(cc->v.k)) { + luaK_setmultret(fs, &cc->v); + luaK_setlist(fs, cc->t->u.s.info, cc->na, LUA_MULTRET); + cc->na--; /* do not count last expression (unknown number of elements) */ + } + else { + if (cc->v.k != VVOID) + luaK_exp2nextreg(fs, &cc->v); + luaK_setlist(fs, cc->t->u.s.info, cc->na, cc->tostore); + } +} + + +static void listfield (LexState *ls, struct ConsControl *cc) { + expr(ls, &cc->v); + luaY_checklimit(ls->fs, cc->na, MAX_INT, "items in a constructor"); + cc->na++; + cc->tostore++; +} + + +static void constructor (LexState *ls, expdesc *t) { + /* constructor -> ?? */ + FuncState *fs = ls->fs; + int line = ls->linenumber; + int pc = luaK_codeABC(fs, OP_NEWTABLE, 0, 0, 0); + struct ConsControl cc; + cc.na = cc.nh = cc.tostore = 0; + cc.t = t; + init_exp(t, VRELOCABLE, pc); + init_exp(&cc.v, VVOID, 0); /* no value (yet) */ + luaK_exp2nextreg(ls->fs, t); /* fix it at stack top (for gc) */ + checknext(ls, '{'); + do { + lua_assert(cc.v.k == VVOID || cc.tostore > 0); + if (ls->t.token == '}') break; + closelistfield(fs, &cc); + switch(ls->t.token) { + case TK_NAME: { /* may be listfields or recfields */ + luaX_lookahead(ls); + if (ls->lookahead.token != '=') /* expression? */ + listfield(ls, &cc); + else + recfield(ls, &cc); + break; + } + case '[': { /* constructor_item -> recfield */ + recfield(ls, &cc); + break; + } + default: { /* constructor_part -> listfield */ + listfield(ls, &cc); + break; + } + } + } while (testnext(ls, ',') || testnext(ls, ';')); + check_match(ls, '}', '{', line); + lastlistfield(fs, &cc); + SETARG_B(fs->f->code[pc], luaO_int2fb(cc.na)); /* set initial array size */ + SETARG_C(fs->f->code[pc], luaO_int2fb(cc.nh)); /* set initial table size */ +} + +/* }====================================================================== */ + + + +static void parlist (LexState *ls) { + /* parlist -> [ param { `,' param } ] */ + FuncState *fs = ls->fs; + Proto *f = fs->f; + int nparams = 0; + f->is_vararg = 0; + if (ls->t.token != ')') { /* is `parlist' not empty? */ + do { + switch (ls->t.token) { + case TK_NAME: { /* param -> NAME */ + new_localvar(ls, str_checkname(ls), nparams++); + break; + } + case TK_DOTS: { /* param -> `...' */ + luaX_next(ls); +#if defined(LUA_COMPAT_VARARG) + /* use `arg' as default name */ + new_localvarliteral(ls, "arg", nparams++); + f->is_vararg = VARARG_HASARG | VARARG_NEEDSARG; +#endif + f->is_vararg |= VARARG_ISVARARG; + break; + } + default: luaX_syntaxerror(ls, " or " LUA_QL("...") " expected"); + } + } while (!f->is_vararg && testnext(ls, ',')); + } + adjustlocalvars(ls, nparams); + f->numparams = cast_byte(fs->nactvar - (f->is_vararg & VARARG_HASARG)); + luaK_reserveregs(fs, fs->nactvar); /* reserve register for parameters */ +} + + +static void body (LexState *ls, expdesc *e, int needself, int line) { + /* body -> `(' parlist `)' chunk END */ + FuncState new_fs; + open_func(ls, &new_fs); + new_fs.f->linedefined = line; + checknext(ls, '('); + if (needself) { + new_localvarliteral(ls, "self", 0); + adjustlocalvars(ls, 1); + } + parlist(ls); + checknext(ls, ')'); + chunk(ls); + new_fs.f->lastlinedefined = ls->linenumber; + check_match(ls, TK_END, TK_FUNCTION, line); + close_func(ls); + pushclosure(ls, &new_fs, e); +} + + +static int explist1 (LexState *ls, expdesc *v) { + /* explist1 -> expr { `,' expr } */ + int n = 1; /* at least one expression */ + expr(ls, v); + while (testnext(ls, ',')) { + luaK_exp2nextreg(ls->fs, v); + expr(ls, v); + n++; + } + return n; +} + + +static void funcargs (LexState *ls, expdesc *f) { + FuncState *fs = ls->fs; + expdesc args; + int base, nparams; + int line = ls->linenumber; + switch (ls->t.token) { + case '(': { /* funcargs -> `(' [ explist1 ] `)' */ + if (line != ls->lastline) + luaX_syntaxerror(ls,"ambiguous syntax (function call x new statement)"); + luaX_next(ls); + if (ls->t.token == ')') /* arg list is empty? */ + args.k = VVOID; + else { + explist1(ls, &args); + luaK_setmultret(fs, &args); + } + check_match(ls, ')', '(', line); + break; + } + case '{': { /* funcargs -> constructor */ + constructor(ls, &args); + break; + } + case TK_STRING: { /* funcargs -> STRING */ + codestring(ls, &args, ls->t.seminfo.ts); + luaX_next(ls); /* must use `seminfo' before `next' */ + break; + } + default: { + luaX_syntaxerror(ls, "function arguments expected"); + return; + } + } + lua_assert(f->k == VNONRELOC); + base = f->u.s.info; /* base register for call */ + if (hasmultret(args.k)) + nparams = LUA_MULTRET; /* open call */ + else { + if (args.k != VVOID) + luaK_exp2nextreg(fs, &args); /* close last argument */ + nparams = fs->freereg - (base+1); + } + init_exp(f, VCALL, luaK_codeABC(fs, OP_CALL, base, nparams+1, 2)); + luaK_fixline(fs, line); + fs->freereg = base+1; /* call remove function and arguments and leaves + (unless changed) one result */ +} + + + + +/* +** {====================================================================== +** Expression parsing +** ======================================================================= +*/ + + +static void prefixexp (LexState *ls, expdesc *v) { + /* prefixexp -> NAME | '(' expr ')' */ + switch (ls->t.token) { + case '(': { + int line = ls->linenumber; + luaX_next(ls); + expr(ls, v); + check_match(ls, ')', '(', line); + luaK_dischargevars(ls->fs, v); + return; + } + case TK_NAME: { + singlevar(ls, v); + return; + } + default: { + luaX_syntaxerror(ls, "unexpected symbol"); + return; + } + } +} + + +static void primaryexp (LexState *ls, expdesc *v) { + /* primaryexp -> + prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs } */ + FuncState *fs = ls->fs; + prefixexp(ls, v); + for (;;) { + switch (ls->t.token) { + case '.': { /* field */ + field(ls, v); + break; + } + case '[': { /* `[' exp1 `]' */ + expdesc key; + luaK_exp2anyreg(fs, v); + yindex(ls, &key); + luaK_indexed(fs, v, &key); + break; + } + case ':': { /* `:' NAME funcargs */ + expdesc key; + luaX_next(ls); + checkname(ls, &key); + luaK_self(fs, v, &key); + funcargs(ls, v); + break; + } + case '(': case TK_STRING: case '{': { /* funcargs */ + luaK_exp2nextreg(fs, v); + funcargs(ls, v); + break; + } + default: return; + } + } +} + + +static void simpleexp (LexState *ls, expdesc *v) { + /* simpleexp -> NUMBER | STRING | NIL | true | false | ... | + constructor | FUNCTION body | primaryexp */ + switch (ls->t.token) { + case TK_NUMBER: { + init_exp(v, VKNUM, 0); + v->u.nval = ls->t.seminfo.r; + break; + } + case TK_STRING: { + codestring(ls, v, ls->t.seminfo.ts); + break; + } + case TK_NIL: { + init_exp(v, VNIL, 0); + break; + } + case TK_TRUE: { + init_exp(v, VTRUE, 0); + break; + } + case TK_FALSE: { + init_exp(v, VFALSE, 0); + break; + } + case TK_DOTS: { /* vararg */ + FuncState *fs = ls->fs; + check_condition(ls, fs->f->is_vararg, + "cannot use " LUA_QL("...") " outside a vararg function"); + fs->f->is_vararg &= ~VARARG_NEEDSARG; /* don't need 'arg' */ + init_exp(v, VVARARG, luaK_codeABC(fs, OP_VARARG, 0, 1, 0)); + break; + } + case '{': { /* constructor */ + constructor(ls, v); + return; + } + case TK_FUNCTION: { + luaX_next(ls); + body(ls, v, 0, ls->linenumber); + return; + } + default: { + primaryexp(ls, v); + return; + } + } + luaX_next(ls); +} + + +static UnOpr getunopr (int op) { + switch (op) { + case TK_NOT: return OPR_NOT; + case '-': return OPR_MINUS; + case '#': return OPR_LEN; + default: return OPR_NOUNOPR; + } +} + + +static BinOpr getbinopr (int op) { + switch (op) { + case '+': return OPR_ADD; + case '-': return OPR_SUB; + case '*': return OPR_MUL; + case '/': return OPR_DIV; + case '%': return OPR_MOD; + case '^': return OPR_POW; + case TK_CONCAT: return OPR_CONCAT; + case TK_NE: return OPR_NE; + case TK_EQ: return OPR_EQ; + case '<': return OPR_LT; + case TK_LE: return OPR_LE; + case '>': return OPR_GT; + case TK_GE: return OPR_GE; + case TK_AND: return OPR_AND; + case TK_OR: return OPR_OR; + default: return OPR_NOBINOPR; + } +} + + +static const struct { + lu_byte left; /* left priority for each binary operator */ + lu_byte right; /* right priority */ +} priority[] = { /* ORDER OPR */ + {6, 6}, {6, 6}, {7, 7}, {7, 7}, {7, 7}, /* `+' `-' `/' `%' */ + {10, 9}, {5, 4}, /* power and concat (right associative) */ + {3, 3}, {3, 3}, /* equality and inequality */ + {3, 3}, {3, 3}, {3, 3}, {3, 3}, /* order */ + {2, 2}, {1, 1} /* logical (and/or) */ +}; + +#define UNARY_PRIORITY 8 /* priority for unary operators */ + + +/* +** subexpr -> (simpleexp | unop subexpr) { binop subexpr } +** where `binop' is any binary operator with a priority higher than `limit' +*/ +static BinOpr subexpr (LexState *ls, expdesc *v, unsigned int limit) { + BinOpr op; + UnOpr uop; + enterlevel(ls); + uop = getunopr(ls->t.token); + if (uop != OPR_NOUNOPR) { + luaX_next(ls); + subexpr(ls, v, UNARY_PRIORITY); + luaK_prefix(ls->fs, uop, v); + } + else simpleexp(ls, v); + /* expand while operators have priorities higher than `limit' */ + op = getbinopr(ls->t.token); + while (op != OPR_NOBINOPR && priority[op].left > limit) { + expdesc v2; + BinOpr nextop; + luaX_next(ls); + luaK_infix(ls->fs, op, v); + /* read sub-expression with higher priority */ + nextop = subexpr(ls, &v2, priority[op].right); + luaK_posfix(ls->fs, op, v, &v2); + op = nextop; + } + leavelevel(ls); + return op; /* return first untreated operator */ +} + + +static void expr (LexState *ls, expdesc *v) { + subexpr(ls, v, 0); +} + +/* }==================================================================== */ + + + +/* +** {====================================================================== +** Rules for Statements +** ======================================================================= +*/ + + +static int block_follow (int token) { + switch (token) { + case TK_ELSE: case TK_ELSEIF: case TK_END: + case TK_UNTIL: case TK_EOS: + return 1; + default: return 0; + } +} + + +static void block (LexState *ls) { + /* block -> chunk */ + FuncState *fs = ls->fs; + BlockCnt bl; + enterblock(fs, &bl, 0); + chunk(ls); + lua_assert(bl.breaklist == NO_JUMP); + leaveblock(fs); +} + + +/* +** structure to chain all variables in the left-hand side of an +** assignment +*/ +struct LHS_assign { + struct LHS_assign *prev; + expdesc v; /* variable (global, local, upvalue, or indexed) */ +}; + + +/* +** check whether, in an assignment to a local variable, the local variable +** is needed in a previous assignment (to a table). If so, save original +** local value in a safe place and use this safe copy in the previous +** assignment. +*/ +static void check_conflict (LexState *ls, struct LHS_assign *lh, expdesc *v) { + FuncState *fs = ls->fs; + int extra = fs->freereg; /* eventual position to save local variable */ + int conflict = 0; + for (; lh; lh = lh->prev) { + if (lh->v.k == VINDEXED) { + if (lh->v.u.s.info == v->u.s.info) { /* conflict? */ + conflict = 1; + lh->v.u.s.info = extra; /* previous assignment will use safe copy */ + } + if (lh->v.u.s.aux == v->u.s.info) { /* conflict? */ + conflict = 1; + lh->v.u.s.aux = extra; /* previous assignment will use safe copy */ + } + } + } + if (conflict) { + luaK_codeABC(fs, OP_MOVE, fs->freereg, v->u.s.info, 0); /* make copy */ + luaK_reserveregs(fs, 1); + } +} + + +static void assignment (LexState *ls, struct LHS_assign *lh, int nvars) { + expdesc e; + check_condition(ls, VLOCAL <= lh->v.k && lh->v.k <= VINDEXED, + "syntax error"); + if (testnext(ls, ',')) { /* assignment -> `,' primaryexp assignment */ + struct LHS_assign nv; + nv.prev = lh; + primaryexp(ls, &nv.v); + if (nv.v.k == VLOCAL) + check_conflict(ls, lh, &nv.v); + luaY_checklimit(ls->fs, nvars, LUAI_MAXCCALLS - ls->L->nCcalls, + "variables in assignment"); + assignment(ls, &nv, nvars+1); + } + else { /* assignment -> `=' explist1 */ + int nexps; + checknext(ls, '='); + nexps = explist1(ls, &e); + if (nexps != nvars) { + adjust_assign(ls, nvars, nexps, &e); + if (nexps > nvars) + ls->fs->freereg -= nexps - nvars; /* remove extra values */ + } + else { + luaK_setoneret(ls->fs, &e); /* close last expression */ + luaK_storevar(ls->fs, &lh->v, &e); + return; /* avoid default */ + } + } + init_exp(&e, VNONRELOC, ls->fs->freereg-1); /* default assignment */ + luaK_storevar(ls->fs, &lh->v, &e); +} + + +static int cond (LexState *ls) { + /* cond -> exp */ + expdesc v; + expr(ls, &v); /* read condition */ + if (v.k == VNIL) v.k = VFALSE; /* `falses' are all equal here */ + luaK_goiftrue(ls->fs, &v); + return v.f; +} + + +static void breakstat (LexState *ls) { + FuncState *fs = ls->fs; + BlockCnt *bl = fs->bl; + int upval = 0; + while (bl && !bl->isbreakable) { + upval |= bl->upval; + bl = bl->previous; + } + if (!bl) + luaX_syntaxerror(ls, "no loop to break"); + if (upval) + luaK_codeABC(fs, OP_CLOSE, bl->nactvar, 0, 0); + luaK_concat(fs, &bl->breaklist, luaK_jump(fs)); +} + + +static void whilestat (LexState *ls, int line) { + /* whilestat -> WHILE cond DO block END */ + FuncState *fs = ls->fs; + int whileinit; + int condexit; + BlockCnt bl; + luaX_next(ls); /* skip WHILE */ + whileinit = luaK_getlabel(fs); + condexit = cond(ls); + enterblock(fs, &bl, 1); + checknext(ls, TK_DO); + block(ls); + luaK_patchlist(fs, luaK_jump(fs), whileinit); + check_match(ls, TK_END, TK_WHILE, line); + leaveblock(fs); + luaK_patchtohere(fs, condexit); /* false conditions finish the loop */ +} + + +static void repeatstat (LexState *ls, int line) { + /* repeatstat -> REPEAT block UNTIL cond */ + int condexit; + FuncState *fs = ls->fs; + int repeat_init = luaK_getlabel(fs); + BlockCnt bl1, bl2; + enterblock(fs, &bl1, 1); /* loop block */ + enterblock(fs, &bl2, 0); /* scope block */ + luaX_next(ls); /* skip REPEAT */ + chunk(ls); + check_match(ls, TK_UNTIL, TK_REPEAT, line); + condexit = cond(ls); /* read condition (inside scope block) */ + if (!bl2.upval) { /* no upvalues? */ + leaveblock(fs); /* finish scope */ + luaK_patchlist(ls->fs, condexit, repeat_init); /* close the loop */ + } + else { /* complete semantics when there are upvalues */ + breakstat(ls); /* if condition then break */ + luaK_patchtohere(ls->fs, condexit); /* else... */ + leaveblock(fs); /* finish scope... */ + luaK_patchlist(ls->fs, luaK_jump(fs), repeat_init); /* and repeat */ + } + leaveblock(fs); /* finish loop */ +} + + +static int exp1 (LexState *ls) { + expdesc e; + int k; + expr(ls, &e); + k = e.k; + luaK_exp2nextreg(ls->fs, &e); + return k; +} + + +static void forbody (LexState *ls, int base, int line, int nvars, int isnum) { + /* forbody -> DO block */ + BlockCnt bl; + FuncState *fs = ls->fs; + int prep, endfor; + adjustlocalvars(ls, 3); /* control variables */ + checknext(ls, TK_DO); + prep = isnum ? luaK_codeAsBx(fs, OP_FORPREP, base, NO_JUMP) : luaK_jump(fs); + enterblock(fs, &bl, 0); /* scope for declared variables */ + adjustlocalvars(ls, nvars); + luaK_reserveregs(fs, nvars); + block(ls); + leaveblock(fs); /* end of scope for declared variables */ + luaK_patchtohere(fs, prep); + endfor = (isnum) ? luaK_codeAsBx(fs, OP_FORLOOP, base, NO_JUMP) : + luaK_codeABC(fs, OP_TFORLOOP, base, 0, nvars); + luaK_fixline(fs, line); /* pretend that `OP_FOR' starts the loop */ + luaK_patchlist(fs, (isnum ? endfor : luaK_jump(fs)), prep + 1); +} + + +static void fornum (LexState *ls, TString *varname, int line) { + /* fornum -> NAME = exp1,exp1[,exp1] forbody */ + FuncState *fs = ls->fs; + int base = fs->freereg; + new_localvarliteral(ls, "(for index)", 0); + new_localvarliteral(ls, "(for limit)", 1); + new_localvarliteral(ls, "(for step)", 2); + new_localvar(ls, varname, 3); + checknext(ls, '='); + exp1(ls); /* initial value */ + checknext(ls, ','); + exp1(ls); /* limit */ + if (testnext(ls, ',')) + exp1(ls); /* optional step */ + else { /* default step = 1 */ + luaK_codeABx(fs, OP_LOADK, fs->freereg, luaK_numberK(fs, 1)); + luaK_reserveregs(fs, 1); + } + forbody(ls, base, line, 1, 1); +} + + +static void forlist (LexState *ls, TString *indexname) { + /* forlist -> NAME {,NAME} IN explist1 forbody */ + FuncState *fs = ls->fs; + expdesc e; + int nvars = 0; + int line; + int base = fs->freereg; + /* create control variables */ + new_localvarliteral(ls, "(for generator)", nvars++); + new_localvarliteral(ls, "(for state)", nvars++); + new_localvarliteral(ls, "(for control)", nvars++); + /* create declared variables */ + new_localvar(ls, indexname, nvars++); + while (testnext(ls, ',')) + new_localvar(ls, str_checkname(ls), nvars++); + checknext(ls, TK_IN); + line = ls->linenumber; + adjust_assign(ls, 3, explist1(ls, &e), &e); + luaK_checkstack(fs, 3); /* extra space to call generator */ + forbody(ls, base, line, nvars - 3, 0); +} + + +static void forstat (LexState *ls, int line) { + /* forstat -> FOR (fornum | forlist) END */ + FuncState *fs = ls->fs; + TString *varname; + BlockCnt bl; + enterblock(fs, &bl, 1); /* scope for loop and control variables */ + luaX_next(ls); /* skip `for' */ + varname = str_checkname(ls); /* first variable name */ + switch (ls->t.token) { + case '=': fornum(ls, varname, line); break; + case ',': case TK_IN: forlist(ls, varname); break; + default: luaX_syntaxerror(ls, LUA_QL("=") " or " LUA_QL("in") " expected"); + } + check_match(ls, TK_END, TK_FOR, line); + leaveblock(fs); /* loop scope (`break' jumps to this point) */ +} + + +static int test_then_block (LexState *ls) { + /* test_then_block -> [IF | ELSEIF] cond THEN block */ + int condexit; + luaX_next(ls); /* skip IF or ELSEIF */ + condexit = cond(ls); + checknext(ls, TK_THEN); + block(ls); /* `then' part */ + return condexit; +} + + +static void ifstat (LexState *ls, int line) { + /* ifstat -> IF cond THEN block {ELSEIF cond THEN block} [ELSE block] END */ + FuncState *fs = ls->fs; + int flist; + int escapelist = NO_JUMP; + flist = test_then_block(ls); /* IF cond THEN block */ + while (ls->t.token == TK_ELSEIF) { + luaK_concat(fs, &escapelist, luaK_jump(fs)); + luaK_patchtohere(fs, flist); + flist = test_then_block(ls); /* ELSEIF cond THEN block */ + } + if (ls->t.token == TK_ELSE) { + luaK_concat(fs, &escapelist, luaK_jump(fs)); + luaK_patchtohere(fs, flist); + luaX_next(ls); /* skip ELSE (after patch, for correct line info) */ + block(ls); /* `else' part */ + } + else + luaK_concat(fs, &escapelist, flist); + luaK_patchtohere(fs, escapelist); + check_match(ls, TK_END, TK_IF, line); +} + + +static void localfunc (LexState *ls) { + expdesc v, b; + FuncState *fs = ls->fs; + new_localvar(ls, str_checkname(ls), 0); + init_exp(&v, VLOCAL, fs->freereg); + luaK_reserveregs(fs, 1); + adjustlocalvars(ls, 1); + body(ls, &b, 0, ls->linenumber); + luaK_storevar(fs, &v, &b); + /* debug information will only see the variable after this point! */ + getlocvar(fs, fs->nactvar - 1).startpc = fs->pc; +} + + +static void localstat (LexState *ls) { + /* stat -> LOCAL NAME {`,' NAME} [`=' explist1] */ + int nvars = 0; + int nexps; + expdesc e; + do { + new_localvar(ls, str_checkname(ls), nvars++); + } while (testnext(ls, ',')); + if (testnext(ls, '=')) + nexps = explist1(ls, &e); + else { + e.k = VVOID; + nexps = 0; + } + adjust_assign(ls, nvars, nexps, &e); + adjustlocalvars(ls, nvars); +} + + +static int funcname (LexState *ls, expdesc *v) { + /* funcname -> NAME {field} [`:' NAME] */ + int needself = 0; + singlevar(ls, v); + while (ls->t.token == '.') + field(ls, v); + if (ls->t.token == ':') { + needself = 1; + field(ls, v); + } + return needself; +} + + +static void funcstat (LexState *ls, int line) { + /* funcstat -> FUNCTION funcname body */ + int needself; + expdesc v, b; + luaX_next(ls); /* skip FUNCTION */ + needself = funcname(ls, &v); + body(ls, &b, needself, line); + luaK_storevar(ls->fs, &v, &b); + luaK_fixline(ls->fs, line); /* definition `happens' in the first line */ +} + + +static void exprstat (LexState *ls) { + /* stat -> func | assignment */ + FuncState *fs = ls->fs; + struct LHS_assign v; + primaryexp(ls, &v.v); + if (v.v.k == VCALL) /* stat -> func */ + SETARG_C(getcode(fs, &v.v), 1); /* call statement uses no results */ + else { /* stat -> assignment */ + v.prev = NULL; + assignment(ls, &v, 1); + } +} + + +static void retstat (LexState *ls) { + /* stat -> RETURN explist */ + FuncState *fs = ls->fs; + expdesc e; + int first, nret; /* registers with returned values */ + luaX_next(ls); /* skip RETURN */ + if (block_follow(ls->t.token) || ls->t.token == ';') + first = nret = 0; /* return no values */ + else { + nret = explist1(ls, &e); /* optional return values */ + if (hasmultret(e.k)) { + luaK_setmultret(fs, &e); + if (e.k == VCALL && nret == 1) { /* tail call? */ + SET_OPCODE(getcode(fs,&e), OP_TAILCALL); + lua_assert(GETARG_A(getcode(fs,&e)) == fs->nactvar); + } + first = fs->nactvar; + nret = LUA_MULTRET; /* return all values */ + } + else { + if (nret == 1) /* only one single value? */ + first = luaK_exp2anyreg(fs, &e); + else { + luaK_exp2nextreg(fs, &e); /* values must go to the `stack' */ + first = fs->nactvar; /* return all `active' values */ + lua_assert(nret == fs->freereg - first); + } + } + } + luaK_ret(fs, first, nret); +} + + +static int statement (LexState *ls) { + int line = ls->linenumber; /* may be needed for error messages */ + switch (ls->t.token) { + case TK_IF: { /* stat -> ifstat */ + ifstat(ls, line); + return 0; + } + case TK_WHILE: { /* stat -> whilestat */ + whilestat(ls, line); + return 0; + } + case TK_DO: { /* stat -> DO block END */ + luaX_next(ls); /* skip DO */ + block(ls); + check_match(ls, TK_END, TK_DO, line); + return 0; + } + case TK_FOR: { /* stat -> forstat */ + forstat(ls, line); + return 0; + } + case TK_REPEAT: { /* stat -> repeatstat */ + repeatstat(ls, line); + return 0; + } + case TK_FUNCTION: { + funcstat(ls, line); /* stat -> funcstat */ + return 0; + } + case TK_LOCAL: { /* stat -> localstat */ + luaX_next(ls); /* skip LOCAL */ + if (testnext(ls, TK_FUNCTION)) /* local function? */ + localfunc(ls); + else + localstat(ls); + return 0; + } + case TK_RETURN: { /* stat -> retstat */ + retstat(ls); + return 1; /* must be last statement */ + } + case TK_BREAK: { /* stat -> breakstat */ + luaX_next(ls); /* skip BREAK */ + breakstat(ls); + return 1; /* must be last statement */ + } + default: { + exprstat(ls); + return 0; /* to avoid warnings */ + } + } +} + + +static void chunk (LexState *ls) { + /* chunk -> { stat [`;'] } */ + int islast = 0; + enterlevel(ls); + while (!islast && !block_follow(ls->t.token)) { + islast = statement(ls); + testnext(ls, ';'); + lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg && + ls->fs->freereg >= ls->fs->nactvar); + ls->fs->freereg = ls->fs->nactvar; /* free registers */ + } + leavelevel(ls); +} + +/* }====================================================================== */ diff --git a/extern/lua-5.1.5/src/lparser.h b/extern/lua-5.1.5/src/lparser.h new file mode 100644 index 00000000..18836afd --- /dev/null +++ b/extern/lua-5.1.5/src/lparser.h @@ -0,0 +1,82 @@ +/* +** $Id: lparser.h,v 1.57.1.1 2007/12/27 13:02:25 roberto Exp $ +** Lua Parser +** See Copyright Notice in lua.h +*/ + +#ifndef lparser_h +#define lparser_h + +#include "llimits.h" +#include "lobject.h" +#include "lzio.h" + + +/* +** Expression descriptor +*/ + +typedef enum { + VVOID, /* no value */ + VNIL, + VTRUE, + VFALSE, + VK, /* info = index of constant in `k' */ + VKNUM, /* nval = numerical value */ + VLOCAL, /* info = local register */ + VUPVAL, /* info = index of upvalue in `upvalues' */ + VGLOBAL, /* info = index of table; aux = index of global name in `k' */ + VINDEXED, /* info = table register; aux = index register (or `k') */ + VJMP, /* info = instruction pc */ + VRELOCABLE, /* info = instruction pc */ + VNONRELOC, /* info = result register */ + VCALL, /* info = instruction pc */ + VVARARG /* info = instruction pc */ +} expkind; + +typedef struct expdesc { + expkind k; + union { + struct { int info, aux; } s; + lua_Number nval; + } u; + int t; /* patch list of `exit when true' */ + int f; /* patch list of `exit when false' */ +} expdesc; + + +typedef struct upvaldesc { + lu_byte k; + lu_byte info; +} upvaldesc; + + +struct BlockCnt; /* defined in lparser.c */ + + +/* state needed to generate code for a given function */ +typedef struct FuncState { + Proto *f; /* current function header */ + Table *h; /* table to find (and reuse) elements in `k' */ + struct FuncState *prev; /* enclosing function */ + struct LexState *ls; /* lexical state */ + struct lua_State *L; /* copy of the Lua state */ + struct BlockCnt *bl; /* chain of current blocks */ + int pc; /* next position to code (equivalent to `ncode') */ + int lasttarget; /* `pc' of last `jump target' */ + int jpc; /* list of pending jumps to `pc' */ + int freereg; /* first free register */ + int nk; /* number of elements in `k' */ + int np; /* number of elements in `p' */ + short nlocvars; /* number of elements in `locvars' */ + lu_byte nactvar; /* number of active local variables */ + upvaldesc upvalues[LUAI_MAXUPVALUES]; /* upvalues */ + unsigned short actvar[LUAI_MAXVARS]; /* declared-variable stack */ +} FuncState; + + +LUAI_FUNC Proto *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, + const char *name); + + +#endif diff --git a/extern/lua-5.1.5/src/lstate.c b/extern/lua-5.1.5/src/lstate.c new file mode 100644 index 00000000..4313b83a --- /dev/null +++ b/extern/lua-5.1.5/src/lstate.c @@ -0,0 +1,214 @@ +/* +** $Id: lstate.c,v 2.36.1.2 2008/01/03 15:20:39 roberto Exp $ +** Global State +** See Copyright Notice in lua.h +*/ + + +#include + +#define lstate_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "llex.h" +#include "lmem.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + +#define state_size(x) (sizeof(x) + LUAI_EXTRASPACE) +#define fromstate(l) (cast(lu_byte *, (l)) - LUAI_EXTRASPACE) +#define tostate(l) (cast(lua_State *, cast(lu_byte *, l) + LUAI_EXTRASPACE)) + + +/* +** Main thread combines a thread state and the global state +*/ +typedef struct LG { + lua_State l; + global_State g; +} LG; + + + +static void stack_init (lua_State *L1, lua_State *L) { + /* initialize CallInfo array */ + L1->base_ci = luaM_newvector(L, BASIC_CI_SIZE, CallInfo); + L1->ci = L1->base_ci; + L1->size_ci = BASIC_CI_SIZE; + L1->end_ci = L1->base_ci + L1->size_ci - 1; + /* initialize stack array */ + L1->stack = luaM_newvector(L, BASIC_STACK_SIZE + EXTRA_STACK, TValue); + L1->stacksize = BASIC_STACK_SIZE + EXTRA_STACK; + L1->top = L1->stack; + L1->stack_last = L1->stack+(L1->stacksize - EXTRA_STACK)-1; + /* initialize first ci */ + L1->ci->func = L1->top; + setnilvalue(L1->top++); /* `function' entry for this `ci' */ + L1->base = L1->ci->base = L1->top; + L1->ci->top = L1->top + LUA_MINSTACK; +} + + +static void freestack (lua_State *L, lua_State *L1) { + luaM_freearray(L, L1->base_ci, L1->size_ci, CallInfo); + luaM_freearray(L, L1->stack, L1->stacksize, TValue); +} + + +/* +** open parts that may cause memory-allocation errors +*/ +static void f_luaopen (lua_State *L, void *ud) { + global_State *g = G(L); + UNUSED(ud); + stack_init(L, L); /* init stack */ + sethvalue(L, gt(L), luaH_new(L, 0, 2)); /* table of globals */ + sethvalue(L, registry(L), luaH_new(L, 0, 2)); /* registry */ + luaS_resize(L, MINSTRTABSIZE); /* initial size of string table */ + luaT_init(L); + luaX_init(L); + luaS_fix(luaS_newliteral(L, MEMERRMSG)); + g->GCthreshold = 4*g->totalbytes; +} + + +static void preinit_state (lua_State *L, global_State *g) { + G(L) = g; + L->stack = NULL; + L->stacksize = 0; + L->errorJmp = NULL; + L->hook = NULL; + L->hookmask = 0; + L->basehookcount = 0; + L->allowhook = 1; + resethookcount(L); + L->openupval = NULL; + L->size_ci = 0; + L->nCcalls = L->baseCcalls = 0; + L->status = 0; + L->base_ci = L->ci = NULL; + L->savedpc = NULL; + L->errfunc = 0; + setnilvalue(gt(L)); +} + + +static void close_state (lua_State *L) { + global_State *g = G(L); + luaF_close(L, L->stack); /* close all upvalues for this thread */ + luaC_freeall(L); /* collect all objects */ + lua_assert(g->rootgc == obj2gco(L)); + lua_assert(g->strt.nuse == 0); + luaM_freearray(L, G(L)->strt.hash, G(L)->strt.size, TString *); + luaZ_freebuffer(L, &g->buff); + freestack(L, L); + lua_assert(g->totalbytes == sizeof(LG)); + (*g->frealloc)(g->ud, fromstate(L), state_size(LG), 0); +} + + +lua_State *luaE_newthread (lua_State *L) { + lua_State *L1 = tostate(luaM_malloc(L, state_size(lua_State))); + luaC_link(L, obj2gco(L1), LUA_TTHREAD); + preinit_state(L1, G(L)); + stack_init(L1, L); /* init stack */ + setobj2n(L, gt(L1), gt(L)); /* share table of globals */ + L1->hookmask = L->hookmask; + L1->basehookcount = L->basehookcount; + L1->hook = L->hook; + resethookcount(L1); + lua_assert(iswhite(obj2gco(L1))); + return L1; +} + + +void luaE_freethread (lua_State *L, lua_State *L1) { + luaF_close(L1, L1->stack); /* close all upvalues for this thread */ + lua_assert(L1->openupval == NULL); + luai_userstatefree(L1); + freestack(L, L1); + luaM_freemem(L, fromstate(L1), state_size(lua_State)); +} + + +LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) { + int i; + lua_State *L; + global_State *g; + void *l = (*f)(ud, NULL, 0, state_size(LG)); + if (l == NULL) return NULL; + L = tostate(l); + g = &((LG *)L)->g; + L->next = NULL; + L->tt = LUA_TTHREAD; + g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT); + L->marked = luaC_white(g); + set2bits(L->marked, FIXEDBIT, SFIXEDBIT); + preinit_state(L, g); + g->frealloc = f; + g->ud = ud; + g->mainthread = L; + g->uvhead.u.l.prev = &g->uvhead; + g->uvhead.u.l.next = &g->uvhead; + g->GCthreshold = 0; /* mark it as unfinished state */ + g->strt.size = 0; + g->strt.nuse = 0; + g->strt.hash = NULL; + setnilvalue(registry(L)); + luaZ_initbuffer(L, &g->buff); + g->panic = NULL; + g->gcstate = GCSpause; + g->rootgc = obj2gco(L); + g->sweepstrgc = 0; + g->sweepgc = &g->rootgc; + g->gray = NULL; + g->grayagain = NULL; + g->weak = NULL; + g->tmudata = NULL; + g->totalbytes = sizeof(LG); + g->gcpause = LUAI_GCPAUSE; + g->gcstepmul = LUAI_GCMUL; + g->gcdept = 0; + for (i=0; imt[i] = NULL; + if (luaD_rawrunprotected(L, f_luaopen, NULL) != 0) { + /* memory allocation error: free partial state */ + close_state(L); + L = NULL; + } + else + luai_userstateopen(L); + return L; +} + + +static void callallgcTM (lua_State *L, void *ud) { + UNUSED(ud); + luaC_callGCTM(L); /* call GC metamethods for all udata */ +} + + +LUA_API void lua_close (lua_State *L) { + L = G(L)->mainthread; /* only the main thread can be closed */ + lua_lock(L); + luaF_close(L, L->stack); /* close all upvalues for this thread */ + luaC_separateudata(L, 1); /* separate udata that have GC metamethods */ + L->errfunc = 0; /* no error function during GC metamethods */ + do { /* repeat until no more errors */ + L->ci = L->base_ci; + L->base = L->top = L->ci->base; + L->nCcalls = L->baseCcalls = 0; + } while (luaD_rawrunprotected(L, callallgcTM, NULL) != 0); + lua_assert(G(L)->tmudata == NULL); + luai_userstateclose(L); + close_state(L); +} + diff --git a/extern/lua-5.1.5/src/lstate.h b/extern/lua-5.1.5/src/lstate.h new file mode 100644 index 00000000..3bc575b6 --- /dev/null +++ b/extern/lua-5.1.5/src/lstate.h @@ -0,0 +1,169 @@ +/* +** $Id: lstate.h,v 2.24.1.2 2008/01/03 15:20:39 roberto Exp $ +** Global State +** See Copyright Notice in lua.h +*/ + +#ifndef lstate_h +#define lstate_h + +#include "lua.h" + +#include "lobject.h" +#include "ltm.h" +#include "lzio.h" + + + +struct lua_longjmp; /* defined in ldo.c */ + + +/* table of globals */ +#define gt(L) (&L->l_gt) + +/* registry */ +#define registry(L) (&G(L)->l_registry) + + +/* extra stack space to handle TM calls and some other extras */ +#define EXTRA_STACK 5 + + +#define BASIC_CI_SIZE 8 + +#define BASIC_STACK_SIZE (2*LUA_MINSTACK) + + + +typedef struct stringtable { + GCObject **hash; + lu_int32 nuse; /* number of elements */ + int size; +} stringtable; + + +/* +** informations about a call +*/ +typedef struct CallInfo { + StkId base; /* base for this function */ + StkId func; /* function index in the stack */ + StkId top; /* top for this function */ + const Instruction *savedpc; + int nresults; /* expected number of results from this function */ + int tailcalls; /* number of tail calls lost under this entry */ +} CallInfo; + + + +#define curr_func(L) (clvalue(L->ci->func)) +#define ci_func(ci) (clvalue((ci)->func)) +#define f_isLua(ci) (!ci_func(ci)->c.isC) +#define isLua(ci) (ttisfunction((ci)->func) && f_isLua(ci)) + + +/* +** `global state', shared by all threads of this state +*/ +typedef struct global_State { + stringtable strt; /* hash table for strings */ + lua_Alloc frealloc; /* function to reallocate memory */ + void *ud; /* auxiliary data to `frealloc' */ + lu_byte currentwhite; + lu_byte gcstate; /* state of garbage collector */ + int sweepstrgc; /* position of sweep in `strt' */ + GCObject *rootgc; /* list of all collectable objects */ + GCObject **sweepgc; /* position of sweep in `rootgc' */ + GCObject *gray; /* list of gray objects */ + GCObject *grayagain; /* list of objects to be traversed atomically */ + GCObject *weak; /* list of weak tables (to be cleared) */ + GCObject *tmudata; /* last element of list of userdata to be GC */ + Mbuffer buff; /* temporary buffer for string concatentation */ + lu_mem GCthreshold; + lu_mem totalbytes; /* number of bytes currently allocated */ + lu_mem estimate; /* an estimate of number of bytes actually in use */ + lu_mem gcdept; /* how much GC is `behind schedule' */ + int gcpause; /* size of pause between successive GCs */ + int gcstepmul; /* GC `granularity' */ + lua_CFunction panic; /* to be called in unprotected errors */ + TValue l_registry; + struct lua_State *mainthread; + UpVal uvhead; /* head of double-linked list of all open upvalues */ + struct Table *mt[NUM_TAGS]; /* metatables for basic types */ + TString *tmname[TM_N]; /* array with tag-method names */ +} global_State; + + +/* +** `per thread' state +*/ +struct lua_State { + CommonHeader; + lu_byte status; + StkId top; /* first free slot in the stack */ + StkId base; /* base of current function */ + global_State *l_G; + CallInfo *ci; /* call info for current function */ + const Instruction *savedpc; /* `savedpc' of current function */ + StkId stack_last; /* last free slot in the stack */ + StkId stack; /* stack base */ + CallInfo *end_ci; /* points after end of ci array*/ + CallInfo *base_ci; /* array of CallInfo's */ + int stacksize; + int size_ci; /* size of array `base_ci' */ + unsigned short nCcalls; /* number of nested C calls */ + unsigned short baseCcalls; /* nested C calls when resuming coroutine */ + lu_byte hookmask; + lu_byte allowhook; + int basehookcount; + int hookcount; + lua_Hook hook; + TValue l_gt; /* table of globals */ + TValue env; /* temporary place for environments */ + GCObject *openupval; /* list of open upvalues in this stack */ + GCObject *gclist; + struct lua_longjmp *errorJmp; /* current error recover point */ + ptrdiff_t errfunc; /* current error handling function (stack index) */ +}; + + +#define G(L) (L->l_G) + + +/* +** Union of all collectable objects +*/ +union GCObject { + GCheader gch; + union TString ts; + union Udata u; + union Closure cl; + struct Table h; + struct Proto p; + struct UpVal uv; + struct lua_State th; /* thread */ +}; + + +/* macros to convert a GCObject into a specific value */ +#define rawgco2ts(o) check_exp((o)->gch.tt == LUA_TSTRING, &((o)->ts)) +#define gco2ts(o) (&rawgco2ts(o)->tsv) +#define rawgco2u(o) check_exp((o)->gch.tt == LUA_TUSERDATA, &((o)->u)) +#define gco2u(o) (&rawgco2u(o)->uv) +#define gco2cl(o) check_exp((o)->gch.tt == LUA_TFUNCTION, &((o)->cl)) +#define gco2h(o) check_exp((o)->gch.tt == LUA_TTABLE, &((o)->h)) +#define gco2p(o) check_exp((o)->gch.tt == LUA_TPROTO, &((o)->p)) +#define gco2uv(o) check_exp((o)->gch.tt == LUA_TUPVAL, &((o)->uv)) +#define ngcotouv(o) \ + check_exp((o) == NULL || (o)->gch.tt == LUA_TUPVAL, &((o)->uv)) +#define gco2th(o) check_exp((o)->gch.tt == LUA_TTHREAD, &((o)->th)) + +/* macro to convert any Lua object into a GCObject */ +#define obj2gco(v) (cast(GCObject *, (v))) + + +LUAI_FUNC lua_State *luaE_newthread (lua_State *L); +LUAI_FUNC void luaE_freethread (lua_State *L, lua_State *L1); + +#endif + diff --git a/extern/lua-5.1.5/src/lstring.c b/extern/lua-5.1.5/src/lstring.c new file mode 100644 index 00000000..49113151 --- /dev/null +++ b/extern/lua-5.1.5/src/lstring.c @@ -0,0 +1,111 @@ +/* +** $Id: lstring.c,v 2.8.1.1 2007/12/27 13:02:25 roberto Exp $ +** String table (keeps all strings handled by Lua) +** See Copyright Notice in lua.h +*/ + + +#include + +#define lstring_c +#define LUA_CORE + +#include "lua.h" + +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" + + + +void luaS_resize (lua_State *L, int newsize) { + GCObject **newhash; + stringtable *tb; + int i; + if (G(L)->gcstate == GCSsweepstring) + return; /* cannot resize during GC traverse */ + newhash = luaM_newvector(L, newsize, GCObject *); + tb = &G(L)->strt; + for (i=0; isize; i++) { + GCObject *p = tb->hash[i]; + while (p) { /* for each node in the list */ + GCObject *next = p->gch.next; /* save next */ + unsigned int h = gco2ts(p)->hash; + int h1 = lmod(h, newsize); /* new position */ + lua_assert(cast_int(h%newsize) == lmod(h, newsize)); + p->gch.next = newhash[h1]; /* chain it */ + newhash[h1] = p; + p = next; + } + } + luaM_freearray(L, tb->hash, tb->size, TString *); + tb->size = newsize; + tb->hash = newhash; +} + + +static TString *newlstr (lua_State *L, const char *str, size_t l, + unsigned int h) { + TString *ts; + stringtable *tb; + if (l+1 > (MAX_SIZET - sizeof(TString))/sizeof(char)) + luaM_toobig(L); + ts = cast(TString *, luaM_malloc(L, (l+1)*sizeof(char)+sizeof(TString))); + ts->tsv.len = l; + ts->tsv.hash = h; + ts->tsv.marked = luaC_white(G(L)); + ts->tsv.tt = LUA_TSTRING; + ts->tsv.reserved = 0; + memcpy(ts+1, str, l*sizeof(char)); + ((char *)(ts+1))[l] = '\0'; /* ending 0 */ + tb = &G(L)->strt; + h = lmod(h, tb->size); + ts->tsv.next = tb->hash[h]; /* chain new entry */ + tb->hash[h] = obj2gco(ts); + tb->nuse++; + if (tb->nuse > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2) + luaS_resize(L, tb->size*2); /* too crowded */ + return ts; +} + + +TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { + GCObject *o; + unsigned int h = cast(unsigned int, l); /* seed */ + size_t step = (l>>5)+1; /* if string is too long, don't hash all its chars */ + size_t l1; + for (l1=l; l1>=step; l1-=step) /* compute hash */ + h = h ^ ((h<<5)+(h>>2)+cast(unsigned char, str[l1-1])); + for (o = G(L)->strt.hash[lmod(h, G(L)->strt.size)]; + o != NULL; + o = o->gch.next) { + TString *ts = rawgco2ts(o); + if (ts->tsv.len == l && (memcmp(str, getstr(ts), l) == 0)) { + /* string may be dead */ + if (isdead(G(L), o)) changewhite(o); + return ts; + } + } + return newlstr(L, str, l, h); /* not found */ +} + + +Udata *luaS_newudata (lua_State *L, size_t s, Table *e) { + Udata *u; + if (s > MAX_SIZET - sizeof(Udata)) + luaM_toobig(L); + u = cast(Udata *, luaM_malloc(L, s + sizeof(Udata))); + u->uv.marked = luaC_white(G(L)); /* is not finalized */ + u->uv.tt = LUA_TUSERDATA; + u->uv.len = s; + u->uv.metatable = NULL; + u->uv.env = e; + /* chain it on udata list (after main thread) */ + u->uv.next = G(L)->mainthread->next; + G(L)->mainthread->next = obj2gco(u); + return u; +} + diff --git a/extern/lua-5.1.5/src/lstring.h b/extern/lua-5.1.5/src/lstring.h new file mode 100644 index 00000000..73a2ff8b --- /dev/null +++ b/extern/lua-5.1.5/src/lstring.h @@ -0,0 +1,31 @@ +/* +** $Id: lstring.h,v 1.43.1.1 2007/12/27 13:02:25 roberto Exp $ +** String table (keep all strings handled by Lua) +** See Copyright Notice in lua.h +*/ + +#ifndef lstring_h +#define lstring_h + + +#include "lgc.h" +#include "lobject.h" +#include "lstate.h" + + +#define sizestring(s) (sizeof(union TString)+((s)->len+1)*sizeof(char)) + +#define sizeudata(u) (sizeof(union Udata)+(u)->len) + +#define luaS_new(L, s) (luaS_newlstr(L, s, strlen(s))) +#define luaS_newliteral(L, s) (luaS_newlstr(L, "" s, \ + (sizeof(s)/sizeof(char))-1)) + +#define luaS_fix(s) l_setbit((s)->tsv.marked, FIXEDBIT) + +LUAI_FUNC void luaS_resize (lua_State *L, int newsize); +LUAI_FUNC Udata *luaS_newudata (lua_State *L, size_t s, Table *e); +LUAI_FUNC TString *luaS_newlstr (lua_State *L, const char *str, size_t l); + + +#endif diff --git a/extern/lua-5.1.5/src/lstrlib.c b/extern/lua-5.1.5/src/lstrlib.c new file mode 100644 index 00000000..7a03489b --- /dev/null +++ b/extern/lua-5.1.5/src/lstrlib.c @@ -0,0 +1,871 @@ +/* +** $Id: lstrlib.c,v 1.132.1.5 2010/05/14 15:34:19 roberto Exp $ +** Standard library for string operations and pattern-matching +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include +#include +#include + +#define lstrlib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +/* macro to `unsign' a character */ +#define uchar(c) ((unsigned char)(c)) + + + +static int str_len (lua_State *L) { + size_t l; + luaL_checklstring(L, 1, &l); + lua_pushinteger(L, l); + return 1; +} + + +static ptrdiff_t posrelat (ptrdiff_t pos, size_t len) { + /* relative string position: negative means back from end */ + if (pos < 0) pos += (ptrdiff_t)len + 1; + return (pos >= 0) ? pos : 0; +} + + +static int str_sub (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + ptrdiff_t start = posrelat(luaL_checkinteger(L, 2), l); + ptrdiff_t end = posrelat(luaL_optinteger(L, 3, -1), l); + if (start < 1) start = 1; + if (end > (ptrdiff_t)l) end = (ptrdiff_t)l; + if (start <= end) + lua_pushlstring(L, s+start-1, end-start+1); + else lua_pushliteral(L, ""); + return 1; +} + + +static int str_reverse (lua_State *L) { + size_t l; + luaL_Buffer b; + const char *s = luaL_checklstring(L, 1, &l); + luaL_buffinit(L, &b); + while (l--) luaL_addchar(&b, s[l]); + luaL_pushresult(&b); + return 1; +} + + +static int str_lower (lua_State *L) { + size_t l; + size_t i; + luaL_Buffer b; + const char *s = luaL_checklstring(L, 1, &l); + luaL_buffinit(L, &b); + for (i=0; i 0) + luaL_addlstring(&b, s, l); + luaL_pushresult(&b); + return 1; +} + + +static int str_byte (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + ptrdiff_t posi = posrelat(luaL_optinteger(L, 2, 1), l); + ptrdiff_t pose = posrelat(luaL_optinteger(L, 3, posi), l); + int n, i; + if (posi <= 0) posi = 1; + if ((size_t)pose > l) pose = l; + if (posi > pose) return 0; /* empty interval; return no values */ + n = (int)(pose - posi + 1); + if (posi + n <= pose) /* overflow? */ + luaL_error(L, "string slice too long"); + luaL_checkstack(L, n, "string slice too long"); + for (i=0; i= ms->level || ms->capture[l].len == CAP_UNFINISHED) + return luaL_error(ms->L, "invalid capture index"); + return l; +} + + +static int capture_to_close (MatchState *ms) { + int level = ms->level; + for (level--; level>=0; level--) + if (ms->capture[level].len == CAP_UNFINISHED) return level; + return luaL_error(ms->L, "invalid pattern capture"); +} + + +static const char *classend (MatchState *ms, const char *p) { + switch (*p++) { + case L_ESC: { + if (*p == '\0') + luaL_error(ms->L, "malformed pattern (ends with " LUA_QL("%%") ")"); + return p+1; + } + case '[': { + if (*p == '^') p++; + do { /* look for a `]' */ + if (*p == '\0') + luaL_error(ms->L, "malformed pattern (missing " LUA_QL("]") ")"); + if (*(p++) == L_ESC && *p != '\0') + p++; /* skip escapes (e.g. `%]') */ + } while (*p != ']'); + return p+1; + } + default: { + return p; + } + } +} + + +static int match_class (int c, int cl) { + int res; + switch (tolower(cl)) { + case 'a' : res = isalpha(c); break; + case 'c' : res = iscntrl(c); break; + case 'd' : res = isdigit(c); break; + case 'l' : res = islower(c); break; + case 'p' : res = ispunct(c); break; + case 's' : res = isspace(c); break; + case 'u' : res = isupper(c); break; + case 'w' : res = isalnum(c); break; + case 'x' : res = isxdigit(c); break; + case 'z' : res = (c == 0); break; + default: return (cl == c); + } + return (islower(cl) ? res : !res); +} + + +static int matchbracketclass (int c, const char *p, const char *ec) { + int sig = 1; + if (*(p+1) == '^') { + sig = 0; + p++; /* skip the `^' */ + } + while (++p < ec) { + if (*p == L_ESC) { + p++; + if (match_class(c, uchar(*p))) + return sig; + } + else if ((*(p+1) == '-') && (p+2 < ec)) { + p+=2; + if (uchar(*(p-2)) <= c && c <= uchar(*p)) + return sig; + } + else if (uchar(*p) == c) return sig; + } + return !sig; +} + + +static int singlematch (int c, const char *p, const char *ep) { + switch (*p) { + case '.': return 1; /* matches any char */ + case L_ESC: return match_class(c, uchar(*(p+1))); + case '[': return matchbracketclass(c, p, ep-1); + default: return (uchar(*p) == c); + } +} + + +static const char *match (MatchState *ms, const char *s, const char *p); + + +static const char *matchbalance (MatchState *ms, const char *s, + const char *p) { + if (*p == 0 || *(p+1) == 0) + luaL_error(ms->L, "unbalanced pattern"); + if (*s != *p) return NULL; + else { + int b = *p; + int e = *(p+1); + int cont = 1; + while (++s < ms->src_end) { + if (*s == e) { + if (--cont == 0) return s+1; + } + else if (*s == b) cont++; + } + } + return NULL; /* string ends out of balance */ +} + + +static const char *max_expand (MatchState *ms, const char *s, + const char *p, const char *ep) { + ptrdiff_t i = 0; /* counts maximum expand for item */ + while ((s+i)src_end && singlematch(uchar(*(s+i)), p, ep)) + i++; + /* keeps trying to match with the maximum repetitions */ + while (i>=0) { + const char *res = match(ms, (s+i), ep+1); + if (res) return res; + i--; /* else didn't match; reduce 1 repetition to try again */ + } + return NULL; +} + + +static const char *min_expand (MatchState *ms, const char *s, + const char *p, const char *ep) { + for (;;) { + const char *res = match(ms, s, ep+1); + if (res != NULL) + return res; + else if (ssrc_end && singlematch(uchar(*s), p, ep)) + s++; /* try with one more repetition */ + else return NULL; + } +} + + +static const char *start_capture (MatchState *ms, const char *s, + const char *p, int what) { + const char *res; + int level = ms->level; + if (level >= LUA_MAXCAPTURES) luaL_error(ms->L, "too many captures"); + ms->capture[level].init = s; + ms->capture[level].len = what; + ms->level = level+1; + if ((res=match(ms, s, p)) == NULL) /* match failed? */ + ms->level--; /* undo capture */ + return res; +} + + +static const char *end_capture (MatchState *ms, const char *s, + const char *p) { + int l = capture_to_close(ms); + const char *res; + ms->capture[l].len = s - ms->capture[l].init; /* close capture */ + if ((res = match(ms, s, p)) == NULL) /* match failed? */ + ms->capture[l].len = CAP_UNFINISHED; /* undo capture */ + return res; +} + + +static const char *match_capture (MatchState *ms, const char *s, int l) { + size_t len; + l = check_capture(ms, l); + len = ms->capture[l].len; + if ((size_t)(ms->src_end-s) >= len && + memcmp(ms->capture[l].init, s, len) == 0) + return s+len; + else return NULL; +} + + +static const char *match (MatchState *ms, const char *s, const char *p) { + init: /* using goto's to optimize tail recursion */ + switch (*p) { + case '(': { /* start capture */ + if (*(p+1) == ')') /* position capture? */ + return start_capture(ms, s, p+2, CAP_POSITION); + else + return start_capture(ms, s, p+1, CAP_UNFINISHED); + } + case ')': { /* end capture */ + return end_capture(ms, s, p+1); + } + case L_ESC: { + switch (*(p+1)) { + case 'b': { /* balanced string? */ + s = matchbalance(ms, s, p+2); + if (s == NULL) return NULL; + p+=4; goto init; /* else return match(ms, s, p+4); */ + } + case 'f': { /* frontier? */ + const char *ep; char previous; + p += 2; + if (*p != '[') + luaL_error(ms->L, "missing " LUA_QL("[") " after " + LUA_QL("%%f") " in pattern"); + ep = classend(ms, p); /* points to what is next */ + previous = (s == ms->src_init) ? '\0' : *(s-1); + if (matchbracketclass(uchar(previous), p, ep-1) || + !matchbracketclass(uchar(*s), p, ep-1)) return NULL; + p=ep; goto init; /* else return match(ms, s, ep); */ + } + default: { + if (isdigit(uchar(*(p+1)))) { /* capture results (%0-%9)? */ + s = match_capture(ms, s, uchar(*(p+1))); + if (s == NULL) return NULL; + p+=2; goto init; /* else return match(ms, s, p+2) */ + } + goto dflt; /* case default */ + } + } + } + case '\0': { /* end of pattern */ + return s; /* match succeeded */ + } + case '$': { + if (*(p+1) == '\0') /* is the `$' the last char in pattern? */ + return (s == ms->src_end) ? s : NULL; /* check end of string */ + else goto dflt; + } + default: dflt: { /* it is a pattern item */ + const char *ep = classend(ms, p); /* points to what is next */ + int m = ssrc_end && singlematch(uchar(*s), p, ep); + switch (*ep) { + case '?': { /* optional */ + const char *res; + if (m && ((res=match(ms, s+1, ep+1)) != NULL)) + return res; + p=ep+1; goto init; /* else return match(ms, s, ep+1); */ + } + case '*': { /* 0 or more repetitions */ + return max_expand(ms, s, p, ep); + } + case '+': { /* 1 or more repetitions */ + return (m ? max_expand(ms, s+1, p, ep) : NULL); + } + case '-': { /* 0 or more repetitions (minimum) */ + return min_expand(ms, s, p, ep); + } + default: { + if (!m) return NULL; + s++; p=ep; goto init; /* else return match(ms, s+1, ep); */ + } + } + } + } +} + + + +static const char *lmemfind (const char *s1, size_t l1, + const char *s2, size_t l2) { + if (l2 == 0) return s1; /* empty strings are everywhere */ + else if (l2 > l1) return NULL; /* avoids a negative `l1' */ + else { + const char *init; /* to search for a `*s2' inside `s1' */ + l2--; /* 1st char will be checked by `memchr' */ + l1 = l1-l2; /* `s2' cannot be found after that */ + while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) { + init++; /* 1st char is already checked */ + if (memcmp(init, s2+1, l2) == 0) + return init-1; + else { /* correct `l1' and `s1' to try again */ + l1 -= init-s1; + s1 = init; + } + } + return NULL; /* not found */ + } +} + + +static void push_onecapture (MatchState *ms, int i, const char *s, + const char *e) { + if (i >= ms->level) { + if (i == 0) /* ms->level == 0, too */ + lua_pushlstring(ms->L, s, e - s); /* add whole match */ + else + luaL_error(ms->L, "invalid capture index"); + } + else { + ptrdiff_t l = ms->capture[i].len; + if (l == CAP_UNFINISHED) luaL_error(ms->L, "unfinished capture"); + if (l == CAP_POSITION) + lua_pushinteger(ms->L, ms->capture[i].init - ms->src_init + 1); + else + lua_pushlstring(ms->L, ms->capture[i].init, l); + } +} + + +static int push_captures (MatchState *ms, const char *s, const char *e) { + int i; + int nlevels = (ms->level == 0 && s) ? 1 : ms->level; + luaL_checkstack(ms->L, nlevels, "too many captures"); + for (i = 0; i < nlevels; i++) + push_onecapture(ms, i, s, e); + return nlevels; /* number of strings pushed */ +} + + +static int str_find_aux (lua_State *L, int find) { + size_t l1, l2; + const char *s = luaL_checklstring(L, 1, &l1); + const char *p = luaL_checklstring(L, 2, &l2); + ptrdiff_t init = posrelat(luaL_optinteger(L, 3, 1), l1) - 1; + if (init < 0) init = 0; + else if ((size_t)(init) > l1) init = (ptrdiff_t)l1; + if (find && (lua_toboolean(L, 4) || /* explicit request? */ + strpbrk(p, SPECIALS) == NULL)) { /* or no special characters? */ + /* do a plain search */ + const char *s2 = lmemfind(s+init, l1-init, p, l2); + if (s2) { + lua_pushinteger(L, s2-s+1); + lua_pushinteger(L, s2-s+l2); + return 2; + } + } + else { + MatchState ms; + int anchor = (*p == '^') ? (p++, 1) : 0; + const char *s1=s+init; + ms.L = L; + ms.src_init = s; + ms.src_end = s+l1; + do { + const char *res; + ms.level = 0; + if ((res=match(&ms, s1, p)) != NULL) { + if (find) { + lua_pushinteger(L, s1-s+1); /* start */ + lua_pushinteger(L, res-s); /* end */ + return push_captures(&ms, NULL, 0) + 2; + } + else + return push_captures(&ms, s1, res); + } + } while (s1++ < ms.src_end && !anchor); + } + lua_pushnil(L); /* not found */ + return 1; +} + + +static int str_find (lua_State *L) { + return str_find_aux(L, 1); +} + + +static int str_match (lua_State *L) { + return str_find_aux(L, 0); +} + + +static int gmatch_aux (lua_State *L) { + MatchState ms; + size_t ls; + const char *s = lua_tolstring(L, lua_upvalueindex(1), &ls); + const char *p = lua_tostring(L, lua_upvalueindex(2)); + const char *src; + ms.L = L; + ms.src_init = s; + ms.src_end = s+ls; + for (src = s + (size_t)lua_tointeger(L, lua_upvalueindex(3)); + src <= ms.src_end; + src++) { + const char *e; + ms.level = 0; + if ((e = match(&ms, src, p)) != NULL) { + lua_Integer newstart = e-s; + if (e == src) newstart++; /* empty match? go at least one position */ + lua_pushinteger(L, newstart); + lua_replace(L, lua_upvalueindex(3)); + return push_captures(&ms, src, e); + } + } + return 0; /* not found */ +} + + +static int gmatch (lua_State *L) { + luaL_checkstring(L, 1); + luaL_checkstring(L, 2); + lua_settop(L, 2); + lua_pushinteger(L, 0); + lua_pushcclosure(L, gmatch_aux, 3); + return 1; +} + + +static int gfind_nodef (lua_State *L) { + return luaL_error(L, LUA_QL("string.gfind") " was renamed to " + LUA_QL("string.gmatch")); +} + + +static void add_s (MatchState *ms, luaL_Buffer *b, const char *s, + const char *e) { + size_t l, i; + const char *news = lua_tolstring(ms->L, 3, &l); + for (i = 0; i < l; i++) { + if (news[i] != L_ESC) + luaL_addchar(b, news[i]); + else { + i++; /* skip ESC */ + if (!isdigit(uchar(news[i]))) + luaL_addchar(b, news[i]); + else if (news[i] == '0') + luaL_addlstring(b, s, e - s); + else { + push_onecapture(ms, news[i] - '1', s, e); + luaL_addvalue(b); /* add capture to accumulated result */ + } + } + } +} + + +static void add_value (MatchState *ms, luaL_Buffer *b, const char *s, + const char *e) { + lua_State *L = ms->L; + switch (lua_type(L, 3)) { + case LUA_TNUMBER: + case LUA_TSTRING: { + add_s(ms, b, s, e); + return; + } + case LUA_TFUNCTION: { + int n; + lua_pushvalue(L, 3); + n = push_captures(ms, s, e); + lua_call(L, n, 1); + break; + } + case LUA_TTABLE: { + push_onecapture(ms, 0, s, e); + lua_gettable(L, 3); + break; + } + } + if (!lua_toboolean(L, -1)) { /* nil or false? */ + lua_pop(L, 1); + lua_pushlstring(L, s, e - s); /* keep original text */ + } + else if (!lua_isstring(L, -1)) + luaL_error(L, "invalid replacement value (a %s)", luaL_typename(L, -1)); + luaL_addvalue(b); /* add result to accumulator */ +} + + +static int str_gsub (lua_State *L) { + size_t srcl; + const char *src = luaL_checklstring(L, 1, &srcl); + const char *p = luaL_checkstring(L, 2); + int tr = lua_type(L, 3); + int max_s = luaL_optint(L, 4, srcl+1); + int anchor = (*p == '^') ? (p++, 1) : 0; + int n = 0; + MatchState ms; + luaL_Buffer b; + luaL_argcheck(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || + tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, + "string/function/table expected"); + luaL_buffinit(L, &b); + ms.L = L; + ms.src_init = src; + ms.src_end = src+srcl; + while (n < max_s) { + const char *e; + ms.level = 0; + e = match(&ms, src, p); + if (e) { + n++; + add_value(&ms, &b, src, e); + } + if (e && e>src) /* non empty match? */ + src = e; /* skip it */ + else if (src < ms.src_end) + luaL_addchar(&b, *src++); + else break; + if (anchor) break; + } + luaL_addlstring(&b, src, ms.src_end-src); + luaL_pushresult(&b); + lua_pushinteger(L, n); /* number of substitutions */ + return 2; +} + +/* }====================================================== */ + + +/* maximum size of each formatted item (> len(format('%99.99f', -1e308))) */ +#define MAX_ITEM 512 +/* valid flags in a format specification */ +#define FLAGS "-+ #0" +/* +** maximum size of each format specification (such as '%-099.99d') +** (+10 accounts for %99.99x plus margin of error) +*/ +#define MAX_FORMAT (sizeof(FLAGS) + sizeof(LUA_INTFRMLEN) + 10) + + +static void addquoted (lua_State *L, luaL_Buffer *b, int arg) { + size_t l; + const char *s = luaL_checklstring(L, arg, &l); + luaL_addchar(b, '"'); + while (l--) { + switch (*s) { + case '"': case '\\': case '\n': { + luaL_addchar(b, '\\'); + luaL_addchar(b, *s); + break; + } + case '\r': { + luaL_addlstring(b, "\\r", 2); + break; + } + case '\0': { + luaL_addlstring(b, "\\000", 4); + break; + } + default: { + luaL_addchar(b, *s); + break; + } + } + s++; + } + luaL_addchar(b, '"'); +} + +static const char *scanformat (lua_State *L, const char *strfrmt, char *form) { + const char *p = strfrmt; + while (*p != '\0' && strchr(FLAGS, *p) != NULL) p++; /* skip flags */ + if ((size_t)(p - strfrmt) >= sizeof(FLAGS)) + luaL_error(L, "invalid format (repeated flags)"); + if (isdigit(uchar(*p))) p++; /* skip width */ + if (isdigit(uchar(*p))) p++; /* (2 digits at most) */ + if (*p == '.') { + p++; + if (isdigit(uchar(*p))) p++; /* skip precision */ + if (isdigit(uchar(*p))) p++; /* (2 digits at most) */ + } + if (isdigit(uchar(*p))) + luaL_error(L, "invalid format (width or precision too long)"); + *(form++) = '%'; + strncpy(form, strfrmt, p - strfrmt + 1); + form += p - strfrmt + 1; + *form = '\0'; + return p; +} + + +static void addintlen (char *form) { + size_t l = strlen(form); + char spec = form[l - 1]; + strcpy(form + l - 1, LUA_INTFRMLEN); + form[l + sizeof(LUA_INTFRMLEN) - 2] = spec; + form[l + sizeof(LUA_INTFRMLEN) - 1] = '\0'; +} + + +static int str_format (lua_State *L) { + int top = lua_gettop(L); + int arg = 1; + size_t sfl; + const char *strfrmt = luaL_checklstring(L, arg, &sfl); + const char *strfrmt_end = strfrmt+sfl; + luaL_Buffer b; + luaL_buffinit(L, &b); + while (strfrmt < strfrmt_end) { + if (*strfrmt != L_ESC) + luaL_addchar(&b, *strfrmt++); + else if (*++strfrmt == L_ESC) + luaL_addchar(&b, *strfrmt++); /* %% */ + else { /* format item */ + char form[MAX_FORMAT]; /* to store the format (`%...') */ + char buff[MAX_ITEM]; /* to store the formatted item */ + if (++arg > top) + luaL_argerror(L, arg, "no value"); + strfrmt = scanformat(L, strfrmt, form); + switch (*strfrmt++) { + case 'c': { + sprintf(buff, form, (int)luaL_checknumber(L, arg)); + break; + } + case 'd': case 'i': { + addintlen(form); + sprintf(buff, form, (LUA_INTFRM_T)luaL_checknumber(L, arg)); + break; + } + case 'o': case 'u': case 'x': case 'X': { + addintlen(form); + sprintf(buff, form, (unsigned LUA_INTFRM_T)luaL_checknumber(L, arg)); + break; + } + case 'e': case 'E': case 'f': + case 'g': case 'G': { + sprintf(buff, form, (double)luaL_checknumber(L, arg)); + break; + } + case 'q': { + addquoted(L, &b, arg); + continue; /* skip the 'addsize' at the end */ + } + case 's': { + size_t l; + const char *s = luaL_checklstring(L, arg, &l); + if (!strchr(form, '.') && l >= 100) { + /* no precision and string is too long to be formatted; + keep original string */ + lua_pushvalue(L, arg); + luaL_addvalue(&b); + continue; /* skip the `addsize' at the end */ + } + else { + sprintf(buff, form, s); + break; + } + } + default: { /* also treat cases `pnLlh' */ + return luaL_error(L, "invalid option " LUA_QL("%%%c") " to " + LUA_QL("format"), *(strfrmt - 1)); + } + } + luaL_addlstring(&b, buff, strlen(buff)); + } + } + luaL_pushresult(&b); + return 1; +} + + +static const luaL_Reg strlib[] = { + {"byte", str_byte}, + {"char", str_char}, + {"dump", str_dump}, + {"find", str_find}, + {"format", str_format}, + {"gfind", gfind_nodef}, + {"gmatch", gmatch}, + {"gsub", str_gsub}, + {"len", str_len}, + {"lower", str_lower}, + {"match", str_match}, + {"rep", str_rep}, + {"reverse", str_reverse}, + {"sub", str_sub}, + {"upper", str_upper}, + {NULL, NULL} +}; + + +static void createmetatable (lua_State *L) { + lua_createtable(L, 0, 1); /* create metatable for strings */ + lua_pushliteral(L, ""); /* dummy string */ + lua_pushvalue(L, -2); + lua_setmetatable(L, -2); /* set string metatable */ + lua_pop(L, 1); /* pop dummy string */ + lua_pushvalue(L, -2); /* string library... */ + lua_setfield(L, -2, "__index"); /* ...is the __index metamethod */ + lua_pop(L, 1); /* pop metatable */ +} + + +/* +** Open string library +*/ +LUALIB_API int luaopen_string (lua_State *L) { + luaL_register(L, LUA_STRLIBNAME, strlib); +#if defined(LUA_COMPAT_GFIND) + lua_getfield(L, -1, "gmatch"); + lua_setfield(L, -2, "gfind"); +#endif + createmetatable(L); + return 1; +} + diff --git a/extern/lua-5.1.5/src/ltable.c b/extern/lua-5.1.5/src/ltable.c new file mode 100644 index 00000000..ec84f4fa --- /dev/null +++ b/extern/lua-5.1.5/src/ltable.c @@ -0,0 +1,588 @@ +/* +** $Id: ltable.c,v 2.32.1.2 2007/12/28 15:32:23 roberto Exp $ +** Lua tables (hash) +** See Copyright Notice in lua.h +*/ + + +/* +** Implementation of tables (aka arrays, objects, or hash tables). +** Tables keep its elements in two parts: an array part and a hash part. +** Non-negative integer keys are all candidates to be kept in the array +** part. The actual size of the array is the largest `n' such that at +** least half the slots between 0 and n are in use. +** Hash uses a mix of chained scatter table with Brent's variation. +** A main invariant of these tables is that, if an element is not +** in its main position (i.e. the `original' position that its hash gives +** to it), then the colliding element is in its own main position. +** Hence even when the load factor reaches 100%, performance remains good. +*/ + +#include +#include + +#define ltable_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "ltable.h" + + +/* +** max size of array part is 2^MAXBITS +*/ +#if LUAI_BITSINT > 26 +#define MAXBITS 26 +#else +#define MAXBITS (LUAI_BITSINT-2) +#endif + +#define MAXASIZE (1 << MAXBITS) + + +#define hashpow2(t,n) (gnode(t, lmod((n), sizenode(t)))) + +#define hashstr(t,str) hashpow2(t, (str)->tsv.hash) +#define hashboolean(t,p) hashpow2(t, p) + + +/* +** for some types, it is better to avoid modulus by power of 2, as +** they tend to have many 2 factors. +*/ +#define hashmod(t,n) (gnode(t, ((n) % ((sizenode(t)-1)|1)))) + + +#define hashpointer(t,p) hashmod(t, IntPoint(p)) + + +/* +** number of ints inside a lua_Number +*/ +#define numints cast_int(sizeof(lua_Number)/sizeof(int)) + + + +#define dummynode (&dummynode_) + +static const Node dummynode_ = { + {{NULL}, LUA_TNIL}, /* value */ + {{{NULL}, LUA_TNIL, NULL}} /* key */ +}; + + +/* +** hash for lua_Numbers +*/ +static Node *hashnum (const Table *t, lua_Number n) { + unsigned int a[numints]; + int i; + if (luai_numeq(n, 0)) /* avoid problems with -0 */ + return gnode(t, 0); + memcpy(a, &n, sizeof(a)); + for (i = 1; i < numints; i++) a[0] += a[i]; + return hashmod(t, a[0]); +} + + + +/* +** returns the `main' position of an element in a table (that is, the index +** of its hash value) +*/ +static Node *mainposition (const Table *t, const TValue *key) { + switch (ttype(key)) { + case LUA_TNUMBER: + return hashnum(t, nvalue(key)); + case LUA_TSTRING: + return hashstr(t, rawtsvalue(key)); + case LUA_TBOOLEAN: + return hashboolean(t, bvalue(key)); + case LUA_TLIGHTUSERDATA: + return hashpointer(t, pvalue(key)); + default: + return hashpointer(t, gcvalue(key)); + } +} + + +/* +** returns the index for `key' if `key' is an appropriate key to live in +** the array part of the table, -1 otherwise. +*/ +static int arrayindex (const TValue *key) { + if (ttisnumber(key)) { + lua_Number n = nvalue(key); + int k; + lua_number2int(k, n); + if (luai_numeq(cast_num(k), n)) + return k; + } + return -1; /* `key' did not match some condition */ +} + + +/* +** returns the index of a `key' for table traversals. First goes all +** elements in the array part, then elements in the hash part. The +** beginning of a traversal is signalled by -1. +*/ +static int findindex (lua_State *L, Table *t, StkId key) { + int i; + if (ttisnil(key)) return -1; /* first iteration */ + i = arrayindex(key); + if (0 < i && i <= t->sizearray) /* is `key' inside array part? */ + return i-1; /* yes; that's the index (corrected to C) */ + else { + Node *n = mainposition(t, key); + do { /* check whether `key' is somewhere in the chain */ + /* key may be dead already, but it is ok to use it in `next' */ + if (luaO_rawequalObj(key2tval(n), key) || + (ttype(gkey(n)) == LUA_TDEADKEY && iscollectable(key) && + gcvalue(gkey(n)) == gcvalue(key))) { + i = cast_int(n - gnode(t, 0)); /* key index in hash table */ + /* hash elements are numbered after array ones */ + return i + t->sizearray; + } + else n = gnext(n); + } while (n); + luaG_runerror(L, "invalid key to " LUA_QL("next")); /* key not found */ + return 0; /* to avoid warnings */ + } +} + + +int luaH_next (lua_State *L, Table *t, StkId key) { + int i = findindex(L, t, key); /* find original element */ + for (i++; i < t->sizearray; i++) { /* try first array part */ + if (!ttisnil(&t->array[i])) { /* a non-nil value? */ + setnvalue(key, cast_num(i+1)); + setobj2s(L, key+1, &t->array[i]); + return 1; + } + } + for (i -= t->sizearray; i < sizenode(t); i++) { /* then hash part */ + if (!ttisnil(gval(gnode(t, i)))) { /* a non-nil value? */ + setobj2s(L, key, key2tval(gnode(t, i))); + setobj2s(L, key+1, gval(gnode(t, i))); + return 1; + } + } + return 0; /* no more elements */ +} + + +/* +** {============================================================= +** Rehash +** ============================================================== +*/ + + +static int computesizes (int nums[], int *narray) { + int i; + int twotoi; /* 2^i */ + int a = 0; /* number of elements smaller than 2^i */ + int na = 0; /* number of elements to go to array part */ + int n = 0; /* optimal size for array part */ + for (i = 0, twotoi = 1; twotoi/2 < *narray; i++, twotoi *= 2) { + if (nums[i] > 0) { + a += nums[i]; + if (a > twotoi/2) { /* more than half elements present? */ + n = twotoi; /* optimal size (till now) */ + na = a; /* all elements smaller than n will go to array part */ + } + } + if (a == *narray) break; /* all elements already counted */ + } + *narray = n; + lua_assert(*narray/2 <= na && na <= *narray); + return na; +} + + +static int countint (const TValue *key, int *nums) { + int k = arrayindex(key); + if (0 < k && k <= MAXASIZE) { /* is `key' an appropriate array index? */ + nums[ceillog2(k)]++; /* count as such */ + return 1; + } + else + return 0; +} + + +static int numusearray (const Table *t, int *nums) { + int lg; + int ttlg; /* 2^lg */ + int ause = 0; /* summation of `nums' */ + int i = 1; /* count to traverse all array keys */ + for (lg=0, ttlg=1; lg<=MAXBITS; lg++, ttlg*=2) { /* for each slice */ + int lc = 0; /* counter */ + int lim = ttlg; + if (lim > t->sizearray) { + lim = t->sizearray; /* adjust upper limit */ + if (i > lim) + break; /* no more elements to count */ + } + /* count elements in range (2^(lg-1), 2^lg] */ + for (; i <= lim; i++) { + if (!ttisnil(&t->array[i-1])) + lc++; + } + nums[lg] += lc; + ause += lc; + } + return ause; +} + + +static int numusehash (const Table *t, int *nums, int *pnasize) { + int totaluse = 0; /* total number of elements */ + int ause = 0; /* summation of `nums' */ + int i = sizenode(t); + while (i--) { + Node *n = &t->node[i]; + if (!ttisnil(gval(n))) { + ause += countint(key2tval(n), nums); + totaluse++; + } + } + *pnasize += ause; + return totaluse; +} + + +static void setarrayvector (lua_State *L, Table *t, int size) { + int i; + luaM_reallocvector(L, t->array, t->sizearray, size, TValue); + for (i=t->sizearray; iarray[i]); + t->sizearray = size; +} + + +static void setnodevector (lua_State *L, Table *t, int size) { + int lsize; + if (size == 0) { /* no elements to hash part? */ + t->node = cast(Node *, dummynode); /* use common `dummynode' */ + lsize = 0; + } + else { + int i; + lsize = ceillog2(size); + if (lsize > MAXBITS) + luaG_runerror(L, "table overflow"); + size = twoto(lsize); + t->node = luaM_newvector(L, size, Node); + for (i=0; ilsizenode = cast_byte(lsize); + t->lastfree = gnode(t, size); /* all positions are free */ +} + + +static void resize (lua_State *L, Table *t, int nasize, int nhsize) { + int i; + int oldasize = t->sizearray; + int oldhsize = t->lsizenode; + Node *nold = t->node; /* save old hash ... */ + if (nasize > oldasize) /* array part must grow? */ + setarrayvector(L, t, nasize); + /* create new hash part with appropriate size */ + setnodevector(L, t, nhsize); + if (nasize < oldasize) { /* array part must shrink? */ + t->sizearray = nasize; + /* re-insert elements from vanishing slice */ + for (i=nasize; iarray[i])) + setobjt2t(L, luaH_setnum(L, t, i+1), &t->array[i]); + } + /* shrink array */ + luaM_reallocvector(L, t->array, oldasize, nasize, TValue); + } + /* re-insert elements from hash part */ + for (i = twoto(oldhsize) - 1; i >= 0; i--) { + Node *old = nold+i; + if (!ttisnil(gval(old))) + setobjt2t(L, luaH_set(L, t, key2tval(old)), gval(old)); + } + if (nold != dummynode) + luaM_freearray(L, nold, twoto(oldhsize), Node); /* free old array */ +} + + +void luaH_resizearray (lua_State *L, Table *t, int nasize) { + int nsize = (t->node == dummynode) ? 0 : sizenode(t); + resize(L, t, nasize, nsize); +} + + +static void rehash (lua_State *L, Table *t, const TValue *ek) { + int nasize, na; + int nums[MAXBITS+1]; /* nums[i] = number of keys between 2^(i-1) and 2^i */ + int i; + int totaluse; + for (i=0; i<=MAXBITS; i++) nums[i] = 0; /* reset counts */ + nasize = numusearray(t, nums); /* count keys in array part */ + totaluse = nasize; /* all those keys are integer keys */ + totaluse += numusehash(t, nums, &nasize); /* count keys in hash part */ + /* count extra key */ + nasize += countint(ek, nums); + totaluse++; + /* compute new size for array part */ + na = computesizes(nums, &nasize); + /* resize the table to new computed sizes */ + resize(L, t, nasize, totaluse - na); +} + + + +/* +** }============================================================= +*/ + + +Table *luaH_new (lua_State *L, int narray, int nhash) { + Table *t = luaM_new(L, Table); + luaC_link(L, obj2gco(t), LUA_TTABLE); + t->metatable = NULL; + t->flags = cast_byte(~0); + /* temporary values (kept only if some malloc fails) */ + t->array = NULL; + t->sizearray = 0; + t->lsizenode = 0; + t->node = cast(Node *, dummynode); + setarrayvector(L, t, narray); + setnodevector(L, t, nhash); + return t; +} + + +void luaH_free (lua_State *L, Table *t) { + if (t->node != dummynode) + luaM_freearray(L, t->node, sizenode(t), Node); + luaM_freearray(L, t->array, t->sizearray, TValue); + luaM_free(L, t); +} + + +static Node *getfreepos (Table *t) { + while (t->lastfree-- > t->node) { + if (ttisnil(gkey(t->lastfree))) + return t->lastfree; + } + return NULL; /* could not find a free place */ +} + + + +/* +** inserts a new key into a hash table; first, check whether key's main +** position is free. If not, check whether colliding node is in its main +** position or not: if it is not, move colliding node to an empty place and +** put new key in its main position; otherwise (colliding node is in its main +** position), new key goes to an empty position. +*/ +static TValue *newkey (lua_State *L, Table *t, const TValue *key) { + Node *mp = mainposition(t, key); + if (!ttisnil(gval(mp)) || mp == dummynode) { + Node *othern; + Node *n = getfreepos(t); /* get a free place */ + if (n == NULL) { /* cannot find a free place? */ + rehash(L, t, key); /* grow table */ + return luaH_set(L, t, key); /* re-insert key into grown table */ + } + lua_assert(n != dummynode); + othern = mainposition(t, key2tval(mp)); + if (othern != mp) { /* is colliding node out of its main position? */ + /* yes; move colliding node into free position */ + while (gnext(othern) != mp) othern = gnext(othern); /* find previous */ + gnext(othern) = n; /* redo the chain with `n' in place of `mp' */ + *n = *mp; /* copy colliding node into free pos. (mp->next also goes) */ + gnext(mp) = NULL; /* now `mp' is free */ + setnilvalue(gval(mp)); + } + else { /* colliding node is in its own main position */ + /* new node will go into free position */ + gnext(n) = gnext(mp); /* chain new position */ + gnext(mp) = n; + mp = n; + } + } + gkey(mp)->value = key->value; gkey(mp)->tt = key->tt; + luaC_barriert(L, t, key); + lua_assert(ttisnil(gval(mp))); + return gval(mp); +} + + +/* +** search function for integers +*/ +const TValue *luaH_getnum (Table *t, int key) { + /* (1 <= key && key <= t->sizearray) */ + if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray)) + return &t->array[key-1]; + else { + lua_Number nk = cast_num(key); + Node *n = hashnum(t, nk); + do { /* check whether `key' is somewhere in the chain */ + if (ttisnumber(gkey(n)) && luai_numeq(nvalue(gkey(n)), nk)) + return gval(n); /* that's it */ + else n = gnext(n); + } while (n); + return luaO_nilobject; + } +} + + +/* +** search function for strings +*/ +const TValue *luaH_getstr (Table *t, TString *key) { + Node *n = hashstr(t, key); + do { /* check whether `key' is somewhere in the chain */ + if (ttisstring(gkey(n)) && rawtsvalue(gkey(n)) == key) + return gval(n); /* that's it */ + else n = gnext(n); + } while (n); + return luaO_nilobject; +} + + +/* +** main search function +*/ +const TValue *luaH_get (Table *t, const TValue *key) { + switch (ttype(key)) { + case LUA_TNIL: return luaO_nilobject; + case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key)); + case LUA_TNUMBER: { + int k; + lua_Number n = nvalue(key); + lua_number2int(k, n); + if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */ + return luaH_getnum(t, k); /* use specialized version */ + /* else go through */ + } + default: { + Node *n = mainposition(t, key); + do { /* check whether `key' is somewhere in the chain */ + if (luaO_rawequalObj(key2tval(n), key)) + return gval(n); /* that's it */ + else n = gnext(n); + } while (n); + return luaO_nilobject; + } + } +} + + +TValue *luaH_set (lua_State *L, Table *t, const TValue *key) { + const TValue *p = luaH_get(t, key); + t->flags = 0; + if (p != luaO_nilobject) + return cast(TValue *, p); + else { + if (ttisnil(key)) luaG_runerror(L, "table index is nil"); + else if (ttisnumber(key) && luai_numisnan(nvalue(key))) + luaG_runerror(L, "table index is NaN"); + return newkey(L, t, key); + } +} + + +TValue *luaH_setnum (lua_State *L, Table *t, int key) { + const TValue *p = luaH_getnum(t, key); + if (p != luaO_nilobject) + return cast(TValue *, p); + else { + TValue k; + setnvalue(&k, cast_num(key)); + return newkey(L, t, &k); + } +} + + +TValue *luaH_setstr (lua_State *L, Table *t, TString *key) { + const TValue *p = luaH_getstr(t, key); + if (p != luaO_nilobject) + return cast(TValue *, p); + else { + TValue k; + setsvalue(L, &k, key); + return newkey(L, t, &k); + } +} + + +static int unbound_search (Table *t, unsigned int j) { + unsigned int i = j; /* i is zero or a present index */ + j++; + /* find `i' and `j' such that i is present and j is not */ + while (!ttisnil(luaH_getnum(t, j))) { + i = j; + j *= 2; + if (j > cast(unsigned int, MAX_INT)) { /* overflow? */ + /* table was built with bad purposes: resort to linear search */ + i = 1; + while (!ttisnil(luaH_getnum(t, i))) i++; + return i - 1; + } + } + /* now do a binary search between them */ + while (j - i > 1) { + unsigned int m = (i+j)/2; + if (ttisnil(luaH_getnum(t, m))) j = m; + else i = m; + } + return i; +} + + +/* +** Try to find a boundary in table `t'. A `boundary' is an integer index +** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil). +*/ +int luaH_getn (Table *t) { + unsigned int j = t->sizearray; + if (j > 0 && ttisnil(&t->array[j - 1])) { + /* there is a boundary in the array part: (binary) search for it */ + unsigned int i = 0; + while (j - i > 1) { + unsigned int m = (i+j)/2; + if (ttisnil(&t->array[m - 1])) j = m; + else i = m; + } + return i; + } + /* else must find a boundary in hash part */ + else if (t->node == dummynode) /* hash part is empty? */ + return j; /* that is easy... */ + else return unbound_search(t, j); +} + + + +#if defined(LUA_DEBUG) + +Node *luaH_mainposition (const Table *t, const TValue *key) { + return mainposition(t, key); +} + +int luaH_isdummy (Node *n) { return n == dummynode; } + +#endif diff --git a/extern/lua-5.1.5/src/ltable.h b/extern/lua-5.1.5/src/ltable.h new file mode 100644 index 00000000..f5b9d5ea --- /dev/null +++ b/extern/lua-5.1.5/src/ltable.h @@ -0,0 +1,40 @@ +/* +** $Id: ltable.h,v 2.10.1.1 2007/12/27 13:02:25 roberto Exp $ +** Lua tables (hash) +** See Copyright Notice in lua.h +*/ + +#ifndef ltable_h +#define ltable_h + +#include "lobject.h" + + +#define gnode(t,i) (&(t)->node[i]) +#define gkey(n) (&(n)->i_key.nk) +#define gval(n) (&(n)->i_val) +#define gnext(n) ((n)->i_key.nk.next) + +#define key2tval(n) (&(n)->i_key.tvk) + + +LUAI_FUNC const TValue *luaH_getnum (Table *t, int key); +LUAI_FUNC TValue *luaH_setnum (lua_State *L, Table *t, int key); +LUAI_FUNC const TValue *luaH_getstr (Table *t, TString *key); +LUAI_FUNC TValue *luaH_setstr (lua_State *L, Table *t, TString *key); +LUAI_FUNC const TValue *luaH_get (Table *t, const TValue *key); +LUAI_FUNC TValue *luaH_set (lua_State *L, Table *t, const TValue *key); +LUAI_FUNC Table *luaH_new (lua_State *L, int narray, int lnhash); +LUAI_FUNC void luaH_resizearray (lua_State *L, Table *t, int nasize); +LUAI_FUNC void luaH_free (lua_State *L, Table *t); +LUAI_FUNC int luaH_next (lua_State *L, Table *t, StkId key); +LUAI_FUNC int luaH_getn (Table *t); + + +#if defined(LUA_DEBUG) +LUAI_FUNC Node *luaH_mainposition (const Table *t, const TValue *key); +LUAI_FUNC int luaH_isdummy (Node *n); +#endif + + +#endif diff --git a/extern/lua-5.1.5/src/ltablib.c b/extern/lua-5.1.5/src/ltablib.c new file mode 100644 index 00000000..b6d9cb4a --- /dev/null +++ b/extern/lua-5.1.5/src/ltablib.c @@ -0,0 +1,287 @@ +/* +** $Id: ltablib.c,v 1.38.1.3 2008/02/14 16:46:58 roberto Exp $ +** Library for Table Manipulation +** See Copyright Notice in lua.h +*/ + + +#include + +#define ltablib_c +#define LUA_LIB + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + +#define aux_getn(L,n) (luaL_checktype(L, n, LUA_TTABLE), luaL_getn(L, n)) + + +static int foreachi (lua_State *L) { + int i; + int n = aux_getn(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + for (i=1; i <= n; i++) { + lua_pushvalue(L, 2); /* function */ + lua_pushinteger(L, i); /* 1st argument */ + lua_rawgeti(L, 1, i); /* 2nd argument */ + lua_call(L, 2, 1); + if (!lua_isnil(L, -1)) + return 1; + lua_pop(L, 1); /* remove nil result */ + } + return 0; +} + + +static int foreach (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, 2, LUA_TFUNCTION); + lua_pushnil(L); /* first key */ + while (lua_next(L, 1)) { + lua_pushvalue(L, 2); /* function */ + lua_pushvalue(L, -3); /* key */ + lua_pushvalue(L, -3); /* value */ + lua_call(L, 2, 1); + if (!lua_isnil(L, -1)) + return 1; + lua_pop(L, 2); /* remove value and result */ + } + return 0; +} + + +static int maxn (lua_State *L) { + lua_Number max = 0; + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushnil(L); /* first key */ + while (lua_next(L, 1)) { + lua_pop(L, 1); /* remove value */ + if (lua_type(L, -1) == LUA_TNUMBER) { + lua_Number v = lua_tonumber(L, -1); + if (v > max) max = v; + } + } + lua_pushnumber(L, max); + return 1; +} + + +static int getn (lua_State *L) { + lua_pushinteger(L, aux_getn(L, 1)); + return 1; +} + + +static int setn (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); +#ifndef luaL_setn + luaL_setn(L, 1, luaL_checkint(L, 2)); +#else + luaL_error(L, LUA_QL("setn") " is obsolete"); +#endif + lua_pushvalue(L, 1); + return 1; +} + + +static int tinsert (lua_State *L) { + int e = aux_getn(L, 1) + 1; /* first empty element */ + int pos; /* where to insert new element */ + switch (lua_gettop(L)) { + case 2: { /* called with only 2 arguments */ + pos = e; /* insert new element at the end */ + break; + } + case 3: { + int i; + pos = luaL_checkint(L, 2); /* 2nd argument is the position */ + if (pos > e) e = pos; /* `grow' array if necessary */ + for (i = e; i > pos; i--) { /* move up elements */ + lua_rawgeti(L, 1, i-1); + lua_rawseti(L, 1, i); /* t[i] = t[i-1] */ + } + break; + } + default: { + return luaL_error(L, "wrong number of arguments to " LUA_QL("insert")); + } + } + luaL_setn(L, 1, e); /* new size */ + lua_rawseti(L, 1, pos); /* t[pos] = v */ + return 0; +} + + +static int tremove (lua_State *L) { + int e = aux_getn(L, 1); + int pos = luaL_optint(L, 2, e); + if (!(1 <= pos && pos <= e)) /* position is outside bounds? */ + return 0; /* nothing to remove */ + luaL_setn(L, 1, e - 1); /* t.n = n-1 */ + lua_rawgeti(L, 1, pos); /* result = t[pos] */ + for ( ;pos= P */ + while (lua_rawgeti(L, 1, ++i), sort_comp(L, -1, -2)) { + if (i>u) luaL_error(L, "invalid order function for sorting"); + lua_pop(L, 1); /* remove a[i] */ + } + /* repeat --j until a[j] <= P */ + while (lua_rawgeti(L, 1, --j), sort_comp(L, -3, -1)) { + if (j + +#define ltm_c +#define LUA_CORE + +#include "lua.h" + +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + + +const char *const luaT_typenames[] = { + "nil", "boolean", "userdata", "number", + "string", "table", "function", "userdata", "thread", + "proto", "upval" +}; + + +void luaT_init (lua_State *L) { + static const char *const luaT_eventname[] = { /* ORDER TM */ + "__index", "__newindex", + "__gc", "__mode", "__eq", + "__add", "__sub", "__mul", "__div", "__mod", + "__pow", "__unm", "__len", "__lt", "__le", + "__concat", "__call" + }; + int i; + for (i=0; itmname[i] = luaS_new(L, luaT_eventname[i]); + luaS_fix(G(L)->tmname[i]); /* never collect these names */ + } +} + + +/* +** function to be used with macro "fasttm": optimized for absence of +** tag methods +*/ +const TValue *luaT_gettm (Table *events, TMS event, TString *ename) { + const TValue *tm = luaH_getstr(events, ename); + lua_assert(event <= TM_EQ); + if (ttisnil(tm)) { /* no tag method? */ + events->flags |= cast_byte(1u<metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(o)->metatable; + break; + default: + mt = G(L)->mt[ttype(o)]; + } + return (mt ? luaH_getstr(mt, G(L)->tmname[event]) : luaO_nilobject); +} + diff --git a/extern/lua-5.1.5/src/ltm.h b/extern/lua-5.1.5/src/ltm.h new file mode 100644 index 00000000..64343b78 --- /dev/null +++ b/extern/lua-5.1.5/src/ltm.h @@ -0,0 +1,54 @@ +/* +** $Id: ltm.h,v 2.6.1.1 2007/12/27 13:02:25 roberto Exp $ +** Tag methods +** See Copyright Notice in lua.h +*/ + +#ifndef ltm_h +#define ltm_h + + +#include "lobject.h" + + +/* +* WARNING: if you change the order of this enumeration, +* grep "ORDER TM" +*/ +typedef enum { + TM_INDEX, + TM_NEWINDEX, + TM_GC, + TM_MODE, + TM_EQ, /* last tag method with `fast' access */ + TM_ADD, + TM_SUB, + TM_MUL, + TM_DIV, + TM_MOD, + TM_POW, + TM_UNM, + TM_LEN, + TM_LT, + TM_LE, + TM_CONCAT, + TM_CALL, + TM_N /* number of elements in the enum */ +} TMS; + + + +#define gfasttm(g,et,e) ((et) == NULL ? NULL : \ + ((et)->flags & (1u<<(e))) ? NULL : luaT_gettm(et, e, (g)->tmname[e])) + +#define fasttm(l,et,e) gfasttm(G(l), et, e) + +LUAI_DATA const char *const luaT_typenames[]; + + +LUAI_FUNC const TValue *luaT_gettm (Table *events, TMS event, TString *ename); +LUAI_FUNC const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, + TMS event); +LUAI_FUNC void luaT_init (lua_State *L); + +#endif diff --git a/extern/lua-5.1.5/src/lua.c b/extern/lua-5.1.5/src/lua.c new file mode 100644 index 00000000..3a466093 --- /dev/null +++ b/extern/lua-5.1.5/src/lua.c @@ -0,0 +1,392 @@ +/* +** $Id: lua.c,v 1.160.1.2 2007/12/28 15:32:23 roberto Exp $ +** Lua stand-alone interpreter +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include +#include + +#define lua_c + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" + + + +static lua_State *globalL = NULL; + +static const char *progname = LUA_PROGNAME; + + + +static void lstop (lua_State *L, lua_Debug *ar) { + (void)ar; /* unused arg. */ + lua_sethook(L, NULL, 0, 0); + luaL_error(L, "interrupted!"); +} + + +static void laction (int i) { + signal(i, SIG_DFL); /* if another SIGINT happens before lstop, + terminate process (default action) */ + lua_sethook(globalL, lstop, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); +} + + +static void print_usage (void) { + fprintf(stderr, + "usage: %s [options] [script [args]].\n" + "Available options are:\n" + " -e stat execute string " LUA_QL("stat") "\n" + " -l name require library " LUA_QL("name") "\n" + " -i enter interactive mode after executing " LUA_QL("script") "\n" + " -v show version information\n" + " -- stop handling options\n" + " - execute stdin and stop handling options\n" + , + progname); + fflush(stderr); +} + + +static void l_message (const char *pname, const char *msg) { + if (pname) fprintf(stderr, "%s: ", pname); + fprintf(stderr, "%s\n", msg); + fflush(stderr); +} + + +static int report (lua_State *L, int status) { + if (status && !lua_isnil(L, -1)) { + const char *msg = lua_tostring(L, -1); + if (msg == NULL) msg = "(error object is not a string)"; + l_message(progname, msg); + lua_pop(L, 1); + } + return status; +} + + +static int traceback (lua_State *L) { + if (!lua_isstring(L, 1)) /* 'message' not a string? */ + return 1; /* keep it intact */ + lua_getfield(L, LUA_GLOBALSINDEX, "debug"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return 1; + } + lua_getfield(L, -1, "traceback"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + return 1; + } + lua_pushvalue(L, 1); /* pass error message */ + lua_pushinteger(L, 2); /* skip this function and traceback */ + lua_call(L, 2, 1); /* call debug.traceback */ + return 1; +} + + +static int docall (lua_State *L, int narg, int clear) { + int status; + int base = lua_gettop(L) - narg; /* function index */ + lua_pushcfunction(L, traceback); /* push traceback function */ + lua_insert(L, base); /* put it under chunk and args */ + signal(SIGINT, laction); + status = lua_pcall(L, narg, (clear ? 0 : LUA_MULTRET), base); + signal(SIGINT, SIG_DFL); + lua_remove(L, base); /* remove traceback function */ + /* force a complete garbage collection in case of errors */ + if (status != 0) lua_gc(L, LUA_GCCOLLECT, 0); + return status; +} + + +static void print_version (void) { + l_message(NULL, LUA_RELEASE " " LUA_COPYRIGHT); +} + + +static int getargs (lua_State *L, char **argv, int n) { + int narg; + int i; + int argc = 0; + while (argv[argc]) argc++; /* count total number of arguments */ + narg = argc - (n + 1); /* number of arguments to the script */ + luaL_checkstack(L, narg + 3, "too many arguments to script"); + for (i=n+1; i < argc; i++) + lua_pushstring(L, argv[i]); + lua_createtable(L, narg, n + 1); + for (i=0; i < argc; i++) { + lua_pushstring(L, argv[i]); + lua_rawseti(L, -2, i - n); + } + return narg; +} + + +static int dofile (lua_State *L, const char *name) { + int status = luaL_loadfile(L, name) || docall(L, 0, 1); + return report(L, status); +} + + +static int dostring (lua_State *L, const char *s, const char *name) { + int status = luaL_loadbuffer(L, s, strlen(s), name) || docall(L, 0, 1); + return report(L, status); +} + + +static int dolibrary (lua_State *L, const char *name) { + lua_getglobal(L, "require"); + lua_pushstring(L, name); + return report(L, docall(L, 1, 1)); +} + + +static const char *get_prompt (lua_State *L, int firstline) { + const char *p; + lua_getfield(L, LUA_GLOBALSINDEX, firstline ? "_PROMPT" : "_PROMPT2"); + p = lua_tostring(L, -1); + if (p == NULL) p = (firstline ? LUA_PROMPT : LUA_PROMPT2); + lua_pop(L, 1); /* remove global */ + return p; +} + + +static int incomplete (lua_State *L, int status) { + if (status == LUA_ERRSYNTAX) { + size_t lmsg; + const char *msg = lua_tolstring(L, -1, &lmsg); + const char *tp = msg + lmsg - (sizeof(LUA_QL("")) - 1); + if (strstr(msg, LUA_QL("")) == tp) { + lua_pop(L, 1); + return 1; + } + } + return 0; /* else... */ +} + + +static int pushline (lua_State *L, int firstline) { + char buffer[LUA_MAXINPUT]; + char *b = buffer; + size_t l; + const char *prmt = get_prompt(L, firstline); + if (lua_readline(L, b, prmt) == 0) + return 0; /* no input */ + l = strlen(b); + if (l > 0 && b[l-1] == '\n') /* line ends with newline? */ + b[l-1] = '\0'; /* remove it */ + if (firstline && b[0] == '=') /* first line starts with `=' ? */ + lua_pushfstring(L, "return %s", b+1); /* change it to `return' */ + else + lua_pushstring(L, b); + lua_freeline(L, b); + return 1; +} + + +static int loadline (lua_State *L) { + int status; + lua_settop(L, 0); + if (!pushline(L, 1)) + return -1; /* no input */ + for (;;) { /* repeat until gets a complete line */ + status = luaL_loadbuffer(L, lua_tostring(L, 1), lua_strlen(L, 1), "=stdin"); + if (!incomplete(L, status)) break; /* cannot try to add lines? */ + if (!pushline(L, 0)) /* no more input? */ + return -1; + lua_pushliteral(L, "\n"); /* add a new line... */ + lua_insert(L, -2); /* ...between the two lines */ + lua_concat(L, 3); /* join them */ + } + lua_saveline(L, 1); + lua_remove(L, 1); /* remove line */ + return status; +} + + +static void dotty (lua_State *L) { + int status; + const char *oldprogname = progname; + progname = NULL; + while ((status = loadline(L)) != -1) { + if (status == 0) status = docall(L, 0, 0); + report(L, status); + if (status == 0 && lua_gettop(L) > 0) { /* any result to print? */ + lua_getglobal(L, "print"); + lua_insert(L, 1); + if (lua_pcall(L, lua_gettop(L)-1, 0, 0) != 0) + l_message(progname, lua_pushfstring(L, + "error calling " LUA_QL("print") " (%s)", + lua_tostring(L, -1))); + } + } + lua_settop(L, 0); /* clear stack */ + fputs("\n", stdout); + fflush(stdout); + progname = oldprogname; +} + + +static int handle_script (lua_State *L, char **argv, int n) { + int status; + const char *fname; + int narg = getargs(L, argv, n); /* collect arguments */ + lua_setglobal(L, "arg"); + fname = argv[n]; + if (strcmp(fname, "-") == 0 && strcmp(argv[n-1], "--") != 0) + fname = NULL; /* stdin */ + status = luaL_loadfile(L, fname); + lua_insert(L, -(narg+1)); + if (status == 0) + status = docall(L, narg, 0); + else + lua_pop(L, narg); + return report(L, status); +} + + +/* check that argument has no extra characters at the end */ +#define notail(x) {if ((x)[2] != '\0') return -1;} + + +static int collectargs (char **argv, int *pi, int *pv, int *pe) { + int i; + for (i = 1; argv[i] != NULL; i++) { + if (argv[i][0] != '-') /* not an option? */ + return i; + switch (argv[i][1]) { /* option */ + case '-': + notail(argv[i]); + return (argv[i+1] != NULL ? i+1 : 0); + case '\0': + return i; + case 'i': + notail(argv[i]); + *pi = 1; /* go through */ + case 'v': + notail(argv[i]); + *pv = 1; + break; + case 'e': + *pe = 1; /* go through */ + case 'l': + if (argv[i][2] == '\0') { + i++; + if (argv[i] == NULL) return -1; + } + break; + default: return -1; /* invalid option */ + } + } + return 0; +} + + +static int runargs (lua_State *L, char **argv, int n) { + int i; + for (i = 1; i < n; i++) { + if (argv[i] == NULL) continue; + lua_assert(argv[i][0] == '-'); + switch (argv[i][1]) { /* option */ + case 'e': { + const char *chunk = argv[i] + 2; + if (*chunk == '\0') chunk = argv[++i]; + lua_assert(chunk != NULL); + if (dostring(L, chunk, "=(command line)") != 0) + return 1; + break; + } + case 'l': { + const char *filename = argv[i] + 2; + if (*filename == '\0') filename = argv[++i]; + lua_assert(filename != NULL); + if (dolibrary(L, filename)) + return 1; /* stop if file fails */ + break; + } + default: break; + } + } + return 0; +} + + +static int handle_luainit (lua_State *L) { + const char *init = getenv(LUA_INIT); + if (init == NULL) return 0; /* status OK */ + else if (init[0] == '@') + return dofile(L, init+1); + else + return dostring(L, init, "=" LUA_INIT); +} + + +struct Smain { + int argc; + char **argv; + int status; +}; + + +static int pmain (lua_State *L) { + struct Smain *s = (struct Smain *)lua_touserdata(L, 1); + char **argv = s->argv; + int script; + int has_i = 0, has_v = 0, has_e = 0; + globalL = L; + if (argv[0] && argv[0][0]) progname = argv[0]; + lua_gc(L, LUA_GCSTOP, 0); /* stop collector during initialization */ + luaL_openlibs(L); /* open libraries */ + lua_gc(L, LUA_GCRESTART, 0); + s->status = handle_luainit(L); + if (s->status != 0) return 0; + script = collectargs(argv, &has_i, &has_v, &has_e); + if (script < 0) { /* invalid args? */ + print_usage(); + s->status = 1; + return 0; + } + if (has_v) print_version(); + s->status = runargs(L, argv, (script > 0) ? script : s->argc); + if (s->status != 0) return 0; + if (script) + s->status = handle_script(L, argv, script); + if (s->status != 0) return 0; + if (has_i) + dotty(L); + else if (script == 0 && !has_e && !has_v) { + if (lua_stdin_is_tty()) { + print_version(); + dotty(L); + } + else dofile(L, NULL); /* executes stdin as a file */ + } + return 0; +} + + +int main (int argc, char **argv) { + int status; + struct Smain s; + lua_State *L = lua_open(); /* create state */ + if (L == NULL) { + l_message(argv[0], "cannot create state: not enough memory"); + return EXIT_FAILURE; + } + s.argc = argc; + s.argv = argv; + status = lua_cpcall(L, &pmain, &s); + report(L, status); + lua_close(L); + return (status || s.status) ? EXIT_FAILURE : EXIT_SUCCESS; +} + diff --git a/extern/lua-5.1.5/src/lua.h b/extern/lua-5.1.5/src/lua.h new file mode 100644 index 00000000..a4b73e74 --- /dev/null +++ b/extern/lua-5.1.5/src/lua.h @@ -0,0 +1,388 @@ +/* +** $Id: lua.h,v 1.218.1.7 2012/01/13 20:36:20 roberto Exp $ +** Lua - An Extensible Extension Language +** Lua.org, PUC-Rio, Brazil (http://www.lua.org) +** See Copyright Notice at the end of this file +*/ + + +#ifndef lua_h +#define lua_h + +#include +#include + + +#include "luaconf.h" + + +#define LUA_VERSION "Lua 5.1" +#define LUA_RELEASE "Lua 5.1.5" +#define LUA_VERSION_NUM 501 +#define LUA_COPYRIGHT "Copyright (C) 1994-2012 Lua.org, PUC-Rio" +#define LUA_AUTHORS "R. Ierusalimschy, L. H. de Figueiredo & W. Celes" + + +/* mark for precompiled code (`Lua') */ +#define LUA_SIGNATURE "\033Lua" + +/* option for multiple returns in `lua_pcall' and `lua_call' */ +#define LUA_MULTRET (-1) + + +/* +** pseudo-indices +*/ +#define LUA_REGISTRYINDEX (-10000) +#define LUA_ENVIRONINDEX (-10001) +#define LUA_GLOBALSINDEX (-10002) +#define lua_upvalueindex(i) (LUA_GLOBALSINDEX-(i)) + + +/* thread status; 0 is OK */ +#define LUA_YIELD 1 +#define LUA_ERRRUN 2 +#define LUA_ERRSYNTAX 3 +#define LUA_ERRMEM 4 +#define LUA_ERRERR 5 + + +typedef struct lua_State lua_State; + +typedef int (*lua_CFunction) (lua_State *L); + + +/* +** functions that read/write blocks when loading/dumping Lua chunks +*/ +typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz); + +typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud); + + +/* +** prototype for memory-allocation functions +*/ +typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize); + + +/* +** basic types +*/ +#define LUA_TNONE (-1) + +#define LUA_TNIL 0 +#define LUA_TBOOLEAN 1 +#define LUA_TLIGHTUSERDATA 2 +#define LUA_TNUMBER 3 +#define LUA_TSTRING 4 +#define LUA_TTABLE 5 +#define LUA_TFUNCTION 6 +#define LUA_TUSERDATA 7 +#define LUA_TTHREAD 8 + + + +/* minimum Lua stack available to a C function */ +#define LUA_MINSTACK 20 + + +/* +** generic extra include file +*/ +#if defined(LUA_USER_H) +#include LUA_USER_H +#endif + + +/* type of numbers in Lua */ +typedef LUA_NUMBER lua_Number; + + +/* type for integer functions */ +typedef LUA_INTEGER lua_Integer; + + + +/* +** state manipulation +*/ +LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud); +LUA_API void (lua_close) (lua_State *L); +LUA_API lua_State *(lua_newthread) (lua_State *L); + +LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf); + + +/* +** basic stack manipulation +*/ +LUA_API int (lua_gettop) (lua_State *L); +LUA_API void (lua_settop) (lua_State *L, int idx); +LUA_API void (lua_pushvalue) (lua_State *L, int idx); +LUA_API void (lua_remove) (lua_State *L, int idx); +LUA_API void (lua_insert) (lua_State *L, int idx); +LUA_API void (lua_replace) (lua_State *L, int idx); +LUA_API int (lua_checkstack) (lua_State *L, int sz); + +LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n); + + +/* +** access functions (stack -> C) +*/ + +LUA_API int (lua_isnumber) (lua_State *L, int idx); +LUA_API int (lua_isstring) (lua_State *L, int idx); +LUA_API int (lua_iscfunction) (lua_State *L, int idx); +LUA_API int (lua_isuserdata) (lua_State *L, int idx); +LUA_API int (lua_type) (lua_State *L, int idx); +LUA_API const char *(lua_typename) (lua_State *L, int tp); + +LUA_API int (lua_equal) (lua_State *L, int idx1, int idx2); +LUA_API int (lua_rawequal) (lua_State *L, int idx1, int idx2); +LUA_API int (lua_lessthan) (lua_State *L, int idx1, int idx2); + +LUA_API lua_Number (lua_tonumber) (lua_State *L, int idx); +LUA_API lua_Integer (lua_tointeger) (lua_State *L, int idx); +LUA_API int (lua_toboolean) (lua_State *L, int idx); +LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len); +LUA_API size_t (lua_objlen) (lua_State *L, int idx); +LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx); +LUA_API void *(lua_touserdata) (lua_State *L, int idx); +LUA_API lua_State *(lua_tothread) (lua_State *L, int idx); +LUA_API const void *(lua_topointer) (lua_State *L, int idx); + + +/* +** push functions (C -> stack) +*/ +LUA_API void (lua_pushnil) (lua_State *L); +LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n); +LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n); +LUA_API void (lua_pushlstring) (lua_State *L, const char *s, size_t l); +LUA_API void (lua_pushstring) (lua_State *L, const char *s); +LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, + va_list argp); +LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...); +LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); +LUA_API void (lua_pushboolean) (lua_State *L, int b); +LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p); +LUA_API int (lua_pushthread) (lua_State *L); + + +/* +** get functions (Lua -> stack) +*/ +LUA_API void (lua_gettable) (lua_State *L, int idx); +LUA_API void (lua_getfield) (lua_State *L, int idx, const char *k); +LUA_API void (lua_rawget) (lua_State *L, int idx); +LUA_API void (lua_rawgeti) (lua_State *L, int idx, int n); +LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec); +LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz); +LUA_API int (lua_getmetatable) (lua_State *L, int objindex); +LUA_API void (lua_getfenv) (lua_State *L, int idx); + + +/* +** set functions (stack -> Lua) +*/ +LUA_API void (lua_settable) (lua_State *L, int idx); +LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k); +LUA_API void (lua_rawset) (lua_State *L, int idx); +LUA_API void (lua_rawseti) (lua_State *L, int idx, int n); +LUA_API int (lua_setmetatable) (lua_State *L, int objindex); +LUA_API int (lua_setfenv) (lua_State *L, int idx); + + +/* +** `load' and `call' functions (load and run Lua code) +*/ +LUA_API void (lua_call) (lua_State *L, int nargs, int nresults); +LUA_API int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc); +LUA_API int (lua_cpcall) (lua_State *L, lua_CFunction func, void *ud); +LUA_API int (lua_load) (lua_State *L, lua_Reader reader, void *dt, + const char *chunkname); + +LUA_API int (lua_dump) (lua_State *L, lua_Writer writer, void *data); + + +/* +** coroutine functions +*/ +LUA_API int (lua_yield) (lua_State *L, int nresults); +LUA_API int (lua_resume) (lua_State *L, int narg); +LUA_API int (lua_status) (lua_State *L); + +/* +** garbage-collection function and options +*/ + +#define LUA_GCSTOP 0 +#define LUA_GCRESTART 1 +#define LUA_GCCOLLECT 2 +#define LUA_GCCOUNT 3 +#define LUA_GCCOUNTB 4 +#define LUA_GCSTEP 5 +#define LUA_GCSETPAUSE 6 +#define LUA_GCSETSTEPMUL 7 + +LUA_API int (lua_gc) (lua_State *L, int what, int data); + + +/* +** miscellaneous functions +*/ + +LUA_API int (lua_error) (lua_State *L); + +LUA_API int (lua_next) (lua_State *L, int idx); + +LUA_API void (lua_concat) (lua_State *L, int n); + +LUA_API lua_Alloc (lua_getallocf) (lua_State *L, void **ud); +LUA_API void lua_setallocf (lua_State *L, lua_Alloc f, void *ud); + + + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ + +#define lua_pop(L,n) lua_settop(L, -(n)-1) + +#define lua_newtable(L) lua_createtable(L, 0, 0) + +#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n))) + +#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0) + +#define lua_strlen(L,i) lua_objlen(L, (i)) + +#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION) +#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE) +#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA) +#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL) +#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN) +#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD) +#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE) +#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0) + +#define lua_pushliteral(L, s) \ + lua_pushlstring(L, "" s, (sizeof(s)/sizeof(char))-1) + +#define lua_setglobal(L,s) lua_setfield(L, LUA_GLOBALSINDEX, (s)) +#define lua_getglobal(L,s) lua_getfield(L, LUA_GLOBALSINDEX, (s)) + +#define lua_tostring(L,i) lua_tolstring(L, (i), NULL) + + + +/* +** compatibility macros and functions +*/ + +#define lua_open() luaL_newstate() + +#define lua_getregistry(L) lua_pushvalue(L, LUA_REGISTRYINDEX) + +#define lua_getgccount(L) lua_gc(L, LUA_GCCOUNT, 0) + +#define lua_Chunkreader lua_Reader +#define lua_Chunkwriter lua_Writer + + +/* hack */ +LUA_API void lua_setlevel (lua_State *from, lua_State *to); + + +/* +** {====================================================================== +** Debug API +** ======================================================================= +*/ + + +/* +** Event codes +*/ +#define LUA_HOOKCALL 0 +#define LUA_HOOKRET 1 +#define LUA_HOOKLINE 2 +#define LUA_HOOKCOUNT 3 +#define LUA_HOOKTAILRET 4 + + +/* +** Event masks +*/ +#define LUA_MASKCALL (1 << LUA_HOOKCALL) +#define LUA_MASKRET (1 << LUA_HOOKRET) +#define LUA_MASKLINE (1 << LUA_HOOKLINE) +#define LUA_MASKCOUNT (1 << LUA_HOOKCOUNT) + +typedef struct lua_Debug lua_Debug; /* activation record */ + + +/* Functions to be called by the debuger in specific events */ +typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar); + + +LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar); +LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar); +LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *lua_getupvalue (lua_State *L, int funcindex, int n); +LUA_API const char *lua_setupvalue (lua_State *L, int funcindex, int n); + +LUA_API int lua_sethook (lua_State *L, lua_Hook func, int mask, int count); +LUA_API lua_Hook lua_gethook (lua_State *L); +LUA_API int lua_gethookmask (lua_State *L); +LUA_API int lua_gethookcount (lua_State *L); + + +struct lua_Debug { + int event; + const char *name; /* (n) */ + const char *namewhat; /* (n) `global', `local', `field', `method' */ + const char *what; /* (S) `Lua', `C', `main', `tail' */ + const char *source; /* (S) */ + int currentline; /* (l) */ + int nups; /* (u) number of upvalues */ + int linedefined; /* (S) */ + int lastlinedefined; /* (S) */ + char short_src[LUA_IDSIZE]; /* (S) */ + /* private part */ + int i_ci; /* active function */ +}; + +/* }====================================================================== */ + + +/****************************************************************************** +* Copyright (C) 1994-2012 Lua.org, PUC-Rio. All rights reserved. +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +******************************************************************************/ + + +#endif diff --git a/extern/lua-5.1.5/src/luac.c b/extern/lua-5.1.5/src/luac.c new file mode 100644 index 00000000..d0701739 --- /dev/null +++ b/extern/lua-5.1.5/src/luac.c @@ -0,0 +1,200 @@ +/* +** $Id: luac.c,v 1.54 2006/06/02 17:37:11 lhf Exp $ +** Lua compiler (saves bytecodes to files; also list bytecodes) +** See Copyright Notice in lua.h +*/ + +#include +#include +#include +#include + +#define luac_c +#define LUA_CORE + +#include "lua.h" +#include "lauxlib.h" + +#include "ldo.h" +#include "lfunc.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstring.h" +#include "lundump.h" + +#define PROGNAME "luac" /* default program name */ +#define OUTPUT PROGNAME ".out" /* default output file */ + +static int listing=0; /* list bytecodes? */ +static int dumping=1; /* dump bytecodes? */ +static int stripping=0; /* strip debug information? */ +static char Output[]={ OUTPUT }; /* default output file name */ +static const char* output=Output; /* actual output file name */ +static const char* progname=PROGNAME; /* actual program name */ + +static void fatal(const char* message) +{ + fprintf(stderr,"%s: %s\n",progname,message); + exit(EXIT_FAILURE); +} + +static void cannot(const char* what) +{ + fprintf(stderr,"%s: cannot %s %s: %s\n",progname,what,output,strerror(errno)); + exit(EXIT_FAILURE); +} + +static void usage(const char* message) +{ + if (*message=='-') + fprintf(stderr,"%s: unrecognized option " LUA_QS "\n",progname,message); + else + fprintf(stderr,"%s: %s\n",progname,message); + fprintf(stderr, + "usage: %s [options] [filenames].\n" + "Available options are:\n" + " - process stdin\n" + " -l list\n" + " -o name output to file " LUA_QL("name") " (default is \"%s\")\n" + " -p parse only\n" + " -s strip debug information\n" + " -v show version information\n" + " -- stop handling options\n", + progname,Output); + exit(EXIT_FAILURE); +} + +#define IS(s) (strcmp(argv[i],s)==0) + +static int doargs(int argc, char* argv[]) +{ + int i; + int version=0; + if (argv[0]!=NULL && *argv[0]!=0) progname=argv[0]; + for (i=1; itop+(i))->l.p) + +static const Proto* combine(lua_State* L, int n) +{ + if (n==1) + return toproto(L,-1); + else + { + int i,pc; + Proto* f=luaF_newproto(L); + setptvalue2s(L,L->top,f); incr_top(L); + f->source=luaS_newliteral(L,"=(" PROGNAME ")"); + f->maxstacksize=1; + pc=2*n+1; + f->code=luaM_newvector(L,pc,Instruction); + f->sizecode=pc; + f->p=luaM_newvector(L,n,Proto*); + f->sizep=n; + pc=0; + for (i=0; ip[i]=toproto(L,i-n-1); + f->code[pc++]=CREATE_ABx(OP_CLOSURE,0,i); + f->code[pc++]=CREATE_ABC(OP_CALL,0,1,1); + } + f->code[pc++]=CREATE_ABC(OP_RETURN,0,1,0); + return f; + } +} + +static int writer(lua_State* L, const void* p, size_t size, void* u) +{ + UNUSED(L); + return (fwrite(p,size,1,(FILE*)u)!=1) && (size!=0); +} + +struct Smain { + int argc; + char** argv; +}; + +static int pmain(lua_State* L) +{ + struct Smain* s = (struct Smain*)lua_touserdata(L, 1); + int argc=s->argc; + char** argv=s->argv; + const Proto* f; + int i; + if (!lua_checkstack(L,argc)) fatal("too many input files"); + for (i=0; i1); + if (dumping) + { + FILE* D= (output==NULL) ? stdout : fopen(output,"wb"); + if (D==NULL) cannot("open"); + lua_lock(L); + luaU_dump(L,f,writer,D,stripping); + lua_unlock(L); + if (ferror(D)) cannot("write"); + if (fclose(D)) cannot("close"); + } + return 0; +} + +int main(int argc, char* argv[]) +{ + lua_State* L; + struct Smain s; + int i=doargs(argc,argv); + argc-=i; argv+=i; + if (argc<=0) usage("no input files given"); + L=lua_open(); + if (L==NULL) fatal("not enough memory for state"); + s.argc=argc; + s.argv=argv; + if (lua_cpcall(L,pmain,&s)!=0) fatal(lua_tostring(L,-1)); + lua_close(L); + return EXIT_SUCCESS; +} diff --git a/extern/lua-5.1.5/src/luaconf.h b/extern/lua-5.1.5/src/luaconf.h new file mode 100644 index 00000000..e2cb2616 --- /dev/null +++ b/extern/lua-5.1.5/src/luaconf.h @@ -0,0 +1,763 @@ +/* +** $Id: luaconf.h,v 1.82.1.7 2008/02/11 16:25:08 roberto Exp $ +** Configuration file for Lua +** See Copyright Notice in lua.h +*/ + + +#ifndef lconfig_h +#define lconfig_h + +#include +#include + + +/* +** ================================================================== +** Search for "@@" to find all configurable definitions. +** =================================================================== +*/ + + +/* +@@ LUA_ANSI controls the use of non-ansi features. +** CHANGE it (define it) if you want Lua to avoid the use of any +** non-ansi feature or library. +*/ +#if defined(__STRICT_ANSI__) +#define LUA_ANSI +#endif + + +#if !defined(LUA_ANSI) && defined(_WIN32) +#define LUA_WIN +#endif + +#if defined(LUA_USE_LINUX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* needs an extra library: -ldl */ +#define LUA_USE_READLINE /* needs some extra libraries */ +#endif + +#if defined(LUA_USE_MACOSX) +#define LUA_USE_POSIX +#define LUA_DL_DYLD /* does not need extra library */ +#endif + + + +/* +@@ LUA_USE_POSIX includes all functionallity listed as X/Open System +@* Interfaces Extension (XSI). +** CHANGE it (define it) if your system is XSI compatible. +*/ +#if defined(LUA_USE_POSIX) +#define LUA_USE_MKSTEMP +#define LUA_USE_ISATTY +#define LUA_USE_POPEN +#define LUA_USE_ULONGJMP +#endif + + +/* +@@ LUA_PATH and LUA_CPATH are the names of the environment variables that +@* Lua check to set its paths. +@@ LUA_INIT is the name of the environment variable that Lua +@* checks for initialization code. +** CHANGE them if you want different names. +*/ +#define LUA_PATH "LUA_PATH" +#define LUA_CPATH "LUA_CPATH" +#define LUA_INIT "LUA_INIT" + + +/* +@@ LUA_PATH_DEFAULT is the default path that Lua uses to look for +@* Lua libraries. +@@ LUA_CPATH_DEFAULT is the default path that Lua uses to look for +@* C libraries. +** CHANGE them if your machine has a non-conventional directory +** hierarchy or if you want to install your libraries in +** non-conventional directories. +*/ +#if defined(_WIN32) +/* +** In Windows, any exclamation mark ('!') in the path is replaced by the +** path of the directory of the executable file of the current process. +*/ +#define LUA_LDIR "!\\lua\\" +#define LUA_CDIR "!\\" +#define LUA_PATH_DEFAULT \ + ".\\?.lua;" LUA_LDIR"?.lua;" LUA_LDIR"?\\init.lua;" \ + LUA_CDIR"?.lua;" LUA_CDIR"?\\init.lua" +#define LUA_CPATH_DEFAULT \ + ".\\?.dll;" LUA_CDIR"?.dll;" LUA_CDIR"loadall.dll" + +#else +#define LUA_ROOT "/usr/local/" +#define LUA_LDIR LUA_ROOT "share/lua/5.1/" +#define LUA_CDIR LUA_ROOT "lib/lua/5.1/" +#define LUA_PATH_DEFAULT \ + "./?.lua;" LUA_LDIR"?.lua;" LUA_LDIR"?/init.lua;" \ + LUA_CDIR"?.lua;" LUA_CDIR"?/init.lua" +#define LUA_CPATH_DEFAULT \ + "./?.so;" LUA_CDIR"?.so;" LUA_CDIR"loadall.so" +#endif + + +/* +@@ LUA_DIRSEP is the directory separator (for submodules). +** CHANGE it if your machine does not use "/" as the directory separator +** and is not Windows. (On Windows Lua automatically uses "\".) +*/ +#if defined(_WIN32) +#define LUA_DIRSEP "\\" +#else +#define LUA_DIRSEP "/" +#endif + + +/* +@@ LUA_PATHSEP is the character that separates templates in a path. +@@ LUA_PATH_MARK is the string that marks the substitution points in a +@* template. +@@ LUA_EXECDIR in a Windows path is replaced by the executable's +@* directory. +@@ LUA_IGMARK is a mark to ignore all before it when bulding the +@* luaopen_ function name. +** CHANGE them if for some reason your system cannot use those +** characters. (E.g., if one of those characters is a common character +** in file/directory names.) Probably you do not need to change them. +*/ +#define LUA_PATHSEP ";" +#define LUA_PATH_MARK "?" +#define LUA_EXECDIR "!" +#define LUA_IGMARK "-" + + +/* +@@ LUA_INTEGER is the integral type used by lua_pushinteger/lua_tointeger. +** CHANGE that if ptrdiff_t is not adequate on your machine. (On most +** machines, ptrdiff_t gives a good choice between int or long.) +*/ +#define LUA_INTEGER ptrdiff_t + + +/* +@@ LUA_API is a mark for all core API functions. +@@ LUALIB_API is a mark for all standard library functions. +** CHANGE them if you need to define those functions in some special way. +** For instance, if you want to create one Windows DLL with the core and +** the libraries, you may want to use the following definition (define +** LUA_BUILD_AS_DLL to get it). +*/ +#if defined(LUA_BUILD_AS_DLL) + +#if defined(LUA_CORE) || defined(LUA_LIB) +#define LUA_API __declspec(dllexport) +#else +#define LUA_API __declspec(dllimport) +#endif + +#else + +#define LUA_API extern + +#endif + +/* more often than not the libs go together with the core */ +#define LUALIB_API LUA_API + + +/* +@@ LUAI_FUNC is a mark for all extern functions that are not to be +@* exported to outside modules. +@@ LUAI_DATA is a mark for all extern (const) variables that are not to +@* be exported to outside modules. +** CHANGE them if you need to mark them in some special way. Elf/gcc +** (versions 3.2 and later) mark them as "hidden" to optimize access +** when Lua is compiled as a shared library. +*/ +#if defined(luaall_c) +#define LUAI_FUNC static +#define LUAI_DATA /* empty */ + +#elif defined(__GNUC__) && ((__GNUC__*100 + __GNUC_MINOR__) >= 302) && \ + defined(__ELF__) +#define LUAI_FUNC __attribute__((visibility("hidden"))) extern +#define LUAI_DATA LUAI_FUNC + +#else +#define LUAI_FUNC extern +#define LUAI_DATA extern +#endif + + + +/* +@@ LUA_QL describes how error messages quote program elements. +** CHANGE it if you want a different appearance. +*/ +#define LUA_QL(x) "'" x "'" +#define LUA_QS LUA_QL("%s") + + +/* +@@ LUA_IDSIZE gives the maximum size for the description of the source +@* of a function in debug information. +** CHANGE it if you want a different size. +*/ +#define LUA_IDSIZE 60 + + +/* +** {================================================================== +** Stand-alone configuration +** =================================================================== +*/ + +#if defined(lua_c) || defined(luaall_c) + +/* +@@ lua_stdin_is_tty detects whether the standard input is a 'tty' (that +@* is, whether we're running lua interactively). +** CHANGE it if you have a better definition for non-POSIX/non-Windows +** systems. +*/ +#if defined(LUA_USE_ISATTY) +#include +#define lua_stdin_is_tty() isatty(0) +#elif defined(LUA_WIN) +#include +#include +#define lua_stdin_is_tty() _isatty(_fileno(stdin)) +#else +#define lua_stdin_is_tty() 1 /* assume stdin is a tty */ +#endif + + +/* +@@ LUA_PROMPT is the default prompt used by stand-alone Lua. +@@ LUA_PROMPT2 is the default continuation prompt used by stand-alone Lua. +** CHANGE them if you want different prompts. (You can also change the +** prompts dynamically, assigning to globals _PROMPT/_PROMPT2.) +*/ +#define LUA_PROMPT "> " +#define LUA_PROMPT2 ">> " + + +/* +@@ LUA_PROGNAME is the default name for the stand-alone Lua program. +** CHANGE it if your stand-alone interpreter has a different name and +** your system is not able to detect that name automatically. +*/ +#define LUA_PROGNAME "lua" + + +/* +@@ LUA_MAXINPUT is the maximum length for an input line in the +@* stand-alone interpreter. +** CHANGE it if you need longer lines. +*/ +#define LUA_MAXINPUT 512 + + +/* +@@ lua_readline defines how to show a prompt and then read a line from +@* the standard input. +@@ lua_saveline defines how to "save" a read line in a "history". +@@ lua_freeline defines how to free a line read by lua_readline. +** CHANGE them if you want to improve this functionality (e.g., by using +** GNU readline and history facilities). +*/ +#if defined(LUA_USE_READLINE) +#include +#include +#include +#define lua_readline(L,b,p) ((void)L, ((b)=readline(p)) != NULL) +#define lua_saveline(L,idx) \ + if (lua_strlen(L,idx) > 0) /* non-empty line? */ \ + add_history(lua_tostring(L, idx)); /* add it to history */ +#define lua_freeline(L,b) ((void)L, free(b)) +#else +#define lua_readline(L,b,p) \ + ((void)L, fputs(p, stdout), fflush(stdout), /* show prompt */ \ + fgets(b, LUA_MAXINPUT, stdin) != NULL) /* get line */ +#define lua_saveline(L,idx) { (void)L; (void)idx; } +#define lua_freeline(L,b) { (void)L; (void)b; } +#endif + +#endif + +/* }================================================================== */ + + +/* +@@ LUAI_GCPAUSE defines the default pause between garbage-collector cycles +@* as a percentage. +** CHANGE it if you want the GC to run faster or slower (higher values +** mean larger pauses which mean slower collection.) You can also change +** this value dynamically. +*/ +#define LUAI_GCPAUSE 200 /* 200% (wait memory to double before next GC) */ + + +/* +@@ LUAI_GCMUL defines the default speed of garbage collection relative to +@* memory allocation as a percentage. +** CHANGE it if you want to change the granularity of the garbage +** collection. (Higher values mean coarser collections. 0 represents +** infinity, where each step performs a full collection.) You can also +** change this value dynamically. +*/ +#define LUAI_GCMUL 200 /* GC runs 'twice the speed' of memory allocation */ + + + +/* +@@ LUA_COMPAT_GETN controls compatibility with old getn behavior. +** CHANGE it (define it) if you want exact compatibility with the +** behavior of setn/getn in Lua 5.0. +*/ +#undef LUA_COMPAT_GETN + +/* +@@ LUA_COMPAT_LOADLIB controls compatibility about global loadlib. +** CHANGE it to undefined as soon as you do not need a global 'loadlib' +** function (the function is still available as 'package.loadlib'). +*/ +#undef LUA_COMPAT_LOADLIB + +/* +@@ LUA_COMPAT_VARARG controls compatibility with old vararg feature. +** CHANGE it to undefined as soon as your programs use only '...' to +** access vararg parameters (instead of the old 'arg' table). +*/ +#define LUA_COMPAT_VARARG + +/* +@@ LUA_COMPAT_MOD controls compatibility with old math.mod function. +** CHANGE it to undefined as soon as your programs use 'math.fmod' or +** the new '%' operator instead of 'math.mod'. +*/ +#define LUA_COMPAT_MOD + +/* +@@ LUA_COMPAT_LSTR controls compatibility with old long string nesting +@* facility. +** CHANGE it to 2 if you want the old behaviour, or undefine it to turn +** off the advisory error when nesting [[...]]. +*/ +#define LUA_COMPAT_LSTR 1 + +/* +@@ LUA_COMPAT_GFIND controls compatibility with old 'string.gfind' name. +** CHANGE it to undefined as soon as you rename 'string.gfind' to +** 'string.gmatch'. +*/ +#define LUA_COMPAT_GFIND + +/* +@@ LUA_COMPAT_OPENLIB controls compatibility with old 'luaL_openlib' +@* behavior. +** CHANGE it to undefined as soon as you replace to 'luaL_register' +** your uses of 'luaL_openlib' +*/ +#define LUA_COMPAT_OPENLIB + + + +/* +@@ luai_apicheck is the assert macro used by the Lua-C API. +** CHANGE luai_apicheck if you want Lua to perform some checks in the +** parameters it gets from API calls. This may slow down the interpreter +** a bit, but may be quite useful when debugging C code that interfaces +** with Lua. A useful redefinition is to use assert.h. +*/ +#if defined(LUA_USE_APICHECK) +#include +#define luai_apicheck(L,o) { (void)L; assert(o); } +#else +#define luai_apicheck(L,o) { (void)L; } +#endif + + +/* +@@ LUAI_BITSINT defines the number of bits in an int. +** CHANGE here if Lua cannot automatically detect the number of bits of +** your machine. Probably you do not need to change this. +*/ +/* avoid overflows in comparison */ +#if INT_MAX-20 < 32760 +#define LUAI_BITSINT 16 +#elif INT_MAX > 2147483640L +/* int has at least 32 bits */ +#define LUAI_BITSINT 32 +#else +#error "you must define LUA_BITSINT with number of bits in an integer" +#endif + + +/* +@@ LUAI_UINT32 is an unsigned integer with at least 32 bits. +@@ LUAI_INT32 is an signed integer with at least 32 bits. +@@ LUAI_UMEM is an unsigned integer big enough to count the total +@* memory used by Lua. +@@ LUAI_MEM is a signed integer big enough to count the total memory +@* used by Lua. +** CHANGE here if for some weird reason the default definitions are not +** good enough for your machine. (The definitions in the 'else' +** part always works, but may waste space on machines with 64-bit +** longs.) Probably you do not need to change this. +*/ +#if LUAI_BITSINT >= 32 +#define LUAI_UINT32 unsigned int +#define LUAI_INT32 int +#define LUAI_MAXINT32 INT_MAX +#define LUAI_UMEM size_t +#define LUAI_MEM ptrdiff_t +#else +/* 16-bit ints */ +#define LUAI_UINT32 unsigned long +#define LUAI_INT32 long +#define LUAI_MAXINT32 LONG_MAX +#define LUAI_UMEM unsigned long +#define LUAI_MEM long +#endif + + +/* +@@ LUAI_MAXCALLS limits the number of nested calls. +** CHANGE it if you need really deep recursive calls. This limit is +** arbitrary; its only purpose is to stop infinite recursion before +** exhausting memory. +*/ +#define LUAI_MAXCALLS 20000 + + +/* +@@ LUAI_MAXCSTACK limits the number of Lua stack slots that a C function +@* can use. +** CHANGE it if you need lots of (Lua) stack space for your C +** functions. This limit is arbitrary; its only purpose is to stop C +** functions to consume unlimited stack space. (must be smaller than +** -LUA_REGISTRYINDEX) +*/ +#define LUAI_MAXCSTACK 8000 + + + +/* +** {================================================================== +** CHANGE (to smaller values) the following definitions if your system +** has a small C stack. (Or you may want to change them to larger +** values if your system has a large C stack and these limits are +** too rigid for you.) Some of these constants control the size of +** stack-allocated arrays used by the compiler or the interpreter, while +** others limit the maximum number of recursive calls that the compiler +** or the interpreter can perform. Values too large may cause a C stack +** overflow for some forms of deep constructs. +** =================================================================== +*/ + + +/* +@@ LUAI_MAXCCALLS is the maximum depth for nested C calls (short) and +@* syntactical nested non-terminals in a program. +*/ +#define LUAI_MAXCCALLS 200 + + +/* +@@ LUAI_MAXVARS is the maximum number of local variables per function +@* (must be smaller than 250). +*/ +#define LUAI_MAXVARS 200 + + +/* +@@ LUAI_MAXUPVALUES is the maximum number of upvalues per function +@* (must be smaller than 250). +*/ +#define LUAI_MAXUPVALUES 60 + + +/* +@@ LUAL_BUFFERSIZE is the buffer size used by the lauxlib buffer system. +*/ +#define LUAL_BUFFERSIZE BUFSIZ + +/* }================================================================== */ + + + + +/* +** {================================================================== +@@ LUA_NUMBER is the type of numbers in Lua. +** CHANGE the following definitions only if you want to build Lua +** with a number type different from double. You may also need to +** change lua_number2int & lua_number2integer. +** =================================================================== +*/ + +#define LUA_NUMBER_DOUBLE +#define LUA_NUMBER double + +/* +@@ LUAI_UACNUMBER is the result of an 'usual argument conversion' +@* over a number. +*/ +#define LUAI_UACNUMBER double + + +/* +@@ LUA_NUMBER_SCAN is the format for reading numbers. +@@ LUA_NUMBER_FMT is the format for writing numbers. +@@ lua_number2str converts a number to a string. +@@ LUAI_MAXNUMBER2STR is maximum size of previous conversion. +@@ lua_str2number converts a string to a number. +*/ +#define LUA_NUMBER_SCAN "%lf" +#define LUA_NUMBER_FMT "%.14g" +#define lua_number2str(s,n) sprintf((s), LUA_NUMBER_FMT, (n)) +#define LUAI_MAXNUMBER2STR 32 /* 16 digits, sign, point, and \0 */ +#define lua_str2number(s,p) strtod((s), (p)) + + +/* +@@ The luai_num* macros define the primitive operations over numbers. +*/ +#if defined(LUA_CORE) +#include +#define luai_numadd(a,b) ((a)+(b)) +#define luai_numsub(a,b) ((a)-(b)) +#define luai_nummul(a,b) ((a)*(b)) +#define luai_numdiv(a,b) ((a)/(b)) +#define luai_nummod(a,b) ((a) - floor((a)/(b))*(b)) +#define luai_numpow(a,b) (pow(a,b)) +#define luai_numunm(a) (-(a)) +#define luai_numeq(a,b) ((a)==(b)) +#define luai_numlt(a,b) ((a)<(b)) +#define luai_numle(a,b) ((a)<=(b)) +#define luai_numisnan(a) (!luai_numeq((a), (a))) +#endif + + +/* +@@ lua_number2int is a macro to convert lua_Number to int. +@@ lua_number2integer is a macro to convert lua_Number to lua_Integer. +** CHANGE them if you know a faster way to convert a lua_Number to +** int (with any rounding method and without throwing errors) in your +** system. In Pentium machines, a naive typecast from double to int +** in C is extremely slow, so any alternative is worth trying. +*/ + +/* On a Pentium, resort to a trick */ +#if defined(LUA_NUMBER_DOUBLE) && !defined(LUA_ANSI) && !defined(__SSE2__) && \ + (defined(__i386) || defined (_M_IX86) || defined(__i386__)) + +/* On a Microsoft compiler, use assembler */ +#if defined(_MSC_VER) + +#define lua_number2int(i,d) __asm fld d __asm fistp i +#define lua_number2integer(i,n) lua_number2int(i, n) + +/* the next trick should work on any Pentium, but sometimes clashes + with a DirectX idiosyncrasy */ +#else + +union luai_Cast { double l_d; long l_l; }; +#define lua_number2int(i,d) \ + { volatile union luai_Cast u; u.l_d = (d) + 6755399441055744.0; (i) = u.l_l; } +#define lua_number2integer(i,n) lua_number2int(i, n) + +#endif + + +/* this option always works, but may be slow */ +#else +#define lua_number2int(i,d) ((i)=(int)(d)) +#define lua_number2integer(i,d) ((i)=(lua_Integer)(d)) + +#endif + +/* }================================================================== */ + + +/* +@@ LUAI_USER_ALIGNMENT_T is a type that requires maximum alignment. +** CHANGE it if your system requires alignments larger than double. (For +** instance, if your system supports long doubles and they must be +** aligned in 16-byte boundaries, then you should add long double in the +** union.) Probably you do not need to change this. +*/ +#define LUAI_USER_ALIGNMENT_T union { double u; void *s; long l; } + + +/* +@@ LUAI_THROW/LUAI_TRY define how Lua does exception handling. +** CHANGE them if you prefer to use longjmp/setjmp even with C++ +** or if want/don't to use _longjmp/_setjmp instead of regular +** longjmp/setjmp. By default, Lua handles errors with exceptions when +** compiling as C++ code, with _longjmp/_setjmp when asked to use them, +** and with longjmp/setjmp otherwise. +*/ +#if defined(__cplusplus) +/* C++ exceptions */ +#define LUAI_THROW(L,c) throw(c) +#define LUAI_TRY(L,c,a) try { a } catch(...) \ + { if ((c)->status == 0) (c)->status = -1; } +#define luai_jmpbuf int /* dummy variable */ + +#elif defined(LUA_USE_ULONGJMP) +/* in Unix, try _longjmp/_setjmp (more efficient) */ +#define LUAI_THROW(L,c) _longjmp((c)->b, 1) +#define LUAI_TRY(L,c,a) if (_setjmp((c)->b) == 0) { a } +#define luai_jmpbuf jmp_buf + +#else +/* default handling with long jumps */ +#define LUAI_THROW(L,c) longjmp((c)->b, 1) +#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a } +#define luai_jmpbuf jmp_buf + +#endif + + +/* +@@ LUA_MAXCAPTURES is the maximum number of captures that a pattern +@* can do during pattern-matching. +** CHANGE it if you need more captures. This limit is arbitrary. +*/ +#define LUA_MAXCAPTURES 32 + + +/* +@@ lua_tmpnam is the function that the OS library uses to create a +@* temporary name. +@@ LUA_TMPNAMBUFSIZE is the maximum size of a name created by lua_tmpnam. +** CHANGE them if you have an alternative to tmpnam (which is considered +** insecure) or if you want the original tmpnam anyway. By default, Lua +** uses tmpnam except when POSIX is available, where it uses mkstemp. +*/ +#if defined(loslib_c) || defined(luaall_c) + +#if defined(LUA_USE_MKSTEMP) +#include +#define LUA_TMPNAMBUFSIZE 32 +#define lua_tmpnam(b,e) { \ + strcpy(b, "/tmp/lua_XXXXXX"); \ + e = mkstemp(b); \ + if (e != -1) close(e); \ + e = (e == -1); } + +#else +#define LUA_TMPNAMBUFSIZE L_tmpnam +#define lua_tmpnam(b,e) { e = (tmpnam(b) == NULL); } +#endif + +#endif + + +/* +@@ lua_popen spawns a new process connected to the current one through +@* the file streams. +** CHANGE it if you have a way to implement it in your system. +*/ +#if defined(LUA_USE_POPEN) + +#define lua_popen(L,c,m) ((void)L, fflush(NULL), popen(c,m)) +#define lua_pclose(L,file) ((void)L, (pclose(file) != -1)) + +#elif defined(LUA_WIN) + +#define lua_popen(L,c,m) ((void)L, _popen(c,m)) +#define lua_pclose(L,file) ((void)L, (_pclose(file) != -1)) + +#else + +#define lua_popen(L,c,m) ((void)((void)c, m), \ + luaL_error(L, LUA_QL("popen") " not supported"), (FILE*)0) +#define lua_pclose(L,file) ((void)((void)L, file), 0) + +#endif + +/* +@@ LUA_DL_* define which dynamic-library system Lua should use. +** CHANGE here if Lua has problems choosing the appropriate +** dynamic-library system for your platform (either Windows' DLL, Mac's +** dyld, or Unix's dlopen). If your system is some kind of Unix, there +** is a good chance that it has dlopen, so LUA_DL_DLOPEN will work for +** it. To use dlopen you also need to adapt the src/Makefile (probably +** adding -ldl to the linker options), so Lua does not select it +** automatically. (When you change the makefile to add -ldl, you must +** also add -DLUA_USE_DLOPEN.) +** If you do not want any kind of dynamic library, undefine all these +** options. +** By default, _WIN32 gets LUA_DL_DLL and MAC OS X gets LUA_DL_DYLD. +*/ +#if defined(LUA_USE_DLOPEN) +#define LUA_DL_DLOPEN +#endif + +#if defined(LUA_WIN) +#define LUA_DL_DLL +#endif + + +/* +@@ LUAI_EXTRASPACE allows you to add user-specific data in a lua_State +@* (the data goes just *before* the lua_State pointer). +** CHANGE (define) this if you really need that. This value must be +** a multiple of the maximum alignment required for your machine. +*/ +#define LUAI_EXTRASPACE 0 + + +/* +@@ luai_userstate* allow user-specific actions on threads. +** CHANGE them if you defined LUAI_EXTRASPACE and need to do something +** extra when a thread is created/deleted/resumed/yielded. +*/ +#define luai_userstateopen(L) ((void)L) +#define luai_userstateclose(L) ((void)L) +#define luai_userstatethread(L,L1) ((void)L) +#define luai_userstatefree(L) ((void)L) +#define luai_userstateresume(L,n) ((void)L) +#define luai_userstateyield(L,n) ((void)L) + + +/* +@@ LUA_INTFRMLEN is the length modifier for integer conversions +@* in 'string.format'. +@@ LUA_INTFRM_T is the integer type correspoding to the previous length +@* modifier. +** CHANGE them if your system supports long long or does not support long. +*/ + +#if defined(LUA_USELONGLONG) + +#define LUA_INTFRMLEN "ll" +#define LUA_INTFRM_T long long + +#else + +#define LUA_INTFRMLEN "l" +#define LUA_INTFRM_T long + +#endif + + + +/* =================================================================== */ + +/* +** Local configuration. You can use this space to add your redefinitions +** without modifying the main part of the file. +*/ + + + +#endif + diff --git a/extern/lua-5.1.5/src/lualib.h b/extern/lua-5.1.5/src/lualib.h new file mode 100644 index 00000000..469417f6 --- /dev/null +++ b/extern/lua-5.1.5/src/lualib.h @@ -0,0 +1,53 @@ +/* +** $Id: lualib.h,v 1.36.1.1 2007/12/27 13:02:25 roberto Exp $ +** Lua standard libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lualib_h +#define lualib_h + +#include "lua.h" + + +/* Key to file-handle type */ +#define LUA_FILEHANDLE "FILE*" + + +#define LUA_COLIBNAME "coroutine" +LUALIB_API int (luaopen_base) (lua_State *L); + +#define LUA_TABLIBNAME "table" +LUALIB_API int (luaopen_table) (lua_State *L); + +#define LUA_IOLIBNAME "io" +LUALIB_API int (luaopen_io) (lua_State *L); + +#define LUA_OSLIBNAME "os" +LUALIB_API int (luaopen_os) (lua_State *L); + +#define LUA_STRLIBNAME "string" +LUALIB_API int (luaopen_string) (lua_State *L); + +#define LUA_MATHLIBNAME "math" +LUALIB_API int (luaopen_math) (lua_State *L); + +#define LUA_DBLIBNAME "debug" +LUALIB_API int (luaopen_debug) (lua_State *L); + +#define LUA_LOADLIBNAME "package" +LUALIB_API int (luaopen_package) (lua_State *L); + + +/* open all previous libraries */ +LUALIB_API void (luaL_openlibs) (lua_State *L); + + + +#ifndef lua_assert +#define lua_assert(x) ((void)0) +#endif + + +#endif diff --git a/extern/lua-5.1.5/src/lundump.c b/extern/lua-5.1.5/src/lundump.c new file mode 100644 index 00000000..8010a457 --- /dev/null +++ b/extern/lua-5.1.5/src/lundump.c @@ -0,0 +1,227 @@ +/* +** $Id: lundump.c,v 2.7.1.4 2008/04/04 19:51:41 roberto Exp $ +** load precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#include + +#define lundump_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstring.h" +#include "lundump.h" +#include "lzio.h" + +typedef struct { + lua_State* L; + ZIO* Z; + Mbuffer* b; + const char* name; +} LoadState; + +#ifdef LUAC_TRUST_BINARIES +#define IF(c,s) +#define error(S,s) +#else +#define IF(c,s) if (c) error(S,s) + +static void error(LoadState* S, const char* why) +{ + luaO_pushfstring(S->L,"%s: %s in precompiled chunk",S->name,why); + luaD_throw(S->L,LUA_ERRSYNTAX); +} +#endif + +#define LoadMem(S,b,n,size) LoadBlock(S,b,(n)*(size)) +#define LoadByte(S) (lu_byte)LoadChar(S) +#define LoadVar(S,x) LoadMem(S,&x,1,sizeof(x)) +#define LoadVector(S,b,n,size) LoadMem(S,b,n,size) + +static void LoadBlock(LoadState* S, void* b, size_t size) +{ + size_t r=luaZ_read(S->Z,b,size); + IF (r!=0, "unexpected end"); +} + +static int LoadChar(LoadState* S) +{ + char x; + LoadVar(S,x); + return x; +} + +static int LoadInt(LoadState* S) +{ + int x; + LoadVar(S,x); + IF (x<0, "bad integer"); + return x; +} + +static lua_Number LoadNumber(LoadState* S) +{ + lua_Number x; + LoadVar(S,x); + return x; +} + +static TString* LoadString(LoadState* S) +{ + size_t size; + LoadVar(S,size); + if (size==0) + return NULL; + else + { + char* s=luaZ_openspace(S->L,S->b,size); + LoadBlock(S,s,size); + return luaS_newlstr(S->L,s,size-1); /* remove trailing '\0' */ + } +} + +static void LoadCode(LoadState* S, Proto* f) +{ + int n=LoadInt(S); + f->code=luaM_newvector(S->L,n,Instruction); + f->sizecode=n; + LoadVector(S,f->code,n,sizeof(Instruction)); +} + +static Proto* LoadFunction(LoadState* S, TString* p); + +static void LoadConstants(LoadState* S, Proto* f) +{ + int i,n; + n=LoadInt(S); + f->k=luaM_newvector(S->L,n,TValue); + f->sizek=n; + for (i=0; ik[i]); + for (i=0; ik[i]; + int t=LoadChar(S); + switch (t) + { + case LUA_TNIL: + setnilvalue(o); + break; + case LUA_TBOOLEAN: + setbvalue(o,LoadChar(S)!=0); + break; + case LUA_TNUMBER: + setnvalue(o,LoadNumber(S)); + break; + case LUA_TSTRING: + setsvalue2n(S->L,o,LoadString(S)); + break; + default: + error(S,"bad constant"); + break; + } + } + n=LoadInt(S); + f->p=luaM_newvector(S->L,n,Proto*); + f->sizep=n; + for (i=0; ip[i]=NULL; + for (i=0; ip[i]=LoadFunction(S,f->source); +} + +static void LoadDebug(LoadState* S, Proto* f) +{ + int i,n; + n=LoadInt(S); + f->lineinfo=luaM_newvector(S->L,n,int); + f->sizelineinfo=n; + LoadVector(S,f->lineinfo,n,sizeof(int)); + n=LoadInt(S); + f->locvars=luaM_newvector(S->L,n,LocVar); + f->sizelocvars=n; + for (i=0; ilocvars[i].varname=NULL; + for (i=0; ilocvars[i].varname=LoadString(S); + f->locvars[i].startpc=LoadInt(S); + f->locvars[i].endpc=LoadInt(S); + } + n=LoadInt(S); + f->upvalues=luaM_newvector(S->L,n,TString*); + f->sizeupvalues=n; + for (i=0; iupvalues[i]=NULL; + for (i=0; iupvalues[i]=LoadString(S); +} + +static Proto* LoadFunction(LoadState* S, TString* p) +{ + Proto* f; + if (++S->L->nCcalls > LUAI_MAXCCALLS) error(S,"code too deep"); + f=luaF_newproto(S->L); + setptvalue2s(S->L,S->L->top,f); incr_top(S->L); + f->source=LoadString(S); if (f->source==NULL) f->source=p; + f->linedefined=LoadInt(S); + f->lastlinedefined=LoadInt(S); + f->nups=LoadByte(S); + f->numparams=LoadByte(S); + f->is_vararg=LoadByte(S); + f->maxstacksize=LoadByte(S); + LoadCode(S,f); + LoadConstants(S,f); + LoadDebug(S,f); + IF (!luaG_checkcode(f), "bad code"); + S->L->top--; + S->L->nCcalls--; + return f; +} + +static void LoadHeader(LoadState* S) +{ + char h[LUAC_HEADERSIZE]; + char s[LUAC_HEADERSIZE]; + luaU_header(h); + LoadBlock(S,s,LUAC_HEADERSIZE); + IF (memcmp(h,s,LUAC_HEADERSIZE)!=0, "bad header"); +} + +/* +** load precompiled chunk +*/ +Proto* luaU_undump (lua_State* L, ZIO* Z, Mbuffer* buff, const char* name) +{ + LoadState S; + if (*name=='@' || *name=='=') + S.name=name+1; + else if (*name==LUA_SIGNATURE[0]) + S.name="binary string"; + else + S.name=name; + S.L=L; + S.Z=Z; + S.b=buff; + LoadHeader(&S); + return LoadFunction(&S,luaS_newliteral(L,"=?")); +} + +/* +* make header +*/ +void luaU_header (char* h) +{ + int x=1; + memcpy(h,LUA_SIGNATURE,sizeof(LUA_SIGNATURE)-1); + h+=sizeof(LUA_SIGNATURE)-1; + *h++=(char)LUAC_VERSION; + *h++=(char)LUAC_FORMAT; + *h++=(char)*(char*)&x; /* endianness */ + *h++=(char)sizeof(int); + *h++=(char)sizeof(size_t); + *h++=(char)sizeof(Instruction); + *h++=(char)sizeof(lua_Number); + *h++=(char)(((lua_Number)0.5)==0); /* is lua_Number integral? */ +} diff --git a/extern/lua-5.1.5/src/lundump.h b/extern/lua-5.1.5/src/lundump.h new file mode 100644 index 00000000..c80189db --- /dev/null +++ b/extern/lua-5.1.5/src/lundump.h @@ -0,0 +1,36 @@ +/* +** $Id: lundump.h,v 1.37.1.1 2007/12/27 13:02:25 roberto Exp $ +** load precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#ifndef lundump_h +#define lundump_h + +#include "lobject.h" +#include "lzio.h" + +/* load one chunk; from lundump.c */ +LUAI_FUNC Proto* luaU_undump (lua_State* L, ZIO* Z, Mbuffer* buff, const char* name); + +/* make header; from lundump.c */ +LUAI_FUNC void luaU_header (char* h); + +/* dump one chunk; from ldump.c */ +LUAI_FUNC int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip); + +#ifdef luac_c +/* print one chunk; from print.c */ +LUAI_FUNC void luaU_print (const Proto* f, int full); +#endif + +/* for header of binary files -- this is Lua 5.1 */ +#define LUAC_VERSION 0x51 + +/* for header of binary files -- this is the official format */ +#define LUAC_FORMAT 0 + +/* size of header of binary files */ +#define LUAC_HEADERSIZE 12 + +#endif diff --git a/extern/lua-5.1.5/src/lvm.c b/extern/lua-5.1.5/src/lvm.c new file mode 100644 index 00000000..e0a0cd85 --- /dev/null +++ b/extern/lua-5.1.5/src/lvm.c @@ -0,0 +1,767 @@ +/* +** $Id: lvm.c,v 2.63.1.5 2011/08/17 20:43:11 roberto Exp $ +** Lua virtual machine +** See Copyright Notice in lua.h +*/ + + +#include +#include +#include + +#define lvm_c +#define LUA_CORE + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + + +/* limit for table tag-method chains (to avoid loops) */ +#define MAXTAGLOOP 100 + + +const TValue *luaV_tonumber (const TValue *obj, TValue *n) { + lua_Number num; + if (ttisnumber(obj)) return obj; + if (ttisstring(obj) && luaO_str2d(svalue(obj), &num)) { + setnvalue(n, num); + return n; + } + else + return NULL; +} + + +int luaV_tostring (lua_State *L, StkId obj) { + if (!ttisnumber(obj)) + return 0; + else { + char s[LUAI_MAXNUMBER2STR]; + lua_Number n = nvalue(obj); + lua_number2str(s, n); + setsvalue2s(L, obj, luaS_new(L, s)); + return 1; + } +} + + +static void traceexec (lua_State *L, const Instruction *pc) { + lu_byte mask = L->hookmask; + const Instruction *oldpc = L->savedpc; + L->savedpc = pc; + if ((mask & LUA_MASKCOUNT) && L->hookcount == 0) { + resethookcount(L); + luaD_callhook(L, LUA_HOOKCOUNT, -1); + } + if (mask & LUA_MASKLINE) { + Proto *p = ci_func(L->ci)->l.p; + int npc = pcRel(pc, p); + int newline = getline(p, npc); + /* call linehook when enter a new function, when jump back (loop), + or when enter a new line */ + if (npc == 0 || pc <= oldpc || newline != getline(p, pcRel(oldpc, p))) + luaD_callhook(L, LUA_HOOKLINE, newline); + } +} + + +static void callTMres (lua_State *L, StkId res, const TValue *f, + const TValue *p1, const TValue *p2) { + ptrdiff_t result = savestack(L, res); + setobj2s(L, L->top, f); /* push function */ + setobj2s(L, L->top+1, p1); /* 1st argument */ + setobj2s(L, L->top+2, p2); /* 2nd argument */ + luaD_checkstack(L, 3); + L->top += 3; + luaD_call(L, L->top - 3, 1); + res = restorestack(L, result); + L->top--; + setobjs2s(L, res, L->top); +} + + + +static void callTM (lua_State *L, const TValue *f, const TValue *p1, + const TValue *p2, const TValue *p3) { + setobj2s(L, L->top, f); /* push function */ + setobj2s(L, L->top+1, p1); /* 1st argument */ + setobj2s(L, L->top+2, p2); /* 2nd argument */ + setobj2s(L, L->top+3, p3); /* 3th argument */ + luaD_checkstack(L, 4); + L->top += 4; + luaD_call(L, L->top - 4, 0); +} + + +void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) { + int loop; + for (loop = 0; loop < MAXTAGLOOP; loop++) { + const TValue *tm; + if (ttistable(t)) { /* `t' is a table? */ + Table *h = hvalue(t); + const TValue *res = luaH_get(h, key); /* do a primitive get */ + if (!ttisnil(res) || /* result is no nil? */ + (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */ + setobj2s(L, val, res); + return; + } + /* else will try the tag method */ + } + else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX))) + luaG_typeerror(L, t, "index"); + if (ttisfunction(tm)) { + callTMres(L, val, tm, t, key); + return; + } + t = tm; /* else repeat with `tm' */ + } + luaG_runerror(L, "loop in gettable"); +} + + +void luaV_settable (lua_State *L, const TValue *t, TValue *key, StkId val) { + int loop; + TValue temp; + for (loop = 0; loop < MAXTAGLOOP; loop++) { + const TValue *tm; + if (ttistable(t)) { /* `t' is a table? */ + Table *h = hvalue(t); + TValue *oldval = luaH_set(L, h, key); /* do a primitive set */ + if (!ttisnil(oldval) || /* result is no nil? */ + (tm = fasttm(L, h->metatable, TM_NEWINDEX)) == NULL) { /* or no TM? */ + setobj2t(L, oldval, val); + h->flags = 0; + luaC_barriert(L, h, val); + return; + } + /* else will try the tag method */ + } + else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_NEWINDEX))) + luaG_typeerror(L, t, "index"); + if (ttisfunction(tm)) { + callTM(L, tm, t, key, val); + return; + } + /* else repeat with `tm' */ + setobj(L, &temp, tm); /* avoid pointing inside table (may rehash) */ + t = &temp; + } + luaG_runerror(L, "loop in settable"); +} + + +static int call_binTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event) { + const TValue *tm = luaT_gettmbyobj(L, p1, event); /* try first operand */ + if (ttisnil(tm)) + tm = luaT_gettmbyobj(L, p2, event); /* try second operand */ + if (ttisnil(tm)) return 0; + callTMres(L, res, tm, p1, p2); + return 1; +} + + +static const TValue *get_compTM (lua_State *L, Table *mt1, Table *mt2, + TMS event) { + const TValue *tm1 = fasttm(L, mt1, event); + const TValue *tm2; + if (tm1 == NULL) return NULL; /* no metamethod */ + if (mt1 == mt2) return tm1; /* same metatables => same metamethods */ + tm2 = fasttm(L, mt2, event); + if (tm2 == NULL) return NULL; /* no metamethod */ + if (luaO_rawequalObj(tm1, tm2)) /* same metamethods? */ + return tm1; + return NULL; +} + + +static int call_orderTM (lua_State *L, const TValue *p1, const TValue *p2, + TMS event) { + const TValue *tm1 = luaT_gettmbyobj(L, p1, event); + const TValue *tm2; + if (ttisnil(tm1)) return -1; /* no metamethod? */ + tm2 = luaT_gettmbyobj(L, p2, event); + if (!luaO_rawequalObj(tm1, tm2)) /* different metamethods? */ + return -1; + callTMres(L, L->top, tm1, p1, p2); + return !l_isfalse(L->top); +} + + +static int l_strcmp (const TString *ls, const TString *rs) { + const char *l = getstr(ls); + size_t ll = ls->tsv.len; + const char *r = getstr(rs); + size_t lr = rs->tsv.len; + for (;;) { + int temp = strcoll(l, r); + if (temp != 0) return temp; + else { /* strings are equal up to a `\0' */ + size_t len = strlen(l); /* index of first `\0' in both strings */ + if (len == lr) /* r is finished? */ + return (len == ll) ? 0 : 1; + else if (len == ll) /* l is finished? */ + return -1; /* l is smaller than r (because r is not finished) */ + /* both strings longer than `len'; go on comparing (after the `\0') */ + len++; + l += len; ll -= len; r += len; lr -= len; + } + } +} + + +int luaV_lessthan (lua_State *L, const TValue *l, const TValue *r) { + int res; + if (ttype(l) != ttype(r)) + return luaG_ordererror(L, l, r); + else if (ttisnumber(l)) + return luai_numlt(nvalue(l), nvalue(r)); + else if (ttisstring(l)) + return l_strcmp(rawtsvalue(l), rawtsvalue(r)) < 0; + else if ((res = call_orderTM(L, l, r, TM_LT)) != -1) + return res; + return luaG_ordererror(L, l, r); +} + + +static int lessequal (lua_State *L, const TValue *l, const TValue *r) { + int res; + if (ttype(l) != ttype(r)) + return luaG_ordererror(L, l, r); + else if (ttisnumber(l)) + return luai_numle(nvalue(l), nvalue(r)); + else if (ttisstring(l)) + return l_strcmp(rawtsvalue(l), rawtsvalue(r)) <= 0; + else if ((res = call_orderTM(L, l, r, TM_LE)) != -1) /* first try `le' */ + return res; + else if ((res = call_orderTM(L, r, l, TM_LT)) != -1) /* else try `lt' */ + return !res; + return luaG_ordererror(L, l, r); +} + + +int luaV_equalval (lua_State *L, const TValue *t1, const TValue *t2) { + const TValue *tm; + lua_assert(ttype(t1) == ttype(t2)); + switch (ttype(t1)) { + case LUA_TNIL: return 1; + case LUA_TNUMBER: return luai_numeq(nvalue(t1), nvalue(t2)); + case LUA_TBOOLEAN: return bvalue(t1) == bvalue(t2); /* true must be 1 !! */ + case LUA_TLIGHTUSERDATA: return pvalue(t1) == pvalue(t2); + case LUA_TUSERDATA: { + if (uvalue(t1) == uvalue(t2)) return 1; + tm = get_compTM(L, uvalue(t1)->metatable, uvalue(t2)->metatable, + TM_EQ); + break; /* will try TM */ + } + case LUA_TTABLE: { + if (hvalue(t1) == hvalue(t2)) return 1; + tm = get_compTM(L, hvalue(t1)->metatable, hvalue(t2)->metatable, TM_EQ); + break; /* will try TM */ + } + default: return gcvalue(t1) == gcvalue(t2); + } + if (tm == NULL) return 0; /* no TM? */ + callTMres(L, L->top, tm, t1, t2); /* call TM */ + return !l_isfalse(L->top); +} + + +void luaV_concat (lua_State *L, int total, int last) { + do { + StkId top = L->base + last + 1; + int n = 2; /* number of elements handled in this pass (at least 2) */ + if (!(ttisstring(top-2) || ttisnumber(top-2)) || !tostring(L, top-1)) { + if (!call_binTM(L, top-2, top-1, top-2, TM_CONCAT)) + luaG_concaterror(L, top-2, top-1); + } else if (tsvalue(top-1)->len == 0) /* second op is empty? */ + (void)tostring(L, top - 2); /* result is first op (as string) */ + else { + /* at least two string values; get as many as possible */ + size_t tl = tsvalue(top-1)->len; + char *buffer; + int i; + /* collect total length */ + for (n = 1; n < total && tostring(L, top-n-1); n++) { + size_t l = tsvalue(top-n-1)->len; + if (l >= MAX_SIZET - tl) luaG_runerror(L, "string length overflow"); + tl += l; + } + buffer = luaZ_openspace(L, &G(L)->buff, tl); + tl = 0; + for (i=n; i>0; i--) { /* concat all strings */ + size_t l = tsvalue(top-i)->len; + memcpy(buffer+tl, svalue(top-i), l); + tl += l; + } + setsvalue2s(L, top-n, luaS_newlstr(L, buffer, tl)); + } + total -= n-1; /* got `n' strings to create 1 new */ + last -= n-1; + } while (total > 1); /* repeat until only 1 result left */ +} + + +static void Arith (lua_State *L, StkId ra, const TValue *rb, + const TValue *rc, TMS op) { + TValue tempb, tempc; + const TValue *b, *c; + if ((b = luaV_tonumber(rb, &tempb)) != NULL && + (c = luaV_tonumber(rc, &tempc)) != NULL) { + lua_Number nb = nvalue(b), nc = nvalue(c); + switch (op) { + case TM_ADD: setnvalue(ra, luai_numadd(nb, nc)); break; + case TM_SUB: setnvalue(ra, luai_numsub(nb, nc)); break; + case TM_MUL: setnvalue(ra, luai_nummul(nb, nc)); break; + case TM_DIV: setnvalue(ra, luai_numdiv(nb, nc)); break; + case TM_MOD: setnvalue(ra, luai_nummod(nb, nc)); break; + case TM_POW: setnvalue(ra, luai_numpow(nb, nc)); break; + case TM_UNM: setnvalue(ra, luai_numunm(nb)); break; + default: lua_assert(0); break; + } + } + else if (!call_binTM(L, rb, rc, ra, op)) + luaG_aritherror(L, rb, rc); +} + + + +/* +** some macros for common tasks in `luaV_execute' +*/ + +#define runtime_check(L, c) { if (!(c)) break; } + +#define RA(i) (base+GETARG_A(i)) +/* to be used after possible stack reallocation */ +#define RB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgR, base+GETARG_B(i)) +#define RC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgR, base+GETARG_C(i)) +#define RKB(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, \ + ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i)) +#define RKC(i) check_exp(getCMode(GET_OPCODE(i)) == OpArgK, \ + ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i)) +#define KBx(i) check_exp(getBMode(GET_OPCODE(i)) == OpArgK, k+GETARG_Bx(i)) + + +#define dojump(L,pc,i) {(pc) += (i); luai_threadyield(L);} + + +#define Protect(x) { L->savedpc = pc; {x;}; base = L->base; } + + +#define arith_op(op,tm) { \ + TValue *rb = RKB(i); \ + TValue *rc = RKC(i); \ + if (ttisnumber(rb) && ttisnumber(rc)) { \ + lua_Number nb = nvalue(rb), nc = nvalue(rc); \ + setnvalue(ra, op(nb, nc)); \ + } \ + else \ + Protect(Arith(L, ra, rb, rc, tm)); \ + } + + + +void luaV_execute (lua_State *L, int nexeccalls) { + LClosure *cl; + StkId base; + TValue *k; + const Instruction *pc; + reentry: /* entry point */ + lua_assert(isLua(L->ci)); + pc = L->savedpc; + cl = &clvalue(L->ci->func)->l; + base = L->base; + k = cl->p->k; + /* main loop of interpreter */ + for (;;) { + const Instruction i = *pc++; + StkId ra; + if ((L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) && + (--L->hookcount == 0 || L->hookmask & LUA_MASKLINE)) { + traceexec(L, pc); + if (L->status == LUA_YIELD) { /* did hook yield? */ + L->savedpc = pc - 1; + return; + } + base = L->base; + } + /* warning!! several calls may realloc the stack and invalidate `ra' */ + ra = RA(i); + lua_assert(base == L->base && L->base == L->ci->base); + lua_assert(base <= L->top && L->top <= L->stack + L->stacksize); + lua_assert(L->top == L->ci->top || luaG_checkopenop(i)); + switch (GET_OPCODE(i)) { + case OP_MOVE: { + setobjs2s(L, ra, RB(i)); + continue; + } + case OP_LOADK: { + setobj2s(L, ra, KBx(i)); + continue; + } + case OP_LOADBOOL: { + setbvalue(ra, GETARG_B(i)); + if (GETARG_C(i)) pc++; /* skip next instruction (if C) */ + continue; + } + case OP_LOADNIL: { + TValue *rb = RB(i); + do { + setnilvalue(rb--); + } while (rb >= ra); + continue; + } + case OP_GETUPVAL: { + int b = GETARG_B(i); + setobj2s(L, ra, cl->upvals[b]->v); + continue; + } + case OP_GETGLOBAL: { + TValue g; + TValue *rb = KBx(i); + sethvalue(L, &g, cl->env); + lua_assert(ttisstring(rb)); + Protect(luaV_gettable(L, &g, rb, ra)); + continue; + } + case OP_GETTABLE: { + Protect(luaV_gettable(L, RB(i), RKC(i), ra)); + continue; + } + case OP_SETGLOBAL: { + TValue g; + sethvalue(L, &g, cl->env); + lua_assert(ttisstring(KBx(i))); + Protect(luaV_settable(L, &g, KBx(i), ra)); + continue; + } + case OP_SETUPVAL: { + UpVal *uv = cl->upvals[GETARG_B(i)]; + setobj(L, uv->v, ra); + luaC_barrier(L, uv, ra); + continue; + } + case OP_SETTABLE: { + Protect(luaV_settable(L, ra, RKB(i), RKC(i))); + continue; + } + case OP_NEWTABLE: { + int b = GETARG_B(i); + int c = GETARG_C(i); + sethvalue(L, ra, luaH_new(L, luaO_fb2int(b), luaO_fb2int(c))); + Protect(luaC_checkGC(L)); + continue; + } + case OP_SELF: { + StkId rb = RB(i); + setobjs2s(L, ra+1, rb); + Protect(luaV_gettable(L, rb, RKC(i), ra)); + continue; + } + case OP_ADD: { + arith_op(luai_numadd, TM_ADD); + continue; + } + case OP_SUB: { + arith_op(luai_numsub, TM_SUB); + continue; + } + case OP_MUL: { + arith_op(luai_nummul, TM_MUL); + continue; + } + case OP_DIV: { + arith_op(luai_numdiv, TM_DIV); + continue; + } + case OP_MOD: { + arith_op(luai_nummod, TM_MOD); + continue; + } + case OP_POW: { + arith_op(luai_numpow, TM_POW); + continue; + } + case OP_UNM: { + TValue *rb = RB(i); + if (ttisnumber(rb)) { + lua_Number nb = nvalue(rb); + setnvalue(ra, luai_numunm(nb)); + } + else { + Protect(Arith(L, ra, rb, rb, TM_UNM)); + } + continue; + } + case OP_NOT: { + int res = l_isfalse(RB(i)); /* next assignment may change this value */ + setbvalue(ra, res); + continue; + } + case OP_LEN: { + const TValue *rb = RB(i); + switch (ttype(rb)) { + case LUA_TTABLE: { + setnvalue(ra, cast_num(luaH_getn(hvalue(rb)))); + break; + } + case LUA_TSTRING: { + setnvalue(ra, cast_num(tsvalue(rb)->len)); + break; + } + default: { /* try metamethod */ + Protect( + if (!call_binTM(L, rb, luaO_nilobject, ra, TM_LEN)) + luaG_typeerror(L, rb, "get length of"); + ) + } + } + continue; + } + case OP_CONCAT: { + int b = GETARG_B(i); + int c = GETARG_C(i); + Protect(luaV_concat(L, c-b+1, c); luaC_checkGC(L)); + setobjs2s(L, RA(i), base+b); + continue; + } + case OP_JMP: { + dojump(L, pc, GETARG_sBx(i)); + continue; + } + case OP_EQ: { + TValue *rb = RKB(i); + TValue *rc = RKC(i); + Protect( + if (equalobj(L, rb, rc) == GETARG_A(i)) + dojump(L, pc, GETARG_sBx(*pc)); + ) + pc++; + continue; + } + case OP_LT: { + Protect( + if (luaV_lessthan(L, RKB(i), RKC(i)) == GETARG_A(i)) + dojump(L, pc, GETARG_sBx(*pc)); + ) + pc++; + continue; + } + case OP_LE: { + Protect( + if (lessequal(L, RKB(i), RKC(i)) == GETARG_A(i)) + dojump(L, pc, GETARG_sBx(*pc)); + ) + pc++; + continue; + } + case OP_TEST: { + if (l_isfalse(ra) != GETARG_C(i)) + dojump(L, pc, GETARG_sBx(*pc)); + pc++; + continue; + } + case OP_TESTSET: { + TValue *rb = RB(i); + if (l_isfalse(rb) != GETARG_C(i)) { + setobjs2s(L, ra, rb); + dojump(L, pc, GETARG_sBx(*pc)); + } + pc++; + continue; + } + case OP_CALL: { + int b = GETARG_B(i); + int nresults = GETARG_C(i) - 1; + if (b != 0) L->top = ra+b; /* else previous instruction set top */ + L->savedpc = pc; + switch (luaD_precall(L, ra, nresults)) { + case PCRLUA: { + nexeccalls++; + goto reentry; /* restart luaV_execute over new Lua function */ + } + case PCRC: { + /* it was a C function (`precall' called it); adjust results */ + if (nresults >= 0) L->top = L->ci->top; + base = L->base; + continue; + } + default: { + return; /* yield */ + } + } + } + case OP_TAILCALL: { + int b = GETARG_B(i); + if (b != 0) L->top = ra+b; /* else previous instruction set top */ + L->savedpc = pc; + lua_assert(GETARG_C(i) - 1 == LUA_MULTRET); + switch (luaD_precall(L, ra, LUA_MULTRET)) { + case PCRLUA: { + /* tail call: put new frame in place of previous one */ + CallInfo *ci = L->ci - 1; /* previous frame */ + int aux; + StkId func = ci->func; + StkId pfunc = (ci+1)->func; /* previous function index */ + if (L->openupval) luaF_close(L, ci->base); + L->base = ci->base = ci->func + ((ci+1)->base - pfunc); + for (aux = 0; pfunc+aux < L->top; aux++) /* move frame down */ + setobjs2s(L, func+aux, pfunc+aux); + ci->top = L->top = func+aux; /* correct top */ + lua_assert(L->top == L->base + clvalue(func)->l.p->maxstacksize); + ci->savedpc = L->savedpc; + ci->tailcalls++; /* one more call lost */ + L->ci--; /* remove new frame */ + goto reentry; + } + case PCRC: { /* it was a C function (`precall' called it) */ + base = L->base; + continue; + } + default: { + return; /* yield */ + } + } + } + case OP_RETURN: { + int b = GETARG_B(i); + if (b != 0) L->top = ra+b-1; + if (L->openupval) luaF_close(L, base); + L->savedpc = pc; + b = luaD_poscall(L, ra); + if (--nexeccalls == 0) /* was previous function running `here'? */ + return; /* no: return */ + else { /* yes: continue its execution */ + if (b) L->top = L->ci->top; + lua_assert(isLua(L->ci)); + lua_assert(GET_OPCODE(*((L->ci)->savedpc - 1)) == OP_CALL); + goto reentry; + } + } + case OP_FORLOOP: { + lua_Number step = nvalue(ra+2); + lua_Number idx = luai_numadd(nvalue(ra), step); /* increment index */ + lua_Number limit = nvalue(ra+1); + if (luai_numlt(0, step) ? luai_numle(idx, limit) + : luai_numle(limit, idx)) { + dojump(L, pc, GETARG_sBx(i)); /* jump back */ + setnvalue(ra, idx); /* update internal index... */ + setnvalue(ra+3, idx); /* ...and external index */ + } + continue; + } + case OP_FORPREP: { + const TValue *init = ra; + const TValue *plimit = ra+1; + const TValue *pstep = ra+2; + L->savedpc = pc; /* next steps may throw errors */ + if (!tonumber(init, ra)) + luaG_runerror(L, LUA_QL("for") " initial value must be a number"); + else if (!tonumber(plimit, ra+1)) + luaG_runerror(L, LUA_QL("for") " limit must be a number"); + else if (!tonumber(pstep, ra+2)) + luaG_runerror(L, LUA_QL("for") " step must be a number"); + setnvalue(ra, luai_numsub(nvalue(ra), nvalue(pstep))); + dojump(L, pc, GETARG_sBx(i)); + continue; + } + case OP_TFORLOOP: { + StkId cb = ra + 3; /* call base */ + setobjs2s(L, cb+2, ra+2); + setobjs2s(L, cb+1, ra+1); + setobjs2s(L, cb, ra); + L->top = cb+3; /* func. + 2 args (state and index) */ + Protect(luaD_call(L, cb, GETARG_C(i))); + L->top = L->ci->top; + cb = RA(i) + 3; /* previous call may change the stack */ + if (!ttisnil(cb)) { /* continue loop? */ + setobjs2s(L, cb-1, cb); /* save control variable */ + dojump(L, pc, GETARG_sBx(*pc)); /* jump back */ + } + pc++; + continue; + } + case OP_SETLIST: { + int n = GETARG_B(i); + int c = GETARG_C(i); + int last; + Table *h; + if (n == 0) { + n = cast_int(L->top - ra) - 1; + L->top = L->ci->top; + } + if (c == 0) c = cast_int(*pc++); + runtime_check(L, ttistable(ra)); + h = hvalue(ra); + last = ((c-1)*LFIELDS_PER_FLUSH) + n; + if (last > h->sizearray) /* needs more space? */ + luaH_resizearray(L, h, last); /* pre-alloc it at once */ + for (; n > 0; n--) { + TValue *val = ra+n; + setobj2t(L, luaH_setnum(L, h, last--), val); + luaC_barriert(L, h, val); + } + continue; + } + case OP_CLOSE: { + luaF_close(L, ra); + continue; + } + case OP_CLOSURE: { + Proto *p; + Closure *ncl; + int nup, j; + p = cl->p->p[GETARG_Bx(i)]; + nup = p->nups; + ncl = luaF_newLclosure(L, nup, cl->env); + ncl->l.p = p; + for (j=0; jl.upvals[j] = cl->upvals[GETARG_B(*pc)]; + else { + lua_assert(GET_OPCODE(*pc) == OP_MOVE); + ncl->l.upvals[j] = luaF_findupval(L, base + GETARG_B(*pc)); + } + } + setclvalue(L, ra, ncl); + Protect(luaC_checkGC(L)); + continue; + } + case OP_VARARG: { + int b = GETARG_B(i) - 1; + int j; + CallInfo *ci = L->ci; + int n = cast_int(ci->base - ci->func) - cl->p->numparams - 1; + if (b == LUA_MULTRET) { + Protect(luaD_checkstack(L, n)); + ra = RA(i); /* previous call may change the stack */ + b = n; + L->top = ra + n; + } + for (j = 0; j < b; j++) { + if (j < n) { + setobjs2s(L, ra + j, ci->base - n + j); + } + else { + setnilvalue(ra + j); + } + } + continue; + } + } + } +} + diff --git a/extern/lua-5.1.5/src/lvm.h b/extern/lua-5.1.5/src/lvm.h new file mode 100644 index 00000000..bfe4f567 --- /dev/null +++ b/extern/lua-5.1.5/src/lvm.h @@ -0,0 +1,36 @@ +/* +** $Id: lvm.h,v 2.5.1.1 2007/12/27 13:02:25 roberto Exp $ +** Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#ifndef lvm_h +#define lvm_h + + +#include "ldo.h" +#include "lobject.h" +#include "ltm.h" + + +#define tostring(L,o) ((ttype(o) == LUA_TSTRING) || (luaV_tostring(L, o))) + +#define tonumber(o,n) (ttype(o) == LUA_TNUMBER || \ + (((o) = luaV_tonumber(o,n)) != NULL)) + +#define equalobj(L,o1,o2) \ + (ttype(o1) == ttype(o2) && luaV_equalval(L, o1, o2)) + + +LUAI_FUNC int luaV_lessthan (lua_State *L, const TValue *l, const TValue *r); +LUAI_FUNC int luaV_equalval (lua_State *L, const TValue *t1, const TValue *t2); +LUAI_FUNC const TValue *luaV_tonumber (const TValue *obj, TValue *n); +LUAI_FUNC int luaV_tostring (lua_State *L, StkId obj); +LUAI_FUNC void luaV_gettable (lua_State *L, const TValue *t, TValue *key, + StkId val); +LUAI_FUNC void luaV_settable (lua_State *L, const TValue *t, TValue *key, + StkId val); +LUAI_FUNC void luaV_execute (lua_State *L, int nexeccalls); +LUAI_FUNC void luaV_concat (lua_State *L, int total, int last); + +#endif diff --git a/extern/lua-5.1.5/src/lzio.c b/extern/lua-5.1.5/src/lzio.c new file mode 100644 index 00000000..293edd59 --- /dev/null +++ b/extern/lua-5.1.5/src/lzio.c @@ -0,0 +1,82 @@ +/* +** $Id: lzio.c,v 1.31.1.1 2007/12/27 13:02:25 roberto Exp $ +** a generic input stream interface +** See Copyright Notice in lua.h +*/ + + +#include + +#define lzio_c +#define LUA_CORE + +#include "lua.h" + +#include "llimits.h" +#include "lmem.h" +#include "lstate.h" +#include "lzio.h" + + +int luaZ_fill (ZIO *z) { + size_t size; + lua_State *L = z->L; + const char *buff; + lua_unlock(L); + buff = z->reader(L, z->data, &size); + lua_lock(L); + if (buff == NULL || size == 0) return EOZ; + z->n = size - 1; + z->p = buff; + return char2int(*(z->p++)); +} + + +int luaZ_lookahead (ZIO *z) { + if (z->n == 0) { + if (luaZ_fill(z) == EOZ) + return EOZ; + else { + z->n++; /* luaZ_fill removed first byte; put back it */ + z->p--; + } + } + return char2int(*z->p); +} + + +void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, void *data) { + z->L = L; + z->reader = reader; + z->data = data; + z->n = 0; + z->p = NULL; +} + + +/* --------------------------------------------------------------- read --- */ +size_t luaZ_read (ZIO *z, void *b, size_t n) { + while (n) { + size_t m; + if (luaZ_lookahead(z) == EOZ) + return n; /* return number of missing bytes */ + m = (n <= z->n) ? n : z->n; /* min. between n and z->n */ + memcpy(b, z->p, m); + z->n -= m; + z->p += m; + b = (char *)b + m; + n -= m; + } + return 0; +} + +/* ------------------------------------------------------------------------ */ +char *luaZ_openspace (lua_State *L, Mbuffer *buff, size_t n) { + if (n > buff->buffsize) { + if (n < LUA_MINBUFFER) n = LUA_MINBUFFER; + luaZ_resizebuffer(L, buff, n); + } + return buff->buffer; +} + + diff --git a/extern/lua-5.1.5/src/lzio.h b/extern/lua-5.1.5/src/lzio.h new file mode 100644 index 00000000..51d695d8 --- /dev/null +++ b/extern/lua-5.1.5/src/lzio.h @@ -0,0 +1,67 @@ +/* +** $Id: lzio.h,v 1.21.1.1 2007/12/27 13:02:25 roberto Exp $ +** Buffered streams +** See Copyright Notice in lua.h +*/ + + +#ifndef lzio_h +#define lzio_h + +#include "lua.h" + +#include "lmem.h" + + +#define EOZ (-1) /* end of stream */ + +typedef struct Zio ZIO; + +#define char2int(c) cast(int, cast(unsigned char, (c))) + +#define zgetc(z) (((z)->n--)>0 ? char2int(*(z)->p++) : luaZ_fill(z)) + +typedef struct Mbuffer { + char *buffer; + size_t n; + size_t buffsize; +} Mbuffer; + +#define luaZ_initbuffer(L, buff) ((buff)->buffer = NULL, (buff)->buffsize = 0) + +#define luaZ_buffer(buff) ((buff)->buffer) +#define luaZ_sizebuffer(buff) ((buff)->buffsize) +#define luaZ_bufflen(buff) ((buff)->n) + +#define luaZ_resetbuffer(buff) ((buff)->n = 0) + + +#define luaZ_resizebuffer(L, buff, size) \ + (luaM_reallocvector(L, (buff)->buffer, (buff)->buffsize, size, char), \ + (buff)->buffsize = size) + +#define luaZ_freebuffer(L, buff) luaZ_resizebuffer(L, buff, 0) + + +LUAI_FUNC char *luaZ_openspace (lua_State *L, Mbuffer *buff, size_t n); +LUAI_FUNC void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, + void *data); +LUAI_FUNC size_t luaZ_read (ZIO* z, void* b, size_t n); /* read next n bytes */ +LUAI_FUNC int luaZ_lookahead (ZIO *z); + + + +/* --------- Private Part ------------------ */ + +struct Zio { + size_t n; /* bytes still unread */ + const char *p; /* current position in buffer */ + lua_Reader reader; + void* data; /* additional data */ + lua_State *L; /* Lua state (for reader) */ +}; + + +LUAI_FUNC int luaZ_fill (ZIO *z); + +#endif diff --git a/extern/lua-5.1.5/src/print.c b/extern/lua-5.1.5/src/print.c new file mode 100644 index 00000000..e240cfc3 --- /dev/null +++ b/extern/lua-5.1.5/src/print.c @@ -0,0 +1,227 @@ +/* +** $Id: print.c,v 1.55a 2006/05/31 13:30:05 lhf Exp $ +** print bytecodes +** See Copyright Notice in lua.h +*/ + +#include +#include + +#define luac_c +#define LUA_CORE + +#include "ldebug.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lundump.h" + +#define PrintFunction luaU_print + +#define Sizeof(x) ((int)sizeof(x)) +#define VOID(p) ((const void*)(p)) + +static void PrintString(const TString* ts) +{ + const char* s=getstr(ts); + size_t i,n=ts->tsv.len; + putchar('"'); + for (i=0; ik[i]; + switch (ttype(o)) + { + case LUA_TNIL: + printf("nil"); + break; + case LUA_TBOOLEAN: + printf(bvalue(o) ? "true" : "false"); + break; + case LUA_TNUMBER: + printf(LUA_NUMBER_FMT,nvalue(o)); + break; + case LUA_TSTRING: + PrintString(rawtsvalue(o)); + break; + default: /* cannot happen */ + printf("? type=%d",ttype(o)); + break; + } +} + +static void PrintCode(const Proto* f) +{ + const Instruction* code=f->code; + int pc,n=f->sizecode; + for (pc=0; pc0) printf("[%d]\t",line); else printf("[-]\t"); + printf("%-9s\t",luaP_opnames[o]); + switch (getOpMode(o)) + { + case iABC: + printf("%d",a); + if (getBMode(o)!=OpArgN) printf(" %d",ISK(b) ? (-1-INDEXK(b)) : b); + if (getCMode(o)!=OpArgN) printf(" %d",ISK(c) ? (-1-INDEXK(c)) : c); + break; + case iABx: + if (getBMode(o)==OpArgK) printf("%d %d",a,-1-bx); else printf("%d %d",a,bx); + break; + case iAsBx: + if (o==OP_JMP) printf("%d",sbx); else printf("%d %d",a,sbx); + break; + } + switch (o) + { + case OP_LOADK: + printf("\t; "); PrintConstant(f,bx); + break; + case OP_GETUPVAL: + case OP_SETUPVAL: + printf("\t; %s", (f->sizeupvalues>0) ? getstr(f->upvalues[b]) : "-"); + break; + case OP_GETGLOBAL: + case OP_SETGLOBAL: + printf("\t; %s",svalue(&f->k[bx])); + break; + case OP_GETTABLE: + case OP_SELF: + if (ISK(c)) { printf("\t; "); PrintConstant(f,INDEXK(c)); } + break; + case OP_SETTABLE: + case OP_ADD: + case OP_SUB: + case OP_MUL: + case OP_DIV: + case OP_POW: + case OP_EQ: + case OP_LT: + case OP_LE: + if (ISK(b) || ISK(c)) + { + printf("\t; "); + if (ISK(b)) PrintConstant(f,INDEXK(b)); else printf("-"); + printf(" "); + if (ISK(c)) PrintConstant(f,INDEXK(c)); else printf("-"); + } + break; + case OP_JMP: + case OP_FORLOOP: + case OP_FORPREP: + printf("\t; to %d",sbx+pc+2); + break; + case OP_CLOSURE: + printf("\t; %p",VOID(f->p[bx])); + break; + case OP_SETLIST: + if (c==0) printf("\t; %d",(int)code[++pc]); + else printf("\t; %d",c); + break; + default: + break; + } + printf("\n"); + } +} + +#define SS(x) (x==1)?"":"s" +#define S(x) x,SS(x) + +static void PrintHeader(const Proto* f) +{ + const char* s=getstr(f->source); + if (*s=='@' || *s=='=') + s++; + else if (*s==LUA_SIGNATURE[0]) + s="(bstring)"; + else + s="(string)"; + printf("\n%s <%s:%d,%d> (%d instruction%s, %d bytes at %p)\n", + (f->linedefined==0)?"main":"function",s, + f->linedefined,f->lastlinedefined, + S(f->sizecode),f->sizecode*Sizeof(Instruction),VOID(f)); + printf("%d%s param%s, %d slot%s, %d upvalue%s, ", + f->numparams,f->is_vararg?"+":"",SS(f->numparams), + S(f->maxstacksize),S(f->nups)); + printf("%d local%s, %d constant%s, %d function%s\n", + S(f->sizelocvars),S(f->sizek),S(f->sizep)); +} + +static void PrintConstants(const Proto* f) +{ + int i,n=f->sizek; + printf("constants (%d) for %p:\n",n,VOID(f)); + for (i=0; isizelocvars; + printf("locals (%d) for %p:\n",n,VOID(f)); + for (i=0; ilocvars[i].varname),f->locvars[i].startpc+1,f->locvars[i].endpc+1); + } +} + +static void PrintUpvalues(const Proto* f) +{ + int i,n=f->sizeupvalues; + printf("upvalues (%d) for %p:\n",n,VOID(f)); + if (f->upvalues==NULL) return; + for (i=0; iupvalues[i])); + } +} + +void PrintFunction(const Proto* f, int full) +{ + int i,n=f->sizep; + PrintHeader(f); + PrintCode(f); + if (full) + { + PrintConstants(f); + PrintLocals(f); + PrintUpvalues(f); + } + for (i=0; ip[i],full); +} diff --git a/extern/lua-5.1.5/test/README b/extern/lua-5.1.5/test/README new file mode 100644 index 00000000..0c7f38bc --- /dev/null +++ b/extern/lua-5.1.5/test/README @@ -0,0 +1,26 @@ +These are simple tests for Lua. Some of them contain useful code. +They are meant to be run to make sure Lua is built correctly and also +to be read, to see how Lua programs look. + +Here is a one-line summary of each program: + + bisect.lua bisection method for solving non-linear equations + cf.lua temperature conversion table (celsius to farenheit) + echo.lua echo command line arguments + env.lua environment variables as automatic global variables + factorial.lua factorial without recursion + fib.lua fibonacci function with cache + fibfor.lua fibonacci numbers with coroutines and generators + globals.lua report global variable usage + hello.lua the first program in every language + life.lua Conway's Game of Life + luac.lua bare-bones luac + printf.lua an implementation of printf + readonly.lua make global variables readonly + sieve.lua the sieve of of Eratosthenes programmed with coroutines + sort.lua two implementations of a sort function + table.lua make table, grouping all data for the same item + trace-calls.lua trace calls + trace-globals.lua trace assigments to global variables + xd.lua hex dump + diff --git a/extern/lua-5.1.5/test/bisect.lua b/extern/lua-5.1.5/test/bisect.lua new file mode 100644 index 00000000..f91e69bf --- /dev/null +++ b/extern/lua-5.1.5/test/bisect.lua @@ -0,0 +1,27 @@ +-- bisection method for solving non-linear equations + +delta=1e-6 -- tolerance + +function bisect(f,a,b,fa,fb) + local c=(a+b)/2 + io.write(n," c=",c," a=",a," b=",b,"\n") + if c==a or c==b or math.abs(a-b) posted to lua-l +-- modified to use ANSI terminal escape sequences +-- modified to use for instead of while + +local write=io.write + +ALIVE="" DEAD="" +ALIVE="O" DEAD="-" + +function delay() -- NOTE: SYSTEM-DEPENDENT, adjust as necessary + for i=1,10000 do end + -- local i=os.clock()+1 while(os.clock() 0 do + local xm1,x,xp1,xi=self.w-1,self.w,1,self.w + while xi > 0 do + local sum = self[ym1][xm1] + self[ym1][x] + self[ym1][xp1] + + self[y][xm1] + self[y][xp1] + + self[yp1][xm1] + self[yp1][x] + self[yp1][xp1] + next[y][x] = ((sum==2) and self[y][x]) or ((sum==3) and 1) or 0 + xm1,x,xp1,xi = x,xp1,xp1+1,xi-1 + end + ym1,y,yp1,yi = y,yp1,yp1+1,yi-1 + end +end + +-- output the array to screen +function _CELLS:draw() + local out="" -- accumulate to reduce flicker + for y=1,self.h do + for x=1,self.w do + out=out..(((self[y][x]>0) and ALIVE) or DEAD) + end + out=out.."\n" + end + write(out) +end + +-- constructor +function CELLS(w,h) + local c = ARRAY2D(w,h) + c.spawn = _CELLS.spawn + c.evolve = _CELLS.evolve + c.draw = _CELLS.draw + return c +end + +-- +-- shapes suitable for use with spawn() above +-- +HEART = { 1,0,1,1,0,1,1,1,1; w=3,h=3 } +GLIDER = { 0,0,1,1,0,1,0,1,1; w=3,h=3 } +EXPLODE = { 0,1,0,1,1,1,1,0,1,0,1,0; w=3,h=4 } +FISH = { 0,1,1,1,1,1,0,0,0,1,0,0,0,0,1,1,0,0,1,0; w=5,h=4 } +BUTTERFLY = { 1,0,0,0,1,0,1,1,1,0,1,0,0,0,1,1,0,1,0,1,1,0,0,0,1; w=5,h=5 } + +-- the main routine +function LIFE(w,h) + -- create two arrays + local thisgen = CELLS(w,h) + local nextgen = CELLS(w,h) + + -- create some life + -- about 1000 generations of fun, then a glider steady-state + thisgen:spawn(GLIDER,5,4) + thisgen:spawn(EXPLODE,25,10) + thisgen:spawn(FISH,4,12) + + -- run until break + local gen=1 + write("\027[2J") -- ANSI clear screen + while 1 do + thisgen:evolve(nextgen) + thisgen,nextgen = nextgen,thisgen + write("\027[H") -- ANSI home cursor + thisgen:draw() + write("Life - generation ",gen,"\n") + gen=gen+1 + if gen>2000 then break end + --delay() -- no delay + end +end + +LIFE(40,20) diff --git a/extern/lua-5.1.5/test/luac.lua b/extern/lua-5.1.5/test/luac.lua new file mode 100644 index 00000000..96a0a97c --- /dev/null +++ b/extern/lua-5.1.5/test/luac.lua @@ -0,0 +1,7 @@ +-- bare-bones luac in Lua +-- usage: lua luac.lua file.lua + +assert(arg[1]~=nil and arg[2]==nil,"usage: lua luac.lua file.lua") +f=assert(io.open("luac.out","wb")) +assert(f:write(string.dump(assert(loadfile(arg[1]))))) +assert(f:close()) diff --git a/extern/lua-5.1.5/test/printf.lua b/extern/lua-5.1.5/test/printf.lua new file mode 100644 index 00000000..58c63ff5 --- /dev/null +++ b/extern/lua-5.1.5/test/printf.lua @@ -0,0 +1,7 @@ +-- an implementation of printf + +function printf(...) + io.write(string.format(...)) +end + +printf("Hello %s from %s on %s\n",os.getenv"USER" or "there",_VERSION,os.date()) diff --git a/extern/lua-5.1.5/test/readonly.lua b/extern/lua-5.1.5/test/readonly.lua new file mode 100644 index 00000000..85c0b4e0 --- /dev/null +++ b/extern/lua-5.1.5/test/readonly.lua @@ -0,0 +1,12 @@ +-- make global variables readonly + +local f=function (t,i) error("cannot redefine global variable `"..i.."'",2) end +local g={} +local G=getfenv() +setmetatable(g,{__index=G,__newindex=f}) +setfenv(1,g) + +-- an example +rawset(g,"x",3) +x=2 +y=1 -- cannot redefine `y' diff --git a/extern/lua-5.1.5/test/sieve.lua b/extern/lua-5.1.5/test/sieve.lua new file mode 100644 index 00000000..0871bb21 --- /dev/null +++ b/extern/lua-5.1.5/test/sieve.lua @@ -0,0 +1,29 @@ +-- the sieve of of Eratosthenes programmed with coroutines +-- typical usage: lua -e N=1000 sieve.lua | column + +-- generate all the numbers from 2 to n +function gen (n) + return coroutine.wrap(function () + for i=2,n do coroutine.yield(i) end + end) +end + +-- filter the numbers generated by `g', removing multiples of `p' +function filter (p, g) + return coroutine.wrap(function () + while 1 do + local n = g() + if n == nil then return end + if math.mod(n, p) ~= 0 then coroutine.yield(n) end + end + end) +end + +N=N or 1000 -- from command line +x = gen(N) -- generate primes up to N +while 1 do + local n = x() -- pick a number until done + if n == nil then break end + print(n) -- must be a prime number + x = filter(n, x) -- now remove its multiples +end diff --git a/extern/lua-5.1.5/test/sort.lua b/extern/lua-5.1.5/test/sort.lua new file mode 100644 index 00000000..0bcb15f8 --- /dev/null +++ b/extern/lua-5.1.5/test/sort.lua @@ -0,0 +1,66 @@ +-- two implementations of a sort function +-- this is an example only. Lua has now a built-in function "sort" + +-- extracted from Programming Pearls, page 110 +function qsort(x,l,u,f) + if ly end) + show("after reverse selection sort",x) + qsort(x,1,n,function (x,y) return x>> ",string.rep(" ",level)) + if t~=nil and t.currentline>=0 then io.write(t.short_src,":",t.currentline," ") end + t=debug.getinfo(2) + if event=="call" then + level=level+1 + else + level=level-1 if level<0 then level=0 end + end + if t.what=="main" then + if event=="call" then + io.write("begin ",t.short_src) + else + io.write("end ",t.short_src) + end + elseif t.what=="Lua" then +-- table.foreach(t,print) + io.write(event," ",t.name or "(Lua)"," <",t.linedefined,":",t.short_src,">") + else + io.write(event," ",t.name or "(C)"," [",t.what,"] ") + end + io.write("\n") +end + +debug.sethook(hook,"cr") +level=0 diff --git a/extern/lua-5.1.5/test/trace-globals.lua b/extern/lua-5.1.5/test/trace-globals.lua new file mode 100644 index 00000000..295e670c --- /dev/null +++ b/extern/lua-5.1.5/test/trace-globals.lua @@ -0,0 +1,38 @@ +-- trace assigments to global variables + +do + -- a tostring that quotes strings. note the use of the original tostring. + local _tostring=tostring + local tostring=function(a) + if type(a)=="string" then + return string.format("%q",a) + else + return _tostring(a) + end + end + + local log=function (name,old,new) + local t=debug.getinfo(3,"Sl") + local line=t.currentline + io.write(t.short_src) + if line>=0 then io.write(":",line) end + io.write(": ",name," is now ",tostring(new)," (was ",tostring(old),")","\n") + end + + local g={} + local set=function (t,name,value) + log(name,g[name],value) + g[name]=value + end + setmetatable(getfenv(),{__index=g,__newindex=set}) +end + +-- an example + +a=1 +b=2 +a=10 +b=20 +b=nil +b=200 +print(a,b,c) diff --git a/extern/lua-5.1.5/test/xd.lua b/extern/lua-5.1.5/test/xd.lua new file mode 100644 index 00000000..ebc3effc --- /dev/null +++ b/extern/lua-5.1.5/test/xd.lua @@ -0,0 +1,14 @@ +-- hex dump +-- usage: lua xd.lua < file + +local offset=0 +while true do + local s=io.read(16) + if s==nil then return end + io.write(string.format("%08X ",offset)) + string.gsub(s,"(.)", + function (c) io.write(string.format("%02X ",string.byte(c))) end) + io.write(string.rep(" ",3*(16-string.len(s)))) + io.write(" ",string.gsub(s,"%c","."),"\n") + offset=offset+16 +end diff --git a/include/addons/addon_manager.hpp b/include/addons/addon_manager.hpp new file mode 100644 index 00000000..cfbfd297 --- /dev/null +++ b/include/addons/addon_manager.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "addons/lua_engine.hpp" +#include "addons/toc_parser.hpp" +#include +#include +#include + +namespace wowee::addons { + +class AddonManager { +public: + AddonManager(); + ~AddonManager(); + + bool initialize(game::GameHandler* gameHandler); + void scanAddons(const std::string& addonsPath); + void loadAllAddons(); + bool runScript(const std::string& code); + void fireEvent(const std::string& event, const std::vector& args = {}); + void update(float deltaTime); + void shutdown(); + + const std::vector& getAddons() const { return addons_; } + LuaEngine* getLuaEngine() { return &luaEngine_; } + bool isInitialized() const { return luaEngine_.isInitialized(); } + + void saveAllSavedVariables(); + void setCharacterName(const std::string& name) { characterName_ = name; } + + /// Re-initialize the Lua VM and reload all addons (used by /reload). + bool reload(); + +private: + LuaEngine luaEngine_; + std::vector addons_; + game::GameHandler* gameHandler_ = nullptr; + std::string addonsPath_; + + bool loadAddon(const TocFile& addon); + std::string getSavedVariablesPath(const TocFile& addon) const; + std::string getSavedVariablesPerCharacterPath(const TocFile& addon) const; + std::string characterName_; +}; + +} // namespace wowee::addons diff --git a/include/addons/lua_engine.hpp b/include/addons/lua_engine.hpp new file mode 100644 index 00000000..6302a64c --- /dev/null +++ b/include/addons/lua_engine.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include + +struct lua_State; + +namespace wowee::game { class GameHandler; } + +namespace wowee::addons { + +struct TocFile; // forward declaration + +class LuaEngine { +public: + LuaEngine(); + ~LuaEngine(); + + LuaEngine(const LuaEngine&) = delete; + LuaEngine& operator=(const LuaEngine&) = delete; + + bool initialize(); + void shutdown(); + + bool executeFile(const std::string& path); + bool executeString(const std::string& code); + + void setGameHandler(game::GameHandler* handler); + + // Fire a WoW event to all registered Lua handlers. + void fireEvent(const std::string& eventName, + const std::vector& args = {}); + + // Try to dispatch a slash command via SlashCmdList. Returns true if handled. + bool dispatchSlashCommand(const std::string& command, const std::string& args); + + // Call OnUpdate scripts on all frames that have one. + void dispatchOnUpdate(float elapsed); + + // SavedVariables: load globals from file, save globals to file + bool loadSavedVariables(const std::string& path); + bool saveSavedVariables(const std::string& path, const std::vector& varNames); + + // Store addon info in registry for GetAddOnInfo/GetNumAddOns + void setAddonList(const std::vector& addons); + + lua_State* getState() { return L_; } + bool isInitialized() const { return L_ != nullptr; } + + // Optional callback for Lua errors (displayed as UI errors to the player) + using LuaErrorCallback = std::function; + void setLuaErrorCallback(LuaErrorCallback cb) { luaErrorCallback_ = std::move(cb); } + +private: + lua_State* L_ = nullptr; + game::GameHandler* gameHandler_ = nullptr; + LuaErrorCallback luaErrorCallback_; + + void registerCoreAPI(); + void registerEventAPI(); +}; + +} // namespace wowee::addons diff --git a/include/addons/toc_parser.hpp b/include/addons/toc_parser.hpp new file mode 100644 index 00000000..b19b4a78 --- /dev/null +++ b/include/addons/toc_parser.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee::addons { + +struct TocFile { + std::string addonName; + std::string basePath; + + std::unordered_map directives; + std::vector files; + + std::string getTitle() const; + std::string getInterface() const; + bool isLoadOnDemand() const; + std::vector getSavedVariables() const; + std::vector getSavedVariablesPerCharacter() const; +}; + +std::optional parseTocFile(const std::string& tocPath); + +} // namespace wowee::addons diff --git a/include/audio/npc_voice_manager.hpp b/include/audio/npc_voice_manager.hpp index 92ab8f32..1bf722fd 100644 --- a/include/audio/npc_voice_manager.hpp +++ b/include/audio/npc_voice_manager.hpp @@ -38,6 +38,10 @@ enum class VoiceType { GNOME_FEMALE, GOBLIN_MALE, GOBLIN_FEMALE, + BLOODELF_MALE, + BLOODELF_FEMALE, + DRAENEI_MALE, + DRAENEI_FEMALE, GENERIC, // Fallback }; diff --git a/include/audio/ui_sound_manager.hpp b/include/audio/ui_sound_manager.hpp index 241014ae..7a9a66b8 100644 --- a/include/audio/ui_sound_manager.hpp +++ b/include/audio/ui_sound_manager.hpp @@ -75,6 +75,12 @@ public: void playTargetSelect(); void playTargetDeselect(); + // Chat notifications + void playWhisperReceived(); + + // Minimap ping + void playMinimapPing(); + private: struct UISample { std::string path; @@ -122,6 +128,8 @@ private: std::vector errorSounds_; std::vector selectTargetSounds_; std::vector deselectTargetSounds_; + std::vector whisperSounds_; + std::vector minimapPingSounds_; // State tracking float volumeScale_ = 1.0f; diff --git a/include/core/application.hpp b/include/core/application.hpp index 7da1469b..28116152 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -26,6 +26,7 @@ namespace auth { class AuthHandler; } namespace game { class GameHandler; class World; class ExpansionRegistry; } namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; struct WMOModel; } namespace audio { enum class VoiceType; } +namespace addons { class AddonManager; } namespace core { @@ -62,6 +63,7 @@ public: game::GameHandler* getGameHandler() { return gameHandler.get(); } game::World* getWorld() { return world.get(); } pipeline::AssetManager* getAssetManager() { return assetManager.get(); } + addons::AddonManager* getAddonManager() { return addonManager_.get(); } game::ExpansionRegistry* getExpansionRegistry() { return expansionRegistry_.get(); } pipeline::DBCLayout* getDBCLayout() { return dbcLayout_.get(); } void reloadExpansionData(); // Reload DBC layouts, opcodes, etc. after expansion change @@ -71,6 +73,7 @@ public: // Weapon loading (called at spawn and on equipment change) void loadEquippedWeapons(); + bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel); // Logout to login screen void logoutToLogin(); @@ -95,6 +98,7 @@ private: void spawnPlayerCharacter(); std::string getPlayerModelPath() const; static const char* mapIdToName(uint32_t mapId); + static const char* mapDisplayName(uint32_t mapId); void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z); void buildFactionHostilityMap(uint8_t playerRace); pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path); @@ -129,6 +133,8 @@ private: std::unique_ptr gameHandler; std::unique_ptr world; std::unique_ptr assetManager; + std::unique_ptr addonManager_; + bool addonsLoaded_ = false; std::unique_ptr expansionRegistry_; std::unique_ptr dbcLayout_; @@ -224,6 +230,7 @@ private: std::future future; }; std::vector asyncCreatureLoads_; + std::unordered_set asyncCreatureDisplayLoads_; // displayIds currently loading in background void processAsyncCreatureResults(bool unlimited = false); static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads std::unordered_set deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose @@ -271,6 +278,7 @@ private: }; std::unordered_map gameObjectDisplayIdToPath_; std::unordered_map gameObjectDisplayIdModelCache_; // displayId → M2 modelId + std::unordered_set gameObjectDisplayIdFailedCache_; // displayIds that permanently fail to load std::unordered_map gameObjectDisplayIdWmoCache_; // displayId → WMO modelId std::unordered_map gameObjectInstances_; // guid → instance info struct PendingTransportMove { @@ -279,7 +287,17 @@ private: float z = 0.0f; float orientation = 0.0f; }; + struct PendingTransportRegistration { + uint64_t guid = 0; + uint32_t entry = 0; + uint32_t displayId = 0; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float orientation = 0.0f; + }; std::unordered_map pendingTransportMoves_; // guid -> latest pre-registration move + std::deque pendingTransportRegistrations_; uint32_t nextGameObjectModelId_ = 20000; uint32_t nextGameObjectWmoModelId_ = 40000; bool testTransportSetup_ = false; @@ -432,6 +450,7 @@ private: }; std::vector pendingTransportDoodadBatches_; static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4; + void processPendingTransportRegistrations(); void processPendingTransportDoodads(); // Quest marker billboard sprites (above NPCs) diff --git a/include/core/logger.hpp b/include/core/logger.hpp index fa8e8158..f3065f71 100644 --- a/include/core/logger.hpp +++ b/include/core/logger.hpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include namespace wowee { namespace core { @@ -144,6 +146,17 @@ private: } \ } while (0) +inline std::string toHexString(const uint8_t* data, size_t len, bool spaces = false) { + std::string s; + s.reserve(len * (spaces ? 3 : 2)); + for (size_t i = 0; i < len; ++i) { + char buf[4]; + std::snprintf(buf, sizeof(buf), spaces ? "%02x " : "%02x", data[i]); + s += buf; + } + return s; +} + } // namespace core } // namespace wowee diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 9f4dfde7..a608f6f5 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -135,6 +135,13 @@ public: bool isEntityMoving() const { return isMoving_; } + /// True only during the active interpolation phase (before reaching destination). + /// Unlike isEntityMoving(), this does NOT include the dead-reckoning overrun window, + /// so animations (Run/Walk) should use this to avoid "running in place" after arrival. + bool isActivelyMoving() const { + return isMoving_ && moveElapsed_ < moveDuration_; + } + // Returns the latest server-authoritative position: destination if moving, current if not. // Unlike getX/Y/Z (which only update via updateMovement), this always reflects the // last known server position regardless of distance culling. @@ -214,6 +221,9 @@ public: void setMaxPower(uint32_t p) { maxPowers[powerType < 7 ? powerType : 0] = p; } void setMaxPowerByType(uint8_t type, uint32_t p) { if (type < 7) maxPowers[type] = p; } + uint32_t getPowerByType(uint8_t type) const { return type < 7 ? powers[type] : 0; } + uint32_t getMaxPowerByType(uint8_t type) const { return type < 7 ? maxPowers[type] : 0; } + uint8_t getPowerType() const { return powerType; } void setPowerType(uint8_t t) { powerType = t; } @@ -274,18 +284,14 @@ protected: /** * Player entity + * Name is inherited from Unit — do NOT redeclare it here or the + * shadowed field will diverge from Unit::name, causing nameplates + * and other Unit*-based lookups to read an empty string. */ class Player : public Unit { public: Player() { type = ObjectType::PLAYER; } explicit Player(uint64_t guid) : Unit(guid) { type = ObjectType::PLAYER; } - - // Name - const std::string& getName() const { return name; } - void setName(const std::string& n) { name = n; } - -protected: - std::string name; }; /** diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 79460a17..67c8f5f3 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -7,6 +7,7 @@ #include "game/inventory.hpp" #include "game/spell_defines.hpp" #include "game/group_defines.hpp" +#include "network/packet.hpp" #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include namespace wowee::game { class TransportManager; @@ -38,8 +40,11 @@ namespace game { struct PlayerSkill { uint32_t skillId = 0; - uint16_t value = 0; + uint16_t value = 0; // base + permanent item bonuses uint16_t maxValue = 0; + uint16_t bonusTemp = 0; // temporary buff bonus (food, potions, etc.) + uint16_t bonusPerm = 0; // permanent spec/misc bonus (rarely non-zero) + uint16_t effectiveValue() const { return value + bonusTemp + bonusPerm; } }; /** @@ -167,6 +172,7 @@ public: * Check if connected to world server */ bool isConnected() const; + bool isInWorld() const { return state == WorldState::IN_WORLD && socket; } /** * Get current connection state @@ -218,6 +224,7 @@ public: pos = homeBindPos_; return true; } + uint32_t getHomeBindZoneId() const { return homeBindZoneId_; } /** * Send a movement packet @@ -273,6 +280,44 @@ public: using ChatBubbleCallback = std::function; void setChatBubbleCallback(ChatBubbleCallback cb) { chatBubbleCallback_ = std::move(cb); } + // Addon chat event callback: fires when any chat message is received (for Lua event dispatch) + using AddonChatCallback = std::function; + void setAddonChatCallback(AddonChatCallback cb) { addonChatCallback_ = std::move(cb); } + + // Generic addon event callback: fires named events with string args + using AddonEventCallback = std::function&)>; + void setAddonEventCallback(AddonEventCallback cb) { addonEventCallback_ = std::move(cb); } + + // Spell icon path resolver: spellId -> texture path string (e.g., "Interface\\Icons\\Spell_Fire_Fireball01") + using SpellIconPathResolver = std::function; + void setSpellIconPathResolver(SpellIconPathResolver r) { spellIconPathResolver_ = std::move(r); } + std::string getSpellIconPath(uint32_t spellId) const { + return spellIconPathResolver_ ? spellIconPathResolver_(spellId) : std::string{}; + } + + // Spell data resolver: spellId -> {castTimeMs, minRange, maxRange} + struct SpellDataInfo { uint32_t castTimeMs = 0; float minRange = 0; float maxRange = 0; uint32_t manaCost = 0; uint8_t powerType = 0; }; + using SpellDataResolver = std::function; + void setSpellDataResolver(SpellDataResolver r) { spellDataResolver_ = std::move(r); } + SpellDataInfo getSpellData(uint32_t spellId) const { + return spellDataResolver_ ? spellDataResolver_(spellId) : SpellDataInfo{}; + } + + // Item icon path resolver: displayInfoId -> texture path (e.g., "Interface\\Icons\\INV_Sword_04") + using ItemIconPathResolver = std::function; + void setItemIconPathResolver(ItemIconPathResolver r) { itemIconPathResolver_ = std::move(r); } + std::string getItemIconPath(uint32_t displayInfoId) const { + return itemIconPathResolver_ ? itemIconPathResolver_(displayInfoId) : std::string{}; + } + + // Random property/suffix name resolver: randomPropertyId -> suffix name (e.g., "of the Eagle") + // Positive IDs → ItemRandomProperties.dbc; negative IDs → ItemRandomSuffix.dbc (abs value) + using RandomPropertyNameResolver = std::function; + void setRandomPropertyNameResolver(RandomPropertyNameResolver r) { randomPropertyNameResolver_ = std::move(r); } + std::string getRandomPropertyName(int32_t id) const { + return randomPropertyNameResolver_ ? randomPropertyNameResolver_(id) : std::string{}; + } + // Emote animation callback: (entityGuid, animationId) using EmoteAnimCallback = std::function; void setEmoteAnimCallback(EmoteAnimCallback cb) { emoteAnimCallback_ = std::move(cb); } @@ -283,6 +328,7 @@ public: * @return Vector of chat messages */ const std::deque& getChatHistory() const { return chatHistory; } + void clearChatHistory() { chatHistory.clear(); } /** * Add a locally-generated chat message (e.g., emote feedback) @@ -292,9 +338,20 @@ public: // Money (copper) uint64_t getMoneyCopper() const { return playerMoneyCopper_; } + // PvP currency (TBC/WotLK only) + uint32_t getHonorPoints() const { return playerHonorPoints_; } + uint32_t getArenaPoints() const { return playerArenaPoints_; } + // Server-authoritative armor (UNIT_FIELD_RESISTANCES[0]) int32_t getArmorRating() const { return playerArmorRating_; } + // Server-authoritative elemental resistances (UNIT_FIELD_RESISTANCES[1-6]). + // school: 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane. Returns 0 if not received. + int32_t getResistance(int school) const { + if (school < 1 || school > 6) return 0; + return playerResistances_[school - 1]; + } + // Server-authoritative primary stats (UNIT_FIELD_STAT0-4: STR, AGI, STA, INT, SPI). // Returns -1 if the server hasn't sent the value yet. int32_t getPlayerStat(int idx) const { @@ -302,6 +359,43 @@ public: return playerStats_[idx]; } + // Server-authoritative attack power (WotLK: UNIT_FIELD_ATTACK_POWER / RANGED). + // Returns -1 if not yet received. + int32_t getMeleeAttackPower() const { return playerMeleeAP_; } + int32_t getRangedAttackPower() const { return playerRangedAP_; } + + // Server-authoritative spell damage / healing bonus (WotLK: PLAYER_FIELD_MOD_*). + // getSpellPower returns the max damage bonus across magic schools 1-6 (Holy/Fire/Nature/Frost/Shadow/Arcane). + // Returns -1 if not yet received. + int32_t getSpellPower() const { + int32_t sp = -1; + for (int i = 1; i <= 6; ++i) { + if (playerSpellDmgBonus_[i] > sp) sp = playerSpellDmgBonus_[i]; + } + return sp; + } + int32_t getHealingPower() const { return playerHealBonus_; } + + // Server-authoritative combat chance percentages (WotLK: PLAYER_* float fields). + // Returns -1.0f if not yet received. + float getDodgePct() const { return playerDodgePct_; } + float getParryPct() const { return playerParryPct_; } + float getBlockPct() const { return playerBlockPct_; } + float getCritPct() const { return playerCritPct_; } + float getRangedCritPct() const { return playerRangedCritPct_; } + // Spell crit by school (0=Physical,1=Holy,2=Fire,3=Nature,4=Frost,5=Shadow,6=Arcane) + float getSpellCritPct(int school = 1) const { + if (school < 0 || school > 6) return -1.0f; + return playerSpellCritPct_[school]; + } + + // Server-authoritative combat ratings (WotLK: PLAYER_FIELD_COMBAT_RATING_1+idx). + // Returns -1 if not yet received. Indices match AzerothCore CombatRating enum. + int32_t getCombatRating(int cr) const { + if (cr < 0 || cr > 24) return -1; + return playerCombatRatings_[cr]; + } + // Inventory Inventory& getInventory() { return inventory; } const Inventory& getInventory() const { return inventory; } @@ -324,6 +418,10 @@ public: std::shared_ptr getFocus() const; bool hasFocus() const { return focusGuid != 0; } + // Mouseover targeting — set each frame by the nameplate renderer + void setMouseoverGuid(uint64_t guid); + uint64_t getMouseoverGuid() const { return mouseoverGuid_; } + // Advanced targeting void targetLastTarget(); void targetEnemy(bool reverse = false); @@ -332,6 +430,16 @@ public: // Inspection void inspectTarget(); + struct InspectArenaTeam { + uint32_t teamId = 0; + uint8_t type = 0; // bracket size: 2, 3, or 5 + uint32_t weekGames = 0; + uint32_t weekWins = 0; + uint32_t seasonGames = 0; + uint32_t seasonWins = 0; + std::string name; + uint32_t personalRating = 0; + }; struct InspectResult { uint64_t guid = 0; std::string playerName; @@ -339,7 +447,9 @@ public: uint32_t unspentTalents = 0; uint8_t talentGroups = 0; uint8_t activeTalentGroup = 0; - std::array itemEntries{}; // 0=head…18=ranged + std::array itemEntries{}; // 0=head…18=ranged + std::array enchantIds{}; // permanent enchant per slot (0 = none) + std::vector arenaTeams; // from MSG_INSPECT_ARENA_TEAMS (WotLK) }; const InspectResult* getInspectResult() const { return inspectResult_.guid ? &inspectResult_ : nullptr; @@ -352,6 +462,19 @@ public: uint32_t getTotalTimePlayed() const { return totalTimePlayed_; } uint32_t getLevelTimePlayed() const { return levelTimePlayed_; } + // Who results (structured, from last SMSG_WHO response) + struct WhoEntry { + std::string name; + std::string guildName; + uint32_t level = 0; + uint32_t classId = 0; + uint32_t raceId = 0; + uint32_t zoneId = 0; + }; + const std::vector& getWhoResults() const { return whoResults_; } + uint32_t getWhoOnlineCount() const { return whoOnlineCount_; } + std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); } + // Social commands void addFriend(const std::string& playerName, const std::string& note = ""); void removeFriend(const std::string& playerName); @@ -370,7 +493,20 @@ public: uint8_t arenaType = 0; uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress uint32_t inviteTimeout = 80; + uint32_t avgWaitTimeSec = 0; // server-estimated average wait (STATUS_WAIT_QUEUE) + uint32_t timeInQueueSec = 0; // time already spent in queue (STATUS_WAIT_QUEUE) std::chrono::steady_clock::time_point inviteReceivedTime{}; + std::string bgName; // human-readable BG/arena name + }; + + // Available BG list (populated by SMSG_BATTLEFIELD_LIST) + struct AvailableBgInfo { + uint32_t bgTypeId = 0; + bool isRegistered = false; + bool isHoliday = false; + uint32_t minLevel = 0; + uint32_t maxLevel = 0; + std::vector instanceIds; }; // Battleground @@ -378,6 +514,45 @@ public: void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF); void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF); const std::array& getBgQueues() const { return bgQueues_; } + const std::vector& getAvailableBgs() const { return availableBgs_; } + + // BG scoreboard (MSG_PVP_LOG_DATA) + struct BgPlayerScore { + uint64_t guid = 0; + std::string name; + uint8_t team = 0; // 0=Horde, 1=Alliance + uint32_t killingBlows = 0; + uint32_t deaths = 0; + uint32_t honorableKills = 0; + uint32_t bonusHonor = 0; + std::vector> bgStats; // BG-specific fields + }; + struct ArenaTeamScore { + std::string teamName; + uint32_t ratingChange = 0; // signed delta packed as uint32 + uint32_t newRating = 0; + }; + struct BgScoreboardData { + std::vector players; + bool hasWinner = false; + uint8_t winner = 0; // 0=Horde, 1=Alliance + bool isArena = false; + // Arena-only fields (valid when isArena=true) + ArenaTeamScore arenaTeams[2]; // team 0 = first, team 1 = second + }; + void requestPvpLog(); + const BgScoreboardData* getBgScoreboard() const { + return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_; + } + + // BG flag carrier / important player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS) + struct BgPlayerPosition { + uint64_t guid = 0; + float wowX = 0.0f; // canonical WoW X (north) + float wowY = 0.0f; // canonical WoW Y (west) + int group = 0; // 0 = first list (usually ally flag carriers), 1 = second list + }; + const std::vector& getBgPlayerPositions() const { return bgPlayerPositions_; } // Network latency (milliseconds, updated each PONG response) uint32_t getLatencyMs() const { return lastLatency; } @@ -385,6 +560,8 @@ public: // Logout commands void requestLogout(); void cancelLogout(); + bool isLoggingOut() const { return loggingOut_; } + float getLogoutCountdown() const { return logoutCountdown_; } // Stand state void setStandState(uint8_t state); // 0=stand, 1=sit, 2=sit_chair, 3=sleep, 4=sit_low_chair, 5=sit_medium_chair, 6=sit_high_chair, 7=dead, 8=kneel, 9=submerged @@ -401,6 +578,7 @@ public: // Follow/Assist void followTarget(); + void cancelFollow(); // Stop following current target void assistTarget(); // PvP @@ -428,6 +606,24 @@ public: // GM Ticket void submitGmTicket(const std::string& text); void deleteGmTicket(); + void requestGmTicket(); ///< Send CMSG_GMTICKET_GETTICKET to query open ticket + + // GM ticket status accessors + bool hasActiveGmTicket() const { return gmTicketActive_; } + const std::string& getGmTicketText() const { return gmTicketText_; } + bool isGmSupportAvailable() const { return gmSupportAvailable_; } + float getGmTicketWaitHours() const { return gmTicketWaitHours_; } + + // Battlefield Manager (Wintergrasp) + bool hasBfMgrInvite() const { return bfMgrInvitePending_; } + bool isInBfMgrZone() const { return bfMgrActive_; } + uint32_t getBfMgrZoneId() const { return bfMgrZoneId_; } + void acceptBfMgrInvite(); + void declineBfMgrInvite(); + + // WotLK Calendar + uint32_t getCalendarPendingInvites() const { return calendarPendingInvites_; } + void requestCalendar(); ///< Send CMSG_CALENDAR_GET_CALENDAR to the server void queryGuildInfo(uint32_t guildId); void createGuild(const std::string& guildName); void addGuildRank(const std::string& rankName); @@ -456,12 +652,44 @@ public: uint32_t getPetitionCost() const { return petitionCost_; } uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; } + // Petition signatures (guild charter signing flow) + struct PetitionSignature { + uint64_t playerGuid = 0; + std::string playerName; // resolved later or empty + }; + struct PetitionInfo { + uint64_t petitionGuid = 0; + uint64_t ownerGuid = 0; + std::string guildName; + uint32_t signatureCount = 0; + uint32_t signaturesRequired = 9; // guild default; arena teams differ + std::vector signatures; + bool showUI = false; + }; + const PetitionInfo& getPetitionInfo() const { return petitionInfo_; } + bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; } + void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; } + void signPetition(uint64_t petitionGuid); + void turnInPetition(uint64_t petitionGuid); + + // Guild name lookup for other players' nameplates + // Returns the guild name for a given guildId, or empty if unknown. + // Automatically queries the server for unknown guild IDs. + const std::string& lookupGuildName(uint32_t guildId); + // Returns the guildId for a player entity (from PLAYER_GUILDID update field). + uint32_t getEntityGuildId(uint64_t guid) const; + // Ready check + struct ReadyCheckResult { + std::string name; + bool ready = false; + }; void initiateReadyCheck(); void respondToReadyCheck(bool ready); bool hasPendingReadyCheck() const { return pendingReadyCheck_; } void dismissReadyCheck() { pendingReadyCheck_ = false; } const std::string& getReadyCheckInitiator() const { return readyCheckInitiator_; } + const std::vector& getReadyCheckResults() const { return readyCheckResults_; } // Duel void forfeitDuel(); @@ -499,6 +727,27 @@ public: } std::string getCachedPlayerName(uint64_t guid) const; std::string getCachedCreatureName(uint32_t entry) const; + // Returns the creature subname/title (e.g. ""), empty if not cached + std::string getCachedCreatureSubName(uint32_t entry) const { + auto it = creatureInfoCache.find(entry); + return (it != creatureInfoCache.end()) ? it->second.subName : ""; + } + // Returns the creature rank (0=Normal,1=Elite,2=RareElite,3=Boss,4=Rare) + // or -1 if not cached yet + int getCreatureRank(uint32_t entry) const { + auto it = creatureInfoCache.find(entry); + return (it != creatureInfoCache.end()) ? static_cast(it->second.rank) : -1; + } + // Returns creature type (1=Beast,2=Dragonkin,...,7=Humanoid,...) or 0 if not cached + uint32_t getCreatureType(uint32_t entry) const { + auto it = creatureInfoCache.find(entry); + return (it != creatureInfoCache.end()) ? it->second.creatureType : 0; + } + // Returns creature family (e.g. pet family for beasts) or 0 + uint32_t getCreatureFamily(uint32_t entry) const { + auto it = creatureInfoCache.find(entry); + return (it != creatureInfoCache.end()) ? it->second.family : 0; + } // ---- Phase 2: Combat ---- void startAutoAttack(uint64_t targetGuid); @@ -513,9 +762,25 @@ public: } uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget; } bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; } + // Timestamp (ms since epoch) of the most recent player melee auto-attack. + // Zero if no swing has occurred this session. + uint64_t getLastMeleeSwingMs() const { return lastMeleeSwingMs_; } const std::vector& getCombatText() const { return combatText; } void updateCombatText(float deltaTime); + // Combat log (persistent rolling history, max MAX_COMBAT_LOG entries) + const std::deque& getCombatLog() const { return combatLog_; } + void clearCombatLog() { combatLog_.clear(); } + + // Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame + bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); } + std::string popAreaTriggerMsg() { + if (areaTriggerMsgs_.empty()) return {}; + std::string msg = areaTriggerMsgs_.front(); + areaTriggerMsgs_.pop_front(); + return msg; + } + // Threat struct ThreatEntry { uint64_t victimGuid = 0; @@ -536,7 +801,10 @@ public: void cancelCast(); void cancelAura(uint32_t spellId); void dismissPet(); + void renamePet(const std::string& newName); bool hasPet() const { return petGuid_ != 0; } + // Returns true once after SMSG_PET_RENAMEABLE; consuming the flag clears it. + bool consumePetRenameablePending() { bool v = petRenameablePending_; petRenameablePending_ = false; return v; } uint64_t getPetGuid() const { return petGuid_; } // ---- Pet state (populated by SMSG_PET_SPELLS / SMSG_PET_MODE) ---- @@ -559,8 +827,36 @@ public: } // Send CMSG_PET_ACTION to issue a pet command void sendPetAction(uint32_t action, uint64_t targetGuid = 0); + // Toggle autocast for a pet spell via CMSG_PET_SPELL_AUTOCAST + 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) + uint32_t entry = 0; // creature entry ID + uint32_t level = 0; + std::string name; + uint32_t displayId = 0; + bool isActive = false; // true = currently summoned/active slot + }; + bool isStableWindowOpen() const { return stableWindowOpen_; } + void closeStableWindow() { stableWindowOpen_ = false; } + uint64_t getStableMasterGuid() const { return stableMasterGuid_; } + uint8_t getStableSlots() const { return stableNumSlots_; } + const std::vector& getStabledPets() const { return stabledPets_; } + void requestStabledPetList(); // CMSG MSG_LIST_STABLED_PETS + void stablePet(uint8_t slot); // CMSG_STABLE_PET (store active pet in slot) + void unstablePet(uint32_t petNumber); // CMSG_UNSTABLE_PET (retrieve to active) + // Player proficiency bitmasks (from SMSG_SET_PROFICIENCY) // itemClass 2 = Weapon (subClassMask bits: 0=Axe1H,1=Axe2H,2=Bow,3=Gun,4=Mace1H,5=Mace2H,6=Polearm,7=Sword1H,8=Sword2H,10=Staff,13=Fist,14=Misc,15=Dagger,16=Thrown,17=Crossbow,18=Wand,19=Fishing) // itemClass 4 = Armor (subClassMask bits: 1=Cloth,2=Leather,3=Mail,4=Plate,6=Shield) @@ -595,13 +891,26 @@ 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); + void cancelCraftQueue(); + int getCraftQueueRemaining() const { return craftQueueRemaining_; } + uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; } + + // 400ms spell-queue window: next spell to cast when current finishes + uint32_t getQueuedSpellId() const { return queuedSpellId_; } + void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; } // 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; + bool interruptible = true; ///< false when SPELL_ATTR_EX_NOT_INTERRUPTIBLE is set }; // Returns cast state for any unit by GUID (empty/non-casting if not found) const UnitCastState* getUnitCastState(uint64_t guid) const { @@ -623,6 +932,10 @@ public: auto* s = getUnitCastState(targetGuid); return s ? s->timeRemaining : 0.0f; } + bool isTargetCastInterruptible() const { + auto* s = getUnitCastState(targetGuid); + return s ? s->interruptible : true; + } // Talents uint8_t getActiveTalentSpec() const { return activeTalentSpec_; } @@ -633,6 +946,14 @@ public: static std::unordered_map empty; return spec < 2 ? learnedTalents_[spec] : empty; } + + // Glyphs (WotLK): up to 6 glyph slots per spec (3 major + 3 minor) + static constexpr uint8_t MAX_GLYPH_SLOTS = 6; + const std::array& getGlyphs() const { return learnedGlyphs_[activeTalentSpec_]; } + const std::array& getGlyphs(uint8_t spec) const { + static std::array empty{}; + return spec < 2 ? learnedGlyphs_[spec] : empty; + } uint8_t getTalentRank(uint32_t talentId) const { auto it = learnedTalents_[activeTalentSpec_].find(talentId); return (it != learnedTalents_[activeTalentSpec_].end()) ? it->second : 0; @@ -665,6 +986,10 @@ public: const std::array& getActionBar() const { return actionBar; } void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id); + // Client-side macro text storage (server sends only macro index; text is stored locally) + const std::string& getMacroText(uint32_t macroId) const; + void setMacroText(uint32_t macroId, const std::string& text); + void saveCharacterConfig(); void loadCharacterConfig(); static std::string getCharacterConfigDir(); @@ -672,6 +997,11 @@ public: // Auras const std::vector& getPlayerAuras() const { return playerAuras; } const std::vector& getTargetAuras() const { return targetAuras; } + // Per-unit aura cache (populated for party members and any unit we receive updates for) + const std::vector* getUnitAuras(uint64_t guid) const { + auto it = unitAurasCache_.find(guid); + return (it != unitAurasCache_.end()) ? &it->second : nullptr; + } // Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE) bool isQuestCompleted(uint32_t questId) const { return completedQuests_.count(questId) > 0; } @@ -693,6 +1023,10 @@ public: using StandStateCallback = std::function; void setStandStateCallback(StandStateCallback cb) { standStateCallback_ = std::move(cb); } + // Appearance changed callback — fired when PLAYER_BYTES or facial features update (barber shop, etc.) + using AppearanceChangedCallback = std::function; + void setAppearanceChangedCallback(AppearanceChangedCallback cb) { appearanceChangedCallback_ = std::move(cb); } + // Ghost state callback — fired when player enters or leaves ghost (spirit) form using GhostStateCallback = std::function; void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); } @@ -706,6 +1040,10 @@ public: using SpellCastAnimCallback = std::function; void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); } + // Fired when the player's own spell cast fails (spellId of the failed spell). + using SpellCastFailedCallback = std::function; + void setSpellCastFailedCallback(SpellCastFailedCallback cb) { spellCastFailedCallback_ = std::move(cb); } + // Unit animation hint: signal jump (animId=38) for other players/NPCs using UnitAnimHintCallback = std::function; void setUnitAnimHintCallback(UnitAnimHintCallback cb) { unitAnimHintCallback_ = std::move(cb); } @@ -743,6 +1081,17 @@ public: float getGameTime() const { return gameTime_; } float getTimeSpeed() const { return timeSpeed_; } + // Global Cooldown (GCD) — set when the server sends a spellId=0 cooldown entry + float getGCDRemaining() const { + if (gcdTotal_ <= 0.0f) return 0.0f; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - gcdStartedAt_).count() / 1000.0f; + float rem = gcdTotal_ - elapsed; + return rem > 0.0f ? rem : 0.0f; + } + float getGCDTotal() const { return gcdTotal_; } + bool isGCDActive() const { return getGCDRemaining() > 0.0f; } + // Weather state (updated by SMSG_WEATHER) // weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog uint32_t getWeatherType() const { return weatherType_; } @@ -756,6 +1105,7 @@ public: const std::map& getPlayerSkills() const { return playerSkills_; } const std::string& getSkillName(uint32_t skillId) const; uint32_t getSkillCategory(uint32_t skillId) const; + bool isProfessionSpell(uint32_t spellId) const; // World entry callback (online mode - triggered when entering world) // Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect @@ -767,6 +1117,11 @@ public: using KnockBackCallback = std::function; void setKnockBackCallback(KnockBackCallback cb) { knockBackCallback_ = std::move(cb); } + // Camera shake callback: called when server sends SMSG_CAMERA_SHAKE. + // Parameters: magnitude (world units), frequency (Hz), duration (seconds). + using CameraShakeCallback = std::function; + void setCameraShakeCallback(CameraShakeCallback cb) { cameraShakeCallback_ = std::move(cb); } + // Unstuck callback (resets player Z to floor height) using UnstuckCallback = std::function; void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); } @@ -894,10 +1249,21 @@ public: // Cooldowns float getSpellCooldown(uint32_t spellId) const; + const std::unordered_map& getSpellCooldowns() const { return spellCooldowns; } // Player GUID uint64_t getPlayerGuid() const { return playerGuid; } + // Look up class/race for a player GUID from name query cache. Returns 0 if unknown. + uint8_t lookupPlayerClass(uint64_t guid) const { + auto it = playerClassRaceCache_.find(guid); + return it != playerClassRaceCache_.end() ? it->second.classId : 0; + } + uint8_t lookupPlayerRace(uint64_t guid) const { + auto it = playerClassRaceCache_.find(guid); + return it != playerClassRaceCache_.end() ? it->second.raceId : 0; + } + // Look up a display name for any guid: checks playerNameCache then entity manager. // Returns empty string if unknown. Used by chat display to resolve names at render time. const std::string& lookupName(uint64_t guid) const { @@ -917,6 +1283,10 @@ public: const Character* ch = getActiveCharacter(); return ch ? static_cast(ch->characterClass) : 0; } + uint8_t getPlayerRace() const { + const Character* ch = getActiveCharacter(); + return ch ? static_cast(ch->race) : 0; + } void setPlayerGuid(uint64_t guid) { playerGuid = guid; } // Player death state @@ -924,18 +1294,42 @@ public: bool isPlayerGhost() const { return releasedSpirit_; } bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; } bool showResurrectDialog() const { return resurrectRequestPending_; } + /** True when SMSG_PRE_RESURRECT arrived — Reincarnation/Twisting Nether available. */ + bool canSelfRes() const { return selfResAvailable_; } + /** Send CMSG_SELF_RES to use Reincarnation / Twisting Nether. */ + void useSelfRes(); const std::string& getResurrectCasterName() const { return resurrectCasterName_; } bool showTalentWipeConfirmDialog() const { return talentWipePending_; } uint32_t getTalentWipeCost() const { return talentWipeCost_; } void confirmTalentWipe(); void cancelTalentWipe() { talentWipePending_ = false; } + // Pet talent respec confirm + bool showPetUnlearnDialog() const { return petUnlearnPending_; } + uint32_t getPetUnlearnCost() const { return petUnlearnCost_; } + void confirmPetUnlearn(); + void cancelPetUnlearn() { petUnlearnPending_ = false; } + + // Barber shop + bool isBarberShopOpen() const { return barberShopOpen_; } + void closeBarberShop() { barberShopOpen_ = false; fireAddonEvent("BARBER_SHOP_CLOSE", {}); } + void sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair); + + // Instance difficulty (0=5N, 1=5H, 2=25N, 3=25H for WotLK) + uint32_t getInstanceDifficulty() const { return instanceDifficulty_; } + bool isInstanceHeroic() const { return instanceIsHeroic_; } + bool isInInstance() const { return inInstance_; } + /** True when ghost is within 40 yards of corpse position (same map). */ bool canReclaimCorpse() const; + /** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */ + float getCorpseReclaimDelaySec() const; /** Distance (yards) from ghost to corpse, or -1 if no corpse data. */ float getCorpseDistance() const { if (corpseMapId_ == 0 || currentMapId_ != corpseMapId_) return -1.0f; - float dx = movementInfo.x - corpseX_; - float dy = movementInfo.y - corpseY_; + // movementInfo is canonical (x=north=server_y, y=west=server_x); + // corpse coords are raw server (x=west, y=north) — swap to compare. + float dx = movementInfo.x - corpseY_; + float dy = movementInfo.y - corpseX_; float dz = movementInfo.z - corpseZ_; return std::sqrt(dx*dx + dy*dy + dz*dz); } @@ -1035,6 +1429,14 @@ public: const std::string& getDuelChallengerName() const { return duelChallengerName_; } void acceptDuel(); // forfeitDuel() already declared at line ~399 + // Returns remaining duel countdown seconds, or 0 if no active countdown + float getDuelCountdownRemaining() const { + if (duelCountdownMs_ == 0) return 0.0f; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - duelCountdownStartedAt_).count(); + float rem = (static_cast(duelCountdownMs_) - static_cast(elapsed)) / 1000.0f; + return rem > 0.0f ? rem : 0.0f; + } // ---- Instance lockouts ---- struct InstanceLockout { @@ -1085,6 +1487,7 @@ public: // roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID void lfgJoin(uint32_t dungeonId, uint8_t roles); void lfgLeave(); + void lfgSetRoles(uint8_t roles); void lfgAcceptProposal(uint32_t proposalId, bool accept); void lfgSetBootVote(bool vote); void lfgTeleport(bool toLfgDungeon = true); @@ -1092,13 +1495,17 @@ public: bool isLfgQueued() const { return lfgState_ == LfgState::Queued; } bool isLfgInDungeon() const { return lfgState_ == LfgState::InDungeon; } uint32_t getLfgDungeonId() const { return lfgDungeonId_; } + std::string getCurrentLfgDungeonName() const { return getLfgDungeonName(lfgDungeonId_); } + std::string getMapName(uint32_t mapId) const; uint32_t getLfgProposalId() const { return lfgProposalId_; } int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; } uint32_t getLfgTimeInQueueMs() const { return lfgTimeInQueueMs_; } - uint32_t getLfgBootVotes() const { return lfgBootVotes_; } - uint32_t getLfgBootTotal() const { return lfgBootTotal_; } - uint32_t getLfgBootTimeLeft() const { return lfgBootTimeLeft_; } - uint32_t getLfgBootNeeded() const { return lfgBootNeeded_; } + uint32_t getLfgBootVotes() const { return lfgBootVotes_; } + uint32_t getLfgBootTotal() const { return lfgBootTotal_; } + uint32_t getLfgBootTimeLeft() const { return lfgBootTimeLeft_; } + uint32_t getLfgBootNeeded() const { return lfgBootNeeded_; } + const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; } + const std::string& getLfgBootReason() const { return lfgBootReason_; } // ---- Arena Team Stats ---- struct ArenaTeamStats { @@ -1109,8 +1516,34 @@ public: uint32_t seasonGames = 0; uint32_t seasonWins = 0; uint32_t rank = 0; + std::string teamName; + uint32_t teamType = 0; // 2, 3, or 5 }; const std::vector& getArenaTeamStats() const { return arenaTeamStats_; } + void requestArenaTeamRoster(uint32_t teamId); + + // ---- Arena Team Roster ---- + struct ArenaTeamMember { + uint64_t guid = 0; + std::string name; + bool online = false; + uint32_t weekGames = 0; + uint32_t weekWins = 0; + uint32_t seasonGames = 0; + uint32_t seasonWins = 0; + uint32_t personalRating = 0; + }; + struct ArenaTeamRoster { + uint32_t teamId = 0; + std::vector members; + }; + // Returns roster for the given teamId, or nullptr if not yet received + const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const { + for (const auto& r : arenaTeamRosters_) { + if (r.teamId == teamId) return &r; + } + return nullptr; + } // ---- Phase 5: Loot ---- void lootTarget(uint64_t guid); @@ -1121,6 +1554,15 @@ public: const LootResponseData& getCurrentLoot() const { return currentLoot; } void setAutoLoot(bool enabled) { autoLoot_ = enabled; } bool isAutoLoot() const { return autoLoot_; } + void setAutoSellGrey(bool enabled) { autoSellGrey_ = enabled; } + bool isAutoSellGrey() const { return autoSellGrey_; } + void setAutoRepair(bool enabled) { autoRepair_ = enabled; } + bool isAutoRepair() const { return autoRepair_; } + + // Master loot candidates (from SMSG_LOOT_MASTER_LIST) + const std::vector& getMasterLootCandidates() const { return masterLootCandidates_; } + bool hasMasterLootCandidates() const { return !masterLootCandidates_.empty(); } + void lootMasterGive(uint8_t lootSlot, uint64_t targetGuid); // Group loot roll struct LootRollEntry { @@ -1129,12 +1571,36 @@ public: uint32_t itemId = 0; std::string itemName; uint8_t itemQuality = 0; + uint32_t rollCountdownMs = 60000; // Duration of roll window in ms + uint8_t voteMask = 0xFF; // Bitmask: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant + std::chrono::steady_clock::time_point rollStartedAt{}; + + struct PlayerRollResult { + std::string playerName; + uint8_t rollNum = 0; + uint8_t rollType = 0; // 0=need,1=greed,2=disenchant,96=pass + }; + std::vector playerRolls; // live roll results from group members }; bool hasPendingLootRoll() const { return pendingLootRollActive_; } const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; } void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType); // rollType: 0=need, 1=greed, 2=disenchant, 96=pass + // Equipment Sets (WotLK): saved gear loadouts + struct EquipmentSetInfo { + uint64_t setGuid = 0; + uint32_t setId = 0; + std::string name; + std::string iconName; + }; + const std::vector& getEquipmentSets() const { return equipmentSetInfo_; } + bool supportsEquipmentSets() const; + void useEquipmentSet(uint32_t setId); + void saveEquipmentSet(const std::string& name, const std::string& iconName = "INV_Misc_QuestionMark", + uint64_t existingGuid = 0, uint32_t setIndex = 0xFFFFFFFF); + void deleteEquipmentSet(uint64_t setGuid); + // NPC Gossip void interactWithNpc(uint64_t guid); void interactWithGameObject(uint64_t guid); @@ -1143,6 +1609,9 @@ public: void acceptQuest(); void declineQuest(); void closeGossip(); + // Quest-starting items: right-click triggers quest offer dialog via questgiver protocol + void offerQuestFromItem(uint64_t itemGuid, uint32_t questId); + uint64_t getBagItemGuid(int bagIndex, int slotIndex) const; bool isGossipWindowOpen() const { return gossipWindowOpen; } const GossipMessageData& getCurrentGossip() const { return currentGossip; } bool isQuestDetailsOpen() { @@ -1214,12 +1683,16 @@ 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); bool isQuestTracked(uint32_t questId) const { return trackedQuestIds_.count(questId) > 0; } void setQuestTracked(uint32_t questId, bool tracked) { if (tracked) trackedQuestIds_.insert(questId); else trackedQuestIds_.erase(questId); + saveCharacterConfig(); } const std::unordered_set& getTrackedQuestIds() const { return trackedQuestIds_; } bool isQuestQueryPending(uint32_t questId) const { @@ -1250,6 +1723,7 @@ public: // Combo points uint8_t getComboPoints() const { return comboPoints_; } + uint8_t getShapeshiftFormId() const { return shapeshiftFormId_; } uint64_t getComboTarget() const { return comboTarget_; } // Death Knight rune state (6 runes: 0-1=Blood, 2-3=Unholy, 4-5=Frost; may become Death=3) @@ -1261,13 +1735,138 @@ public: }; const std::array& getPlayerRunes() const { return playerRunes_; } + // Talent-driven spell modifiers (SMSG_SET_FLAT_SPELL_MODIFIER / SMSG_SET_PCT_SPELL_MODIFIER) + // SpellModOp matches WotLK SpellModOp enum (server-side). + enum class SpellModOp : uint8_t { + Damage = 0, + Duration = 1, + Threat = 2, + Effect1 = 3, + Charges = 4, + Range = 5, + Radius = 6, + CritChance = 7, + AllEffects = 8, + NotLoseCastingTime = 9, + CastingTime = 10, + Cooldown = 11, + Effect2 = 12, + IgnoreArmor = 13, + Cost = 14, + CritDamageBonus = 15, + ResistMissChance = 16, + JumpTargets = 17, + ChanceOfSuccess = 18, + ActivationTime = 19, + Efficiency = 20, + MultipleValue = 21, + ResistDispelChance = 22, + Effect3 = 23, + BonusMultiplier = 24, + ProcPerMinute = 25, + ValueMultiplier = 26, + ResistPushback = 27, + MechanicDuration = 28, + StartCooldown = 29, + PeriodicBonus = 30, + AttackPower = 31, + }; + static constexpr int SPELL_MOD_OP_COUNT = 32; + + // Key: (SpellModOp, groupIndex) — value: accumulated flat or pct modifier + // pct values are stored in integer percent (e.g. -20 means -20% reduction). + struct SpellModKey { + SpellModOp op; + uint8_t group; + bool operator==(const SpellModKey& o) const { + return op == o.op && group == o.group; + } + }; + struct SpellModKeyHash { + std::size_t operator()(const SpellModKey& k) const { + return std::hash()( + (static_cast(static_cast(k.op)) << 8) | k.group); + } + }; + + // Returns the sum of all flat modifiers for a given op across all groups. + // (Callers that need per-group resolution can use getSpellFlatMods() directly.) + int32_t getSpellFlatMod(SpellModOp op) const { + int32_t total = 0; + for (const auto& [k, v] : spellFlatMods_) + if (k.op == op) total += v; + return total; + } + // Returns the sum of all pct modifiers for a given op across all groups (in %). + int32_t getSpellPctMod(SpellModOp op) const { + int32_t total = 0; + for (const auto& [k, v] : spellPctMods_) + if (k.op == op) total += v; + return total; + } + + // Convenience: apply flat+pct modifier to a base value. + // result = (base + flatMod) * (1.0 + pctMod/100.0), clamped to >= 0. + static int32_t applySpellMod(int32_t base, int32_t flat, int32_t pct) { + int64_t v = static_cast(base) + flat; + if (pct != 0) v = v + (v * pct + 50) / 100; // round half-up + return static_cast(v < 0 ? 0 : v); + } + struct FactionStandingInit { uint8_t flags = 0; int32_t standing = 0; }; + // Faction flag bitmask constants (from Faction.dbc ReputationFlags / SMSG_INITIALIZE_FACTIONS) + static constexpr uint8_t FACTION_FLAG_VISIBLE = 0x01; // shown in reputation list + static constexpr uint8_t FACTION_FLAG_AT_WAR = 0x02; // player is at war + static constexpr uint8_t FACTION_FLAG_HIDDEN = 0x04; // never shown + static constexpr uint8_t FACTION_FLAG_INVISIBLE_FORCED = 0x08; + static constexpr uint8_t FACTION_FLAG_PEACE_FORCED = 0x10; + const std::vector& getInitialFactions() const { return initialFactions_; } const std::unordered_map& getFactionStandings() const { return factionStandings_; } + + // Returns true if the player has "at war" toggled for the faction at repListId + bool isFactionAtWar(uint32_t repListId) const { + if (repListId >= initialFactions_.size()) return false; + return (initialFactions_[repListId].flags & FACTION_FLAG_AT_WAR) != 0; + } + // Returns true if the faction is visible in the reputation list + bool isFactionVisible(uint32_t repListId) const { + if (repListId >= initialFactions_.size()) return false; + const uint8_t f = initialFactions_[repListId].flags; + if (f & FACTION_FLAG_HIDDEN) return false; + if (f & FACTION_FLAG_INVISIBLE_FORCED) return false; + return (f & FACTION_FLAG_VISIBLE) != 0; + } + // Returns the faction ID for a given repListId (0 if unknown) + uint32_t getFactionIdByRepListId(uint32_t repListId) const; + // Returns the repListId for a given faction ID (0xFFFFFFFF if not found) + uint32_t getRepListIdByFactionId(uint32_t factionId) const; + // Shaman totems (4 slots: 0=Earth, 1=Fire, 2=Water, 3=Air) + struct TotemSlot { + uint32_t spellId = 0; + uint32_t durationMs = 0; + std::chrono::steady_clock::time_point placedAt{}; + bool active() const { return spellId != 0 && remainingMs() > 0; } + float remainingMs() const { + if (spellId == 0 || durationMs == 0) return 0.0f; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - placedAt).count(); + float rem = static_cast(durationMs) - static_cast(elapsed); + return rem > 0.0f ? rem : 0.0f; + } + }; + static constexpr int NUM_TOTEM_SLOTS = 4; + const TotemSlot& getTotemSlot(int slot) const { + static TotemSlot empty; + return (slot >= 0 && slot < NUM_TOTEM_SLOTS) ? activeTotemSlots_[slot] : empty; + } + const std::string& getFactionNamePublic(uint32_t factionId) const; + uint32_t getWatchedFactionId() const { return watchedFactionId_; } + void setWatchedFactionId(uint32_t factionId); uint32_t getLastContactListMask() const { return lastContactListMask_; } uint32_t getLastContactListCount() const { return lastContactListCount_; } bool isServerMovementAllowed() const { return serverMovementAllowed_; } @@ -1288,6 +1887,32 @@ public: using LevelUpCallback = std::function; void setLevelUpCallback(LevelUpCallback cb) { levelUpCallback_ = std::move(cb); } + // Stat deltas from the last SMSG_LEVELUP_INFO (valid until next level-up) + struct LevelUpDeltas { + uint32_t hp = 0; + uint32_t mana = 0; + uint32_t str = 0, agi = 0, sta = 0, intel = 0, spi = 0; + }; + const LevelUpDeltas& getLastLevelUpDeltas() const { return lastLevelUpDeltas_; } + + // Temporary weapon enchant timers (from SMSG_ITEM_ENCHANT_TIME_UPDATE) + // Slot: 0=main-hand, 1=off-hand, 2=ranged. Value: expire time (steady_clock ms). + struct TempEnchantTimer { + uint32_t slot = 0; + uint64_t expireMs = 0; // std::chrono::steady_clock ms timestamp when it expires + }; + const std::vector& getTempEnchantTimers() const { return tempEnchantTimers_; } + // Returns remaining ms for a given slot, or 0 if absent/expired. + uint32_t getTempEnchantRemainingMs(uint32_t slot) const; + static constexpr const char* kTempEnchantSlotNames[] = { "Main Hand", "Off Hand", "Ranged" }; + + // ---- Readable text (books / scrolls / notes) ---- + // Populated by handlePageTextQueryResponse(); multi-page items chain via nextPageId. + struct BookPage { uint32_t pageId = 0; std::string text; }; + const std::vector& getBookPages() const { return bookPages_; } + bool hasBookOpen() const { return !bookPages_.empty(); } + void clearBook() { bookPages_.clear(); } + // Other player level-up callback — fires when another player gains a level using OtherPlayerLevelUpCallback = std::function; void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); } @@ -1296,7 +1921,31 @@ public: using AchievementEarnedCallback = std::function; void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); } const std::unordered_set& getEarnedAchievements() const { return earnedAchievements_; } + + // Title system — earned title bits and the currently displayed title + const std::unordered_set& getKnownTitleBits() const { return knownTitleBits_; } + int32_t getChosenTitleBit() const { return chosenTitleBit_; } + /// Returns the formatted title string for a given bit (replaces %s with player name), or empty. + std::string getFormattedTitle(uint32_t bit) const; + /// Send CMSG_SET_TITLE to activate a title (bit >= 0) or clear it (bit = -1). + void sendSetTitle(int32_t bit); + + // Area discovery callback — fires when SMSG_EXPLORATION_EXPERIENCE is received + using AreaDiscoveryCallback = std::function; + void setAreaDiscoveryCallback(AreaDiscoveryCallback cb) { areaDiscoveryCallback_ = std::move(cb); } + + // Quest objective progress callback — fires on SMSG_QUESTUPDATE_ADD_KILL / ADD_ITEM + // questTitle: name of the quest; objectiveName: creature/item name; current/required counts + using QuestProgressCallback = std::function; + void setQuestProgressCallback(QuestProgressCallback cb) { questProgressCallback_ = std::move(cb); } const std::unordered_map& getCriteriaProgress() const { return criteriaProgress_; } + /// Returns the WoW PackedTime earn date for an achievement, or 0 if unknown. + uint32_t getAchievementDate(uint32_t id) const { + auto it = achievementDates_.find(id); + return (it != achievementDates_.end()) ? it->second : 0u; + } /// Returns the name of an achievement by ID, or empty string if unknown. const std::string& getAchievementName(uint32_t id) const { auto it = achievementNameCache_.find(id); @@ -1304,6 +1953,24 @@ public: static const std::string kEmpty; return kEmpty; } + /// Returns the description of an achievement by ID, or empty string if unknown. + const std::string& getAchievementDescription(uint32_t id) const { + auto it = achievementDescCache_.find(id); + if (it != achievementDescCache_.end()) return it->second; + static const std::string kEmpty; + return kEmpty; + } + /// Returns the point value of an achievement by ID, or 0 if unknown. + uint32_t getAchievementPoints(uint32_t id) const { + auto it = achievementPointsCache_.find(id); + return (it != achievementPointsCache_.end()) ? it->second : 0u; + } + /// Returns the set of achievement IDs earned by an inspected player (via SMSG_RESPOND_INSPECT_ACHIEVEMENTS). + /// Returns nullptr if no inspect data is available for the given GUID. + const std::unordered_set* getInspectedPlayerAchievements(uint64_t guid) const { + auto it = inspectedPlayerAchievements_.find(guid); + return (it != inspectedPlayerAchievements_.end()) ? &it->second : nullptr; + } // Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received. // The soundId corresponds to a SoundEntries.dbc record. The receiver is @@ -1324,12 +1991,36 @@ public: // UI error frame: prominent on-screen error messages (spell can't be cast, etc.) using UIErrorCallback = std::function; void setUIErrorCallback(UIErrorCallback cb) { uiErrorCallback_ = std::move(cb); } - void addUIError(const std::string& msg) { if (uiErrorCallback_) uiErrorCallback_(msg); } + void addUIError(const std::string& msg) { + if (uiErrorCallback_) uiErrorCallback_(msg); + fireAddonEvent("UI_ERROR_MESSAGE", {msg}); + } + void addUIInfoMessage(const std::string& msg) { + fireAddonEvent("UI_INFO_MESSAGE", {msg}); + } + void fireAddonEvent(const std::string& event, const std::vector& args = {}) { + if (addonEventCallback_) addonEventCallback_(event, args); + } + // Convenience: invoke a callback with a sound manager obtained from the renderer. + template + void withSoundManager(ManagerGetter getter, Callback cb); // Reputation change toast: factionName, delta, new standing using RepChangeCallback = std::function; void setRepChangeCallback(RepChangeCallback cb) { repChangeCallback_ = std::move(cb); } + // PvP honor credit callback (honorable kill or BG reward) + using PvpHonorCallback = std::function; + void setPvpHonorCallback(PvpHonorCallback cb) { pvpHonorCallback_ = std::move(cb); } + + // Item looted / received callback (SMSG_ITEM_PUSH_RESULT when showInChat is set) + using ItemLootCallback = std::function; + void setItemLootCallback(ItemLootCallback cb) { itemLootCallback_ = std::move(cb); } + + // Quest turn-in completion callback + using QuestCompleteCallback = std::function; + void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); } + // Mount state using MountCallback = std::function; // 0 = dismount void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } @@ -1346,8 +2037,13 @@ public: using TaxiFlightStartCallback = std::function; void setTaxiFlightStartCallback(TaxiFlightStartCallback cb) { taxiFlightStartCallback_ = std::move(cb); } + // Callback fired when server sends SMSG_OPEN_LFG_DUNGEON_FINDER (open dungeon finder UI) + using OpenLfgCallback = std::function; + void setOpenLfgCallback(OpenLfgCallback cb) { openLfgCallback_ = std::move(cb); } + bool isMounted() const { return currentMountDisplayId_ != 0; } bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; } + bool isHostileFactionPublic(uint32_t factionTemplateId) const { return isHostileFaction(factionTemplateId); } float getServerRunSpeed() const { return serverRunSpeed_; } float getServerWalkSpeed() const { return serverWalkSpeed_; } float getServerSwimSpeed() const { return serverSwimSpeed_; } @@ -1392,6 +2088,7 @@ public: bool isTaxiMountActive() const { return taxiMountActive_; } bool isTaxiActivationPending() const { return taxiActivatePending_; } void forceClearTaxiAndMovementState(); + const std::string& getTaxiDestName() const { return taxiDestName_; } const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; } uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; } @@ -1416,12 +2113,22 @@ public: float x = 0, y = 0, z = 0; }; const std::unordered_map& getTaxiNodes() const { return taxiNodes_; } + bool isKnownTaxiNode(uint32_t nodeId) const { + if (nodeId == 0 || nodeId > 384) return false; + uint32_t idx = nodeId - 1; + return (knownTaxiMask_[idx / 32] & (1u << (idx % 32))) != 0; + } uint32_t getTaxiCostTo(uint32_t destNodeId) const; bool taxiNpcHasRoutes(uint64_t guid) const { auto it = taxiNpcHasRoutes_.find(guid); return it != taxiNpcHasRoutes_.end() && it->second; } + // Vehicle (WotLK) + bool isInVehicle() const { return vehicleId_ != 0; } + uint32_t getVehicleId() const { return vehicleId_; } + void sendRequestVehicleExit(); + // Vendor void openVendor(uint64_t npcGuid); void closeVendor(); @@ -1442,7 +2149,11 @@ public: void autoEquipItemInBag(int bagIndex, int slotIndex); void useItemBySlot(int backpackIndex); void useItemInBag(int bagIndex, int slotIndex); + // CMSG_OPEN_ITEM — for locked containers (lockboxes); server checks keyring automatically + void openItemBySlot(int backpackIndex); + void openItemInBag(int bagIndex, int slotIndex); void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1); + void splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count); void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot); void swapBagSlots(int srcBagIndex, int dstBagIndex); void useItemById(uint32_t itemId); @@ -1479,7 +2190,7 @@ public: const std::array& getMailAttachments() const { return mailAttachments_; } int getMailAttachmentCount() const; void mailTakeMoney(uint32_t mailId); - void mailTakeItem(uint32_t mailId, uint32_t itemIndex); + void mailTakeItem(uint32_t mailId, uint32_t itemGuidLow); void mailDelete(uint32_t mailId); void mailMarkAsRead(uint32_t mailId); void refreshMailList(); @@ -1538,7 +2249,21 @@ public: void closeTrainer(); const std::string& getSpellName(uint32_t spellId) const; const std::string& getSpellRank(uint32_t spellId) const; + /// Returns the tooltip/description text from Spell.dbc (empty if unknown or has no text). + const std::string& getSpellDescription(uint32_t spellId) const; + const int32_t* getSpellEffectBasePoints(uint32_t spellId) const; + float getSpellDuration(uint32_t spellId) const; + std::string getEnchantName(uint32_t enchantId) const; const std::string& getSkillLineName(uint32_t spellId) const; + /// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other) + uint8_t getSpellDispelType(uint32_t spellId) const; + /// Returns true if the spell can be interrupted by abilities like Kick/Counterspell. + /// False for spells with SPELL_ATTR_EX_NOT_INTERRUPTIBLE (attrEx bit 4 = 0x10). + bool isSpellInterruptible(uint32_t spellId) const; + /// Returns the school bitmask for the spell from Spell.dbc + /// (0x01=Physical, 0x02=Holy, 0x04=Fire, 0x08=Nature, 0x10=Frost, 0x20=Shadow, 0x40=Arcane). + /// Returns 0 if unknown. + uint32_t getSpellSchoolMask(uint32_t spellId) const; struct TrainerTab { std::string name; @@ -1549,6 +2274,7 @@ public: auto it = itemInfoCache_.find(itemId); return (it != itemInfoCache_.end()) ? &it->second : nullptr; } + const std::unordered_map& getItemInfoCache() const { return itemInfoCache_; } // Request item info from server if not already cached/pending void ensureItemInfo(uint32_t entry) { if (entry == 0 || itemInfoCache_.count(entry) || pendingItemQueries_.count(entry)) return; @@ -1558,6 +2284,22 @@ public: if (index < 0 || index >= static_cast(backpackSlotGuids_.size())) return 0; return backpackSlotGuids_[index]; } + uint64_t getEquipSlotGuid(int slot) const { + if (slot < 0 || slot >= static_cast(equipSlotGuids_.size())) return 0; + return equipSlotGuids_[slot]; + } + // Returns the permanent and temporary enchant IDs for an item by GUID (0 if unknown). + std::pair getItemEnchantIds(uint64_t guid) const { + auto it = onlineItems_.find(guid); + if (it == onlineItems_.end()) return {0, 0}; + return {it->second.permanentEnchantId, it->second.temporaryEnchantId}; + } + // Returns the socket gem enchant IDs (3 slots; 0 = empty socket) for an item by GUID. + std::array getItemSocketEnchantIds(uint64_t guid) const { + auto it = onlineItems_.find(guid); + if (it == onlineItems_.end()) return {}; + return it->second.socketEnchantIds; + } uint64_t getVendorGuid() const { return currentVendorItems.vendorGuid; } /** @@ -1572,6 +2314,11 @@ public: * @param deltaTime Time since last update in seconds */ void update(float deltaTime); + void updateNetworking(float deltaTime); + void updateTimers(float deltaTime); + void updateEntityInterpolation(float deltaTime); + void updateTaxiAndMountState(float deltaTime); + void updateAutoAttack(float deltaTime); /** * Reset DBC-backed caches so they reload from new expansion data. @@ -1586,6 +2333,20 @@ private: * Handle incoming packet from world server */ void handlePacket(network::Packet& packet); + void registerOpcodeHandlers(); + void registerSkipHandler(LogicalOpcode op); + void registerErrorHandler(LogicalOpcode op, const char* msg); + void registerHandler(LogicalOpcode op, void (GameHandler::*handler)(network::Packet&)); + void registerWorldHandler(LogicalOpcode op, void (GameHandler::*handler)(network::Packet&)); + void enqueueIncomingPacket(const network::Packet& packet); + void enqueueIncomingPacketFront(network::Packet&& packet); + void processQueuedIncomingPackets(); + void enqueueUpdateObjectWork(UpdateObjectData&& data); + void processPendingUpdateObjectWork(const std::chrono::steady_clock::time_point& start, + float budgetMs); + void processOutOfRangeObjects(const std::vector& guids); + void applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated); + void finalizeUpdateObjectBatch(bool newItemCreated); /** * Handle SMSG_AUTH_CHALLENGE from server @@ -1733,6 +2494,9 @@ private: void handleGuildInvite(network::Packet& packet); void handleGuildCommandResult(network::Packet& packet); void handlePetitionShowlist(network::Packet& packet); + void handlePetitionQueryResponse(network::Packet& packet); + void handlePetitionShowSignatures(network::Packet& packet); + void handlePetitionSignResults(network::Packet& packet); void handlePetSpells(network::Packet& packet); void handleTurnInPetitionResults(network::Packet& packet); @@ -1749,6 +2513,7 @@ private: // ---- Other player movement (MSG_MOVE_* from server) ---- void handleOtherPlayerMovement(network::Packet& packet); + void handleMoveSetSpeed(network::Packet& packet); // ---- Phase 5 handlers ---- void handleLootResponse(network::Packet& packet); @@ -1764,6 +2529,10 @@ private: void clearPendingQuestAccept(uint32_t questId); void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason); bool hasQuestInLog(uint32_t questId) const; + std::string guidToUnitId(uint64_t guid) const; + Unit* getUnitByGuid(uint64_t guid); + std::string getQuestTitle(uint32_t questId) const; + const QuestLogEntry* findQuestLogEntry(uint32_t questId) const; int findQuestLogSlotIndexFromServer(uint32_t questId) const; void addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives); bool resyncQuestLogFromServerSlots(bool forceQueryMetadata); @@ -1815,10 +2584,12 @@ private: void handleInstanceDifficulty(network::Packet& packet); void handleArenaTeamCommandResult(network::Packet& packet); void handleArenaTeamQueryResponse(network::Packet& packet); + void handleArenaTeamRoster(network::Packet& packet); void handleArenaTeamInvite(network::Packet& packet); void handleArenaTeamEvent(network::Packet& packet); void handleArenaTeamStats(network::Packet& packet); void handleArenaError(network::Packet& packet); + void handlePvpLogData(network::Packet& packet); // ---- Bank handlers ---- void handleShowBank(network::Packet& packet); @@ -1861,7 +2632,9 @@ private: void handleLogoutResponse(network::Packet& packet); void handleLogoutComplete(network::Packet& packet); - void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource); + void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType = 0, + uint64_t srcGuid = 0, uint64_t dstGuid = 0); + bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId); void addSystemChatMessage(const std::string& message); /** @@ -1894,6 +2667,10 @@ private: float localOrientation); void clearTransportAttachment(uint64_t childGuid); + // Opcode dispatch table — built once in registerOpcodeHandlers(), called by handlePacket() + using PacketHandler = std::function; + std::unordered_map dispatchTable_; + // Opcode translation table (expansion-specific wire ↔ logical mapping) OpcodeTable opcodeTable_; @@ -1905,6 +2682,14 @@ private: // Network std::unique_ptr socket; + std::deque pendingIncomingPackets_; + struct PendingUpdateObjectWork { + UpdateObjectData data; + size_t nextBlockIndex = 0; + bool outOfRangeProcessed = false; + bool newItemCreated = false; + }; + std::deque pendingUpdateObjectWork_; // State WorldState state = WorldState::DISCONNECTED; @@ -1926,6 +2711,8 @@ private: std::chrono::steady_clock::time_point movementClockStart_ = std::chrono::steady_clock::now(); uint32_t lastMovementTimestampMs_ = 0; bool serverMovementAllowed_ = true; + uint32_t monsterMovePacketsThisTick_ = 0; + uint32_t monsterMovePacketsDroppedThisTick_ = 0; // Fall/jump tracking for movement packet correctness. // fallTime must be the elapsed ms since the FALLING flag was set; the server @@ -1944,12 +2731,19 @@ private: size_t maxChatHistory = 100; // Maximum chat messages to keep std::vector joinedChannels_; // Active channel memberships ChatBubbleCallback chatBubbleCallback_; + AddonChatCallback addonChatCallback_; + AddonEventCallback addonEventCallback_; + SpellIconPathResolver spellIconPathResolver_; + ItemIconPathResolver itemIconPathResolver_; + SpellDataResolver spellDataResolver_; + RandomPropertyNameResolver randomPropertyNameResolver_; EmoteAnimCallback emoteAnimCallback_; // Targeting uint64_t targetGuid = 0; uint64_t focusGuid = 0; // Focus target uint64_t lastTargetGuid = 0; // Previous target + uint64_t mouseoverGuid_ = 0; // Set each frame by nameplate renderer std::vector tabCycleList; int tabCycleIndex = -1; bool tabCycleStale = true; @@ -1960,6 +2754,15 @@ private: float pingInterval = 30.0f; // Ping interval (30 seconds) float timeSinceLastMoveHeartbeat_ = 0.0f; // Periodic movement heartbeat to keep server position synced float moveHeartbeatInterval_ = 0.5f; + uint32_t lastHeartbeatSendTimeMs_ = 0; + float lastHeartbeatX_ = 0.0f; + float lastHeartbeatY_ = 0.0f; + float lastHeartbeatZ_ = 0.0f; + uint32_t lastHeartbeatFlags_ = 0; + uint64_t lastHeartbeatTransportGuid_ = 0; + uint32_t lastNonHeartbeatMoveSendTimeMs_ = 0; + uint32_t lastFacingSendTimeMs_ = 0; + float lastFacingSentOrientation_ = 0.0f; uint32_t lastLatency = 0; // Last measured latency (milliseconds) std::chrono::steady_clock::time_point pingTimestamp_; // Time CMSG_PING was sent @@ -1968,10 +2771,14 @@ private: uint32_t currentMapId_ = 0; bool hasHomeBind_ = false; uint32_t homeBindMapId_ = 0; + uint32_t homeBindZoneId_ = 0; glm::vec3 homeBindPos_{0.0f}; // ---- Phase 1: Name caches ---- std::unordered_map playerNameCache; + // Class/race cache from SMSG_NAME_QUERY_RESPONSE (guid → {classId, raceId}) + struct PlayerClassRace { uint8_t classId = 0; uint8_t raceId = 0; }; + std::unordered_map playerClassRaceCache_; std::unordered_set pendingNameQueries; std::unordered_map creatureInfoCache; std::unordered_set pendingCreatureQueries; @@ -1995,7 +2802,8 @@ private: std::unordered_map ignoreCache; // name -> guid // ---- Logout state ---- - bool loggingOut_ = false; + bool loggingOut_ = false; + float logoutCountdown_ = 0.0f; // seconds remaining before server logs us out (0 = instant/done) // ---- Display state ---- bool helmVisible_ = true; @@ -2018,12 +2826,24 @@ private: uint32_t stackCount = 1; uint32_t curDurability = 0; uint32_t maxDurability = 0; + uint32_t permanentEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 0 (enchanting) + uint32_t temporaryEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 1 (sharpening stones, poisons) + std::array socketEnchantIds{}; // ITEM_ENCHANTMENT_SLOT 2-4 (gems) }; std::unordered_map onlineItems_; std::unordered_map itemInfoCache_; std::unordered_set pendingItemQueries_; + + // Deferred SMSG_ITEM_PUSH_RESULT notifications for items whose info wasn't + // cached at arrival time; emitted once the query response arrives. + struct PendingItemPushNotif { + uint32_t itemId = 0; + uint32_t count = 1; + }; + std::vector pendingItemPushNotifs_; std::array equipSlotGuids_{}; std::array backpackSlotGuids_{}; + std::array keyringSlotGuids_{}; // Container (bag) contents: containerGuid -> array of item GUIDs per slot struct ContainerInfo { uint32_t numSlots = 0; @@ -2057,6 +2877,7 @@ private: // ---- Phase 2: Combat ---- bool autoAttacking = false; bool autoAttackRequested_ = false; // local intent (CMSG_ATTACKSWING sent) + bool autoAttackRetryPending_ = false; // one-shot retry after local start or server stop uint64_t autoAttackTarget = 0; bool autoAttackOutOfRange_ = false; float autoAttackOutOfRangeTime_ = 0.0f; @@ -2064,13 +2885,26 @@ private: float autoAttackResendTimer_ = 0.0f; // Re-send CMSG_ATTACKSWING every ~1s while attacking float autoAttackFacingSyncTimer_ = 0.0f; // Periodic facing sync while meleeing std::unordered_set hostileAttackers_; + bool wasCombat_ = false; // Previous frame combat state for PLAYER_REGEN edge detection std::vector combatText; + static constexpr size_t MAX_COMBAT_LOG = 500; + struct RecentSpellstealLogEntry { + uint64_t casterGuid = 0; + uint64_t victimGuid = 0; + uint32_t spellId = 0; + std::chrono::steady_clock::time_point timestamp{}; + }; + static constexpr size_t MAX_RECENT_SPELLSTEAL_LOGS = 32; + std::deque combatLog_; + std::deque recentSpellstealLogs_; + std::deque areaTriggerMsgs_; // unitGuid → sorted threat list (descending by threat value) std::unordered_map> threatLists_; // ---- Phase 3: Spells ---- WorldEntryCallback worldEntryCallback_; KnockBackCallback knockBackCallback_; + CameraShakeCallback cameraShakeCallback_; UnstuckCallback unstuckCallback_; UnstuckCallback unstuckGyCallback_; UnstuckCallback unstuckHearthCallback_; @@ -2115,6 +2949,12 @@ private: bool castIsChannel = false; uint32_t currentCastSpellId = 0; float castTimeRemaining = 0.0f; + // Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes + uint32_t craftQueueSpellId_ = 0; + int craftQueueRemaining_ = 0; + // Spell queue: next spell to cast within the 400ms window before current cast ends + uint32_t queuedSpellId_ = 0; + uint64_t queuedSpellTarget_ = 0; // Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START) std::unordered_map unitCastStates_; uint64_t pendingGameObjectInteractGuid_ = 0; @@ -2123,6 +2963,7 @@ private: uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1) uint8_t unspentTalentPoints_[2] = {0, 0}; // Unspent points per spec std::unordered_map learnedTalents_[2]; // Learned talents per spec + std::array, 2> learnedGlyphs_{}; // Glyphs per spec std::unordered_map talentCache_; // talentId -> entry std::unordered_map talentTabCache_; // tabId -> entry bool talentDbcLoaded_ = false; @@ -2145,21 +2986,36 @@ private: float castTimeTotal = 0.0f; std::array actionBar{}; + std::unordered_map macros_; // client-side macro text (persisted in char config) std::vector playerAuras; std::vector targetAuras; + std::unordered_map> unitAurasCache_; // per-unit aura cache uint64_t petGuid_ = 0; uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots) uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss uint8_t petReact_ = 1; // 0=passive,1=defensive,2=aggressive + bool petRenameablePending_ = false; // set by SMSG_PET_RENAMEABLE, consumed by UI std::vector petSpellList_; // known pet spells std::unordered_set petAutocastSpells_; // spells with autocast on + // ---- Pet Stable ---- + bool stableWindowOpen_ = false; + uint64_t stableMasterGuid_ = 0; + uint8_t stableNumSlots_ = 0; + std::vector stabledPets_; + void handleListStabledPets(network::Packet& packet); + // ---- Battleground queue state ---- std::array bgQueues_{}; + // ---- Available battleground list (SMSG_BATTLEFIELD_LIST) ---- + std::vector availableBgs_; + void handleBattlefieldList(network::Packet& packet); + // Instance difficulty uint32_t instanceDifficulty_ = 0; bool instanceIsHeroic_ = false; + bool inInstance_ = false; // Raid target markers (icon 0-7 -> guid; 0 = empty slot) std::array raidTargetGuids_ = {}; @@ -2167,6 +3023,8 @@ private: // Mirror timers (0=fatigue, 1=breath, 2=feigndeath) MirrorTimer mirrorTimers_[3]; + // Shapeshift form (from UNIT_FIELD_BYTES_1 byte 3) + uint8_t shapeshiftFormId_ = 0; // Combo points (rogues/druids) uint8_t comboPoints_ = 0; uint64_t comboTarget_ = 0; @@ -2175,7 +3033,15 @@ private: std::vector instanceLockouts_; // Arena team stats (indexed by team slot, updated by SMSG_ARENA_TEAM_STATS) - std::vector arenaTeamStats_; + std::vector arenaTeamStats_; + // Arena team rosters (updated by SMSG_ARENA_TEAM_ROSTER) + std::vector arenaTeamRosters_; + + // BG scoreboard (MSG_PVP_LOG_DATA) + BgScoreboardData bgScoreboard_; + + // BG flag carrier / player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS) + std::vector bgPlayerPositions_; // Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT) std::array encounterUnitGuids_ = {}; // 0 = empty slot @@ -2190,19 +3056,26 @@ private: uint32_t lfgBootTotal_ = 0; // total votes cast uint32_t lfgBootTimeLeft_ = 0; // seconds remaining uint32_t lfgBootNeeded_ = 0; // votes needed to kick + std::string lfgBootTargetName_; // name of player being voted on + std::string lfgBootReason_; // reason given for kick // Ready check state bool pendingReadyCheck_ = false; uint32_t readyCheckReadyCount_ = 0; uint32_t readyCheckNotReadyCount_ = 0; std::string readyCheckInitiator_; + std::vector readyCheckResults_; // per-player status live during check // Faction standings (factionId → absolute standing value) std::unordered_map factionStandings_; // Faction name cache (factionId → name), populated lazily from Faction.dbc - std::unordered_map factionNameCache_; - bool factionNameCacheLoaded_ = false; - void loadFactionNameCache(); + mutable std::unordered_map factionNameCache_; + // repListId → factionId mapping (populated with factionNameCache) + mutable std::unordered_map factionRepListToId_; + // factionId → repListId reverse mapping + mutable std::unordered_map factionIdToRepList_; + mutable bool factionNameCacheLoaded_ = false; + void loadFactionNameCache() const; std::string getFactionName(uint32_t factionId) const; // ---- Phase 4: Group ---- @@ -2229,6 +3102,10 @@ private: uint32_t totalTimePlayed_ = 0; uint32_t levelTimePlayed_ = 0; + // Who results (last SMSG_WHO response) + std::vector whoResults_; + uint32_t whoOnlineCount_ = 0; + // Trade state TradeStatus tradeStatus_ = TradeStatus::None; uint64_t tradePeerGuid_= 0; @@ -2238,11 +3115,16 @@ private: uint64_t myTradeGold_ = 0; uint64_t peerTradeGold_ = 0; + // Shaman totem state + TotemSlot activeTotemSlots_[NUM_TOTEM_SLOTS]; + // Duel state bool pendingDuelRequest_ = false; uint64_t duelChallengerGuid_= 0; uint64_t duelFlagGuid_ = 0; std::string duelChallengerName_; + uint32_t duelCountdownMs_ = 0; // 0 = no active countdown + std::chrono::steady_clock::time_point duelCountdownStartedAt_{}; // ---- Guild state ---- std::string guildName_; @@ -2251,20 +3133,29 @@ private: GuildInfoData guildInfoData_; GuildQueryResponseData guildQueryData_; bool hasGuildRoster_ = false; + std::unordered_map guildNameCache_; // guildId → guild name + std::unordered_set pendingGuildNameQueries_; // in-flight guild queries bool pendingGuildInvite_ = false; std::string pendingGuildInviterName_; std::string pendingGuildInviteGuildName_; bool showPetitionDialog_ = false; uint32_t petitionCost_ = 0; uint64_t petitionNpcGuid_ = 0; + PetitionInfo petitionInfo_; uint64_t activeCharacterGuid_ = 0; Race playerRace_ = Race::HUMAN; + // Barber shop + bool barberShopOpen_ = false; + // ---- Phase 5: Loot ---- bool lootWindowOpen = false; bool autoLoot_ = false; + bool autoSellGrey_ = false; + bool autoRepair_ = false; LootResponseData currentLoot; + std::vector masterLootCandidates_; // from SMSG_LOOT_MASTER_LIST // Group loot roll state bool pendingLootRollActive_ = false; @@ -2272,6 +3163,7 @@ private: struct LocalLootState { LootResponseData data; bool moneyTaken = false; + bool itemAutoLootSent = false; }; std::unordered_map localLootState_; struct PendingLootRetry { @@ -2286,14 +3178,32 @@ private: float timer = 0.0f; }; std::vector pendingGameObjectLootOpens_; + // Tracks the last GO we sent CMSG_GAMEOBJ_USE to; used in handleSpellGo + // to send CMSG_LOOT after a gather cast (mining/herbalism) completes. + uint64_t lastInteractedGoGuid_ = 0; uint64_t pendingLootMoneyGuid_ = 0; uint32_t pendingLootMoneyAmount_ = 0; float pendingLootMoneyNotifyTimer_ = 0.0f; std::unordered_map recentLootMoneyAnnounceCooldowns_; uint64_t playerMoneyCopper_ = 0; + uint32_t playerHonorPoints_ = 0; + uint32_t playerArenaPoints_ = 0; int32_t playerArmorRating_ = 0; + int32_t playerResistances_[6] = {}; // [0]=Holy,[1]=Fire,[2]=Nature,[3]=Frost,[4]=Shadow,[5]=Arcane // Server-authoritative primary stats: [0]=STR [1]=AGI [2]=STA [3]=INT [4]=SPI; -1 = not received yet int32_t playerStats_[5] = {-1, -1, -1, -1, -1}; + // WotLK secondary combat stats (-1 = not yet received) + int32_t playerMeleeAP_ = -1; + int32_t playerRangedAP_ = -1; + int32_t playerSpellDmgBonus_[7] = {-1,-1,-1,-1,-1,-1,-1}; // per school 0-6 + int32_t playerHealBonus_ = -1; + float playerDodgePct_ = -1.0f; + float playerParryPct_ = -1.0f; + float playerBlockPct_ = -1.0f; + float playerCritPct_ = -1.0f; + float playerRangedCritPct_ = -1.0f; + float playerSpellCritPct_[7] = {-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f}; + int32_t playerCombatRatings_[25] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}; // Some servers/custom clients shift update field indices. We can auto-detect coinage by correlating // money-notify deltas with update-field diffs and then overriding UF::PLAYER_FIELD_COINAGE at runtime. uint32_t pendingMoneyDelta_ = 0; @@ -2324,6 +3234,7 @@ private: // Quest log std::vector questLog_; + int selectedQuestLogIndex_ = 0; std::unordered_set pendingQuestQueryIds_; std::unordered_set trackedQuestIds_; bool pendingLoginQuestResync_ = false; @@ -2339,6 +3250,9 @@ private: return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown } + // Vehicle (WotLK): non-zero when player is seated in a vehicle + uint32_t vehicleId_ = 0; + // Taxi / Flight Paths std::unordered_map taxiNpcHasRoutes_; // guid -> has new/available routes std::unordered_map taxiNodes_; @@ -2349,6 +3263,7 @@ private: ShowTaxiNodesData currentTaxiData_; uint64_t taxiNpcGuid_ = 0; bool onTaxiFlight_ = false; + std::string taxiDestName_; bool taxiMountActive_ = false; uint32_t taxiMountDisplayId_ = 0; bool taxiActivatePending_ = false; @@ -2434,28 +3349,63 @@ private: // Trainer bool trainerWindowOpen_ = false; TrainerListData currentTrainerList_; - struct SpellNameEntry { std::string name; std::string rank; uint32_t schoolMask = 0; }; - std::unordered_map spellNameCache_; - bool spellNameCacheLoaded_ = false; + struct SpellNameEntry { + std::string name; std::string rank; std::string description; + uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 0; + int32_t effectBasePoints[3] = {0, 0, 0}; + float durationSec = 0.0f; // resolved from DurationIndex → SpellDuration.dbc + }; + mutable std::unordered_map spellNameCache_; + mutable bool spellNameCacheLoaded_ = false; - // Achievement name cache (lazy-loaded from Achievement.dbc on first earned event) + // Title cache: maps titleBit → title string (lazy-loaded from CharTitles.dbc) + // The strings use "%s" as a player-name placeholder (e.g. "Commander %s", "%s the Explorer"). + mutable std::unordered_map titleNameCache_; + mutable bool titleNameCacheLoaded_ = false; + void loadTitleNameCache() const; + // Set of title bit-indices known to the player (from SMSG_TITLE_EARNED). + std::unordered_set knownTitleBits_; + // Currently selected title bit, or -1 for no title. Updated from PLAYER_CHOSEN_TITLE. + int32_t chosenTitleBit_ = -1; + + // Achievement caches (lazy-loaded from Achievement.dbc on first earned event) std::unordered_map achievementNameCache_; + std::unordered_map achievementDescCache_; + std::unordered_map achievementPointsCache_; bool achievementNameCacheLoaded_ = false; void loadAchievementNameCache(); // Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA) std::unordered_set earnedAchievements_; + // Earn dates: achievementId → WoW PackedTime (from SMSG_ACHIEVEMENT_EARNED / SMSG_ALL_ACHIEVEMENT_DATA) + std::unordered_map achievementDates_; // Criteria progress: criteriaId → current value (from SMSG_CRITERIA_UPDATE) std::unordered_map criteriaProgress_; void handleAllAchievementData(network::Packet& packet); + // Per-player achievement data from SMSG_RESPOND_INSPECT_ACHIEVEMENTS + // Key: inspected player's GUID; value: set of earned achievement IDs + std::unordered_map> inspectedPlayerAchievements_; + void handleRespondInspectAchievements(network::Packet& packet); + // Area name cache (lazy-loaded from WorldMapArea.dbc; maps AreaTable ID → display name) - std::unordered_map areaNameCache_; - bool areaNameCacheLoaded_ = false; - void loadAreaNameCache(); + mutable std::unordered_map areaNameCache_; + mutable bool areaNameCacheLoaded_ = false; + void loadAreaNameCache() const; std::string getAreaName(uint32_t areaId) const; + + // Map name cache (lazy-loaded from Map.dbc; maps mapId → localized display name) + mutable std::unordered_map mapNameCache_; + mutable bool mapNameCacheLoaded_ = false; + void loadMapNameCache() const; + + // LFG dungeon name cache (lazy-loaded from LFGDungeons.dbc; WotLK only) + mutable std::unordered_map lfgDungeonNameCache_; + mutable bool lfgDungeonNameCacheLoaded_ = false; + void loadLfgDungeonDbc() const; + std::string getLfgDungeonName(uint32_t dungeonId) const; std::vector trainerTabs_; void handleTrainerList(network::Packet& packet); - void loadSpellNameCache(); + void loadSpellNameCache() const; void categorizeTrainerSpells(); // Callbacks @@ -2505,6 +3455,14 @@ private: uint8_t wardenCheckOpcodes_[9] = {}; bool loadWardenCRFile(const std::string& moduleHashHex); + // Async Warden response: avoids 5-second main-loop stalls from PAGE_A/PAGE_B code pattern searches + std::future> wardenPendingEncrypted_; // encrypted response bytes + bool wardenResponsePending_ = false; + + // ---- RX silence detection ---- + std::chrono::steady_clock::time_point lastRxTime_{}; + bool rxSilenceLogged_ = false; + // ---- XP tracking ---- uint32_t playerXp_ = 0; uint32_t playerNextLevelXp_ = 0; @@ -2518,6 +3476,10 @@ private: float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour) void handleLoginSetTimeSpeed(network::Packet& packet); + // ---- Global Cooldown (GCD) ---- + float gcdTotal_ = 0.0f; + std::chrono::steady_clock::time_point gcdStartedAt_{}; + // ---- Weather state (SMSG_WEATHER) ---- uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm float weatherIntensity_ = 0.0f; // 0.0 to 1.0 @@ -2531,6 +3493,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; @@ -2550,9 +3514,12 @@ private: NpcAggroCallback npcAggroCallback_; NpcRespawnCallback npcRespawnCallback_; StandStateCallback standStateCallback_; + AppearanceChangedCallback appearanceChangedCallback_; GhostStateCallback ghostStateCallback_; MeleeSwingCallback meleeSwingCallback_; + uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing SpellCastAnimCallback spellCastAnimCallback_; + SpellCastFailedCallback spellCastFailedCallback_; UnitAnimHintCallback unitAnimHintCallback_; UnitMoveFlagsCallback unitMoveFlagsCallback_; NpcSwingCallback npcSwingCallback_; @@ -2561,12 +3528,18 @@ private: NpcVendorCallback npcVendorCallback_; ChargeCallback chargeCallback_; LevelUpCallback levelUpCallback_; + LevelUpDeltas lastLevelUpDeltas_; + std::vector tempEnchantTimers_; + std::vector bookPages_; // pages collected for the current readable item OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_; AchievementEarnedCallback achievementEarnedCallback_; + AreaDiscoveryCallback areaDiscoveryCallback_; + QuestProgressCallback questProgressCallback_; MountCallback mountCallback_; TaxiPrecacheCallback taxiPrecacheCallback_; TaxiOrientationCallback taxiOrientationCallback_; TaxiFlightStartCallback taxiFlightStartCallback_; + OpenLfgCallback openLfgCallback_; uint32_t currentMountDisplayId_ = 0; uint32_t mountAuraSpellId_ = 0; // Spell ID of the aura that caused mounting (for CMSG_CANCEL_AURA fallback) float serverRunSpeed_ = 7.0f; @@ -2582,6 +3555,10 @@ private: bool releasedSpirit_ = false; uint32_t corpseMapId_ = 0; float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f; + uint64_t corpseGuid_ = 0; + // Absolute time (ms since epoch) when PvP corpse-reclaim delay expires. + // 0 means no active delay (reclaim allowed immediately upon proximity). + uint64_t corpseReclaimAvailableMs_ = 0; // Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially std::array playerRunes_ = [] { std::array r{}; @@ -2593,10 +3570,15 @@ private: uint64_t pendingSpiritHealerGuid_ = 0; bool resurrectPending_ = false; bool resurrectRequestPending_ = false; + bool selfResAvailable_ = false; // SMSG_PRE_RESURRECT received — Reincarnation/Twisting Nether // ---- Talent wipe confirm dialog ---- bool talentWipePending_ = false; uint64_t talentWipeNpcGuid_ = 0; uint32_t talentWipeCost_ = 0; + // ---- Pet talent respec confirm dialog ---- + bool petUnlearnPending_ = false; + uint64_t petUnlearnGuid_ = 0; + uint32_t petUnlearnCost_ = 0; bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST uint64_t resurrectCasterGuid_ = 0; std::string resurrectCasterName_; @@ -2616,6 +3598,9 @@ private: std::array itemGuids{}; }; std::vector equipmentSets_; + std::string pendingSaveSetName_; // Saved between CMSG_EQUIPMENT_SET_SAVE and SMSG_EQUIPMENT_SET_SAVED + std::string pendingSaveSetIcon_; + std::vector equipmentSetInfo_; // public-facing copy // ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ---- std::unordered_map forcedReactions_; // factionId -> reaction tier @@ -2630,6 +3615,35 @@ private: // ---- Reputation change callback ---- RepChangeCallback repChangeCallback_; + uint32_t watchedFactionId_ = 0; // auto-set to most recently changed faction + + // ---- PvP honor credit callback ---- + PvpHonorCallback pvpHonorCallback_; + + // ---- Item loot callback ---- + ItemLootCallback itemLootCallback_; + + // ---- Quest completion callback ---- + QuestCompleteCallback questCompleteCallback_; + + // ---- GM Ticket state (SMSG_GMTICKET_GETTICKET / SMSG_GMTICKET_SYSTEMSTATUS) ---- + bool gmTicketActive_ = false; ///< True when an open ticket exists on the server + std::string gmTicketText_; ///< Text of the open ticket (from SMSG_GMTICKET_GETTICKET) + float gmTicketWaitHours_ = 0.0f; ///< Server-estimated wait time in hours + bool gmSupportAvailable_ = true; ///< GM support system online (SMSG_GMTICKET_SYSTEMSTATUS) + + // ---- Battlefield Manager state (WotLK Wintergrasp / outdoor battlefields) ---- + bool bfMgrInvitePending_ = false; ///< True when an entry/queue invite is pending acceptance + bool bfMgrActive_ = false; ///< True while the player is inside an outdoor battlefield + uint32_t bfMgrZoneId_ = 0; ///< Zone ID of the pending/active battlefield + + // ---- WotLK Calendar: pending invite counter ---- + uint32_t calendarPendingInvites_ = 0; ///< Unacknowledged calendar invites (SMSG_CALENDAR_SEND_NUM_PENDING) + + // ---- Spell modifiers (SMSG_SET_FLAT_SPELL_MODIFIER / SMSG_SET_PCT_SPELL_MODIFIER) ---- + // Keyed by (SpellModOp, groupIndex); cleared on logout/character change. + std::unordered_map spellFlatMods_; + std::unordered_map spellPctMods_; }; } // namespace game diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index 7a3bcb8c..6ae07a2d 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -15,6 +15,8 @@ enum class ItemQuality : uint8_t { RARE = 3, // Blue EPIC = 4, // Purple LEGENDARY = 5, // Orange + ARTIFACT = 6, // Yellow (unused in 3.3.5a but valid quality value) + HEIRLOOM = 7, // Yellow/gold (WotLK bind-on-account heirlooms) }; enum class EquipSlot : uint8_t { @@ -67,6 +69,7 @@ struct ItemSlot { class Inventory { public: static constexpr int BACKPACK_SLOTS = 16; + static constexpr int KEYRING_SLOTS = 32; static constexpr int NUM_EQUIP_SLOTS = 23; static constexpr int NUM_BAG_SLOTS = 4; static constexpr int MAX_BAG_SIZE = 36; @@ -86,6 +89,12 @@ public: bool setEquipSlot(EquipSlot slot, const ItemDef& item); bool clearEquipSlot(EquipSlot slot); + // Keyring + const ItemSlot& getKeyringSlot(int index) const; + bool setKeyringSlot(int index, const ItemDef& item); + bool clearKeyringSlot(int index); + int getKeyringSize() const { return KEYRING_SLOTS; } + // Extra bags int getBagSize(int bagIndex) const; void setBagSize(int bagIndex, int size); @@ -116,11 +125,28 @@ public: int findFreeBackpackSlot() const; bool addItem(const ItemDef& item); + // Sort all bag slots (backpack + equip bags) by quality desc → itemId asc → stackCount desc. + // Purely client-side: reorders the local inventory struct without server interaction. + void sortBags(); + + // A single swap operation using WoW bag/slot addressing (for CMSG_SWAP_ITEM). + struct SwapOp { + uint8_t srcBag; + uint8_t srcSlot; + uint8_t dstBag; + uint8_t dstSlot; + }; + + // Compute the CMSG_SWAP_ITEM operations needed to reach sorted order. + // Does NOT modify the inventory — caller is responsible for sending packets. + std::vector computeSortSwaps() const; + // Test data void populateTestItems(); private: std::array backpack{}; + std::array keyring_{}; std::array equipment{}; struct BagData { diff --git a/include/game/opcode_aliases_generated.inc b/include/game/opcode_aliases_generated.inc index ad488110..ed20d098 100644 --- a/include/game/opcode_aliases_generated.inc +++ b/include/game/opcode_aliases_generated.inc @@ -41,5 +41,4 @@ {"SMSG_SPLINE_MOVE_SET_RUN_BACK_SPEED", "SMSG_SPLINE_SET_RUN_BACK_SPEED"}, {"SMSG_SPLINE_MOVE_SET_RUN_SPEED", "SMSG_SPLINE_SET_RUN_SPEED"}, {"SMSG_SPLINE_MOVE_SET_SWIM_SPEED", "SMSG_SPLINE_SET_SWIM_SPEED"}, - {"SMSG_UPDATE_AURA_DURATION", "SMSG_EQUIPMENT_SET_SAVED"}, {"SMSG_VICTIMSTATEUPDATE_OBSOLETE", "SMSG_BATTLEFIELD_PORT_DENIED"}, diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 91242206..966542b9 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -33,7 +33,10 @@ class OpcodeTable { public: /** * Load opcode mappings from a JSON file. - * Format: { "CMSG_PING": "0x1DC", "SMSG_AUTH_CHALLENGE": "0x1EC", ... } + * Format: + * { "CMSG_PING": "0x1DC", "SMSG_AUTH_CHALLENGE": "0x1EC", ... } + * or a delta file with: + * { "_extends": "../classic/opcodes.json", "_remove": ["MSG_FOO"], ...overrides } */ bool loadFromJson(const std::string& path); @@ -49,12 +52,14 @@ public: /** Number of mapped opcodes. */ size_t size() const { return logicalToWire_.size(); } + /** Get canonical enum name for a logical opcode. */ + static const char* logicalToName(LogicalOpcode op); + private: std::unordered_map logicalToWire_; // LogicalOpcode → wire std::unordered_map wireToLogical_; // wire → LogicalOpcode static std::optional nameToLogical(const std::string& name); - static const char* logicalToName(LogicalOpcode op); }; /** diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 40655045..261cae66 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -26,6 +26,10 @@ public: // Classic: none, TBC: u8, WotLK: u16. virtual uint8_t movementFlags2Size() const { return 2; } + // Wire-format movement flag that gates transport data in MSG_MOVE_* payloads. + // WotLK/TBC: 0x200, Classic/Turtle: 0x02000000. + virtual uint32_t wireOnTransportFlag() const { return 0x00000200; } + // --- Movement --- /** Parse movement block from SMSG_UPDATE_OBJECT */ @@ -266,8 +270,8 @@ public: virtual bool parseMailList(network::Packet& packet, std::vector& inbox); /** Build CMSG_MAIL_TAKE_ITEM */ - virtual network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) { - return MailTakeItemPacket::build(mailboxGuid, mailId, itemSlot); + virtual network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) { + return MailTakeItemPacket::build(mailboxGuid, mailId, itemGuidLow); } /** Build CMSG_MAIL_DELETE */ @@ -279,12 +283,12 @@ public: /** Read a packed GUID from the packet */ virtual uint64_t readPackedGuid(network::Packet& packet) { - return UpdateObjectParser::readPackedGuid(packet); + return packet.readPackedGuid(); } /** Write a packed GUID to the packet */ virtual void writePackedGuid(network::Packet& packet, uint64_t guid) { - MovementPacket::writePackedGuid(packet, guid); + packet.writePackedGuid(guid); } }; @@ -361,6 +365,20 @@ public: // TBC/Classic SMSG_QUESTGIVER_QUEST_DETAILS lacks informUnit(u64), flags(u32), // isFinished(u8) that WotLK added; uses variable item counts + emote section. bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override; + // TBC 2.4.3 SMSG_GUILD_ROSTER: same rank structure as WotLK (variable rankCount + + // goldLimit + bank tabs), but NO gender byte per member (WotLK added it) + bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override; + // TBC 2.4.3 SMSG_QUESTGIVER_STATUS: uint32 status (WotLK uses uint8) + uint8_t readQuestGiverStatus(network::Packet& packet) override; + // TBC 2.4.3 SMSG_MESSAGECHAT: no senderGuid/unknown prefix before type-specific data + bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; + // TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE: 2 extra strings after names + // (iconName + castBarCaption); WotLK has 3 (adds unk1) + bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override; + // TBC 2.4.3 CMSG_JOIN_CHANNEL: name+password only (WotLK prepends channelId+hasVoice+joinedByZone) + network::Packet buildJoinChannel(const std::string& channelName, const std::string& password) override; + // TBC 2.4.3 CMSG_LEAVE_CHANNEL: name only (WotLK prepends channelId) + network::Packet buildLeaveChannel(const std::string& channelName) override; }; /** @@ -380,6 +398,7 @@ public: class ClassicPacketParsers : public TbcPacketParsers { public: uint8_t movementFlags2Size() const override { return 0; } + uint32_t wireOnTransportFlag() const override { return 0x02000000; } bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override; bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override; void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override; @@ -404,7 +423,7 @@ public: uint32_t money, uint32_t cod, const std::vector& itemGuids = {}) override; bool parseMailList(network::Packet& packet, std::vector& inbox) override; - network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) override; + network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) override; network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override; network::Packet buildItemQuery(uint32_t entry, uint64_t guid) override; bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override; @@ -439,18 +458,21 @@ public: }; /** - * Turtle WoW (build 7234) packet parsers. + * Turtle WoW packet parsers. * - * Turtle WoW is a heavily modified vanilla server that sends TBC-style - * movement blocks (moveFlags2, transport timestamps, 8 speeds including flight) - * while keeping all other Classic packet formats. + * Turtle is Classic-based but not wire-identical to vanilla MaNGOS. It keeps + * most Classic packet formats, while overriding the movement-bearing paths that + * have proven to vary in live traffic: + * - update-object movement blocks use a Turtle-specific hybrid layout + * - update-object parsing falls back through Classic/TBC/WotLK movement layouts + * - monster-move parsing falls back through Vanilla, TBC, and guarded WotLK layouts * - * Inherits all Classic overrides (charEnum, chat, gossip, mail, items, etc.) - * but delegates movement block parsing to TBC format. + * Everything else inherits the Classic parser behavior. */ class TurtlePacketParsers : public ClassicPacketParsers { public: uint8_t movementFlags2Size() const override { return 0; } + bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) override; bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override; bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override; }; diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index c4d70380..c6fe3663 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -50,19 +51,38 @@ struct ActionBarSlot { struct CombatTextEntry { enum Type : uint8_t { MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, - CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, - ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST + EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, + ENERGIZE, POWER_DRAIN, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER, + DISPEL, STEAL, INTERRUPT, INSTAKILL, HONOR_GAIN, GLANCING, CRUSHING }; Type type; int32_t amount = 0; uint32_t spellId = 0; float age = 0.0f; // Seconds since creation (for fadeout) bool isPlayerSource = false; // True if player dealt this + uint8_t powerType = 0; // For ENERGIZE/POWER_DRAIN: 0=mana,1=rage,2=focus,3=energy,6=runicpower + uint64_t srcGuid = 0; // Source entity (attacker/caster) + uint64_t dstGuid = 0; // Destination entity (victim/target) — used for world-space positioning + float xSeed = 0.0f; // Random horizontal offset seed (-1..1) to stagger overlapping text static constexpr float LIFETIME = 2.5f; bool isExpired() const { return age >= LIFETIME; } }; +/** + * Persistent combat log entry (stored in a rolling deque, survives beyond floating-text lifetime) + */ +struct CombatLogEntry { + CombatTextEntry::Type type = CombatTextEntry::MELEE_DAMAGE; + int32_t amount = 0; + uint32_t spellId = 0; + bool isPlayerSource = false; + uint8_t powerType = 0; // For ENERGIZE/DRAIN: power type; for ENVIRONMENTAL: env damage type + time_t timestamp = 0; // Wall-clock time (std::time(nullptr)) + std::string sourceName; // Resolved display name of attacker/caster + std::string targetName; // Resolved display name of victim/target +}; + /** * Spell cooldown entry received from server */ diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 07c735fd..d48065e4 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -20,6 +20,7 @@ enum class UF : uint16_t { UNIT_FIELD_TARGET_LO, UNIT_FIELD_TARGET_HI, UNIT_FIELD_BYTES_0, + UNIT_FIELD_BYTES_1, // byte3 = shapeshift form ID UNIT_FIELD_HEALTH, UNIT_FIELD_POWER1, UNIT_FIELD_MAXHEALTH, @@ -31,6 +32,7 @@ enum class UF : uint16_t { UNIT_FIELD_DISPLAYID, UNIT_FIELD_MOUNTDISPLAYID, UNIT_FIELD_AURAS, // Start of aura spell ID array (48 consecutive uint32 slots, classic/vanilla only) + UNIT_FIELD_AURAFLAGS, // Aura flags packed 4-per-uint32 (12 uint32 slots); 0x01=cancelable,0x02=harmful,0x04=helpful UNIT_NPC_FLAGS, UNIT_DYNAMIC_FLAGS, UNIT_FIELD_RESISTANCES, // Physical armor (index 0 of the resistance array) @@ -41,6 +43,10 @@ enum class UF : uint16_t { UNIT_FIELD_STAT4, // Spirit UNIT_END, + // Unit combat fields (WotLK: PRIVATE+OWNER — only visible for the player character) + UNIT_FIELD_ATTACK_POWER, // Melee attack power (int32) + UNIT_FIELD_RANGED_ATTACK_POWER, // Ranged attack power (int32) + // Player fields PLAYER_FLAGS, PLAYER_BYTES, @@ -52,10 +58,29 @@ enum class UF : uint16_t { PLAYER_QUEST_LOG_START, PLAYER_FIELD_INV_SLOT_HEAD, PLAYER_FIELD_PACK_SLOT_1, + PLAYER_FIELD_KEYRING_SLOT_1, PLAYER_FIELD_BANK_SLOT_1, PLAYER_FIELD_BANKBAG_SLOT_1, PLAYER_SKILL_INFO_START, PLAYER_EXPLORED_ZONES_START, + PLAYER_CHOSEN_TITLE, // Active title index (-1 = no title) + + // Player spell power / healing bonus (WotLK: PRIVATE — int32 per school) + PLAYER_FIELD_MOD_DAMAGE_DONE_POS, // Spell damage bonus (first of 7 schools) + PLAYER_FIELD_MOD_HEALING_DONE_POS, // Healing bonus + + // Player combat stats (WotLK: PRIVATE — float values) + PLAYER_BLOCK_PERCENTAGE, // Block chance % + PLAYER_DODGE_PERCENTAGE, // Dodge chance % + PLAYER_PARRY_PERCENTAGE, // Parry chance % + PLAYER_CRIT_PERCENTAGE, // Melee crit chance % + PLAYER_RANGED_CRIT_PERCENTAGE, // Ranged crit chance % + PLAYER_SPELL_CRIT_PERCENTAGE1, // Spell crit chance % (first school; 7 consecutive float fields) + PLAYER_FIELD_COMBAT_RATING_1, // First of 25 int32 combat rating slots (CR_* indices) + + // Player PvP currency (TBC/WotLK only — Classic uses the old weekly honor system) + PLAYER_FIELD_HONOR_CURRENCY, // Accumulated honor points (uint32) + PLAYER_FIELD_ARENA_CURRENCY, // Accumulated arena points (uint32) // GameObject fields GAMEOBJECT_DISPLAYID, diff --git a/include/game/warden_emulator.hpp b/include/game/warden_emulator.hpp index 320afd0d..30a0759f 100644 --- a/include/game/warden_emulator.hpp +++ b/include/game/warden_emulator.hpp @@ -147,11 +147,21 @@ private: uint32_t heapSize_; // Heap size uint32_t apiStubBase_; // API stub base address - // API hooks: DLL name -> Function name -> Handler + // API hooks: DLL name -> Function name -> stub address std::map> apiAddresses_; + // API stub dispatch: stub address -> {argCount, handler} + struct ApiHookEntry { + int argCount; + std::function&)> handler; + }; + std::map apiHandlers_; + uint32_t nextApiStubAddr_; // tracks next free stub slot (replaces static local) + bool apiCodeHookRegistered_; // true once UC_HOOK_CODE for stub range is added + // Memory allocation tracking std::map allocations_; + std::map freeBlocks_; // free-list keyed by base address uint32_t nextHeapAddr_; // Hook handles for cleanup diff --git a/include/game/warden_memory.hpp b/include/game/warden_memory.hpp index 335ad7a9..39a2abf2 100644 --- a/include/game/warden_memory.hpp +++ b/include/game/warden_memory.hpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace wowee { namespace game { @@ -18,8 +19,9 @@ public: ~WardenMemory(); /** Search standard candidate dirs for WoW.exe and load it. - * @param build Client build number (e.g. 5875 for Classic 1.12.1) to select the right exe. */ - bool load(uint16_t build = 0); + * @param build Client build number (e.g. 5875 for Classic 1.12.1) to select the right exe. + * @param isTurtle If true, prefer the Turtle WoW custom exe (different code bytes). */ + bool load(uint16_t build = 0, bool isTurtle = false); /** Load PE image from a specific file path. */ bool loadFromFile(const std::string& exePath); @@ -32,6 +34,23 @@ public: bool isLoaded() const { return loaded_; } + /** + * Search PE image for a byte pattern matching HMAC-SHA1(seed, pattern). + * Used for FIND_MEM_IMAGE_CODE_BY_HASH and FIND_CODE_BY_HASH scans. + * @param seed 4-byte HMAC key + * @param expectedHash 20-byte expected HMAC-SHA1 digest + * @param patternLen Length of the pattern to search for + * @param imageOnly If true, search only executable sections (.text) + * @param hintOffset RVA hint from PAGE_A request — check this position first + * @return true if a matching pattern was found in the PE image + */ + bool searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], + uint8_t patternLen, bool imageOnly, + uint32_t hintOffset = 0, bool hintOnly = false) const; + + /** Write a little-endian uint32 at the given virtual address in the PE image. */ + void writeLE32(uint32_t va, uint32_t value); + private: bool loaded_ = false; uint32_t imageBase_ = 0; @@ -46,9 +65,15 @@ private: bool parsePE(const std::vector& fileData); void initKuserSharedData(); void patchRuntimeGlobals(); - void writeLE32(uint32_t va, uint32_t value); + void patchTurtleWowBinary(); + void verifyWardenScanEntries(); + bool isTurtle_ = false; std::string findWowExe(uint16_t build) const; - static uint32_t expectedImageSizeForBuild(uint16_t build); + static uint32_t expectedImageSizeForBuild(uint16_t build, bool isTurtle); + + // Cache for searchCodePattern results to avoid repeated 5-second brute-force searches. + // Key: hex string of seed(4)+hash(20)+patLen(1)+imageOnly(1) = 26 bytes. + mutable std::unordered_map codePatternCache_; }; } // namespace game diff --git a/include/game/warden_module.hpp b/include/game/warden_module.hpp index bcea989f..e11bc4f9 100644 --- a/include/game/warden_module.hpp +++ b/include/game/warden_module.hpp @@ -140,6 +140,7 @@ private: size_t relocDataOffset_ = 0; // Offset into decompressedData_ where relocation data starts WardenFuncList funcList_; // Callback functions std::unique_ptr emulator_; // Cross-platform x86 emulator + uint32_t emulatedPacketHandlerAddr_ = 0; // Raw emulated VA for 4-arg PacketHandler call // Validation and loading steps bool verifyMD5(const std::vector& data, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 6e5721fd..e315b213 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, }; /** @@ -448,7 +449,6 @@ struct MovementInfo { */ class MovementPacket { public: - static void writePackedGuid(network::Packet& packet, uint64_t guid); static void writeMovementPayload(network::Packet& packet, const MovementInfo& info); /** @@ -525,14 +525,6 @@ public: */ static bool parse(network::Packet& packet, UpdateObjectData& data); - /** - * Read packed GUID from packet - * - * @param packet Packet to read from - * @return GUID value - */ - static uint64_t readPackedGuid(network::Packet& packet); - /** * Parse a single update block * @@ -697,6 +689,7 @@ public: * Get human-readable string for chat type */ const char* getChatTypeString(ChatType type); +const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass); // ============================================================ // Text Emotes @@ -756,38 +749,40 @@ public: * Channel notification types */ enum class ChannelNotifyType : uint8_t { - YOU_JOINED = 0x00, - YOU_LEFT = 0x01, - WRONG_PASSWORD = 0x02, - NOT_MEMBER = 0x03, - NOT_MODERATOR = 0x04, - PASSWORD_CHANGED = 0x05, - OWNER_CHANGED = 0x06, - PLAYER_NOT_FOUND = 0x07, - NOT_OWNER = 0x08, - CHANNEL_OWNER = 0x09, - MODE_CHANGE = 0x0A, - ANNOUNCEMENTS_ON = 0x0B, - ANNOUNCEMENTS_OFF = 0x0C, - MODERATION_ON = 0x0D, - MODERATION_OFF = 0x0E, - MUTED = 0x0F, - PLAYER_KICKED = 0x10, - BANNED = 0x11, - PLAYER_BANNED = 0x12, - PLAYER_UNBANNED = 0x13, - PLAYER_NOT_BANNED = 0x14, - PLAYER_ALREADY_MEMBER = 0x15, - INVITE = 0x16, - INVITE_WRONG_FACTION = 0x17, - WRONG_FACTION = 0x18, - INVALID_NAME = 0x19, - NOT_MODERATED = 0x1A, - PLAYER_INVITED = 0x1B, - PLAYER_INVITE_BANNED = 0x1C, - THROTTLED = 0x1D, - NOT_IN_AREA = 0x1E, - NOT_IN_LFG = 0x1F, + PLAYER_JOINED = 0x00, + PLAYER_LEFT = 0x01, + YOU_JOINED = 0x02, + YOU_LEFT = 0x03, + WRONG_PASSWORD = 0x04, + NOT_MEMBER = 0x05, + NOT_MODERATOR = 0x06, + PASSWORD_CHANGED = 0x07, + OWNER_CHANGED = 0x08, + PLAYER_NOT_FOUND = 0x09, + NOT_OWNER = 0x0A, + CHANNEL_OWNER = 0x0B, + MODE_CHANGE = 0x0C, + ANNOUNCEMENTS_ON = 0x0D, + ANNOUNCEMENTS_OFF = 0x0E, + MODERATION_ON = 0x0F, + MODERATION_OFF = 0x10, + MUTED = 0x11, + PLAYER_KICKED = 0x12, + BANNED = 0x13, + PLAYER_BANNED = 0x14, + PLAYER_UNBANNED = 0x15, + PLAYER_NOT_BANNED = 0x16, + PLAYER_ALREADY_MEMBER = 0x17, + INVITE = 0x18, + INVITE_WRONG_FACTION = 0x19, + WRONG_FACTION = 0x1A, + INVALID_NAME = 0x1B, + NOT_MODERATED = 0x1C, + PLAYER_INVITED = 0x1D, + PLAYER_INVITE_BANNED = 0x1E, + THROTTLED = 0x1F, + NOT_IN_AREA = 0x20, + NOT_IN_LFG = 0x21, }; /** @@ -947,6 +942,21 @@ public: static network::Packet build(uint8_t state); }; +// ============================================================ +// Action Bar +// ============================================================ + +/** CMSG_SET_ACTION_BUTTON packet builder */ +class SetActionButtonPacket { +public: + // button: 0-based slot index + // type: ActionBarSlot::Type (SPELL=0, ITEM=1, MACRO=2, EMPTY=0) + // id: spellId, itemId, or macroId (0 to clear) + // isClassic: true for Vanilla/Turtle format (5-byte payload), + // false for TBC/WotLK (5-byte packed uint32) + static network::Packet build(uint8_t button, uint8_t type, uint32_t id, bool isClassic); +}; + // ============================================================ // Display Toggles // ============================================================ @@ -1448,6 +1458,12 @@ public: static network::Packet build(uint64_t targetGuid); }; +/** CMSG_QUERY_INSPECT_ACHIEVEMENTS packet builder (WotLK 3.3.5a) */ +class QueryInspectAchievementsPacket { +public: + static network::Packet build(uint64_t targetGuid); +}; + /** CMSG_NAME_QUERY packet builder */ class NameQueryPacket { public: @@ -1566,13 +1582,21 @@ struct ItemQueryResponseData { uint32_t subClass = 0; uint32_t displayInfoId = 0; uint32_t quality = 0; + uint32_t itemFlags = 0; // Item flag bitmask (Heroic=0x8, Unique-Equipped=0x1000000) uint32_t inventoryType = 0; + int32_t maxCount = 0; // Max that can be carried (1 = Unique, 0 = unlimited) int32_t maxStack = 1; uint32_t containerSlots = 0; float damageMin = 0.0f; float damageMax = 0.0f; uint32_t delayMs = 0; int32_t armor = 0; + int32_t holyRes = 0; + int32_t fireRes = 0; + int32_t natureRes = 0; + int32_t frostRes = 0; + int32_t shadowRes = 0; + int32_t arcaneRes = 0; int32_t stamina = 0; int32_t strength = 0; int32_t agility = 0; @@ -1594,6 +1618,17 @@ struct ItemQueryResponseData { struct ExtraStat { uint32_t statType = 0; int32_t statValue = 0; }; std::vector extraStats; uint32_t startQuestId = 0; // Non-zero: item begins a quest + // Gem socket slots (WotLK/TBC): 0=no socket; color mask: 1=Meta,2=Red,4=Yellow,8=Blue + std::array socketColor{}; + uint32_t socketBonus = 0; // enchantmentId of socket bonus; 0=none + uint32_t itemSetId = 0; // ItemSet.dbc entry; 0=not part of a set + // Requirement fields + uint32_t requiredSkill = 0; // SkillLine.dbc ID (0 = no skill required) + uint32_t requiredSkillRank = 0; // Minimum skill value + uint32_t allowableClass = 0; // Class bitmask (0 = all classes) + uint32_t allowableRace = 0; // Race bitmask (0 = all races) + uint32_t requiredReputationFaction = 0; // Faction.dbc ID (0 = none) + uint32_t requiredReputationRank = 0; // 0=Hated..8=Exalted bool valid = false; }; @@ -1677,8 +1712,10 @@ struct AttackerStateUpdateData { uint32_t blocked = 0; bool isValid() const { return attackerGuid != 0; } - bool isCrit() const { return (hitInfo & 0x200) != 0; } - bool isMiss() const { return (hitInfo & 0x10) != 0; } + bool isCrit() const { return (hitInfo & 0x0200) != 0; } + bool isMiss() const { return (hitInfo & 0x0010) != 0; } + bool isGlancing() const { return (hitInfo & 0x0800) != 0; } + bool isCrushing() const { return (hitInfo & 0x1000) != 0; } }; class AttackerStateUpdateParser { @@ -1818,7 +1855,7 @@ public: /** SMSG_SPELL_GO data (simplified) */ struct SpellGoMissEntry { uint64_t targetGuid = 0; - uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST + uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST 11=REFLECT }; struct SpellGoData { @@ -1831,6 +1868,7 @@ struct SpellGoData { std::vector hitTargets; uint8_t missCount = 0; std::vector missTargets; + uint64_t targetGuid = 0; ///< Primary target GUID from SpellCastTargets (0 = none/AoE) bool isValid() const { return spellId != 0; } }; @@ -1982,6 +2020,12 @@ public: static network::Packet build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0); }; +/** CMSG_OPEN_ITEM packet builder (for locked containers / lockboxes) */ +class OpenItemPacket { +public: + static network::Packet build(uint8_t bagIndex, uint8_t slotIndex); +}; + /** CMSG_AUTOEQUIP_ITEM packet builder */ class AutoEquipItemPacket { public: @@ -1995,6 +2039,13 @@ public: static network::Packet build(uint8_t dstBag, uint8_t dstSlot, uint8_t srcBag, uint8_t srcSlot); }; +/** CMSG_SPLIT_ITEM packet builder */ +class SplitItemPacket { +public: + static network::Packet build(uint8_t srcBag, uint8_t srcSlot, + uint8_t dstBag, uint8_t dstSlot, uint8_t count); +}; + /** CMSG_SWAP_INV_ITEM packet builder */ class SwapInvItemPacket { public: @@ -2018,8 +2069,9 @@ public: /** SMSG_LOOT_RESPONSE parser */ class LootResponseParser { public: - // isWotlkFormat: true for WotLK 3.3.5a (22 bytes/item with randomSuffix+randomProp), - // false for Classic 1.12 and TBC 2.4.3 (14 bytes/item). + // isWotlkFormat: true for WotLK (has trailing quest item section), + // false for Classic/TBC (no quest item section). + // Per-item size is 22 bytes across all expansions. static bool parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat = true); }; @@ -2401,6 +2453,12 @@ public: static network::Packet build(); }; +/** CMSG_RECLAIM_CORPSE packet builder */ +class ReclaimCorpsePacket { +public: + static network::Packet build(uint64_t guid); +}; + /** CMSG_SPIRIT_HEALER_ACTIVATE packet builder */ class SpiritHealerActivatePacket { public: @@ -2471,7 +2529,7 @@ public: /** CMSG_MAIL_TAKE_ITEM packet builder */ class MailTakeItemPacket { public: - static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex); + static network::Packet build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow); }; /** CMSG_MAIL_DELETE packet builder */ @@ -2695,5 +2753,48 @@ public: static bool parse(network::Packet& packet, AuctionCommandResult& data); }; +/** Pet Stable packet builders */ +class ListStabledPetsPacket { +public: + /** MSG_LIST_STABLED_PETS (CMSG): request list from stable master */ + static network::Packet build(uint64_t stableMasterGuid); +}; + +class StablePetPacket { +public: + /** CMSG_STABLE_PET: store active pet in the given stable slot (1-based) */ + static network::Packet build(uint64_t stableMasterGuid, uint8_t slot); +}; + +class UnstablePetPacket { +public: + /** CMSG_UNSTABLE_PET: retrieve a stabled pet by its server-side petNumber */ + static network::Packet build(uint64_t stableMasterGuid, uint32_t petNumber); +}; + +class PetRenamePacket { +public: + /** CMSG_PET_RENAME: rename the player's active pet. + * petGuid: the pet's object GUID (from GameHandler::getPetGuid()) + * name: new name (max 12 chars; server validates and may reject) + * isDeclined: 0 for non-Cyrillic locales (no declined name forms) */ + static network::Packet build(uint64_t petGuid, const std::string& name, uint8_t isDeclined = 0); +}; + +/** CMSG_SET_TITLE packet builder. + * titleBit >= 0: activate the title with that bit index. + * titleBit == -1: clear the current title (show no title). */ +class SetTitlePacket { +public: + static network::Packet build(int32_t titleBit); +}; + +/** CMSG_ALTER_APPEARANCE – barber shop: change hair style, color, facial hair. + * Payload: uint32 hairStyle, uint32 hairColor, uint32 facialHair. */ +class AlterAppearancePacket { +public: + static network::Packet build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair); +}; + } // namespace game } // namespace wowee diff --git a/include/network/packet.hpp b/include/network/packet.hpp index fbfb85bf..b773fc3c 100644 --- a/include/network/packet.hpp +++ b/include/network/packet.hpp @@ -27,13 +27,27 @@ public: uint32_t readUInt32(); uint64_t readUInt64(); float readFloat(); + uint64_t readPackedGuid(); + void writePackedGuid(uint64_t guid); std::string readString(); uint16_t getOpcode() const { return opcode; } const std::vector& getData() const { return data; } size_t getReadPos() const { return readPos; } size_t getSize() const { return data.size(); } + size_t getRemainingSize() const { return data.size() - readPos; } + bool hasRemaining(size_t need) const { return readPos <= data.size() && need <= (data.size() - readPos); } + bool hasFullPackedGuid() const { + if (readPos >= data.size()) return false; + uint8_t mask = data[readPos]; + size_t guidBytes = 1; + for (int bit = 0; bit < 8; ++bit) + if (mask & (1u << bit)) ++guidBytes; + return getRemainingSize() >= guidBytes; + } void setReadPos(size_t pos) { readPos = pos; } + bool hasData() const { return readPos < data.size(); } + void skipAll() { readPos = data.size(); } private: uint16_t opcode = 0; diff --git a/include/network/world_socket.hpp b/include/network/world_socket.hpp index 1d8f1d00..543e570f 100644 --- a/include/network/world_socket.hpp +++ b/include/network/world_socket.hpp @@ -7,7 +7,14 @@ #include "auth/vanilla_crypt.hpp" #include #include +#include #include +#include +#include +#include +#include +#include +#include namespace wowee { namespace network { @@ -66,6 +73,8 @@ public: */ void initEncryption(const std::vector& sessionKey, uint32_t build = 12340); + void tracePacketsFor(std::chrono::milliseconds duration, const std::string& reason); + /** * Check if header encryption is enabled */ @@ -76,11 +85,25 @@ private: * Try to parse complete packets from receive buffer */ void tryParsePackets(); + void pumpNetworkIO(); + void dispatchQueuedPackets(); + void asyncPumpLoop(); + void startAsyncPump(); + void stopAsyncPump(); + void closeSocketNoJoin(); + void recordRecentPacket(bool outbound, uint16_t opcode, uint16_t payloadLen); + void dumpRecentPacketHistoryLocked(const char* reason, size_t bufferedBytes); socket_t sockfd = INVALID_SOCK; bool connected = false; bool encryptionEnabled = false; bool useVanillaCrypt = false; // true = XOR cipher, false = RC4 + bool useAsyncPump_ = true; + std::thread asyncPumpThread_; + std::atomic asyncPumpStop_{false}; + std::atomic asyncPumpRunning_{false}; + mutable std::mutex ioMutex_; + mutable std::mutex callbackMutex_; // WotLK RC4 ciphers for header encryption/decryption auth::RC4 encryptCipher; @@ -94,6 +117,8 @@ private: size_t receiveReadOffset_ = 0; // Optional reused packet queue (feature-gated) to reduce per-update allocations. std::vector parsedPacketsScratch_; + // Parsed packets waiting for callback dispatch; drained with a strict per-update budget. + std::deque pendingPacketCallbacks_; // Runtime-gated network optimization toggles (default off). bool useFastRecvAppend_ = false; @@ -105,6 +130,17 @@ private: // Debug-only tracing window for post-auth packet framing verification. int headerTracePacketsLeft = 0; + std::chrono::steady_clock::time_point packetTraceStart_{}; + std::chrono::steady_clock::time_point packetTraceUntil_{}; + std::string packetTraceReason_; + + struct RecentPacketTrace { + std::chrono::steady_clock::time_point when{}; + bool outbound = false; + uint16_t opcode = 0; + uint16_t payloadLen = 0; + }; + std::deque recentPacketHistory_; // Packet callback std::function packetCallback; diff --git a/include/pipeline/dbc_layout.hpp b/include/pipeline/dbc_layout.hpp index 154aef08..0bbb2b29 100644 --- a/include/pipeline/dbc_layout.hpp +++ b/include/pipeline/dbc_layout.hpp @@ -57,5 +57,40 @@ inline uint32_t dbcField(const std::string& dbcName, const std::string& fieldNam return fm ? fm->field(fieldName) : 0xFFFFFFFF; } +// Forward declaration +class DBCFile; + +/** + * Resolved CharSections.dbc field indices. + * + * Stock WotLK 3.3.5a uses: Texture1=4, Texture2=5, Texture3=6, Flags=7, + * VariationIndex=8, ColorIndex=9 (textures first). + * Classic/TBC/Turtle and HD-texture WotLK use: VariationIndex=4, ColorIndex=5, + * Texture1=6, Texture2=7, Texture3=8, Flags=9 (variation first). + * + * detectCharSectionsFields() auto-detects which layout the actual DBC uses + * by sampling field-4 values: small integers (0-15) => variation-first, + * large values (string offsets) => texture-first. + */ +struct CharSectionsFields { + uint32_t raceId = 1; + uint32_t sexId = 2; + uint32_t baseSection = 3; + uint32_t variationIndex = 4; + uint32_t colorIndex = 5; + uint32_t texture1 = 6; + uint32_t texture2 = 7; + uint32_t texture3 = 8; + uint32_t flags = 9; +}; + +/** + * Detect the actual CharSections.dbc field layout by probing record data. + * @param dbc Loaded CharSections.dbc file (must not be null). + * @param csL JSON-derived field map (may be null — defaults used). + * @return Resolved field indices for this particular DBC binary. + */ +CharSectionsFields detectCharSectionsFields(const DBCFile* dbc, const DBCFieldMap* csL); + } // namespace pipeline } // namespace wowee diff --git a/include/pipeline/m2_loader.hpp b/include/pipeline/m2_loader.hpp index d3949f88..185ca653 100644 --- a/include/pipeline/m2_loader.hpp +++ b/include/pipeline/m2_loader.hpp @@ -165,6 +165,29 @@ struct M2ParticleEmitter { bool enabled = true; }; +// Ribbon emitter definition parsed from M2 (WotLK format) +struct M2RibbonEmitter { + int32_t ribbonId = 0; + uint32_t bone = 0; // Bone that drives the ribbon spine + glm::vec3 position{0.0f}; // Offset from bone pivot + + uint16_t textureIndex = 0; // First texture lookup index + uint16_t materialIndex = 0; // First material lookup index (blend mode) + + // Animated tracks + M2AnimationTrack colorTrack; // RGB 0..1 + M2AnimationTrack alphaTrack; // float 0..1 (stored as fixed16 on disk) + M2AnimationTrack heightAboveTrack; // Half-width above bone + M2AnimationTrack heightBelowTrack; // Half-width below bone + M2AnimationTrack visibilityTrack; // 0=hidden, 1=visible + + float edgesPerSecond = 15.0f; // How many edge points are generated per second + float edgeLifetime = 0.5f; // Seconds before edges expire + float gravity = 0.0f; // Downward pull on edges per s² + uint16_t textureRows = 1; + uint16_t textureCols = 1; +}; + // Complete M2 model structure struct M2Model { // Model metadata @@ -213,6 +236,9 @@ struct M2Model { // Particle emitters std::vector particleEmitters; + // Ribbon emitters + std::vector ribbonEmitters; + // Collision mesh (simplified geometry for physics) std::vector collisionVertices; std::vector collisionIndices; // 3 per triangle diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 7401ffdd..fae92812 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -44,6 +44,7 @@ public: } void reset(); + void resetAngles(); void teleportTo(const glm::vec3& pos); void setOnlineMode(bool online) { onlineMode = online; } @@ -129,6 +130,12 @@ public: // vspeed: raw packet vspeed field (server sends negative for upward launch) void applyKnockBack(float vcos, float vsin, float hspeed, float vspeed); + // Trigger a camera shake effect (e.g. from SMSG_CAMERA_SHAKE). + // magnitude: peak positional offset in world units + // frequency: oscillation frequency in Hz + // duration: shake duration in seconds + void triggerShake(float magnitude, float frequency, float duration); + // For first-person player hiding void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) { characterRenderer = cr; @@ -156,7 +163,7 @@ private: // Mouse settings float mouseSensitivity = 0.2f; - bool invertMouse = false; + bool invertMouse = true; bool mouseButtonDown = false; bool leftMouseDown = false; bool rightMouseDown = false; @@ -180,7 +187,7 @@ private: static constexpr float COLLISION_FOCUS_RADIUS_THIRD_PERSON = 20.0f; // Reduced for performance static constexpr float COLLISION_FOCUS_RADIUS_FREE_FLY = 20.0f; static constexpr float MIN_PITCH = -88.0f; // Look almost straight down - static constexpr float MAX_PITCH = 35.0f; // Limited upward look + static constexpr float MAX_PITCH = 88.0f; // Look almost straight up (WoW standard) glm::vec3* followTarget = nullptr; glm::vec3 smoothedCamPos = glm::vec3(0.0f); // For smooth camera movement float smoothedCollisionDist_ = -1.0f; // Asymmetrically-smoothed WMO collision limit (-1 = uninitialised) @@ -369,6 +376,12 @@ private: glm::vec2 knockbackHorizVel_ = glm::vec2(0.0f); // render-space horizontal velocity (units/s) // Horizontal velocity decays via WoW-like drag so the player doesn't slide forever. static constexpr float KNOCKBACK_HORIZ_DRAG = 4.5f; // exponential decay rate (1/s) + + // Camera shake state (SMSG_CAMERA_SHAKE) + float shakeElapsed_ = 0.0f; + float shakeDuration_ = 0.0f; + float shakeMagnitude_ = 0.0f; + float shakeFrequency_ = 0.0f; }; } // namespace rendering diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 67b2274a..6129940f 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -296,7 +296,9 @@ private: std::unordered_map textureColorKeyBlackByPtr_; std::unordered_map compositeCache_; // key → texture for reuse std::unordered_set failedTextureCache_; // negative cache for budget exhaustion + std::unordered_map failedTextureRetryAt_; std::unordered_set loggedTextureLoadFails_; // dedup warning logs + uint64_t textureLookupSerial_ = 0; size_t textureCacheBytes_ = 0; uint64_t textureCacheCounter_ = 0; size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024; diff --git a/include/rendering/loading_screen.hpp b/include/rendering/loading_screen.hpp index afd134b9..a0ed13a5 100644 --- a/include/rendering/loading_screen.hpp +++ b/include/rendering/loading_screen.hpp @@ -30,6 +30,7 @@ public: void setProgress(float progress) { loadProgress = progress; } void setStatus(const std::string& status) { statusText = status; } + void setZoneName(const std::string& name) { zoneName = name; } // Must be set before initialize() for Vulkan texture upload void setVkContext(VkContext* ctx) { vkCtx = ctx; } @@ -53,6 +54,7 @@ private: float loadProgress = 0.0f; std::string statusText = "Loading..."; + std::string zoneName; int imageWidth = 0; int imageHeight = 0; diff --git a/include/rendering/m2_model_classifier.hpp b/include/rendering/m2_model_classifier.hpp new file mode 100644 index 00000000..8ef09aab --- /dev/null +++ b/include/rendering/m2_model_classifier.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace rendering { + +/** + * Output of classifyM2Model(): all name/geometry-based flags for an M2 model. + * Pure data — no Vulkan, GPU, or asset-manager dependencies. + */ +struct M2ClassificationResult { + // --- Collision shape selectors --- + bool collisionNoBlock = false; ///< Foliage/soft-trees/rugs: no blocking + bool collisionBridge = false; ///< Walk-on-top bridge/plank/walkway + bool collisionPlanter = false; ///< Low stepped planter/curb + bool collisionSteppedFountain = false; ///< Stepped fountain base + bool collisionSteppedLowPlatform = false; ///< Low stepped platform (curb/planter/bridge) + bool collisionStatue = false; ///< Statue/monument/sculpture + bool collisionSmallSolidProp = false; ///< Blockable solid prop (crate/chest/barrel) + bool collisionNarrowVerticalProp = false; ///< Narrow tall prop (lamp/post/pole) + bool collisionTreeTrunk = false; ///< Tree trunk cylinder + + // --- Rendering / effect classification --- + bool isFoliageLike = false; ///< Foliage or tree (wind sway, disabled animation) + bool isSpellEffect = false; ///< Spell effect / particle-dominated visual + bool isLavaModel = false; ///< Lava surface (UV scroll animation) + bool isInstancePortal = false; ///< Instance portal (additive, spin, no collision) + bool isWaterVegetation = false; ///< Aquatic vegetation (cattails, kelp, reeds, etc.) + bool isFireflyEffect = false; ///< Ambient creature (exempt from particle dampeners) + bool isElvenLike = false; ///< Night elf / Blood elf themed model + bool isLanternLike = false; ///< Lantern/lamp/light model + bool isKoboldFlame = false; ///< Kobold candle/torch model + bool isGroundDetail = false; ///< Ground-clutter detail doodad (always non-blocking) + bool isInvisibleTrap = false; ///< Event-object invisible trap (no render, no collision) + bool isSmoke = false; ///< Smoke model (UV scroll animation) + + // --- Animation flags --- + bool disableAnimation = false; ///< Keep visually stable (foliage, chest lids, etc.) + bool shadowWindFoliage = false; ///< Apply wind sway in shadow pass for foliage/trees +}; + +/** + * Classify an M2 model by name and geometry. + * + * Pure function — no Vulkan, VkContext, or AssetManager dependencies. + * All results are derived solely from the model name string and tight vertex bounds. + * + * @param name Full model path/name from the M2 header (any case) + * @param boundsMin Per-vertex tight bounding-box minimum + * @param boundsMax Per-vertex tight bounding-box maximum + * @param vertexCount Number of mesh vertices + * @param emitterCount Number of particle emitters + */ +M2ClassificationResult classifyM2Model( + const std::string& name, + const glm::vec3& boundsMin, + const glm::vec3& boundsMax, + std::size_t vertexCount, + std::size_t emitterCount); + +// --------------------------------------------------------------------------- +// Batch texture classification +// --------------------------------------------------------------------------- + +/** + * Per-batch texture key classification — glow / tint token flags. + * Input must be a lowercased, backslash-normalised texture path (as stored in + * M2Renderer's textureKeysLower vector). Pure data — no Vulkan dependencies. + */ +struct M2BatchTexClassification { + bool exactLanternGlowTex = false; ///< One of the known exact lantern-glow texture paths + bool hasGlowToken = false; ///< glow / flare / halo / light + bool hasFlameToken = false; ///< flame / fire / flamelick / ember + bool hasGlowCardToken = false; ///< glow / flamelick / lensflare / t_vfx / lightbeam / glowball / genericglow + bool likelyFlame = false; ///< fire / flame / torch + bool lanternFamily = false; ///< lantern / lamp / elf / silvermoon / quel / thalas + int glowTint = 0; ///< 0 = neutral, 1 = cool (blue/arcane), 2 = warm (red/scarlet) +}; + +/** + * Classify a batch texture by its lowercased path for glow/tint hinting. + * + * Pure function — no Vulkan, VkContext, or AssetManager dependencies. + * + * @param lowerTexKey Lowercased, backslash-normalised texture path (may be empty) + */ +M2BatchTexClassification classifyBatchTexture(const std::string& lowerTexKey); + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 3d79379f..fe8d7f61 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include namespace wowee { @@ -130,6 +132,11 @@ struct M2ModelGPU { std::vector particleTextures; // Resolved Vulkan textures per emitter std::vector particleTexSets; // Pre-allocated descriptor sets per emitter (stable, avoids per-frame alloc) + // Ribbon emitter data (kept from M2Model) + std::vector ribbonEmitters; + std::vector ribbonTextures; // Resolved texture per ribbon emitter + std::vector ribbonTexSets; // Descriptor sets per ribbon emitter + // Texture transform data for UV animation std::vector textureTransforms; std::vector textureTransformLookup; @@ -180,6 +187,19 @@ struct M2Instance { std::vector emitterAccumulators; // fractional particle counter per emitter std::vector particles; + // Ribbon emitter state + struct RibbonEdge { + glm::vec3 worldPos; // Spine world position when this edge was born + glm::vec3 color; // Interpolated color at birth + float alpha; // Interpolated alpha at birth + float heightAbove;// Half-width above spine + float heightBelow;// Half-width below spine + float age; // Seconds since spawned + }; + // One deque of edges per ribbon emitter on this instance + std::vector> ribbonEdges; + std::vector ribbonEdgeAccumulators; // fractional edge counter per emitter + // Cached model flags (set at creation to avoid per-frame hash lookups) bool cachedHasAnimation = false; bool cachedDisableAnimation = false; @@ -295,9 +315,15 @@ public: */ void renderSmokeParticles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); + /** + * Render M2 ribbon emitters (spell trails / wing effects) + */ + void renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet); + void setInstancePosition(uint32_t instanceId, const glm::vec3& position); void setInstanceTransform(uint32_t instanceId, const glm::mat4& transform); void setInstanceAnimationFrozen(uint32_t instanceId, bool frozen); + float getInstanceAnimDuration(uint32_t instanceId) const; void removeInstance(uint32_t instanceId); void removeInstances(const std::vector& instanceIds); void setSkipCollision(uint32_t instanceId, bool skip); @@ -374,6 +400,11 @@ private: VkPipeline smokePipeline_ = VK_NULL_HANDLE; // Smoke particles VkPipelineLayout smokePipelineLayout_ = VK_NULL_HANDLE; + // Ribbon pipelines (additive + alpha-blend) + VkPipeline ribbonPipeline_ = VK_NULL_HANDLE; // Alpha-blend ribbons + VkPipeline ribbonAdditivePipeline_ = VK_NULL_HANDLE; // Additive ribbons + VkPipelineLayout ribbonPipelineLayout_ = VK_NULL_HANDLE; + // Descriptor set layouts VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1 VkDescriptorSetLayout boneSetLayout_ = VK_NULL_HANDLE; // set 2 @@ -382,8 +413,21 @@ private: // Descriptor pools VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE; VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE; - static constexpr uint32_t MAX_MATERIAL_SETS = 8192; - static constexpr uint32_t MAX_BONE_SETS = 8192; + static constexpr uint32_t MAX_MATERIAL_SETS = 16384; + static constexpr uint32_t MAX_BONE_SETS = 16384; + + // 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; + VmaAllocation ribbonVBAlloc_ = VK_NULL_HANDLE; + void* ribbonVBMapped_ = nullptr; // Dynamic particle buffers ::VkBuffer smokeVB_ = VK_NULL_HANDLE; @@ -399,6 +443,9 @@ private: void* glowVBMapped_ = nullptr; std::unordered_map models; + // Grace period for model cleanup: track when a model first became instanceless. + // Models are only evicted after 60 seconds with no instances. + std::unordered_map modelUnusedSince_; std::vector instances; // O(1) dedup: key = (modelId, quantized x, quantized y, quantized z) → instanceId @@ -442,7 +489,9 @@ private: uint64_t textureCacheCounter_ = 0; size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; std::unordered_set failedTextureCache_; + std::unordered_map failedTextureRetryAt_; std::unordered_set loggedTextureLoadFails_; + uint64_t textureLookupSerial_ = 0; uint32_t textureBudgetRejectWarnings_ = 0; std::unique_ptr whiteTexture_; std::unique_ptr glowTexture_; @@ -535,6 +584,7 @@ private: glm::vec3 interpFBlockVec3(const pipeline::M2FBlock& fb, float lifeRatio); void emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt); void updateParticles(M2Instance& inst, float dt); + void updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt); // Helper to allocate descriptor sets VkDescriptorSet allocateMaterialSet(); diff --git a/include/rendering/minimap.hpp b/include/rendering/minimap.hpp index ca7c5345..906f4666 100644 --- a/include/rendering/minimap.hpp +++ b/include/rendering/minimap.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace wowee { @@ -73,7 +74,10 @@ private: bool trsParsed = false; // Tile texture cache: hash → VkTexture + // Evicted (FIFO) when the count of successfully-loaded tiles exceeds MAX_TILE_CACHE. + static constexpr size_t MAX_TILE_CACHE = 128; std::unordered_map> tileTextureCache; + std::deque tileInsertionOrder; // hashes of successfully loaded tiles, oldest first std::unique_ptr noDataTexture; // Composite render target (3x3 tiles = 768x768) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 93bbed03..80b33fe6 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,6 +41,7 @@ class StarField; class Clouds; class LensFlare; class Weather; +class Lightning; class LightingManager; class SwimEffects; class MountDust; @@ -127,6 +130,7 @@ public: Clouds* getClouds() const { return skySystem ? skySystem->getClouds() : nullptr; } LensFlare* getLensFlare() const { return skySystem ? skySystem->getLensFlare() : nullptr; } Weather* getWeather() const { return weather.get(); } + Lightning* getLightning() const { return lightning.get(); } CharacterRenderer* getCharacterRenderer() const { return characterRenderer.get(); } WMORenderer* getWMORenderer() const { return wmoRenderer.get(); } M2Renderer* getM2Renderer() const { return m2Renderer.get(); } @@ -135,6 +139,7 @@ public: QuestMarkerRenderer* getQuestMarkerRenderer() const { return questMarkerRenderer.get(); } SkySystem* getSkySystem() const { return skySystem.get(); } const std::string& getCurrentZoneName() const { return currentZoneName; } + bool isPlayerIndoors() const { return playerIndoors_; } VkContext* getVkContext() const { return vkCtx; } VkDescriptorSetLayout getPerFrameSetLayout() const { return perFrameSetLayout; } VkRenderPass getShadowRenderPass() const { return shadowRenderPass; } @@ -150,6 +155,14 @@ public: void playEmote(const std::string& emoteName); void triggerLevelUpEffect(const glm::vec3& position); void cancelEmote(); + + // Screenshot capture — copies swapchain image to PNG file + bool captureScreenshot(const std::string& outputPath); + + // Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT) + // useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path + void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, + bool useImpactKit = false); bool isEmoteActive() const { return emoteActive; } static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr); static uint32_t getEmoteDbcId(const std::string& emoteName); @@ -159,6 +172,7 @@ public: // Targeting support void setTargetPosition(const glm::vec3* pos); void setInCombat(bool combat) { inCombat_ = combat; } + void resetCombatVisualState(); bool isMoving() const; void triggerMeleeSwing(); void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } @@ -216,6 +230,7 @@ private: std::unique_ptr clouds; std::unique_ptr lensFlare; std::unique_ptr weather; + std::unique_ptr lightning; std::unique_ptr lightingManager; std::unique_ptr skySystem; // Coordinator for sky rendering std::unique_ptr swimEffects; @@ -271,6 +286,10 @@ public: float getShadowDistance() const { return shadowDistance_; } void setMsaaSamples(VkSampleCountFlagBits samples); + // FXAA post-process anti-aliasing (combinable with MSAA) + void setFXAAEnabled(bool enabled); + bool isFXAAEnabled() const { return fxaa_.enabled; } + // FSR (FidelityFX Super Resolution) upscaling void setFSREnabled(bool enabled); bool isFSREnabled() const { return fsr_.enabled; } @@ -315,10 +334,30 @@ private: glm::mat4 computeLightSpaceMatrix(); pipeline::AssetManager* cachedAssetManager = nullptr; + + // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT + struct SpellVisualInstance { + uint32_t instanceId; + float elapsed; + float duration; // per-instance lifetime in seconds (from M2 anim or default) + }; + std::vector activeSpellVisuals_; + std::unordered_map spellVisualCastPath_; // visualId → cast M2 path + std::unordered_map spellVisualImpactPath_; // visualId → impact M2 path + std::unordered_map spellVisualModelIds_; // M2 path → M2Renderer modelId + std::unordered_set spellVisualFailedModels_; // modelIds that failed to load (negative cache) + uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799 + bool spellVisualDbcLoaded_ = false; + void loadSpellVisualDbc(); + void updateSpellVisuals(float deltaTime); + static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f; + static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f; + uint32_t currentZoneId = 0; std::string currentZoneName; bool inTavern_ = false; bool inBlacksmith_ = false; + bool playerIndoors_ = false; // Cached WMO inside state for macro conditionals float musicSwitchCooldown_ = 0.0f; bool deferredWorldInitEnabled_ = true; bool deferredWorldInitPending_ = false; @@ -333,6 +372,10 @@ private: // Character animation state enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE }; CharAnimState charAnimState = CharAnimState::IDLE; + float locomotionStopGraceTimer_ = 0.0f; + bool locomotionWasSprinting_ = false; + uint32_t lastPlayerAnimRequest_ = UINT32_MAX; + bool lastPlayerAnimLoopRequest_ = true; void updateCharacterAnimation(); bool isFootstepAnimationState() const; bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs); @@ -369,6 +412,13 @@ private: void initOverlayPipeline(); void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE); + // Brightness (1.0 = default, <1 darkens, >1 brightens) + float brightness_ = 1.0f; +public: + void setBrightness(float b) { brightness_ = b; } + float getBrightness() const { return brightness_; } +private: + // FSR 1.0 upscaling state struct FSRState { bool enabled = false; @@ -398,6 +448,31 @@ private: void destroyFSRResources(); void renderFSRUpscale(); + // FXAA post-process state + struct FXAAState { + bool enabled = false; + bool needsRecreate = false; + + // Off-screen scene target (same resolution as swapchain — no scaling) + AllocatedImage sceneColor{}; // 1x resolved color target + AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count) + AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x) + AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve) + VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; + VkSampler sceneSampler = VK_NULL_HANDLE; + + // FXAA fullscreen pipeline + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descPool = VK_NULL_HANDLE; + VkDescriptorSet descSet = VK_NULL_HANDLE; + }; + FXAAState fxaa_; + bool initFXAAResources(); + void destroyFXAAResources(); + void renderFXAAPass(); + // FSR 2.2 temporal upscaling state struct FSR2State { bool enabled = false; @@ -593,6 +668,7 @@ private: VkCommandBuffer secondaryCmds_[NUM_SECONDARIES][MAX_FRAMES] = {}; bool parallelRecordingEnabled_ = false; // set true after pools/buffers created + bool endFrameInlineMode_ = false; // true when endFrame switched to INLINE render pass bool createSecondaryCommandResources(); void destroySecondaryCommandResources(); VkCommandBuffer beginSecondary(uint32_t secondaryIndex); @@ -609,6 +685,8 @@ private: bool terrainEnabled = true; bool terrainLoaded = false; + bool ghostMode_ = false; // set each frame from gameHandler->isPlayerGhost() + // CPU timing stats (last frame/update). double lastUpdateMs = 0.0; double lastRenderMs = 0.0; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 290c45eb..ab6e881f 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -279,6 +279,9 @@ public: /** Process one ready tile (for loading screens with per-tile progress updates) */ void processOneReadyTile(); + /** Process a bounded batch of ready tiles with async GPU upload (no sync wait) */ + void processReadyTiles(); + private: /** * Get tile coordinates from GL world position @@ -317,10 +320,6 @@ private: */ void workerLoop(); - /** - * Main thread: poll for completed tiles and upload to GPU - */ - void processReadyTiles(); void ensureGroundEffectTablesLoaded(); void generateGroundClutterPlacements(std::shared_ptr& pending, std::unordered_set& preparedModelIds); @@ -395,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/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index f4994792..5bc13252 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -171,7 +171,7 @@ private: // Descriptor pool for material sets VkDescriptorPool materialDescPool = VK_NULL_HANDLE; - static constexpr uint32_t MAX_MATERIAL_SETS = 16384; + static constexpr uint32_t MAX_MATERIAL_SETS = 65536; // Loaded terrain chunks std::vector chunks; diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp index 154a4f98..c9926cf5 100644 --- a/include/rendering/vk_context.hpp +++ b/include/rendering/vk_context.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include namespace wowee { namespace rendering { @@ -57,14 +59,29 @@ public: void pollUploadBatches(); // Check completed async uploads, free staging buffers void waitAllUploads(); // Block until all in-flight uploads complete + // Defer resource destruction until it is safe with multiple frames in flight. + // + // This queues work to run after the fence for the *current frame slot* has + // signaled the next time we enter beginFrame() for that slot (i.e. after + // MAX_FRAMES_IN_FLIGHT submissions). Use this for resources that may still + // be referenced by command buffers submitted in the previous frame(s), + // such as descriptor sets and buffers freed during streaming/unload. + void deferAfterFrameFence(std::function&& fn); + // Accessors VkInstance getInstance() const { return instance; } VkPhysicalDevice getPhysicalDevice() const { return physicalDevice; } VkDevice getDevice() const { return device; } + uint32_t getGpuVendorId() const { return gpuVendorId_; } + const char* getGpuName() const { return gpuName_; } + bool isAmdGpu() const { return gpuVendorId_ == 0x1002; } + bool isNvidiaGpu() const { return gpuVendorId_ == 0x10DE; } VkQueue getGraphicsQueue() const { return graphicsQueue; } uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; } + bool hasDedicatedTransferQueue() const { return hasDedicatedTransfer_; } VmaAllocator getAllocator() const { return allocator; } VkSurfaceKHR getSurface() const { return surface; } + VkPipelineCache getPipelineCache() const { return pipelineCache_; } VkSwapchainKHR getSwapchain() const { return swapchain; } VkFormat getSwapchainFormat() const { return swapchainFormat; } @@ -105,6 +122,18 @@ public: VkImageView getDepthResolveImageView() const { return depthResolveImageView; } VkImageView getDepthImageView() const { return depthImageView; } + // Sampler cache: returns a shared VkSampler matching the given create info. + // Callers must NOT destroy the returned sampler — it is owned by VkContext. + // Automatically clamps anisotropy if the device doesn't support it. + VkSampler getOrCreateSampler(const VkSamplerCreateInfo& info); + + // Whether the physical device supports sampler anisotropy. + bool isSamplerAnisotropySupported() const { return samplerAnisotropySupported_; } + + // Global sampler cache accessor (set during VkContext::initialize, cleared on shutdown). + // Used by VkTexture and other code that only has a VkDevice handle. + static VkContext* globalInstance() { return sInstance_; } + // UI texture upload: creates a Vulkan texture from RGBA data and returns // a VkDescriptorSet suitable for use as ImTextureID. // The caller does NOT need to free the result — resources are tracked and @@ -121,6 +150,8 @@ private: void destroySwapchain(); bool createCommandPools(); bool createSyncObjects(); + bool createPipelineCache(); + void savePipelineCache(); bool createImGuiResources(); void destroyImGuiResources(); @@ -135,11 +166,22 @@ private: VkDevice device = VK_NULL_HANDLE; VmaAllocator allocator = VK_NULL_HANDLE; + // Pipeline cache (persisted to disk for faster startup) + VkPipelineCache pipelineCache_ = VK_NULL_HANDLE; + uint32_t gpuVendorId_ = 0; + char gpuName_[256] = {}; + VkQueue graphicsQueue = VK_NULL_HANDLE; VkQueue presentQueue = VK_NULL_HANDLE; uint32_t graphicsQueueFamily = 0; uint32_t presentQueueFamily = 0; + // Dedicated transfer queue (second queue from same graphics family) + VkQueue transferQueue_ = VK_NULL_HANDLE; + VkCommandPool transferCommandPool_ = VK_NULL_HANDLE; + bool hasDedicatedTransfer_ = false; + uint32_t graphicsQueueFamilyQueueCount_ = 1; // queried in selectPhysicalDevice + // Swapchain VkSwapchainKHR swapchain = VK_NULL_HANDLE; VkFormat swapchainFormat = VK_FORMAT_UNDEFINED; @@ -173,6 +215,9 @@ private: }; std::vector inFlightBatches_; + void runDeferredCleanup(uint32_t frameIndex); + std::vector> deferredCleanup_[MAX_FRAMES_IN_FLIGHT]; + // Depth buffer (shared across all framebuffers) VkImage depthImage = VK_NULL_HANDLE; VkImageView depthImageView = VK_NULL_HANDLE; @@ -215,6 +260,13 @@ private: }; std::vector uiTextures_; + // Sampler cache — deduplicates VkSamplers by configuration hash. + std::mutex samplerCacheMutex_; + std::unordered_map samplerCache_; + bool samplerAnisotropySupported_ = false; + + static VkContext* sInstance_; + #ifndef NDEBUG bool enableValidation = true; #else diff --git a/include/rendering/vk_pipeline.hpp b/include/rendering/vk_pipeline.hpp index ea0a3e10..e95337f8 100644 --- a/include/rendering/vk_pipeline.hpp +++ b/include/rendering/vk_pipeline.hpp @@ -75,8 +75,8 @@ public: // Dynamic state PipelineBuilder& setDynamicStates(const std::vector& states); - // Build the pipeline - VkPipeline build(VkDevice device) const; + // Build the pipeline (pass a VkPipelineCache for faster creation) + VkPipeline build(VkDevice device, VkPipelineCache cache = VK_NULL_HANDLE) const; // Common blend states static VkPipelineColorBlendAttachmentState blendDisabled(); diff --git a/include/rendering/vk_render_target.hpp b/include/rendering/vk_render_target.hpp index ffa1cd4f..a954bc5b 100644 --- a/include/rendering/vk_render_target.hpp +++ b/include/rendering/vk_render_target.hpp @@ -73,6 +73,7 @@ private: bool hasDepth_ = false; VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT; VkSampler sampler_ = VK_NULL_HANDLE; + bool ownsSampler_ = true; VkRenderPass renderPass_ = VK_NULL_HANDLE; VkFramebuffer framebuffer_ = VK_NULL_HANDLE; }; diff --git a/include/rendering/vk_texture.hpp b/include/rendering/vk_texture.hpp index 83167d9d..51c57db8 100644 --- a/include/rendering/vk_texture.hpp +++ b/include/rendering/vk_texture.hpp @@ -72,6 +72,7 @@ private: AllocatedImage image_{}; VkSampler sampler_ = VK_NULL_HANDLE; uint32_t mipLevels_ = 1; + bool ownsSampler_ = true; // false when sampler comes from VkContext cache }; } // namespace rendering diff --git a/include/rendering/weather.hpp b/include/rendering/weather.hpp index b92c963d..3349526f 100644 --- a/include/rendering/weather.hpp +++ b/include/rendering/weather.hpp @@ -28,7 +28,8 @@ public: enum class Type { NONE, RAIN, - SNOW + SNOW, + STORM }; Weather(); diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 08108dc0..2431628e 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -69,6 +69,12 @@ public: */ bool loadModel(const pipeline::WMOModel& model, uint32_t id); + /** + * Check if a WMO model is currently resident in the renderer + * @param id WMO model identifier + */ + bool isModelLoaded(uint32_t id) const; + /** * Unload WMO model and free GPU resources * @param id WMO model identifier @@ -650,7 +656,7 @@ private: // Descriptor pool for material sets VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE; - static constexpr uint32_t MAX_MATERIAL_SETS = 8192; + static constexpr uint32_t MAX_MATERIAL_SETS = 32768; // Texture cache (path -> VkTexture) struct TextureCacheEntry { @@ -665,7 +671,9 @@ private: uint64_t textureCacheCounter_ = 0; size_t textureCacheBudgetBytes_ = 8192ull * 1024 * 1024; // 8 GB default, overridden at init std::unordered_set failedTextureCache_; + std::unordered_map failedTextureRetryAt_; std::unordered_set loggedTextureLoadFails_; + uint64_t textureLookupSerial_ = 0; uint32_t textureBudgetRejectWarnings_ = 0; // Default white texture diff --git a/include/rendering/world_map.hpp b/include/rendering/world_map.hpp index 47956b42..fee98b6d 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,22 @@ class VkContext; class VkTexture; class VkRenderTarget; +/// Party member dot passed in from the UI layer for world map overlay. +struct WorldMapPartyDot { + glm::vec3 renderPos; ///< Position in render-space coordinates + uint32_t color; ///< RGBA packed color (IM_COL32 format) + std::string name; ///< Member name (shown as tooltip on hover) +}; + +/// Taxi (flight master) node passed from the UI layer for world map overlay. +struct WorldMapTaxiNode { + uint32_t id = 0; ///< TaxiNodes.dbc ID + uint32_t mapId = 0; ///< WoW internal map ID (0=EK,1=Kal,530=Outland,571=Northrend) + float wowX = 0, wowY = 0, wowZ = 0; ///< Canonical WoW coordinates + std::string name; ///< Node name (shown as tooltip) + bool known = false; ///< Player has discovered this node +}; + struct WorldMapZone { uint32_t wmaID = 0; uint32_t areaID = 0; // 0 = continent level @@ -43,10 +60,27 @@ public: void compositePass(VkCommandBuffer cmd); /// ImGui overlay — call INSIDE the main render pass (during ImGui frame). - void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); + void render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, + float playerYawDeg = 0.0f); void setMapName(const std::string& name); void setServerExplorationMask(const std::vector& masks, bool hasData); + void setPartyDots(std::vector dots) { partyDots_ = std::move(dots); } + void setTaxiNodes(std::vector nodes) { taxiNodes_ = std::move(nodes); } + + /// Quest POI marker for world map overlay (from SMSG_QUEST_POI_QUERY_RESPONSE). + struct QuestPoi { + float wowX = 0, wowY = 0; ///< Canonical WoW coordinates (centroid of POI area) + std::string name; ///< Quest title + }; + void setQuestPois(std::vector pois) { questPois_ = std::move(pois); } + /// Set the player's corpse position for overlay rendering. + /// @param hasCorpse True when the player is a ghost with an unclaimed corpse on this map. + /// @param renderPos Corpse position in render-space coordinates. + void setCorpsePos(bool hasCorpse, glm::vec3 renderPos) { + hasCorpse_ = hasCorpse; + corpseRenderPos_ = renderPos; + } bool isOpen() const { return open; } void close() { open = false; } @@ -62,7 +96,8 @@ private: float& top, float& bottom) const; void loadZoneTextures(int zoneIdx); void requestComposite(int zoneIdx); - void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); + void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, + float playerYawDeg); void updateExploration(const glm::vec3& playerRenderPos); void zoomIn(const glm::vec3& playerRenderPos); void zoomOut(); @@ -113,6 +148,20 @@ private: // Texture storage (owns all VkTexture objects for zone tiles) std::vector> zoneTextures; + // Party member dots (set each frame from the UI layer) + std::vector partyDots_; + + // Taxi node markers (set each frame from the UI layer) + std::vector taxiNodes_; + int currentMapId_ = -1; ///< WoW map ID currently loaded (set in loadZonesFromDBC) + + // Quest POI markers (set each frame from the UI layer) + std::vector questPois_; + + // Corpse marker (ghost state — set each frame from the UI layer) + bool hasCorpse_ = false; + glm::vec3 corpseRenderPos_ = {}; + // Exploration / fog of war std::vector serverExplorationMask; bool hasServerExplorationMask = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 769b1558..3a974846 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -16,6 +16,7 @@ namespace wowee { namespace pipeline { class AssetManager; } +namespace rendering { class Renderer; } namespace ui { /** @@ -40,27 +41,49 @@ public: void saveSettings(); void loadSettings(); + void applyAudioVolumes(rendering::Renderer* renderer); private: // Chat state char chatInputBuffer[512] = ""; char whisperTargetBuffer[256] = ""; bool chatInputActive = false; - int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, 3=GUILD, 4=WHISPER + int selectedChatType = 0; // 0=SAY, 1=YELL, 2=PARTY, 3=GUILD, 4=WHISPER, ..., 10=CHANNEL int lastChatType = 0; // Track chat type changes + int selectedChannelIdx = 0; // Index into joinedChannels_ when selectedChatType==10 bool chatInputMoveCursorToEnd = false; // Chat sent-message history (Up/Down arrow recall) std::vector chatSentHistory_; int chatHistoryIdx_ = -1; // -1 = not browsing history + // Set to true by /stopmacro; checked in executeMacroText to halt remaining commands. + bool macroStopped_ = false; + + // Action bar error-flash: spellId → wall-clock time (seconds) when the flash ends. + // Populated by the SpellCastFailedCallback; queried during action bar button rendering. + std::unordered_map actionFlashEndTimes_; + + // Cached game handler for input callbacks (set each frame in render) + game::GameHandler* cachedGameHandler_ = nullptr; + + // Tab-completion state for slash commands and player names + std::string chatTabPrefix_; // prefix captured on first Tab press + std::vector chatTabMatches_; // matching command list + int chatTabMatchIdx_ = -1; // active match index (-1 = inactive) + + // Mention notification: plays a sound when the player's name appears in chat + size_t chatMentionSeenCount_ = 0; // how many messages have been scanned for mentions + // Chat tabs int activeChatTab_ = 0; struct ChatTab { std::string name; - uint32_t typeMask; // bitmask of ChatType values to show + uint64_t typeMask; // bitmask of ChatType values to show (64-bit: types go up to 84) }; std::vector chatTabs_; + std::vector chatTabUnread_; // unread message count per tab (0 = none) + size_t chatTabSeenCount_ = 0; // how many history messages have been processed void initChatTabs(); bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const; @@ -68,27 +91,65 @@ private: bool showEntityWindow = false; bool showChatWindow = true; bool showMinimap_ = true; // M key toggles minimap - bool showNameplates_ = true; // V key toggles nameplates + bool showNameplates_ = true; // V key toggles enemy/NPC nameplates + bool showFriendlyNameplates_ = true; // Shift+V toggles friendly player nameplates float nameplateScale_ = 1.0f; // Scale multiplier for nameplate bar dimensions uint64_t nameplateCtxGuid_ = 0; // GUID of nameplate right-clicked (0 = none) ImVec2 nameplateCtxPos_{}; // Screen position of nameplate right-click uint32_t lastPlayerHp_ = 0; // Previous frame HP for damage flash detection float damageFlashAlpha_ = 0.0f; // Screen edge flash intensity (fades to 0) bool damageFlashEnabled_ = true; + bool lowHealthVignetteEnabled_ = true; // Persistent pulsing red vignette below 20% HP float levelUpFlashAlpha_ = 0.0f; // Golden level-up burst effect (fades to 0) uint32_t levelUpDisplayLevel_ = 0; // Level shown in level-up text + // Raid Warning / Boss Emote big-text overlay (center-screen, fades after 5s) + struct RaidWarnEntry { + std::string text; + float age = 0.0f; + bool isBossEmote = false; // true = amber, false (raid warning) = red+yellow + static constexpr float LIFETIME = 5.0f; + }; + std::vector raidWarnEntries_; + bool raidWarnCallbackSet_ = false; + size_t raidWarnChatSeenCount_ = 0; // index into chat history for unread scan + // UIErrorsFrame: WoW-style center-bottom error messages (spell fails, out of range, etc.) struct UIErrorEntry { std::string text; float age = 0.0f; }; std::vector uiErrors_; bool uiErrorCallbackSet_ = false; static constexpr float kUIErrorLifetime = 2.5f; + bool castFailedCallbackSet_ = false; + static constexpr float kActionFlashDuration = 0.5f; // seconds for error-red overlay to fade // Reputation change toast: brief colored slide-in below minimap struct RepToastEntry { std::string factionName; int32_t delta = 0; int32_t standing = 0; float age = 0.0f; }; std::vector repToasts_; bool repChangeCallbackSet_ = false; static constexpr float kRepToastLifetime = 3.5f; + + // Quest completion toast: slide-in when a quest is turned in + struct QuestCompleteToastEntry { uint32_t questId = 0; std::string title; float age = 0.0f; }; + std::vector questCompleteToasts_; + bool questCompleteCallbackSet_ = false; + static constexpr float kQuestCompleteToastLifetime = 4.0f; + + // Zone entry toast: brief banner when entering a new zone + struct ZoneToastEntry { std::string zoneName; float age = 0.0f; }; + std::vector zoneToasts_; + + struct AreaTriggerToast { std::string text; float age = 0.0f; }; + std::vector areaTriggerToasts_; + void renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler); + std::string lastKnownZone_; + static constexpr float kZoneToastLifetime = 3.0f; + + // Death screen: elapsed time since the death dialog first appeared + float deathElapsed_ = 0.0f; + bool deathTimerRunning_ = false; + // WoW forces release after ~6 minutes; show countdown until then + static constexpr float kForcedReleaseSec = 360.0f; + void renderZoneToasts(float deltaTime); bool showPlayerInfo = false; bool showSocialFrame_ = false; // O key toggles social/friends list bool showGuildRoster_ = false; @@ -111,6 +172,10 @@ private: bool chatWindowLocked = true; ImVec2 chatWindowPos_ = ImVec2(0.0f, 0.0f); bool chatWindowPosInit_ = false; + ImVec2 questTrackerPos_ = ImVec2(-1.0f, -1.0f); // <0 = use default + ImVec2 questTrackerSize_ = ImVec2(220.0f, 200.0f); // saved size + float questTrackerRightOffset_ = -1.0f; // pixels from right edge; <0 = use default + bool questTrackerPosInit_ = false; bool showEscapeMenu = false; bool showEscapeSettingsNotice = false; bool showSettingsWindow = false; @@ -120,7 +185,8 @@ private: int pendingResIndex = 0; bool pendingShadows = true; float pendingShadowDistance = 300.0f; - bool pendingWaterRefraction = false; + bool pendingWaterRefraction = true; + int pendingBrightness = 50; // 0-100, maps to 0.0-2.0 (50 = 1.0 default) int pendingMasterVolume = 100; int pendingMusicVolume = 30; int pendingAmbientVolume = 100; @@ -142,11 +208,18 @@ private: bool pendingMinimapNpcDots = false; bool pendingShowLatencyMeter = true; bool pendingSeparateBags = true; + bool pendingShowKeyring = true; bool pendingAutoLoot = false; + bool pendingAutoSellGrey = false; + bool pendingAutoRepair = false; // Keybinding customization int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index bool awaitingKeyPress = false; + // Macro editor popup state + uint32_t macroEditorId_ = 0; // macro index being edited + bool macroEditorOpen_ = false; // deferred OpenPopup flag + char macroEditorBuf_[256] = {}; // edit buffer bool pendingUseOriginalSoundtrack = true; bool pendingShowActionBar2 = true; // Show second action bar above main bar float pendingActionBarScale = 1.0f; // Multiplier for action bar slot size (0.5–1.5) @@ -158,6 +231,7 @@ private: float pendingLeftBarOffsetY = 0.0f; // Vertical offset from screen center int pendingGroundClutterDensity = 100; int pendingAntiAliasing = 0; // 0=Off, 1=2x, 2=4x, 3=8x + bool pendingFXAA = false; // FXAA post-process (combinable with MSAA) bool pendingNormalMapping = true; // on by default float pendingNormalMapStrength = 0.8f; // 0.0-2.0 bool pendingPOM = true; // on by default @@ -192,6 +266,7 @@ private: bool minimapSettingsApplied_ = false; bool volumeSettingsApplied_ = false; // True once saved volume settings applied to audio managers bool msaaSettingsApplied_ = false; // True once saved MSAA setting applied to renderer + bool fxaaSettingsApplied_ = false; // True once saved FXAA setting applied to renderer bool waterRefractionApplied_ = false; bool normalMapSettingsApplied_ = false; // True once saved normal map/POM settings applied @@ -218,6 +293,7 @@ private: * Send chat message */ void sendChatMessage(game::GameHandler& gameHandler); + void executeMacroText(game::GameHandler& gameHandler, const std::string& macroText); /** * Get chat type name @@ -244,6 +320,7 @@ private: * Render pet frame (below player frame when player has an active pet) */ void renderPetFrame(game::GameHandler& gameHandler); + void renderTotemFrame(game::GameHandler& gameHandler); /** * Process targeting input (Tab, Escape, click) @@ -262,17 +339,23 @@ private: // ---- New UI renders ---- void renderActionBar(game::GameHandler& gameHandler); + void renderStanceBar(game::GameHandler& gameHandler); void renderBagBar(game::GameHandler& gameHandler); void renderXpBar(game::GameHandler& gameHandler); + void renderRepBar(game::GameHandler& gameHandler); void renderCastBar(game::GameHandler& gameHandler); void renderMirrorTimers(game::GameHandler& gameHandler); + void renderCooldownTracker(game::GameHandler& gameHandler); void renderCombatText(game::GameHandler& gameHandler); + void renderRaidWarningOverlay(game::GameHandler& gameHandler); void renderPartyFrames(game::GameHandler& gameHandler); void renderBossFrames(game::GameHandler& gameHandler); void renderUIErrors(game::GameHandler& gameHandler, float deltaTime); void renderRepToasts(float deltaTime); + void renderQuestCompleteToasts(float deltaTime); void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderDuelRequestPopup(game::GameHandler& gameHandler); + void renderDuelCountdown(game::GameHandler& gameHandler); void renderLootRollPopup(game::GameHandler& gameHandler); void renderTradeRequestPopup(game::GameHandler& gameHandler); void renderTradeWindow(game::GameHandler& gameHandler); @@ -288,13 +371,23 @@ private: void renderQuestOfferRewardWindow(game::GameHandler& gameHandler); void renderVendorWindow(game::GameHandler& gameHandler); void renderTrainerWindow(game::GameHandler& gameHandler); + void renderBarberShopWindow(game::GameHandler& gameHandler); + void renderStableWindow(game::GameHandler& gameHandler); void renderTaxiWindow(game::GameHandler& gameHandler); + void renderLogoutCountdown(game::GameHandler& gameHandler); void renderDeathScreen(game::GameHandler& gameHandler); void renderReclaimCorpseButton(game::GameHandler& gameHandler); void renderResurrectDialog(game::GameHandler& gameHandler); void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler); + void renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler); void renderEscapeMenu(); void renderSettingsWindow(); + void renderSettingsAudioTab(); + void renderSettingsChatTab(); + void renderSettingsAboutTab(); + void renderSettingsInterfaceTab(); + void renderSettingsGameplayTab(); + void renderSettingsControlsTab(); void applyGraphicsPreset(GraphicsPreset preset); void updateGraphicsPresetFromCurrentSettings(); void renderQuestMarkers(game::GameHandler& gameHandler); @@ -304,7 +397,9 @@ private: void renderGuildInvitePopup(game::GameHandler& gameHandler); void renderReadyCheckPopup(game::GameHandler& gameHandler); void renderBgInvitePopup(game::GameHandler& gameHandler); + void renderBfMgrInvitePopup(game::GameHandler& gameHandler); void renderLfgProposalPopup(game::GameHandler& gameHandler); + void renderLfgRoleCheckPopup(game::GameHandler& gameHandler); void renderChatBubbles(game::GameHandler& gameHandler); void renderMailWindow(game::GameHandler& gameHandler); void renderMailComposeWindow(game::GameHandler& gameHandler); @@ -312,10 +407,12 @@ private: void renderGuildBankWindow(game::GameHandler& gameHandler); void renderAuctionHouseWindow(game::GameHandler& gameHandler); void renderDungeonFinderWindow(game::GameHandler& gameHandler); - void renderObjectiveTracker(game::GameHandler& gameHandler); void renderInstanceLockouts(game::GameHandler& gameHandler); void renderNameplates(game::GameHandler& gameHandler); void renderBattlegroundScore(game::GameHandler& gameHandler); + void renderDPSMeter(game::GameHandler& gameHandler); + void renderDurabilityWarning(game::GameHandler& gameHandler); + void takeScreenshot(game::GameHandler& gameHandler); /** * Inventory screen @@ -338,6 +435,23 @@ private: bool spellIconDbLoaded_ = false; VkDescriptorSet getSpellIcon(uint32_t spellId, pipeline::AssetManager* am); + // ItemExtendedCost.dbc cache: extendedCostId -> cost details + struct ExtendedCostEntry { + uint32_t honorPoints = 0; + uint32_t arenaPoints = 0; + uint32_t itemId[5] = {}; + uint32_t itemCount[5] = {}; + }; + std::unordered_map extendedCostCache_; + bool extendedCostDbLoaded_ = false; + void loadExtendedCostDBC(); + std::string formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler); + + // Macro cooldown cache: maps macro ID → resolved primary spell ID (0 = no spell found) + std::unordered_map macroPrimarySpellCache_; + size_t macroCacheSpellCount_ = 0; // invalidates cache when spell list changes + uint32_t resolveMacroPrimarySpellId(uint32_t macroId, game::GameHandler& gameHandler); + // Death Knight rune bar: client-predicted fill (0.0=depleted, 1.0=ready) for smooth animation float runeClientFill_[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; @@ -351,6 +465,14 @@ private: int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none) int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none) + // Who Results window + bool showWhoWindow_ = false; + void renderWhoWindow(game::GameHandler& gameHandler); + + // Combat Log window + bool showCombatLog_ = false; + void renderCombatLog(game::GameHandler& gameHandler); + // Instance Lockouts window bool showInstanceLockouts_ = false; @@ -362,18 +484,44 @@ private: char achievementSearchBuf_[128] = {}; void renderAchievementWindow(game::GameHandler& gameHandler); + // Skills / Professions window (K key) + bool showSkillsWindow_ = false; + void renderSkillsWindow(game::GameHandler& gameHandler); + + // Titles window + bool showTitlesWindow_ = false; + void renderTitlesWindow(game::GameHandler& gameHandler); + + // Equipment Set Manager window + bool showEquipSetWindow_ = false; + void renderEquipSetWindow(game::GameHandler& gameHandler); + // GM Ticket window - bool showGmTicketWindow_ = false; + bool showGmTicketWindow_ = false; + bool gmTicketWindowWasOpen_ = false; ///< Previous frame state; used to fire one-shot query char gmTicketBuf_[2048] = {}; void renderGmTicketWindow(game::GameHandler& gameHandler); + // Pet rename modal (triggered from pet frame context menu) + bool petRenameOpen_ = false; + char petRenameBuf_[16] = {}; + // Inspect window bool showInspectWindow_ = false; void renderInspectWindow(game::GameHandler& gameHandler); + // Readable text window (books / scrolls / notes) + bool showBookWindow_ = false; + int bookCurrentPage_ = 0; + void renderBookWindow(game::GameHandler& gameHandler); + // Threat window bool showThreatWindow_ = false; void renderThreatWindow(game::GameHandler& gameHandler); + + // BG scoreboard window + bool showBgScoreboard_ = false; + void renderBgScoreboard(game::GameHandler& gameHandler); uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps) uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK) @@ -416,6 +564,24 @@ private: // Vendor search filter char vendorSearchFilter_[128] = ""; + // Vendor purchase confirmation for expensive items + bool vendorConfirmOpen_ = false; + uint64_t vendorConfirmGuid_ = 0; + uint32_t vendorConfirmItemId_ = 0; + uint32_t vendorConfirmSlot_ = 0; + uint32_t vendorConfirmQty_ = 1; + uint32_t vendorConfirmPrice_ = 0; + std::string vendorConfirmItemName_; + + // Barber shop UI state + int barberHairStyle_ = 0; + int barberHairColor_ = 0; + int barberFacialHair_ = 0; + int barberOrigHairStyle_ = 0; + int barberOrigHairColor_ = 0; + int barberOrigFacialHair_ = 0; + bool barberInitialized_ = false; + // Trainer search filter char trainerSearchFilter_[128] = ""; @@ -432,6 +598,7 @@ private: uint32_t auctionBrowseOffset_ = 0; // Pagination offset for browse results int auctionItemClass_ = -1; // Item class filter (-1 = All) int auctionItemSubClass_ = -1; // Item subclass filter (-1 = All) + bool auctionUsableOnly_ = false; // Filter to items usable by current class/level // Guild bank money input int guildBankMoneyInput_[3] = {0, 0, 0}; // gold, silver, copper @@ -441,9 +608,12 @@ private: bool leftClickWasPress_ = false; // Level-up ding animation - static constexpr float DING_DURATION = 3.0f; + static constexpr float DING_DURATION = 4.0f; float dingTimer_ = 0.0f; uint32_t dingLevel_ = 0; + uint32_t dingHpDelta_ = 0; + uint32_t dingManaDelta_ = 0; + uint32_t dingStats_[5] = {}; // str/agi/sta/int/spi deltas void renderDingEffect(); // Achievement toast banner @@ -453,16 +623,110 @@ private: std::string achievementToastName_; void renderAchievementToast(); + // Area discovery toast ("Discovered! +XP XP") + static constexpr float DISCOVERY_TOAST_DURATION = 4.0f; + float discoveryToastTimer_ = 0.0f; + std::string discoveryToastName_; + uint32_t discoveryToastXP_ = 0; + bool areaDiscoveryCallbackSet_ = false; + void renderDiscoveryToast(); + + // Whisper toast — brief overlay at screen top when a whisper arrives while chat is not focused + struct WhisperToastEntry { + std::string sender; + std::string preview; // first ~60 chars of message + float age = 0.0f; + }; + static constexpr float WHISPER_TOAST_DURATION = 5.0f; + std::vector whisperToasts_; + size_t whisperSeenCount_ = 0; // how many chat entries have been scanned for whispers + void renderWhisperToasts(); + + // Quest objective progress toast ("Quest: X/Y") + struct QuestProgressToastEntry { + std::string questTitle; + std::string objectiveName; + uint32_t current = 0; + uint32_t required = 0; + float age = 0.0f; + }; + static constexpr float QUEST_TOAST_DURATION = 4.0f; + std::vector questToasts_; + bool questProgressCallbackSet_ = false; + void renderQuestProgressToasts(); + + // Nearby player level-up toast (" is now level X!") + struct PlayerLevelUpToastEntry { + uint64_t guid = 0; + std::string playerName; // resolved lazily at render time + uint32_t newLevel = 0; + float age = 0.0f; + }; + static constexpr float PLAYER_LEVELUP_TOAST_DURATION = 4.0f; + std::vector playerLevelUpToasts_; + bool otherPlayerLevelUpCallbackSet_ = false; + void renderPlayerLevelUpToasts(game::GameHandler& gameHandler); + + // PvP honor credit toast ("+N Honor" shown when an honorable kill is credited) + struct PvpHonorToastEntry { + uint32_t honor = 0; + uint32_t victimRank = 0; // 0 = unranked / not available + float age = 0.0f; + }; + static constexpr float PVP_HONOR_TOAST_DURATION = 3.5f; + std::vector pvpHonorToasts_; + bool pvpHonorCallbackSet_ = false; + void renderPvpHonorToasts(); + + // Item loot toast — quality-coloured popup when an item is received + struct ItemLootToastEntry { + uint32_t itemId = 0; + uint32_t count = 0; + uint32_t quality = 1; // 0=grey,1=white,2=green,3=blue,4=purple,5=orange + std::string name; + float age = 0.0f; + }; + static constexpr float ITEM_LOOT_TOAST_DURATION = 3.0f; + std::vector itemLootToasts_; + bool itemLootCallbackSet_ = false; + void renderItemLootToasts(); + + // Resurrection flash: brief "You have been resurrected!" overlay on ghost→alive transition + float resurrectFlashTimer_ = 0.0f; + static constexpr float kResurrectFlashDuration = 3.0f; + bool ghostStateCallbackSet_ = false; + bool appearanceCallbackSet_ = false; + bool ghostOpacityStateKnown_ = false; + bool ghostOpacityLastState_ = false; + uint32_t ghostOpacityLastInstanceId_ = 0; + void renderResurrectFlash(); + // Zone discovery text ("Entering: ") static constexpr float ZONE_TEXT_DURATION = 5.0f; float zoneTextTimer_ = 0.0f; std::string zoneTextName_; std::string lastKnownZoneName_; - void renderZoneText(); + uint32_t lastKnownWorldStateZoneId_ = 0; + void renderZoneText(game::GameHandler& gameHandler); + void renderWeatherOverlay(game::GameHandler& gameHandler); + + // Cooldown tracker + bool showCooldownTracker_ = false; + + // DPS / HPS meter + bool showDPSMeter_ = false; + float dpsCombatAge_ = 0.0f; // seconds in current combat (for accurate early-combat DPS) + bool dpsWasInCombat_ = false; + float dpsEncounterDamage_ = 0.0f; // total player damage this combat + float dpsEncounterHeal_ = 0.0f; // total player healing this combat + size_t dpsLogSeenCount_ = 0; // log entries already scanned public: - void triggerDing(uint32_t newLevel); + void triggerDing(uint32_t newLevel, uint32_t hpDelta = 0, uint32_t manaDelta = 0, + uint32_t str = 0, uint32_t agi = 0, uint32_t sta = 0, + uint32_t intel = 0, uint32_t spi = 0); void triggerAchievementToast(uint32_t achievementId, std::string name = {}); + void openDungeonFinder() { showDungeonFinder_ = true; } }; } // namespace ui diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 3453e966..d350f210 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ public: bool isSeparateBags() const { return separateBags_; } void toggleCompactBags() { compactBags_ = !compactBags_; } bool isCompactBags() const { return compactBags_; } + void setShowKeyring(bool show) { showKeyring_ = show; } + bool isShowKeyring() const { return showKeyring_; } bool isBackpackOpen() const { return backpackOpen_; } bool isBagOpen(int idx) const { return idx >= 0 && idx < 4 ? bagOpen_[idx] : false; } @@ -79,6 +82,7 @@ private: bool bKeyWasDown = false; bool separateBags_ = true; bool compactBags_ = false; + bool showKeyring_ = true; bool backpackOpen_ = false; std::array bagOpen_{}; bool cKeyWasDown = false; @@ -96,7 +100,7 @@ private: std::unordered_map iconCache_; public: VkDescriptorSet getItemIcon(uint32_t displayInfoId); - void renderItemTooltip(const game::ItemQueryResponseData& info); + void renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory = nullptr, uint64_t itemGuid = 0); private: // Character model preview @@ -149,7 +153,8 @@ private: void renderEquipmentPanel(game::Inventory& inventory); void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false); void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0, - const int32_t* serverStats = nullptr); + const int32_t* serverStats = nullptr, const int32_t* serverResists = nullptr, + const game::GameHandler* gh = nullptr); void renderReputationPanel(game::GameHandler& gameHandler); void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot, @@ -157,7 +162,7 @@ private: SlotKind kind, int backpackIndex, game::EquipSlot equipSlot, int bagIndex = -1, int bagSlotIndex = -1); - void renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory = nullptr); + void renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory = nullptr, uint64_t itemGuid = 0); // Held item helpers void pickupFromBackpack(game::Inventory& inv, int index); @@ -183,6 +188,17 @@ private: uint8_t destroyCount_ = 1; std::string destroyItemName_; + // Stack split popup state + bool splitConfirmOpen_ = false; + uint8_t splitBag_ = 0xFF; + uint8_t splitSlot_ = 0; + int splitMax_ = 1; + int splitCount_ = 1; + std::string splitItemName_; + + // Server-side bag sort swap queue (one swap per frame) + std::deque sortSwapQueue_; + // Pending chat item link from shift-click std::string pendingChatItemLink_; diff --git a/include/ui/keybinding_manager.hpp b/include/ui/keybinding_manager.hpp index 385340ab..9a1320a9 100644 --- a/include/ui/keybinding_manager.hpp +++ b/include/ui/keybinding_manager.hpp @@ -1,5 +1,4 @@ -#ifndef WOWEE_KEYBINDING_MANAGER_HPP -#define WOWEE_KEYBINDING_MANAGER_HPP +#pragma once #include #include @@ -29,8 +28,8 @@ public: TOGGLE_WORLD_MAP, TOGGLE_NAMEPLATES, TOGGLE_RAID_FRAMES, - TOGGLE_QUEST_LOG, TOGGLE_ACHIEVEMENTS, + TOGGLE_SKILLS, ACTION_COUNT }; @@ -86,5 +85,3 @@ private: }; } // namespace wowee::ui - -#endif // WOWEE_KEYBINDING_MANAGER_HPP diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 6cc13270..2bc0f866 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -54,6 +54,16 @@ public: uint32_t getDragSpellId() const { return dragSpellId_; } void consumeDragSpell() { draggingSpell_ = false; dragSpellId_ = 0; dragSpellIconTex_ = VK_NULL_HANDLE; } + /// Returns the max range in yards for a spell (0 if self-cast, unknown, or melee). + /// Triggers DBC load if needed. Used by the action bar for out-of-range tinting. + uint32_t getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager); + + /// Returns the power cost and type for a spell (cost=0 if unknown/free). + /// powerType: 0=mana, 1=rage, 2=focus, 3=energy, 6=runic power. + /// Triggers DBC load if needed. Used by the action bar for insufficient-power tinting. + void getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager, + uint32_t& outCost, uint32_t& outPowerType); + /// Returns a WoW spell link string if the user shift-clicked a spell, then clears it. std::string getAndClearPendingChatLink() { std::string out = std::move(pendingChatSpellLink_); diff --git a/include/ui/talent_screen.hpp b/include/ui/talent_screen.hpp index 18bbe152..82a674e4 100644 --- a/include/ui/talent_screen.hpp +++ b/include/ui/talent_screen.hpp @@ -28,6 +28,8 @@ private: void loadSpellDBC(pipeline::AssetManager* assetManager); void loadSpellIconDBC(pipeline::AssetManager* assetManager); + void loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager); + void renderGlyphs(game::GameHandler& gameHandler); VkDescriptorSet getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager); bool open = false; @@ -36,11 +38,22 @@ private: // DBC caches bool spellDbcLoaded = false; bool iconDbcLoaded = false; + bool glyphDbcLoaded = false; std::unordered_map spellIconIds; // spellId -> iconId std::unordered_map spellIconPaths; // iconId -> path std::unordered_map spellIconCache; // iconId -> texture std::unordered_map spellTooltips; // spellId -> description std::unordered_map bgTextureCache_; // tabId -> bg texture + + // Talent learn confirmation + bool talentConfirmOpen_ = false; + uint32_t pendingTalentId_ = 0; + uint32_t pendingTalentRank_ = 0; + std::string pendingTalentName_; + + // GlyphProperties.dbc cache: glyphId -> { spellId, isMajor } + struct GlyphInfo { uint32_t spellId = 0; bool isMajor = false; }; + std::unordered_map glyphProperties_; // glyphId -> info }; } // namespace ui diff --git a/include/ui/ui_colors.hpp b/include/ui/ui_colors.hpp new file mode 100644 index 00000000..ef1e02f0 --- /dev/null +++ b/include/ui/ui_colors.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include "game/inventory.hpp" + +namespace wowee::ui { + +// ---- Common UI colors ---- +namespace colors { + constexpr ImVec4 kRed = {1.0f, 0.3f, 0.3f, 1.0f}; + constexpr ImVec4 kGreen = {0.4f, 1.0f, 0.4f, 1.0f}; + constexpr ImVec4 kBrightGreen = {0.3f, 1.0f, 0.3f, 1.0f}; + constexpr ImVec4 kYellow = {1.0f, 1.0f, 0.3f, 1.0f}; + constexpr ImVec4 kGray = {0.6f, 0.6f, 0.6f, 1.0f}; + constexpr ImVec4 kDarkGray = {0.5f, 0.5f, 0.5f, 1.0f}; + constexpr ImVec4 kLightGray = {0.7f, 0.7f, 0.7f, 1.0f}; + constexpr ImVec4 kWhite = {1.0f, 1.0f, 1.0f, 1.0f}; + + // Coin colors + constexpr ImVec4 kGold = {1.00f, 0.82f, 0.00f, 1.0f}; + constexpr ImVec4 kSilver = {0.80f, 0.80f, 0.80f, 1.0f}; + constexpr ImVec4 kCopper = {0.72f, 0.45f, 0.20f, 1.0f}; +} // namespace colors + +// ---- Item quality colors ---- +inline ImVec4 getQualityColor(game::ItemQuality quality) { + switch (quality) { + case game::ItemQuality::POOR: return {0.62f, 0.62f, 0.62f, 1.0f}; + case game::ItemQuality::COMMON: return {1.0f, 1.0f, 1.0f, 1.0f}; + case game::ItemQuality::UNCOMMON: return {0.12f, 1.0f, 0.0f, 1.0f}; + case game::ItemQuality::RARE: return {0.0f, 0.44f, 0.87f, 1.0f}; + case game::ItemQuality::EPIC: return {0.64f, 0.21f, 0.93f, 1.0f}; + case game::ItemQuality::LEGENDARY: return {1.0f, 0.50f, 0.0f, 1.0f}; + case game::ItemQuality::ARTIFACT: return {0.90f, 0.80f, 0.50f, 1.0f}; + case game::ItemQuality::HEIRLOOM: return {0.90f, 0.80f, 0.50f, 1.0f}; + default: return {1.0f, 1.0f, 1.0f, 1.0f}; + } +} + +// ---- Coin display (gold/silver/copper) ---- +inline void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { + bool any = false; + if (g > 0) { + ImGui::TextColored(colors::kGold, "%ug", g); + any = true; + } + if (s > 0 || g > 0) { + if (any) ImGui::SameLine(0, 3); + ImGui::TextColored(colors::kSilver, "%us", s); + any = true; + } + if (any) ImGui::SameLine(0, 3); + ImGui::TextColored(colors::kCopper, "%uc", c); +} + +// Convenience overload: decompose copper amount and render as gold/silver/copper +inline void renderCoinsFromCopper(uint64_t copper) { + renderCoinsText(static_cast(copper / 10000), + static_cast((copper / 100) % 100), + static_cast(copper % 100)); +} + +} // namespace wowee::ui diff --git a/src/addons/addon_manager.cpp b/src/addons/addon_manager.cpp new file mode 100644 index 00000000..ca91e92d --- /dev/null +++ b/src/addons/addon_manager.cpp @@ -0,0 +1,173 @@ +#include "addons/addon_manager.hpp" +#include "core/logger.hpp" +#include +#include + +namespace fs = std::filesystem; + +namespace wowee::addons { + +AddonManager::AddonManager() = default; +AddonManager::~AddonManager() { shutdown(); } + +bool AddonManager::initialize(game::GameHandler* gameHandler) { + gameHandler_ = gameHandler; + if (!luaEngine_.initialize()) return false; + luaEngine_.setGameHandler(gameHandler); + return true; +} + +void AddonManager::scanAddons(const std::string& addonsPath) { + addonsPath_ = addonsPath; + addons_.clear(); + + std::error_code ec; + if (!fs::is_directory(addonsPath, ec)) { + LOG_INFO("AddonManager: no AddOns directory at ", addonsPath); + return; + } + + std::vector dirs; + for (const auto& entry : fs::directory_iterator(addonsPath, ec)) { + if (entry.is_directory()) dirs.push_back(entry.path()); + } + // Sort alphabetically for deterministic load order + std::sort(dirs.begin(), dirs.end()); + + for (const auto& dir : dirs) { + std::string dirName = dir.filename().string(); + std::string tocPath = (dir / (dirName + ".toc")).string(); + auto toc = parseTocFile(tocPath); + if (!toc) continue; + + if (toc->isLoadOnDemand()) { + LOG_DEBUG("AddonManager: skipping LoadOnDemand addon: ", dirName); + continue; + } + + LOG_INFO("AddonManager: registered addon '", toc->getTitle(), + "' (", toc->files.size(), " files)"); + addons_.push_back(std::move(*toc)); + } + + LOG_INFO("AddonManager: scanned ", addons_.size(), " addons"); +} + +void AddonManager::loadAllAddons() { + luaEngine_.setAddonList(addons_); + int loaded = 0, failed = 0; + for (const auto& addon : addons_) { + if (loadAddon(addon)) loaded++; + else failed++; + } + LOG_INFO("AddonManager: loaded ", loaded, " addons", + (failed > 0 ? (", " + std::to_string(failed) + " failed") : "")); +} + +std::string AddonManager::getSavedVariablesPath(const TocFile& addon) const { + return addon.basePath + "/" + addon.addonName + ".lua.saved"; +} + +std::string AddonManager::getSavedVariablesPerCharacterPath(const TocFile& addon) const { + if (characterName_.empty()) return ""; + return addon.basePath + "/" + addon.addonName + "." + characterName_ + ".lua.saved"; +} + +bool AddonManager::loadAddon(const TocFile& addon) { + // Load SavedVariables before addon code (so globals are available at load time) + auto savedVars = addon.getSavedVariables(); + if (!savedVars.empty()) { + std::string svPath = getSavedVariablesPath(addon); + luaEngine_.loadSavedVariables(svPath); + LOG_DEBUG("AddonManager: loaded saved variables for '", addon.addonName, "'"); + } + // Load per-character SavedVariables + auto savedVarsPC = addon.getSavedVariablesPerCharacter(); + if (!savedVarsPC.empty()) { + std::string svpcPath = getSavedVariablesPerCharacterPath(addon); + if (!svpcPath.empty()) { + luaEngine_.loadSavedVariables(svpcPath); + LOG_DEBUG("AddonManager: loaded per-character saved variables for '", addon.addonName, "'"); + } + } + + bool success = true; + for (const auto& filename : addon.files) { + std::string lower = filename; + for (char& c : lower) c = static_cast(std::tolower(static_cast(c))); + + if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".lua") { + std::string fullPath = addon.basePath + "/" + filename; + if (!luaEngine_.executeFile(fullPath)) { + success = false; + } + } else if (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".xml") { + LOG_DEBUG("AddonManager: skipping XML file '", filename, + "' in addon '", addon.addonName, "' (XML frames not yet implemented)"); + } + } + + // Fire ADDON_LOADED event after all addon files are executed + // This is the standard WoW pattern for addon initialization + if (success) { + luaEngine_.fireEvent("ADDON_LOADED", {addon.addonName}); + } + return success; +} + +bool AddonManager::runScript(const std::string& code) { + return luaEngine_.executeString(code); +} + +void AddonManager::fireEvent(const std::string& event, const std::vector& args) { + luaEngine_.fireEvent(event, args); +} + +void AddonManager::update(float deltaTime) { + luaEngine_.dispatchOnUpdate(deltaTime); +} + +void AddonManager::saveAllSavedVariables() { + for (const auto& addon : addons_) { + auto savedVars = addon.getSavedVariables(); + if (!savedVars.empty()) { + std::string svPath = getSavedVariablesPath(addon); + luaEngine_.saveSavedVariables(svPath, savedVars); + } + auto savedVarsPC = addon.getSavedVariablesPerCharacter(); + if (!savedVarsPC.empty()) { + std::string svpcPath = getSavedVariablesPerCharacterPath(addon); + if (!svpcPath.empty()) { + luaEngine_.saveSavedVariables(svpcPath, savedVarsPC); + } + } + } +} + +bool AddonManager::reload() { + LOG_INFO("AddonManager: reloading all addons..."); + saveAllSavedVariables(); + addons_.clear(); + luaEngine_.shutdown(); + + if (!luaEngine_.initialize()) { + LOG_ERROR("AddonManager: failed to reinitialize Lua VM during reload"); + return false; + } + luaEngine_.setGameHandler(gameHandler_); + + if (!addonsPath_.empty()) { + scanAddons(addonsPath_); + loadAllAddons(); + } + LOG_INFO("AddonManager: reload complete"); + return true; +} + +void AddonManager::shutdown() { + saveAllSavedVariables(); + addons_.clear(); + luaEngine_.shutdown(); +} + +} // namespace wowee::addons diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp new file mode 100644 index 00000000..3f16e67a --- /dev/null +++ b/src/addons/lua_engine.cpp @@ -0,0 +1,7709 @@ +#include "addons/lua_engine.hpp" +#include "addons/toc_parser.hpp" +#include "game/game_handler.hpp" +#include "game/entity.hpp" +#include "game/update_field_table.hpp" +#include "core/logger.hpp" +#include "core/application.hpp" +#include "rendering/renderer.hpp" +#include "audio/ui_sound_manager.hpp" +#include "game/expansion_profile.hpp" +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +namespace wowee::addons { + +static void toLowerInPlace(std::string& s) { + for (char& c : s) c = static_cast(std::tolower(static_cast(c))); +} + +// Lua return helpers — used 200+ times as guard/fallback returns +static int luaReturnNil(lua_State* L) { return luaReturnNil(L); } +static int luaReturnZero(lua_State* L) { return luaReturnZero(L); } +static int luaReturnFalse(lua_State* L){ return luaReturnFalse(L); } + +// Shared GetTime() epoch — all time-returning functions must use this same origin +// so that addon calculations like (start + duration - GetTime()) are consistent. +static const auto kLuaTimeEpoch = std::chrono::steady_clock::now(); + +static double luaGetTimeNow() { + return std::chrono::duration(std::chrono::steady_clock::now() - kLuaTimeEpoch).count(); +} + +// Retrieve GameHandler pointer stored in Lua registry +static game::GameHandler* getGameHandler(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, "wowee_game_handler"); + auto* gh = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return gh; +} + +// WoW-compatible print() — outputs to chat window instead of stdout +static int lua_wow_print(lua_State* L) { + int nargs = lua_gettop(L); + std::string result; + for (int i = 1; i <= nargs; i++) { + if (i > 1) result += '\t'; + // Lua 5.1: use lua_tostring (luaL_tolstring is 5.3+) + if (lua_isstring(L, i) || lua_isnumber(L, i)) { + const char* s = lua_tostring(L, i); + if (s) result += s; + } else if (lua_isboolean(L, i)) { + result += lua_toboolean(L, i) ? "true" : "false"; + } else if (lua_isnil(L, i)) { + result += "nil"; + } else { + result += lua_typename(L, lua_type(L, i)); + } + } + + auto* gh = getGameHandler(L); + if (gh) { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = result; + gh->addLocalChatMessage(msg); + } + LOG_INFO("[Lua] ", result); + return 0; +} + +// WoW-compatible message() — same as print for now +static int lua_wow_message(lua_State* L) { + return lua_wow_print(L); +} + +// Helper: resolve WoW unit IDs to GUID +// Read UNIT_FIELD_TARGET_LO/HI from an entity's update fields to get what it's targeting +static uint64_t getEntityTargetGuid(game::GameHandler* gh, uint64_t guid) { + if (guid == 0) return 0; + // If asking for the player's target, use direct accessor + if (guid == gh->getPlayerGuid()) return gh->getTargetGuid(); + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity) return 0; + const auto& fields = entity->getFields(); + auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (loIt == fields.end()) return 0; + uint64_t targetGuid = loIt->second; + auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (hiIt != fields.end()) + targetGuid |= (static_cast(hiIt->second) << 32); + return targetGuid; +} + +static uint64_t resolveUnitGuid(game::GameHandler* gh, const std::string& uid) { + if (uid == "player") return gh->getPlayerGuid(); + if (uid == "target") return gh->getTargetGuid(); + if (uid == "focus") return gh->getFocusGuid(); + if (uid == "mouseover") return gh->getMouseoverGuid(); + if (uid == "pet") return gh->getPetGuid(); + // Compound unit IDs: targettarget, focustarget, pettarget, mouseovertarget + if (uid == "targettarget") return getEntityTargetGuid(gh, gh->getTargetGuid()); + if (uid == "focustarget") return getEntityTargetGuid(gh, gh->getFocusGuid()); + if (uid == "pettarget") return getEntityTargetGuid(gh, gh->getPetGuid()); + if (uid == "mouseovertarget") return getEntityTargetGuid(gh, gh->getMouseoverGuid()); + // party1-party4, raid1-raid40 + if (uid.rfind("party", 0) == 0 && uid.size() > 5) { + int idx = 0; + try { idx = std::stoi(uid.substr(5)); } catch (...) { return 0; } + if (idx < 1 || idx > 4) return 0; + const auto& pd = gh->getPartyData(); + // party members exclude self; index 1-based + int found = 0; + for (const auto& m : pd.members) { + if (m.guid == gh->getPlayerGuid()) continue; + if (++found == idx) return m.guid; + } + return 0; + } + if (uid.rfind("raid", 0) == 0 && uid.size() > 4 && uid[4] != 'p') { + int idx = 0; + try { idx = std::stoi(uid.substr(4)); } catch (...) { return 0; } + if (idx < 1 || idx > 40) return 0; + const auto& pd = gh->getPartyData(); + if (idx <= static_cast(pd.members.size())) + return pd.members[idx - 1].guid; + return 0; + } + return 0; +} + +// Helper: resolve unit IDs (player, target, focus, mouseover, pet, targettarget, focustarget, etc.) to entity +static game::Unit* resolveUnit(lua_State* L, const char* unitId) { + auto* gh = getGameHandler(L); + if (!gh || !unitId) return nullptr; + std::string uid(unitId); + toLowerInPlace(uid); + + uint64_t guid = resolveUnitGuid(gh, uid); + if (guid == 0) return nullptr; + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity) return nullptr; + return dynamic_cast(entity.get()); +} + +// --- WoW Unit API --- + +// Helper: find GroupMember data for a GUID (for party members out of entity range) +static const game::GroupMember* findPartyMember(game::GameHandler* gh, uint64_t guid) { + if (!gh || guid == 0) return nullptr; + for (const auto& m : gh->getPartyData().members) { + if (m.guid == guid && m.hasPartyStats) return &m; + } + return nullptr; +} + +static int lua_UnitName(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit && !unit->getName().empty()) { + lua_pushstring(L, unit->getName().c_str()); + } else { + // Fallback: party member name for out-of-range members + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + if (pm && !pm->name.empty()) { + lua_pushstring(L, pm->name.c_str()); + } else if (gh && guid != 0) { + // Try player name cache + const std::string& cached = gh->lookupName(guid); + lua_pushstring(L, cached.empty() ? "Unknown" : cached.c_str()); + } else { + lua_pushstring(L, "Unknown"); + } + } + return 1; +} + + +static int lua_UnitHealth(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushnumber(L, unit->getHealth()); + } else { + // Fallback: party member stats for out-of-range members + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + lua_pushnumber(L, pm ? pm->curHealth : 0); + } + return 1; +} + +static int lua_UnitHealthMax(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushnumber(L, unit->getMaxHealth()); + } else { + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + lua_pushnumber(L, pm ? pm->maxHealth : 0); + } + return 1; +} + +static int lua_UnitPower(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushnumber(L, unit->getPower()); + } else { + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + lua_pushnumber(L, pm ? pm->curPower : 0); + } + return 1; +} + +static int lua_UnitPowerMax(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushnumber(L, unit->getMaxPower()); + } else { + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + lua_pushnumber(L, pm ? pm->maxPower : 0); + } + return 1; +} + +static int lua_UnitLevel(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushnumber(L, unit->getLevel()); + } else { + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + lua_pushnumber(L, pm ? pm->level : 0); + } + return 1; +} + +static int lua_UnitExists(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushboolean(L, 1); + } else { + // Party members in other zones don't have entities but still "exist" + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + lua_pushboolean(L, guid != 0 && findPartyMember(gh, guid) != nullptr); + } + return 1; +} + +static int lua_UnitIsDead(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + if (unit) { + lua_pushboolean(L, unit->getHealth() == 0); + } else { + // Fallback: party member stats for out-of-range members + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + lua_pushboolean(L, pm ? (pm->curHealth == 0 && pm->maxHealth > 0) : 0); + } + return 1; +} + +static int lua_UnitClass(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + auto* unit = resolveUnit(L, uid); + if (unit && gh) { + static const char* kClasses[] = {"", "Warrior","Paladin","Hunter","Rogue","Priest", + "Death Knight","Shaman","Mage","Warlock","","Druid"}; + uint8_t classId = 0; + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr == "player") { + classId = gh->getPlayerClass(); + } else { + // Read class from UNIT_FIELD_BYTES_0 (class is byte 1) + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + uint32_t bytes0 = entity->getField( + game::fieldIndex(game::UF::UNIT_FIELD_BYTES_0)); + classId = static_cast((bytes0 >> 8) & 0xFF); + } + } + // Fallback: check name query class/race cache + if (classId == 0 && guid != 0) { + classId = gh->lookupPlayerClass(guid); + } + } + const char* name = (classId > 0 && classId < 12) ? kClasses[classId] : "Unknown"; + lua_pushstring(L, name); + lua_pushstring(L, name); // WoW returns localized + English + lua_pushnumber(L, classId); + return 3; + } + lua_pushstring(L, "Unknown"); + lua_pushstring(L, "Unknown"); + lua_pushnumber(L, 0); + return 3; +} + +// UnitIsGhost(unit) — true if unit is in ghost form +static int lua_UnitIsGhost(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr == "player") { + lua_pushboolean(L, gh->isPlayerGhost()); + } else { + // Check UNIT_FIELD_FLAGS for UNIT_FLAG_GHOST (0x00000100) — best approximation + uint64_t guid = resolveUnitGuid(gh, uidStr); + bool ghost = false; + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + uint32_t flags = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + ghost = (flags & 0x00000100) != 0; // PLAYER_FLAGS_GHOST + } + } + lua_pushboolean(L, ghost); + } + return 1; +} + +// UnitIsDeadOrGhost(unit) +static int lua_UnitIsDeadOrGhost(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + auto* gh = getGameHandler(L); + bool dead = (unit && unit->getHealth() == 0); + if (!dead && gh) { + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr == "player") dead = gh->isPlayerGhost() || gh->isPlayerDead(); + } + lua_pushboolean(L, dead); + return 1; +} + +// UnitIsAFK(unit), UnitIsDND(unit) +static int lua_UnitIsAFK(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + // PLAYER_FLAGS at UNIT_FIELD_FLAGS: PLAYER_FLAGS_AFK = 0x01 + uint32_t playerFlags = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + lua_pushboolean(L, (playerFlags & 0x01) != 0); + return 1; + } + } + lua_pushboolean(L, 0); + return 1; +} + +static int lua_UnitIsDND(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + uint32_t playerFlags = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + lua_pushboolean(L, (playerFlags & 0x02) != 0); // PLAYER_FLAGS_DND + return 1; + } + } + lua_pushboolean(L, 0); + return 1; +} + +// UnitPlayerControlled(unit) — true for players and player-controlled pets +static int lua_UnitPlayerControlled(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnFalse(L); } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity) { return luaReturnFalse(L); } + // Players are always player-controlled; pets check UNIT_FLAG_PLAYER_CONTROLLED (0x01000000) + if (entity->getType() == game::ObjectType::PLAYER) { + lua_pushboolean(L, 1); + } else { + uint32_t flags = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + lua_pushboolean(L, (flags & 0x01000000) != 0); + } + return 1; +} + +// UnitIsTapped(unit) — true if mob is tapped (tagged by any player) +static int lua_UnitIsTapped(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "target"); + auto* unit = resolveUnit(L, uid); + if (!unit) { return luaReturnFalse(L); } + lua_pushboolean(L, (unit->getDynamicFlags() & 0x0004) != 0); // UNIT_DYNFLAG_TAPPED_BY_PLAYER + return 1; +} + +// UnitIsTappedByPlayer(unit) — true if tapped by the local player (can loot) +static int lua_UnitIsTappedByPlayer(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "target"); + auto* unit = resolveUnit(L, uid); + if (!unit) { return luaReturnFalse(L); } + uint32_t df = unit->getDynamicFlags(); + // Tapped by player: has TAPPED flag but also LOOTABLE or TAPPED_BY_ALL + bool tapped = (df & 0x0004) != 0; + bool lootable = (df & 0x0001) != 0; + bool sharedTag = (df & 0x0008) != 0; + lua_pushboolean(L, tapped && (lootable || sharedTag)); + return 1; +} + +// UnitIsTappedByAllThreatList(unit) — true if shared-tag mob +static int lua_UnitIsTappedByAllThreatList(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "target"); + auto* unit = resolveUnit(L, uid); + if (!unit) { return luaReturnFalse(L); } + lua_pushboolean(L, (unit->getDynamicFlags() & 0x0008) != 0); + return 1; +} + +// UnitThreatSituation(unit, mobUnit) → 0=not tanking, 1=not tanking but threat, 2=insecurely tanking, 3=securely tanking +static int lua_UnitThreatSituation(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + const char* uid = luaL_optstring(L, 1, "player"); + const char* mobUid = luaL_optstring(L, 2, nullptr); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t playerUnitGuid = resolveUnitGuid(gh, uidStr); + if (playerUnitGuid == 0) { return luaReturnZero(L); } + // If no mob specified, check general combat threat against current target + uint64_t mobGuid = 0; + if (mobUid && *mobUid) { + std::string mStr(mobUid); + toLowerInPlace(mStr); + mobGuid = resolveUnitGuid(gh, mStr); + } + // Approximate threat: check if the mob is targeting this unit + if (mobGuid != 0) { + auto mobEntity = gh->getEntityManager().getEntity(mobGuid); + if (mobEntity) { + const auto& fields = mobEntity->getFields(); + auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (loIt != fields.end()) { + uint64_t mobTarget = loIt->second; + auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (hiIt != fields.end()) + mobTarget |= (static_cast(hiIt->second) << 32); + if (mobTarget == playerUnitGuid) { + lua_pushnumber(L, 3); // securely tanking + return 1; + } + } + } + } + // Check if player is in combat (basic threat indicator) + if (playerUnitGuid == gh->getPlayerGuid() && gh->isInCombat()) { + lua_pushnumber(L, 1); // in combat but not tanking + return 1; + } + lua_pushnumber(L, 0); + return 1; +} + +// UnitDetailedThreatSituation(unit, mobUnit) → isTanking, status, threatPct, rawThreatPct, threatValue +static int lua_UnitDetailedThreatSituation(lua_State* L) { + // Use UnitThreatSituation logic for the basics + auto* gh = getGameHandler(L); + if (!gh) { + lua_pushboolean(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 0); + return 5; + } + const char* uid = luaL_optstring(L, 1, "player"); + const char* mobUid = luaL_optstring(L, 2, nullptr); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t unitGuid = resolveUnitGuid(gh, uidStr); + bool isTanking = false; + int status = 0; + if (unitGuid != 0 && mobUid && *mobUid) { + std::string mStr(mobUid); + toLowerInPlace(mStr); + uint64_t mobGuid = resolveUnitGuid(gh, mStr); + if (mobGuid != 0) { + auto mobEnt = gh->getEntityManager().getEntity(mobGuid); + if (mobEnt) { + const auto& f = mobEnt->getFields(); + auto lo = f.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (lo != f.end()) { + uint64_t mt = lo->second; + auto hi = f.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (hi != f.end()) mt |= (static_cast(hi->second) << 32); + if (mt == unitGuid) { isTanking = true; status = 3; } + } + } + } + } + lua_pushboolean(L, isTanking); + lua_pushnumber(L, status); + lua_pushnumber(L, isTanking ? 100.0 : 0.0); // threatPct + lua_pushnumber(L, isTanking ? 100.0 : 0.0); // rawThreatPct + lua_pushnumber(L, 0); // threatValue (not available without server threat data) + return 5; +} + +// UnitDistanceSquared(unit) → distSq, canCalculate +static int lua_UnitDistanceSquared(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushboolean(L, 0); return 2; } + const char* uid = luaL_checkstring(L, 1); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0 || guid == gh->getPlayerGuid()) { lua_pushnumber(L, 0); lua_pushboolean(L, 0); return 2; } + auto targetEnt = gh->getEntityManager().getEntity(guid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { lua_pushnumber(L, 0); lua_pushboolean(L, 0); return 2; } + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + lua_pushnumber(L, dx*dx + dy*dy + dz*dz); + lua_pushboolean(L, 1); + return 2; +} + +// CheckInteractDistance(unit, distIndex) → boolean +// distIndex: 1=inspect(28yd), 2=trade(11yd), 3=duel(10yd), 4=follow(28yd) +static int lua_CheckInteractDistance(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + const char* uid = luaL_checkstring(L, 1); + int distIdx = static_cast(luaL_optnumber(L, 2, 4)); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnFalse(L); } + auto targetEnt = gh->getEntityManager().getEntity(guid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { return luaReturnFalse(L); } + 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); + float maxDist = 28.0f; // default: follow/inspect range + switch (distIdx) { + case 1: maxDist = 28.0f; break; // inspect + case 2: maxDist = 11.11f; break; // trade + case 3: maxDist = 9.9f; break; // duel + case 4: maxDist = 28.0f; break; // follow + } + lua_pushboolean(L, dist <= maxDist); + return 1; +} + +// IsSpellInRange(spellName, unit) → 0 or 1 (nil if can't determine) +static int lua_IsSpellInRange(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + const char* spellNameOrId = luaL_checkstring(L, 1); + const char* uid = luaL_optstring(L, 2, "target"); + + // Resolve spell ID + uint32_t spellId = 0; + if (spellNameOrId[0] >= '0' && spellNameOrId[0] <= '9') { + spellId = static_cast(strtoul(spellNameOrId, nullptr, 10)); + } else { + std::string nameLow(spellNameOrId); + toLowerInPlace(nameLow); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn == nameLow) { spellId = sid; break; } + } + } + if (spellId == 0) { return luaReturnNil(L); } + + // Get spell max range from DBC + auto data = gh->getSpellData(spellId); + if (data.maxRange <= 0.0f) { return luaReturnNil(L); } + + // Resolve target position + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnNil(L); } + auto targetEnt = gh->getEntityManager().getEntity(guid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { return luaReturnNil(L); } + + 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; +} + +// UnitIsVisible(unit) → boolean (entity exists in the client's entity manager) +static int lua_UnitIsVisible(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "target"); + auto* unit = resolveUnit(L, uid); + lua_pushboolean(L, unit != nullptr); + return 1; +} + +// UnitGroupRolesAssigned(unit) → "TANK", "HEALER", "DAMAGER", or "NONE" +static int lua_UnitGroupRolesAssigned(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, "NONE"); return 1; } + const char* uid = luaL_optstring(L, 1, "player"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushstring(L, "NONE"); return 1; } + const auto& pd = gh->getPartyData(); + for (const auto& m : pd.members) { + if (m.guid == guid) { + // WotLK roles bitmask: 0x02=Tank, 0x04=Healer, 0x08=DPS + if (m.roles & 0x02) { lua_pushstring(L, "TANK"); return 1; } + if (m.roles & 0x04) { lua_pushstring(L, "HEALER"); return 1; } + if (m.roles & 0x08) { lua_pushstring(L, "DAMAGER"); return 1; } + break; + } + } + lua_pushstring(L, "NONE"); + return 1; +} + +// UnitCanAttack(unit, otherUnit) → boolean +static int lua_UnitCanAttack(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + const char* uid1 = luaL_checkstring(L, 1); + const char* uid2 = luaL_checkstring(L, 2); + std::string u1(uid1), u2(uid2); + toLowerInPlace(u1); + toLowerInPlace(u2); + uint64_t g1 = resolveUnitGuid(gh, u1); + uint64_t g2 = resolveUnitGuid(gh, u2); + if (g1 == 0 || g2 == 0 || g1 == g2) { return luaReturnFalse(L); } + // Check if unit2 is hostile to unit1 + auto* unit2 = resolveUnit(L, uid2); + if (unit2 && unit2->isHostile()) { + lua_pushboolean(L, 1); + } else { + lua_pushboolean(L, 0); + } + return 1; +} + +// UnitCanCooperate(unit, otherUnit) → boolean +static int lua_UnitCanCooperate(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + (void)luaL_checkstring(L, 1); // unit1 (unused — cooperation is based on unit2's hostility) + const char* uid2 = luaL_checkstring(L, 2); + auto* unit2 = resolveUnit(L, uid2); + if (!unit2) { return luaReturnFalse(L); } + lua_pushboolean(L, !unit2->isHostile()); + return 1; +} + +// UnitCreatureFamily(unit) → familyName or nil +static int lua_UnitCreatureFamily(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + const char* uid = luaL_optstring(L, 1, "target"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnNil(L); } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity || entity->getType() == game::ObjectType::PLAYER) { return luaReturnNil(L); } + auto unit = std::dynamic_pointer_cast(entity); + if (!unit) { return luaReturnNil(L); } + uint32_t family = gh->getCreatureFamily(unit->getEntry()); + if (family == 0) { return luaReturnNil(L); } + static const char* kFamilies[] = { + "", "Wolf", "Cat", "Spider", "Bear", "Boar", "Crocolisk", "Carrion Bird", + "Crab", "Gorilla", "Raptor", "", "Tallstrider", "", "", "Felhunter", + "Voidwalker", "Succubus", "", "Doomguard", "Scorpid", "Turtle", "", + "Imp", "Bat", "Hyena", "Bird of Prey", "Wind Serpent", "", "Dragonhawk", + "Ravager", "Warp Stalker", "Sporebat", "Nether Ray", "Serpent", "Moth", + "Chimaera", "Devilsaur", "Ghoul", "Silithid", "Worm", "Rhino", "Wasp", + "Core Hound", "Spirit Beast" + }; + lua_pushstring(L, (family < sizeof(kFamilies)/sizeof(kFamilies[0]) && kFamilies[family][0]) + ? kFamilies[family] : "Beast"); + return 1; +} + +// UnitOnTaxi(unit) → boolean (true if on a flight path) +static int lua_UnitOnTaxi(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr == "player") { + lua_pushboolean(L, gh->isOnTaxiFlight()); + } else { + lua_pushboolean(L, 0); // Can't determine for other units + } + return 1; +} + +// UnitSex(unit) → 1=unknown, 2=male, 3=female +static int lua_UnitSex(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 1); return 1; } + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + // Gender is byte 2 of UNIT_FIELD_BYTES_0 (0=male, 1=female) + uint32_t bytes0 = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_BYTES_0)); + uint8_t gender = static_cast((bytes0 >> 16) & 0xFF); + lua_pushnumber(L, gender == 0 ? 2 : (gender == 1 ? 3 : 1)); // WoW: 2=male, 3=female + return 1; + } + } + lua_pushnumber(L, 1); // unknown + return 1; +} + +// --- Player/Game API --- + +static int lua_GetMoney(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? static_cast(gh->getMoneyCopper()) : 0.0); + return 1; +} + +// --- Merchant/Vendor API --- + +static int lua_GetMerchantNumItems(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + lua_pushnumber(L, gh->getVendorItems().items.size()); + return 1; +} + +// GetMerchantItemInfo(index) → name, texture, price, stackCount, numAvailable, isUsable +static int lua_GetMerchantItemInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& items = gh->getVendorItems().items; + if (index > static_cast(items.size())) { return luaReturnNil(L); } + const auto& vi = items[index - 1]; + const auto* info = gh->getItemInfo(vi.itemId); + std::string name = info ? info->name : ("Item #" + std::to_string(vi.itemId)); + lua_pushstring(L, name.c_str()); // name + // texture + std::string iconPath; + if (info && info->displayInfoId != 0) + iconPath = gh->getItemIconPath(info->displayInfoId); + if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); + else lua_pushnil(L); + lua_pushnumber(L, vi.buyPrice); // price (copper) + lua_pushnumber(L, vi.stackCount > 0 ? vi.stackCount : 1); // stackCount + lua_pushnumber(L, vi.maxCount == -1 ? -1 : vi.maxCount); // numAvailable (-1=unlimited) + lua_pushboolean(L, 1); // isUsable + return 6; +} + +// GetMerchantItemLink(index) → item link +static int lua_GetMerchantItemLink(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& items = gh->getVendorItems().items; + if (index > static_cast(items.size())) { return luaReturnNil(L); } + const auto& vi = items[index - 1]; + const auto* info = gh->getItemInfo(vi.itemId); + if (!info) { return luaReturnNil(L); } + static const char* kQH[] = {"ff9d9d9d","ffffffff","ff1eff00","ff0070dd","ffa335ee","ffff8000","ffe6cc80","ff00ccff"}; + const char* ch = (info->quality < 8) ? kQH[info->quality] : "ffffffff"; + char link[256]; + snprintf(link, sizeof(link), "|c%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", ch, vi.itemId, info->name.c_str()); + lua_pushstring(L, link); + return 1; +} + +static int lua_CanMerchantRepair(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->getVendorItems().canRepair ? 1 : 0); + 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) { return luaReturnZero(L); } + 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) { return luaReturnZero(L); } + 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()); + return 1; +} + +static int lua_IsInRaid(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isInGroup() && gh->getPartyData().groupType == 1); + return 1; +} + +// PlaySound(soundId) — play a WoW UI sound by ID or name +static int lua_PlaySound(lua_State* L) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (!renderer) return 0; + auto* sfx = renderer->getUiSoundManager(); + if (!sfx) return 0; + + // Accept numeric sound ID or string name + std::string sound; + if (lua_isnumber(L, 1)) { + uint32_t id = static_cast(lua_tonumber(L, 1)); + // Map common WoW sound IDs to named sounds + switch (id) { + case 856: case 1115: sfx->playButtonClick(); return 0; // igMainMenuOption + case 840: sfx->playQuestActivate(); return 0; // igQuestListOpen + case 841: sfx->playQuestComplete(); return 0; // igQuestListComplete + case 862: sfx->playBagOpen(); return 0; // igBackPackOpen + case 863: sfx->playBagClose(); return 0; // igBackPackClose + case 867: sfx->playError(); return 0; // igPlayerInvite + case 888: sfx->playLevelUp(); return 0; // LEVELUPSOUND + default: return 0; + } + } else { + const char* name = luaL_optstring(L, 1, ""); + sound = name; + for (char& c : sound) c = static_cast(std::toupper(static_cast(c))); + if (sound == "IGMAINMENUOPTION" || sound == "IGMAINMENUOPTIONCHECKBOXON") + sfx->playButtonClick(); + else if (sound == "IGQUESTLISTOPEN") sfx->playQuestActivate(); + else if (sound == "IGQUESTLISTCOMPLETE") sfx->playQuestComplete(); + else if (sound == "IGBACKPACKOPEN") sfx->playBagOpen(); + else if (sound == "IGBACKPACKCLOSE") sfx->playBagClose(); + else if (sound == "LEVELUPSOUND") sfx->playLevelUp(); + else if (sound == "IGPLAYERINVITEACCEPTED") sfx->playButtonClick(); + else if (sound == "TALENTSCREENOPEN") sfx->playCharacterSheetOpen(); + else if (sound == "TALENTSCREENCLOSE") sfx->playCharacterSheetClose(); + } + return 0; +} + +// PlaySoundFile(path) — stub (file-based sounds not loaded from Lua) +static int lua_PlaySoundFile(lua_State* L) { (void)L; return 0; } + +static int lua_GetPlayerMapPosition(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) { + const auto& mi = gh->getMovementInfo(); + lua_pushnumber(L, mi.x); + lua_pushnumber(L, mi.y); + return 2; + } + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + 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 +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; } + std::string uid(luaL_optstring(L, 1, "player")); + toLowerInPlace(uid); + static const char* kRaces[] = {"","Human","Orc","Dwarf","Night Elf","Undead", + "Tauren","Gnome","Troll","","Blood Elf","Draenei"}; + uint8_t raceId = 0; + if (uid == "player") { + raceId = gh->getPlayerRace(); + } else { + // Read race from UNIT_FIELD_BYTES_0 (race is byte 0) + uint64_t guid = resolveUnitGuid(gh, uid); + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + uint32_t bytes0 = entity->getField( + game::fieldIndex(game::UF::UNIT_FIELD_BYTES_0)); + raceId = static_cast(bytes0 & 0xFF); + } + // Fallback: name query class/race cache + if (raceId == 0) raceId = gh->lookupPlayerRace(guid); + } + } + const char* name = (raceId > 0 && raceId < 12) ? kRaces[raceId] : "Unknown"; + lua_pushstring(L, name); // 1: localized race + lua_pushstring(L, name); // 2: English race + lua_pushnumber(L, raceId); // 3: raceId (WoW returns 3 values) + return 3; +} + +static int lua_UnitPowerType(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + static const char* kPowerNames[] = {"MANA","RAGE","FOCUS","ENERGY","HAPPINESS","","RUNIC_POWER"}; + if (unit) { + uint8_t pt = unit->getPowerType(); + lua_pushnumber(L, pt); + lua_pushstring(L, (pt < 7) ? kPowerNames[pt] : "MANA"); + return 2; + } + // Fallback: party member stats for out-of-range members + auto* gh = getGameHandler(L); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = gh ? resolveUnitGuid(gh, uidStr) : 0; + const auto* pm = findPartyMember(gh, guid); + if (pm) { + uint8_t pt = pm->powerType; + lua_pushnumber(L, pt); + lua_pushstring(L, (pt < 7) ? kPowerNames[pt] : "MANA"); + return 2; + } + lua_pushnumber(L, 0); + lua_pushstring(L, "MANA"); + return 2; +} + +static int lua_GetNumGroupMembers(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getPartyData().memberCount : 0); + return 1; +} + +static int lua_UnitGUID(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnNil(L); } + char buf[32]; + snprintf(buf, sizeof(buf), "0x%016llX", (unsigned long long)guid); + lua_pushstring(L, buf); + return 1; +} + +static int lua_UnitIsPlayer(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + auto entity = guid ? gh->getEntityManager().getEntity(guid) : nullptr; + lua_pushboolean(L, entity && entity->getType() == game::ObjectType::PLAYER); + return 1; +} + +static int lua_InCombatLockdown(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isInCombat()); + return 1; +} + +// --- Addon Info API --- +// These need the AddonManager pointer stored in registry + +static int lua_GetNumAddOns(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, "wowee_addon_count"); + return 1; +} + +static int lua_GetAddOnInfo(lua_State* L) { + // Accept index (1-based) or addon name + lua_getfield(L, LUA_REGISTRYINDEX, "wowee_addon_info"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return luaReturnNil(L); + } + + int idx = 0; + if (lua_isnumber(L, 1)) { + idx = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + // Search by name + const char* name = lua_tostring(L, 1); + int count = static_cast(lua_objlen(L, -1)); + for (int i = 1; i <= count; i++) { + lua_rawgeti(L, -1, i); + lua_getfield(L, -1, "name"); + const char* aName = lua_tostring(L, -1); + lua_pop(L, 1); + if (aName && strcmp(aName, name) == 0) { idx = i; lua_pop(L, 1); break; } + lua_pop(L, 1); + } + } + + if (idx < 1) { lua_pop(L, 1); lua_pushnil(L); return 1; } + + lua_rawgeti(L, -1, idx); + if (!lua_istable(L, -1)) { lua_pop(L, 2); lua_pushnil(L); return 1; } + + lua_getfield(L, -1, "name"); + lua_getfield(L, -2, "title"); + lua_getfield(L, -3, "notes"); + lua_pushboolean(L, 1); // loadable (always true for now) + lua_pushstring(L, "INSECURE"); // security + lua_pop(L, 1); // pop addon info entry (keep others) + // Return: name, title, notes, loadable, reason, security + return 5; +} + +// GetAddOnMetadata(addonNameOrIndex, key) → value +static int lua_GetAddOnMetadata(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, "wowee_addon_info"); + if (!lua_istable(L, -1)) { lua_pop(L, 1); lua_pushnil(L); return 1; } + + int idx = 0; + if (lua_isnumber(L, 1)) { + idx = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + const char* name = lua_tostring(L, 1); + int count = static_cast(lua_objlen(L, -1)); + for (int i = 1; i <= count; i++) { + lua_rawgeti(L, -1, i); + lua_getfield(L, -1, "name"); + const char* aName = lua_tostring(L, -1); + lua_pop(L, 1); + if (aName && strcmp(aName, name) == 0) { idx = i; lua_pop(L, 1); break; } + lua_pop(L, 1); + } + } + if (idx < 1) { lua_pop(L, 1); lua_pushnil(L); return 1; } + + const char* key = luaL_checkstring(L, 2); + lua_rawgeti(L, -1, idx); + if (!lua_istable(L, -1)) { lua_pop(L, 2); lua_pushnil(L); return 1; } + lua_getfield(L, -1, "metadata"); + if (!lua_istable(L, -1)) { lua_pop(L, 3); lua_pushnil(L); return 1; } + lua_getfield(L, -1, key); + return 1; +} + +// UnitBuff(unitId, index) / UnitDebuff(unitId, index) +// Returns: name, rank, icon, count, debuffType, duration, expirationTime, caster, isStealable, shouldConsolidate, spellId +static int lua_UnitAura(lua_State* L, bool wantBuff) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + const char* uid = luaL_optstring(L, 1, "player"); + int index = static_cast(luaL_optnumber(L, 2, 1)); + if (index < 1) { return luaReturnNil(L); } + + std::string uidStr(uid); + toLowerInPlace(uidStr); + + const std::vector* auras = nullptr; + if (uidStr == "player") auras = &gh->getPlayerAuras(); + else if (uidStr == "target") auras = &gh->getTargetAuras(); + else { + // Try party/raid/focus via GUID lookup in unitAurasCache + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) auras = gh->getUnitAuras(guid); + } + if (!auras) { return luaReturnNil(L); } + + // Filter to buffs or debuffs and find the Nth one + int found = 0; + for (const auto& aura : *auras) { + if (aura.isEmpty() || aura.spellId == 0) continue; + bool isDebuff = (aura.flags & 0x80) != 0; + if (wantBuff ? isDebuff : !isDebuff) continue; + found++; + if (found == index) { + // Return: name, rank, icon, count, debuffType, duration, expirationTime, ...spellId + std::string name = gh->getSpellName(aura.spellId); + lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name + lua_pushstring(L, ""); // rank + std::string iconPath = gh->getSpellIconPath(aura.spellId); + if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); + else lua_pushnil(L); // icon texture path + lua_pushnumber(L, aura.charges); // count + // debuffType: resolve from Spell.dbc dispel type + { + uint8_t dt = gh->getSpellDispelType(aura.spellId); + switch (dt) { + case 1: lua_pushstring(L, "Magic"); break; + case 2: lua_pushstring(L, "Curse"); break; + case 3: lua_pushstring(L, "Disease"); break; + case 4: lua_pushstring(L, "Poison"); break; + default: lua_pushnil(L); break; + } + } + lua_pushnumber(L, aura.maxDurationMs > 0 ? aura.maxDurationMs / 1000.0 : 0); // duration + // expirationTime: GetTime() + remaining seconds (so addons can compute countdown) + if (aura.durationMs > 0) { + uint64_t auraNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + int32_t remMs = aura.getRemainingMs(auraNowMs); + lua_pushnumber(L, luaGetTimeNow() + remMs / 1000.0); + } else { + lua_pushnumber(L, 0); // permanent aura + } + // caster: return unit ID string if caster is known + if (aura.casterGuid != 0) { + if (aura.casterGuid == gh->getPlayerGuid()) + lua_pushstring(L, "player"); + else if (aura.casterGuid == gh->getTargetGuid()) + lua_pushstring(L, "target"); + else if (aura.casterGuid == gh->getFocusGuid()) + lua_pushstring(L, "focus"); + else if (aura.casterGuid == gh->getPetGuid()) + lua_pushstring(L, "pet"); + else { + char cBuf[32]; + snprintf(cBuf, sizeof(cBuf), "0x%016llX", (unsigned long long)aura.casterGuid); + lua_pushstring(L, cBuf); + } + } else { + lua_pushnil(L); + } + lua_pushboolean(L, 0); // isStealable + lua_pushboolean(L, 0); // shouldConsolidate + lua_pushnumber(L, aura.spellId); // spellId + return 11; + } + } + lua_pushnil(L); + return 1; +} + +static int lua_UnitBuff(lua_State* L) { return lua_UnitAura(L, true); } +static int lua_UnitDebuff(lua_State* L) { return lua_UnitAura(L, false); } + +// UnitAura(unit, index, filter) — generic aura query with filter string +// filter: "HELPFUL" = buffs, "HARMFUL" = debuffs, "PLAYER" = cast by player, +// "HELPFUL|PLAYER" = buffs cast by player, etc. +static int lua_UnitAuraGeneric(lua_State* L) { + const char* filter = luaL_optstring(L, 3, "HELPFUL"); + std::string f(filter ? filter : "HELPFUL"); + for (char& c : f) c = static_cast(std::toupper(static_cast(c))); + bool wantBuff = (f.find("HARMFUL") == std::string::npos); + 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) { return luaReturnNil(L); } + + const char* uid = luaL_optstring(L, 1, "player"); + std::string uidStr(uid ? uid : "player"); + + // Use shared GetTime() epoch for consistent timestamps + double nowSec = luaGetTimeNow(); + + // 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) { return luaReturnNil(L); } + const auto* state = gh->getUnitCastState(guid); + if (!state) { return luaReturnNil(L); } + isCasting = state->casting; + isChannel = state->isChannel; + spellId = state->spellId; + timeTotal = state->timeTotal; + timeRemaining = state->timeRemaining; + interruptible = state->interruptible; + } + + if (!isCasting) { return luaReturnNil(L); } + + // UnitCastingInfo: only returns for non-channel casts + // UnitChannelInfo: only returns for channels + if (wantChannel != isChannel) { return luaReturnNil(L); } + + // 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) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* msg = luaL_checkstring(L, 1); + const char* chatType = luaL_optstring(L, 2, "SAY"); + // language arg (3) ignored — server determines language + const char* target = luaL_optstring(L, 4, ""); + + std::string typeStr(chatType); + for (char& c : typeStr) c = static_cast(std::toupper(static_cast(c))); + + game::ChatType ct = game::ChatType::SAY; + if (typeStr == "SAY") ct = game::ChatType::SAY; + else if (typeStr == "YELL") ct = game::ChatType::YELL; + else if (typeStr == "PARTY") ct = game::ChatType::PARTY; + else if (typeStr == "GUILD") ct = game::ChatType::GUILD; + else if (typeStr == "OFFICER") ct = game::ChatType::OFFICER; + else if (typeStr == "RAID") ct = game::ChatType::RAID; + else if (typeStr == "WHISPER") ct = game::ChatType::WHISPER; + else if (typeStr == "BATTLEGROUND") ct = game::ChatType::BATTLEGROUND; + + std::string targetStr(target && *target ? target : ""); + gh->sendChatMessage(ct, msg, targetStr); + return 0; +} + +static int lua_CastSpellByName(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* name = luaL_checkstring(L, 1); + if (!name || !*name) return 0; + + // Find highest rank of spell by name (same logic as /cast) + std::string nameLow(name); + toLowerInPlace(nameLow); + + uint32_t bestId = 0; + int bestRank = -1; + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn != nameLow) continue; + int rank = 0; + const std::string& rk = gh->getSpellRank(sid); + if (!rk.empty()) { + std::string rkl = rk; + toLowerInPlace(rkl); + if (rkl.rfind("rank ", 0) == 0) { + try { rank = std::stoi(rkl.substr(5)); } catch (...) {} + } + } + if (rank > bestRank) { bestRank = rank; bestId = sid; } + } + if (bestId != 0) { + uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0; + gh->castSpell(bestId, target); + } + return 0; +} + +// SendAddonMessage(prefix, text, chatType, target) — send addon message +static int lua_SendAddonMessage(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* prefix = luaL_checkstring(L, 1); + const char* text = luaL_checkstring(L, 2); + const char* chatType = luaL_optstring(L, 3, "PARTY"); + const char* target = luaL_optstring(L, 4, ""); + + // Build addon message: prefix + TAB + text, send via the appropriate channel + std::string typeStr(chatType); + for (char& c : typeStr) c = static_cast(std::toupper(static_cast(c))); + + game::ChatType ct = game::ChatType::PARTY; + if (typeStr == "PARTY") ct = game::ChatType::PARTY; + else if (typeStr == "RAID") ct = game::ChatType::RAID; + else if (typeStr == "GUILD") ct = game::ChatType::GUILD; + else if (typeStr == "OFFICER") ct = game::ChatType::OFFICER; + else if (typeStr == "BATTLEGROUND") ct = game::ChatType::BATTLEGROUND; + else if (typeStr == "WHISPER") ct = game::ChatType::WHISPER; + + // Encode as prefix\ttext (WoW addon message format) + std::string encoded = std::string(prefix) + "\t" + text; + std::string targetStr(target && *target ? target : ""); + gh->sendChatMessage(ct, encoded, targetStr); + return 0; +} + +// RegisterAddonMessagePrefix(prefix) — register prefix for receiving addon messages +static int lua_RegisterAddonMessagePrefix(lua_State* L) { + const char* prefix = luaL_checkstring(L, 1); + // Store in a global Lua table for filtering + lua_getglobal(L, "__WoweeAddonPrefixes"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setglobal(L, "__WoweeAddonPrefixes"); + } + lua_pushboolean(L, 1); + lua_setfield(L, -2, prefix); + lua_pop(L, 1); + lua_pushboolean(L, 1); // success + return 1; +} + +// IsAddonMessagePrefixRegistered(prefix) → boolean +static int lua_IsAddonMessagePrefixRegistered(lua_State* L) { + const char* prefix = luaL_checkstring(L, 1); + lua_getglobal(L, "__WoweeAddonPrefixes"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, prefix); + lua_pushboolean(L, lua_toboolean(L, -1)); + return 1; + } + lua_pushboolean(L, 0); + return 1; +} + +static int lua_IsSpellKnown(lua_State* L) { + auto* gh = getGameHandler(L); + uint32_t spellId = static_cast(luaL_checknumber(L, 1)); + lua_pushboolean(L, gh && gh->getKnownSpells().count(spellId)); + return 1; +} + +// --- Spell Book Tab API --- + +// GetNumSpellTabs() → count +static int lua_GetNumSpellTabs(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + 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) { + return luaReturnNil(L); + } + const auto& tabs = gh->getSpellBookTabs(); + if (tabIdx > static_cast(tabs.size())) { + return luaReturnNil(L); + } + // 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) { return luaReturnNil(L); } + 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; +} + +// GetSpellDescription(spellId) → description string +// Clean spell description template variables for display +static std::string cleanSpellDescription(const std::string& raw, const int32_t effectBase[3] = nullptr, float durationSec = 0.0f) { + if (raw.empty() || raw.find('$') == std::string::npos) return raw; + std::string result; + result.reserve(raw.size()); + for (size_t i = 0; i < raw.size(); ++i) { + if (raw[i] == '$' && i + 1 < raw.size()) { + char next = raw[i + 1]; + if (next == 's' || next == 'S') { + // $s1, $s2, $s3 — substitute with effect base points + 1 + i += 1; // skip 's' + int idx = 0; + if (i + 1 < raw.size() && raw[i + 1] >= '1' && raw[i + 1] <= '3') { + idx = raw[i + 1] - '1'; + ++i; + } + if (effectBase && effectBase[idx] != 0) { + int32_t val = std::abs(effectBase[idx]) + 1; + result += std::to_string(val); + } else { + result += 'X'; + } + while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i; + } else if (next == 'o' || next == 'O') { + // $o1 = periodic total (base * ticks). Ticks = duration / 3sec for most spells + i += 1; + int idx = 0; + if (i + 1 < raw.size() && raw[i + 1] >= '1' && raw[i + 1] <= '3') { + idx = raw[i + 1] - '1'; + ++i; + } + if (effectBase && effectBase[idx] != 0 && durationSec > 0.0f) { + int32_t perTick = std::abs(effectBase[idx]) + 1; + int ticks = static_cast(durationSec / 3.0f); + if (ticks < 1) ticks = 1; + result += std::to_string(perTick * ticks); + } else { + result += 'X'; + } + while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i; + } else if (next == 'e' || next == 'E' || next == 't' || next == 'T' || + next == 'h' || next == 'H' || next == 'u' || next == 'U') { + // Other variables — insert "X" placeholder + result += 'X'; + i += 1; + while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i; + } else if (next == 'd' || next == 'D') { + // $d = duration + if (durationSec > 0.0f) { + if (durationSec >= 60.0f) + result += std::to_string(static_cast(durationSec / 60.0f)) + " min"; + else + result += std::to_string(static_cast(durationSec)) + " sec"; + } else { + result += "X sec"; + } + ++i; + while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i; + } else if (next == 'a' || next == 'A') { + // $a1 = radius + result += "X"; + ++i; + while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i; + } else if (next == 'b' || next == 'B' || next == 'n' || next == 'N' || + next == 'i' || next == 'I' || next == 'x' || next == 'X') { + // misc variables + result += "X"; + ++i; + while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i; + } else if (next == '$') { + // $$ = literal $ + result += '$'; + ++i; + } else if (next == '{' || next == '<') { + // ${...} or $<...> — skip entire block + char close = (next == '{') ? '}' : '>'; + size_t end = raw.find(close, i + 2); + if (end != std::string::npos) i = end; + else result += raw[i]; // no closing — keep $ + } else { + result += raw[i]; // unknown $ pattern — keep + } + } else { + result += raw[i]; + } + } + return result; +} + +static int lua_GetSpellDescription(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, ""); return 1; } + uint32_t spellId = static_cast(luaL_checknumber(L, 1)); + const std::string& desc = gh->getSpellDescription(spellId); + const int32_t* ebp = gh->getSpellEffectBasePoints(spellId); + float dur = gh->getSpellDuration(spellId); + std::string cleaned = cleanSpellDescription(desc, ebp, dur); + lua_pushstring(L, cleaned.c_str()); + return 1; +} + +// GetEnchantInfo(enchantId) → name or nil +static int lua_GetEnchantInfo(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + uint32_t enchantId = static_cast(luaL_checknumber(L, 1)); + std::string name = gh->getEnchantName(enchantId); + if (name.empty()) { return luaReturnNil(L); } + lua_pushstring(L, name.c_str()); + 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; } + // Accept spell name or ID + uint32_t spellId = 0; + if (lua_isnumber(L, 1)) { + spellId = static_cast(lua_tonumber(L, 1)); + } else { + const char* name = luaL_checkstring(L, 1); + std::string nameLow(name); + toLowerInPlace(nameLow); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn == nameLow) { spellId = sid; break; } + } + } + float cd = gh->getSpellCooldown(spellId); + // Also check GCD — if spell has no individual cooldown but GCD is active, + // return the GCD timing (this is how WoW handles it) + float gcdRem = gh->getGCDRemaining(); + float gcdTotal = gh->getGCDTotal(); + + // WoW returns (start, duration, enabled) where remaining = start + duration - GetTime() + double nowSec = luaGetTimeNow(); + + if (cd > 0.01f) { + // Spell-specific cooldown (longer than GCD) + double start = nowSec - 0.01; // approximate start as "just now" minus epsilon + lua_pushnumber(L, start); + lua_pushnumber(L, cd); + } else if (gcdRem > 0.01f) { + // GCD is active — return GCD timing + double elapsed = gcdTotal - gcdRem; + double start = nowSec - elapsed; + lua_pushnumber(L, start); + lua_pushnumber(L, gcdTotal); + } else { + lua_pushnumber(L, 0); // not on cooldown + lua_pushnumber(L, 0); + } + lua_pushnumber(L, 1); // enabled + return 3; +} + +static int lua_HasTarget(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->hasTarget()); + return 1; +} + +// TargetUnit(unitId) — set current target +static int lua_TargetUnit(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* uid = luaL_checkstring(L, 1); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) gh->setTarget(guid); + return 0; +} + +// ClearTarget() — clear current target +static int lua_ClearTarget(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) gh->clearTarget(); + return 0; +} + +// FocusUnit(unitId) — set focus target +static int lua_FocusUnit(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* uid = luaL_optstring(L, 1, nullptr); + if (!uid || !*uid) return 0; + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid != 0) gh->setFocus(guid); + return 0; +} + +// ClearFocus() — clear focus target +static int lua_ClearFocus(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) gh->clearFocus(); + return 0; +} + +// AssistUnit(unitId) — target whatever the given unit is targeting +static int lua_AssistUnit(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* uid = luaL_optstring(L, 1, "target"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) return 0; + uint64_t theirTarget = getEntityTargetGuid(gh, guid); + if (theirTarget != 0) gh->setTarget(theirTarget); + return 0; +} + +// TargetLastTarget() — re-target previous target +static int lua_TargetLastTarget(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) gh->targetLastTarget(); + return 0; +} + +// TargetNearestEnemy() — tab-target nearest enemy +static int lua_TargetNearestEnemy(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) gh->targetEnemy(false); + return 0; +} + +// TargetNearestFriend() — target nearest friendly unit +static int lua_TargetNearestFriend(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) gh->targetFriend(false); + return 0; +} + +// GetRaidTargetIndex(unit) → icon index (1-8) or nil +static int lua_GetRaidTargetIndex(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + const char* uid = luaL_optstring(L, 1, "target"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnNil(L); } + uint8_t mark = gh->getEntityRaidMark(guid); + if (mark == 0xFF) { return luaReturnNil(L); } + lua_pushnumber(L, mark + 1); // WoW uses 1-indexed (1=Star, 2=Circle, ... 8=Skull) + return 1; +} + +// SetRaidTarget(unit, index) — set raid marker (1-8, or 0 to clear) +static int lua_SetRaidTarget(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* uid = luaL_optstring(L, 1, "target"); + int index = static_cast(luaL_checknumber(L, 2)); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) return 0; + if (index >= 1 && index <= 8) + gh->setRaidMark(guid, static_cast(index - 1)); + else if (index == 0) + gh->setRaidMark(guid, 0xFF); // clear + return 0; +} + +// GetSpellPowerCost(spellId) → {{ type=powerType, cost=manaCost, name=powerName }} +static int lua_GetSpellPowerCost(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_newtable(L); return 1; } + uint32_t spellId = static_cast(luaL_checknumber(L, 1)); + auto data = gh->getSpellData(spellId); + lua_newtable(L); // outer table (array of cost entries) + if (data.manaCost > 0) { + lua_newtable(L); // cost entry + lua_pushnumber(L, data.powerType); + lua_setfield(L, -2, "type"); + lua_pushnumber(L, data.manaCost); + lua_setfield(L, -2, "cost"); + static const char* kPowerNames[] = {"MANA","RAGE","FOCUS","ENERGY","HAPPINESS","","RUNIC_POWER"}; + lua_pushstring(L, data.powerType < 7 ? kPowerNames[data.powerType] : "MANA"); + lua_setfield(L, -2, "name"); + lua_rawseti(L, -2, 1); // outer[1] = entry + } + return 1; +} + +// --- GetSpellInfo / GetSpellTexture --- +// GetSpellInfo(spellIdOrName) -> name, rank, icon, castTime, minRange, maxRange, spellId +static int lua_GetSpellInfo(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + + uint32_t spellId = 0; + if (lua_isnumber(L, 1)) { + spellId = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + const char* name = lua_tostring(L, 1); + if (!name || !*name) { return luaReturnNil(L); } + std::string nameLow(name); + toLowerInPlace(nameLow); + int bestRank = -1; + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn != nameLow) continue; + int rank = 0; + const std::string& rk = gh->getSpellRank(sid); + if (!rk.empty()) { + std::string rkl = rk; + toLowerInPlace(rkl); + if (rkl.rfind("rank ", 0) == 0) { + try { rank = std::stoi(rkl.substr(5)); } catch (...) {} + } + } + if (rank > bestRank) { bestRank = rank; spellId = sid; } + } + } + + if (spellId == 0) { return luaReturnNil(L); } + std::string name = gh->getSpellName(spellId); + if (name.empty()) { return luaReturnNil(L); } + + lua_pushstring(L, name.c_str()); // 1: name + const std::string& rank = gh->getSpellRank(spellId); + lua_pushstring(L, rank.c_str()); // 2: rank + std::string iconPath = gh->getSpellIconPath(spellId); + if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); + else lua_pushnil(L); // 3: icon texture path + // Resolve cast time and range from Spell.dbc → SpellCastTimes.dbc / SpellRange.dbc + auto spellData = gh->getSpellData(spellId); + lua_pushnumber(L, spellData.castTimeMs); // 4: castTime (ms) + lua_pushnumber(L, spellData.minRange); // 5: minRange (yards) + lua_pushnumber(L, spellData.maxRange); // 6: maxRange (yards) + lua_pushnumber(L, spellId); // 7: spellId + return 7; +} + +// GetSpellTexture(spellIdOrName) -> icon texture path string +static int lua_GetSpellTexture(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + + uint32_t spellId = 0; + if (lua_isnumber(L, 1)) { + spellId = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + const char* name = lua_tostring(L, 1); + if (!name || !*name) { return luaReturnNil(L); } + std::string nameLow(name); + toLowerInPlace(nameLow); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn == nameLow) { spellId = sid; break; } + } + } + if (spellId == 0) { return luaReturnNil(L); } + std::string iconPath = gh->getSpellIconPath(spellId); + if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); + else lua_pushnil(L); + return 1; +} + +// GetItemInfo(itemId) -> name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, vendorPrice +static int lua_GetItemInfo(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + + uint32_t itemId = 0; + if (lua_isnumber(L, 1)) { + itemId = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + // Try to parse "item:12345" link format + const char* s = lua_tostring(L, 1); + std::string str(s ? s : ""); + auto pos = str.find("item:"); + if (pos != std::string::npos) { + try { itemId = static_cast(std::stoul(str.substr(pos + 5))); } catch (...) {} + } + } + if (itemId == 0) { return luaReturnNil(L); } + + const auto* info = gh->getItemInfo(itemId); + if (!info) { return luaReturnNil(L); } + + lua_pushstring(L, info->name.c_str()); // 1: name + // Build item link with quality-colored text + static const char* kQualityHex[] = { + "ff9d9d9d", // 0 Poor (gray) + "ffffffff", // 1 Common (white) + "ff1eff00", // 2 Uncommon (green) + "ff0070dd", // 3 Rare (blue) + "ffa335ee", // 4 Epic (purple) + "ffff8000", // 5 Legendary (orange) + "ffe6cc80", // 6 Artifact (gold) + "ff00ccff", // 7 Heirloom (cyan) + }; + const char* colorHex = (info->quality < 8) ? kQualityHex[info->quality] : "ffffffff"; + char link[256]; + snprintf(link, sizeof(link), "|c%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", + colorHex, itemId, info->name.c_str()); + lua_pushstring(L, link); // 2: link + lua_pushnumber(L, info->quality); // 3: quality + lua_pushnumber(L, info->itemLevel); // 4: iLevel + lua_pushnumber(L, info->requiredLevel); // 5: requiredLevel + // 6: class (type string) — map itemClass to display name + { + static const char* kItemClasses[] = { + "Consumable", "Bag", "Weapon", "Gem", "Armor", "Reagent", "Projectile", + "Trade Goods", "Generic", "Recipe", "Money", "Quiver", "Quest", "Key", + "Permanent", "Miscellaneous", "Glyph" + }; + if (info->itemClass < 17) + lua_pushstring(L, kItemClasses[info->itemClass]); + else + lua_pushstring(L, "Miscellaneous"); + } + // 7: subclass — use subclassName from ItemDef if available, else generic + lua_pushstring(L, info->subclassName.empty() ? "" : info->subclassName.c_str()); + lua_pushnumber(L, info->maxStack > 0 ? info->maxStack : 1); // 8: maxStack + // 9: equipSlot — WoW inventoryType to INVTYPE string + { + static const char* kInvTypes[] = { + "", "INVTYPE_HEAD", "INVTYPE_NECK", "INVTYPE_SHOULDER", + "INVTYPE_BODY", "INVTYPE_CHEST", "INVTYPE_WAIST", "INVTYPE_LEGS", + "INVTYPE_FEET", "INVTYPE_WRIST", "INVTYPE_HAND", "INVTYPE_FINGER", + "INVTYPE_TRINKET", "INVTYPE_WEAPON", "INVTYPE_SHIELD", + "INVTYPE_RANGED", "INVTYPE_CLOAK", "INVTYPE_2HWEAPON", + "INVTYPE_BAG", "INVTYPE_TABARD", "INVTYPE_ROBE", + "INVTYPE_WEAPONMAINHAND", "INVTYPE_WEAPONOFFHAND", "INVTYPE_HOLDABLE", + "INVTYPE_AMMO", "INVTYPE_THROWN", "INVTYPE_RANGEDRIGHT", + "INVTYPE_QUIVER", "INVTYPE_RELIC" + }; + uint32_t invType = info->inventoryType; + lua_pushstring(L, invType < 29 ? kInvTypes[invType] : ""); + } + // 10: texture (icon path from ItemDisplayInfo.dbc) + if (info->displayInfoId != 0) { + std::string iconPath = gh->getItemIconPath(info->displayInfoId); + if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str()); + else lua_pushnil(L); + } else { + lua_pushnil(L); + } + lua_pushnumber(L, info->sellPrice); // 11: vendorPrice + return 11; +} + +// GetItemQualityColor(quality) → r, g, b, hex +// Quality: 0=Poor(gray), 1=Common(white), 2=Uncommon(green), 3=Rare(blue), +// 4=Epic(purple), 5=Legendary(orange), 6=Artifact(gold), 7=Heirloom(gold) +static int lua_GetItemQualityColor(lua_State* L) { + int q = static_cast(luaL_checknumber(L, 1)); + struct QC { float r, g, b; const char* hex; }; + static const QC colors[] = { + {0.62f, 0.62f, 0.62f, "ff9d9d9d"}, // 0 Poor + {1.00f, 1.00f, 1.00f, "ffffffff"}, // 1 Common + {0.12f, 1.00f, 0.00f, "ff1eff00"}, // 2 Uncommon + {0.00f, 0.44f, 0.87f, "ff0070dd"}, // 3 Rare + {0.64f, 0.21f, 0.93f, "ffa335ee"}, // 4 Epic + {1.00f, 0.50f, 0.00f, "ffff8000"}, // 5 Legendary + {0.90f, 0.80f, 0.50f, "ffe6cc80"}, // 6 Artifact + {0.00f, 0.80f, 1.00f, "ff00ccff"}, // 7 Heirloom + }; + if (q < 0 || q > 7) q = 1; + lua_pushnumber(L, colors[q].r); + lua_pushnumber(L, colors[q].g); + lua_pushnumber(L, colors[q].b); + lua_pushstring(L, colors[q].hex); + return 4; +} + +// GetItemCount(itemId [, includeBank]) → count +static int lua_GetItemCount(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + uint32_t itemId = static_cast(luaL_checknumber(L, 1)); + const auto& inv = gh->getInventory(); + uint32_t count = 0; + // Backpack + for (int i = 0; i < inv.getBackpackSize(); ++i) { + const auto& s = inv.getBackpackSlot(i); + if (!s.empty() && s.item.itemId == itemId) + count += (s.item.stackCount > 0 ? s.item.stackCount : 1); + } + // Bags 1-4 + for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) { + int sz = inv.getBagSize(b); + for (int i = 0; i < sz; ++i) { + const auto& s = inv.getBagSlot(b, i); + if (!s.empty() && s.item.itemId == itemId) + count += (s.item.stackCount > 0 ? s.item.stackCount : 1); + } + } + lua_pushnumber(L, count); + return 1; +} + +// UseContainerItem(bag, slot) — use/equip an item from a bag +static int lua_UseContainerItem(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + int bag = static_cast(luaL_checknumber(L, 1)); + int slot = static_cast(luaL_checknumber(L, 2)); + const auto& inv = gh->getInventory(); + const game::ItemSlot* itemSlot = nullptr; + if (bag == 0 && slot >= 1 && slot <= inv.getBackpackSize()) + itemSlot = &inv.getBackpackSlot(slot - 1); + else if (bag >= 1 && bag <= 4) { + int sz = inv.getBagSize(bag - 1); + if (slot >= 1 && slot <= sz) + itemSlot = &inv.getBagSlot(bag - 1, slot - 1); + } + if (itemSlot && !itemSlot->empty()) + gh->useItemById(itemSlot->item.itemId); + return 0; +} + +// _GetItemTooltipData(itemId) → table with armor, bind, stats, damage, description +// Returns a Lua table with detailed item info for tooltip building +static int lua_GetItemTooltipData(lua_State* L) { + auto* gh = getGameHandler(L); + uint32_t itemId = static_cast(luaL_checknumber(L, 1)); + if (!gh || itemId == 0) { return luaReturnNil(L); } + const auto* info = gh->getItemInfo(itemId); + if (!info) { return luaReturnNil(L); } + + lua_newtable(L); + // Unique / Heroic flags + if (info->maxCount == 1) { lua_pushboolean(L, 1); lua_setfield(L, -2, "isUnique"); } + if (info->itemFlags & 0x8) { lua_pushboolean(L, 1); lua_setfield(L, -2, "isHeroic"); } + if (info->itemFlags & 0x1000000) { lua_pushboolean(L, 1); lua_setfield(L, -2, "isUniqueEquipped"); } + // Bind type + lua_pushnumber(L, info->bindType); + lua_setfield(L, -2, "bindType"); + // Armor + lua_pushnumber(L, info->armor); + lua_setfield(L, -2, "armor"); + // Damage + lua_pushnumber(L, info->damageMin); + lua_setfield(L, -2, "damageMin"); + lua_pushnumber(L, info->damageMax); + lua_setfield(L, -2, "damageMax"); + lua_pushnumber(L, info->delayMs); + lua_setfield(L, -2, "speed"); + // Primary stats + if (info->stamina != 0) { lua_pushnumber(L, info->stamina); lua_setfield(L, -2, "stamina"); } + if (info->strength != 0) { lua_pushnumber(L, info->strength); lua_setfield(L, -2, "strength"); } + if (info->agility != 0) { lua_pushnumber(L, info->agility); lua_setfield(L, -2, "agility"); } + if (info->intellect != 0) { lua_pushnumber(L, info->intellect); lua_setfield(L, -2, "intellect"); } + if (info->spirit != 0) { lua_pushnumber(L, info->spirit); lua_setfield(L, -2, "spirit"); } + // Description + if (!info->description.empty()) { + lua_pushstring(L, info->description.c_str()); + lua_setfield(L, -2, "description"); + } + // Required level + lua_pushnumber(L, info->requiredLevel); + lua_setfield(L, -2, "requiredLevel"); + // Extra stats (hit, crit, haste, AP, SP, etc.) as array of {type, value} pairs + if (!info->extraStats.empty()) { + lua_newtable(L); + for (size_t i = 0; i < info->extraStats.size(); ++i) { + lua_newtable(L); + lua_pushnumber(L, info->extraStats[i].statType); + lua_setfield(L, -2, "type"); + lua_pushnumber(L, info->extraStats[i].statValue); + lua_setfield(L, -2, "value"); + lua_rawseti(L, -2, static_cast(i) + 1); + } + lua_setfield(L, -2, "extraStats"); + } + // Resistances + if (info->fireRes != 0) { lua_pushnumber(L, info->fireRes); lua_setfield(L, -2, "fireRes"); } + if (info->natureRes != 0) { lua_pushnumber(L, info->natureRes); lua_setfield(L, -2, "natureRes"); } + if (info->frostRes != 0) { lua_pushnumber(L, info->frostRes); lua_setfield(L, -2, "frostRes"); } + if (info->shadowRes != 0) { lua_pushnumber(L, info->shadowRes); lua_setfield(L, -2, "shadowRes"); } + if (info->arcaneRes != 0) { lua_pushnumber(L, info->arcaneRes); lua_setfield(L, -2, "arcaneRes"); } + // Item spell effects (Use: / Equip: / Chance on Hit:) + { + lua_newtable(L); + int spellCount = 0; + for (int i = 0; i < 5; ++i) { + if (info->spells[i].spellId == 0) continue; + ++spellCount; + lua_newtable(L); + lua_pushnumber(L, info->spells[i].spellId); + lua_setfield(L, -2, "spellId"); + lua_pushnumber(L, info->spells[i].spellTrigger); + lua_setfield(L, -2, "trigger"); + // Get spell name for display + const std::string& sName = gh->getSpellName(info->spells[i].spellId); + if (!sName.empty()) { lua_pushstring(L, sName.c_str()); lua_setfield(L, -2, "name"); } + // Get description + const std::string& sDesc = gh->getSpellDescription(info->spells[i].spellId); + if (!sDesc.empty()) { lua_pushstring(L, sDesc.c_str()); lua_setfield(L, -2, "description"); } + lua_rawseti(L, -2, spellCount); + } + if (spellCount > 0) lua_setfield(L, -2, "itemSpells"); + else lua_pop(L, 1); + } + // Gem sockets (WotLK/TBC) + int numSockets = 0; + for (int i = 0; i < 3; ++i) { + if (info->socketColor[i] != 0) ++numSockets; + } + if (numSockets > 0) { + lua_newtable(L); + for (int i = 0; i < 3; ++i) { + if (info->socketColor[i] != 0) { + lua_newtable(L); + lua_pushnumber(L, info->socketColor[i]); + lua_setfield(L, -2, "color"); + lua_rawseti(L, -2, i + 1); + } + } + lua_setfield(L, -2, "sockets"); + } + // Item set + if (info->itemSetId != 0) { + lua_pushnumber(L, info->itemSetId); + lua_setfield(L, -2, "itemSetId"); + } + // Quest-starting item + if (info->startQuestId != 0) { + lua_pushboolean(L, 1); + lua_setfield(L, -2, "startsQuest"); + } + return 1; +} + +// --- Locale/Build/Realm info --- + +static int lua_GetLocale(lua_State* L) { + lua_pushstring(L, "enUS"); + return 1; +} + +static int lua_GetBuildInfo(lua_State* L) { + // Return WotLK defaults; expansion-specific version detection would need + // access to the expansion registry which isn't available here. + lua_pushstring(L, "3.3.5a"); // 1: version + lua_pushnumber(L, 12340); // 2: buildNumber + lua_pushstring(L, "Jan 1 2025");// 3: date + lua_pushnumber(L, 30300); // 4: tocVersion + return 4; +} + +static int lua_GetCurrentMapAreaID(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getCurrentMapId() : 0); + return 1; +} + +// GetZoneText() / GetRealZoneText() → current zone name +static int lua_GetZoneText(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, ""); return 1; } + uint32_t zoneId = gh->getWorldStateZoneId(); + if (zoneId != 0) { + std::string name = gh->getWhoAreaName(zoneId); + if (!name.empty()) { lua_pushstring(L, name.c_str()); return 1; } + } + lua_pushstring(L, ""); + return 1; +} + +// GetSubZoneText() → subzone name (same as zone for now — server doesn't always send subzone) +static int lua_GetSubZoneText(lua_State* L) { + return lua_GetZoneText(L); // Best-effort: zone and subzone often overlap +} + +// GetMinimapZoneText() → zone name displayed near minimap +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. + +static int lua_IsMounted(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isMounted()); + return 1; +} + +static int lua_IsFlying(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isPlayerFlying()); + return 1; +} + +static int lua_IsSwimming(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isSwimming()); + return 1; +} + +static int lua_IsResting(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isPlayerResting()); + return 1; +} + +static int lua_IsFalling(lua_State* L) { + auto* gh = getGameHandler(L); + // Check FALLING movement flag + if (!gh) { return luaReturnFalse(L); } + const auto& mi = gh->getMovementInfo(); + lua_pushboolean(L, (mi.flags & 0x2000) != 0); // MOVEFLAG_FALLING = 0x2000 + return 1; +} + +static int lua_IsStealthed(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + // Check for stealth auras (aura flags bit 0x40 = is harmful, stealth is a buff) + // WoW detects stealth via unit flags: UNIT_FLAG_IMMUNE (0x02) or specific aura IDs + // Simplified: check player auras for known stealth spell IDs + bool stealthed = false; + for (const auto& a : gh->getPlayerAuras()) { + if (a.isEmpty() || a.spellId == 0) continue; + // Common stealth IDs: 1784 (Stealth), 5215 (Prowl), 66 (Invisibility) + if (a.spellId == 1784 || a.spellId == 5215 || a.spellId == 66 || + a.spellId == 1785 || a.spellId == 1786 || a.spellId == 1787 || + a.spellId == 11305 || a.spellId == 11306) { + stealthed = true; + break; + } + } + lua_pushboolean(L, stealthed); + return 1; +} + +static int lua_GetUnitSpeed(lua_State* L) { + auto* gh = getGameHandler(L); + const char* uid = luaL_optstring(L, 1, "player"); + if (!gh || std::string(uid) != "player") { + lua_pushnumber(L, 0); + return 1; + } + lua_pushnumber(L, gh->getServerRunSpeed()); + return 1; +} + +// --- Container/Bag API --- +// WoW bags: container 0 = backpack (16 slots), containers 1-4 = equipped bags + +static int lua_GetContainerNumSlots(lua_State* L) { + auto* gh = getGameHandler(L); + int container = static_cast(luaL_checknumber(L, 1)); + if (!gh) { return luaReturnZero(L); } + const auto& inv = gh->getInventory(); + if (container == 0) { + lua_pushnumber(L, inv.getBackpackSize()); + } else if (container >= 1 && container <= 4) { + lua_pushnumber(L, inv.getBagSize(container - 1)); + } else { + lua_pushnumber(L, 0); + } + return 1; +} + +// GetContainerItemInfo(container, slot) → texture, count, locked, quality, readable, lootable, link +static int lua_GetContainerItemInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int container = static_cast(luaL_checknumber(L, 1)); + int slot = static_cast(luaL_checknumber(L, 2)); + if (!gh) { return luaReturnNil(L); } + + const auto& inv = gh->getInventory(); + const game::ItemSlot* itemSlot = nullptr; + + if (container == 0 && slot >= 1 && slot <= inv.getBackpackSize()) { + itemSlot = &inv.getBackpackSlot(slot - 1); // WoW uses 1-based + } else if (container >= 1 && container <= 4) { + int bagIdx = container - 1; + int bagSize = inv.getBagSize(bagIdx); + if (slot >= 1 && slot <= bagSize) + itemSlot = &inv.getBagSlot(bagIdx, slot - 1); + } + + if (!itemSlot || itemSlot->empty()) { return luaReturnNil(L); } + + // Get item info for quality/icon + const auto* info = gh->getItemInfo(itemSlot->item.itemId); + + lua_pushnil(L); // texture (icon path — would need ItemDisplayInfo icon resolver) + lua_pushnumber(L, itemSlot->item.stackCount); // count + lua_pushboolean(L, 0); // locked + lua_pushnumber(L, info ? info->quality : 0); // quality + lua_pushboolean(L, 0); // readable + lua_pushboolean(L, 0); // lootable + // Build item link with quality color + std::string name = info ? info->name : ("Item #" + std::to_string(itemSlot->item.itemId)); + uint32_t q = info ? info->quality : 0; + static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"}; + uint32_t qi = q < 8 ? q : 1u; + char link[256]; + snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", + kQH[qi], itemSlot->item.itemId, name.c_str()); + lua_pushstring(L, link); // link + return 7; +} + +// GetContainerItemLink(container, slot) → item link string +static int lua_GetContainerItemLink(lua_State* L) { + auto* gh = getGameHandler(L); + int container = static_cast(luaL_checknumber(L, 1)); + int slot = static_cast(luaL_checknumber(L, 2)); + if (!gh) { return luaReturnNil(L); } + + const auto& inv = gh->getInventory(); + const game::ItemSlot* itemSlot = nullptr; + + if (container == 0 && slot >= 1 && slot <= inv.getBackpackSize()) { + itemSlot = &inv.getBackpackSlot(slot - 1); + } else if (container >= 1 && container <= 4) { + int bagIdx = container - 1; + int bagSize = inv.getBagSize(bagIdx); + if (slot >= 1 && slot <= bagSize) + itemSlot = &inv.getBagSlot(bagIdx, slot - 1); + } + + if (!itemSlot || itemSlot->empty()) { return luaReturnNil(L); } + const auto* info = gh->getItemInfo(itemSlot->item.itemId); + std::string name = info ? info->name : ("Item #" + std::to_string(itemSlot->item.itemId)); + uint32_t q = info ? info->quality : 0; + char link[256]; + static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"}; + uint32_t qi = q < 8 ? q : 1u; + snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", + kQH[qi], itemSlot->item.itemId, name.c_str()); + lua_pushstring(L, link); + return 1; +} + +// GetContainerNumFreeSlots(container) → numFreeSlots, bagType +static int lua_GetContainerNumFreeSlots(lua_State* L) { + auto* gh = getGameHandler(L); + int container = static_cast(luaL_checknumber(L, 1)); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + + const auto& inv = gh->getInventory(); + int freeSlots = 0; + int totalSlots = 0; + + if (container == 0) { + totalSlots = inv.getBackpackSize(); + for (int i = 0; i < totalSlots; ++i) + if (inv.getBackpackSlot(i).empty()) ++freeSlots; + } else if (container >= 1 && container <= 4) { + totalSlots = inv.getBagSize(container - 1); + for (int i = 0; i < totalSlots; ++i) + if (inv.getBagSlot(container - 1, i).empty()) ++freeSlots; + } + + lua_pushnumber(L, freeSlots); + lua_pushnumber(L, 0); // bagType (0 = normal) + return 2; +} + +// --- Equipment Slot API --- +// WoW inventory slot IDs: 1=Head,2=Neck,3=Shoulders,4=Shirt,5=Chest, +// 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"); + int slotId = static_cast(luaL_checknumber(L, 2)); + if (!gh || slotId < 1 || slotId > 19) { return luaReturnNil(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr != "player") { return luaReturnNil(L); } + + const auto& inv = gh->getInventory(); + const auto& slot = inv.getEquipSlot(static_cast(slotId - 1)); + if (slot.empty()) { return luaReturnNil(L); } + + const auto* info = gh->getItemInfo(slot.item.itemId); + std::string name = info ? info->name : slot.item.name; + uint32_t q = info ? info->quality : static_cast(slot.item.quality); + static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"}; + uint32_t qi = q < 8 ? q : 1u; + char link[256]; + snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", + kQH[qi], slot.item.itemId, name.c_str()); + lua_pushstring(L, link); + return 1; +} + +static int lua_GetInventoryItemID(lua_State* L) { + auto* gh = getGameHandler(L); + const char* uid = luaL_optstring(L, 1, "player"); + int slotId = static_cast(luaL_checknumber(L, 2)); + if (!gh || slotId < 1 || slotId > 19) { return luaReturnNil(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr != "player") { return luaReturnNil(L); } + + const auto& inv = gh->getInventory(); + const auto& slot = inv.getEquipSlot(static_cast(slotId - 1)); + if (slot.empty()) { return luaReturnNil(L); } + lua_pushnumber(L, slot.item.itemId); + return 1; +} + +static int lua_GetInventoryItemTexture(lua_State* L) { + auto* gh = getGameHandler(L); + const char* uid = luaL_optstring(L, 1, "player"); + int slotId = static_cast(luaL_checknumber(L, 2)); + if (!gh || slotId < 1 || slotId > 19) { return luaReturnNil(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr != "player") { return luaReturnNil(L); } + + const auto& inv = gh->getInventory(); + const auto& slot = inv.getEquipSlot(static_cast(slotId - 1)); + if (slot.empty()) { return luaReturnNil(L); } + // Return spell icon path for the item's on-use spell, or nil + lua_pushnil(L); + return 1; +} + +// --- Time & XP API --- + +static int lua_GetGameTime(lua_State* L) { + // Returns server game time as hours, minutes + auto* gh = getGameHandler(L); + if (gh) { + float gt = gh->getGameTime(); + int hours = static_cast(gt) % 24; + int mins = static_cast((gt - static_cast(gt)) * 60.0f); + lua_pushnumber(L, hours); + lua_pushnumber(L, mins); + } else { + lua_pushnumber(L, 12); + lua_pushnumber(L, 0); + } + return 2; +} + +static int lua_GetServerTime(lua_State* L) { + lua_pushnumber(L, static_cast(std::time(nullptr))); + return 1; +} + +static int lua_UnitXP(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + std::string u(uid); + toLowerInPlace(u); + if (u == "player") lua_pushnumber(L, gh->getPlayerXp()); + else lua_pushnumber(L, 0); + return 1; +} + +static int lua_UnitXPMax(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 1); return 1; } + std::string u(uid); + toLowerInPlace(u); + if (u == "player") { + uint32_t nlxp = gh->getPlayerNextLevelXp(); + lua_pushnumber(L, nlxp > 0 ? nlxp : 1); + } else { + lua_pushnumber(L, 1); + } + return 1; +} + +// GetXPExhaustion() → rested XP pool remaining (nil if none) +static int lua_GetXPExhaustion(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + uint32_t rested = gh->getPlayerRestedXp(); + if (rested > 0) lua_pushnumber(L, rested); + else lua_pushnil(L); + return 1; +} + +// GetRestState() → 1 = normal, 2 = rested +static int lua_GetRestState(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, (gh && gh->isPlayerResting()) ? 2 : 1); + return 1; +} + +// --- Quest Log API --- + +static int lua_GetNumQuestLogEntries(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + const auto& ql = gh->getQuestLog(); + lua_pushnumber(L, ql.size()); // numEntries + lua_pushnumber(L, 0); // numQuests (headers not tracked) + return 2; +} + +// GetQuestLogTitle(index) → title, level, suggestedGroup, isHeader, isCollapsed, isComplete, frequency, questID +static int lua_GetQuestLogTitle(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { return luaReturnNil(L); } + const auto& q = ql[index - 1]; // 1-based + lua_pushstring(L, q.title.c_str()); // title + lua_pushnumber(L, 0); // level (not tracked) + lua_pushnumber(L, 0); // suggestedGroup + lua_pushboolean(L, 0); // isHeader + lua_pushboolean(L, 0); // isCollapsed + lua_pushboolean(L, q.complete); // isComplete + lua_pushnumber(L, 0); // frequency + lua_pushnumber(L, q.questId); // questID + return 8; +} + +// GetQuestLogQuestText(index) → description, objectives +static int lua_GetQuestLogQuestText(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { return luaReturnNil(L); } + const auto& q = ql[index - 1]; + lua_pushstring(L, ""); // description (not stored) + lua_pushstring(L, q.objectives.c_str()); // objectives + return 2; +} + +// IsQuestComplete(questID) → boolean +static int lua_IsQuestComplete(lua_State* L) { + auto* gh = getGameHandler(L); + uint32_t questId = static_cast(luaL_checknumber(L, 1)); + if (!gh) { return luaReturnFalse(L); } + for (const auto& q : gh->getQuestLog()) { + if (q.questId == questId) { + lua_pushboolean(L, q.complete); + return 1; + } + } + lua_pushboolean(L, 0); + 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) { return luaReturnNil(L); } + 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) { return luaReturnFalse(L); } + 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) { return luaReturnNil(L); } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { return luaReturnNil(L); } + 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) { return luaReturnZero(L); } + const auto& ql = gh->getQuestLog(); + if (index > static_cast(ql.size())) { return luaReturnZero(L); } + 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) { return luaReturnNil(L); } + const auto& ql = gh->getQuestLog(); + if (questIdx > static_cast(ql.size())) { return luaReturnNil(L); } + 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 +static int lua_GetNumSkillLines(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + lua_pushnumber(L, gh->getPlayerSkills().size()); + return 1; +} + +// GetSkillLineInfo(index) → skillName, isHeader, isExpanded, skillRank, numTempPoints, skillModifier, skillMaxRank, isAbandonable, stepCost, rankCost, minLevel, skillCostType +static int lua_GetSkillLineInfo(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& skills = gh->getPlayerSkills(); + if (index > static_cast(skills.size())) { + lua_pushnil(L); + return 1; + } + // Skills are in a map — iterate to the Nth entry + auto it = skills.begin(); + std::advance(it, index - 1); + const auto& skill = it->second; + std::string name = gh->getSkillName(skill.skillId); + if (name.empty()) name = "Skill " + std::to_string(skill.skillId); + + lua_pushstring(L, name.c_str()); // 1: skillName + lua_pushboolean(L, 0); // 2: isHeader (false — flat list) + lua_pushboolean(L, 1); // 3: isExpanded + lua_pushnumber(L, skill.effectiveValue()); // 4: skillRank + lua_pushnumber(L, skill.bonusTemp); // 5: numTempPoints + lua_pushnumber(L, skill.bonusPerm); // 6: skillModifier + lua_pushnumber(L, skill.maxValue); // 7: skillMaxRank + lua_pushboolean(L, 0); // 8: isAbandonable + lua_pushnumber(L, 0); // 9: stepCost + lua_pushnumber(L, 0); // 10: rankCost + lua_pushnumber(L, 0); // 11: minLevel + lua_pushnumber(L, 0); // 12: skillCostType + return 12; +} + +// --- Friends/Ignore API --- + +// GetNumFriends() → count +static int lua_GetNumFriends(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + int count = 0; + for (const auto& c : gh->getContacts()) + if (c.isFriend()) count++; + lua_pushnumber(L, count); + return 1; +} + +// GetFriendInfo(index) → name, level, class, area, connected, status, note +static int lua_GetFriendInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { + return luaReturnNil(L); + } + int found = 0; + for (const auto& c : gh->getContacts()) { + if (!c.isFriend()) continue; + if (++found == index) { + lua_pushstring(L, c.name.c_str()); // 1: name + lua_pushnumber(L, c.level); // 2: level + static const char* kClasses[] = {"","Warrior","Paladin","Hunter","Rogue","Priest", + "Death Knight","Shaman","Mage","Warlock","","Druid"}; + lua_pushstring(L, c.classId < 12 ? kClasses[c.classId] : "Unknown"); // 3: class + std::string area; + if (c.areaId != 0) area = gh->getWhoAreaName(c.areaId); + lua_pushstring(L, area.c_str()); // 4: area + lua_pushboolean(L, c.isOnline()); // 5: connected + lua_pushstring(L, c.status == 2 ? "" : (c.status == 3 ? "" : "")); // 6: status + lua_pushstring(L, c.note.c_str()); // 7: note + return 7; + } + } + lua_pushnil(L); + return 1; +} + +// --- Guild API --- + +// IsInGuild() → boolean +static int lua_IsInGuild(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isInGuild()); + return 1; +} + +// GetGuildInfo("player") → guildName, guildRankName, guildRankIndex +static int lua_GetGuildInfoFunc(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh || !gh->isInGuild()) { return luaReturnNil(L); } + lua_pushstring(L, gh->getGuildName().c_str()); + // Get rank name for the player + const auto& roster = gh->getGuildRoster(); + std::string rankName; + uint32_t rankIndex = 0; + for (const auto& m : roster.members) { + if (m.guid == gh->getPlayerGuid()) { + rankIndex = m.rankIndex; + const auto& rankNames = gh->getGuildRankNames(); + if (rankIndex < rankNames.size()) rankName = rankNames[rankIndex]; + break; + } + } + lua_pushstring(L, rankName.c_str()); + lua_pushnumber(L, rankIndex); + return 3; +} + +// GetNumGuildMembers() → totalMembers, onlineMembers +static int lua_GetNumGuildMembers(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + const auto& roster = gh->getGuildRoster(); + int online = 0; + for (const auto& m : roster.members) + if (m.online) online++; + lua_pushnumber(L, roster.members.size()); + lua_pushnumber(L, online); + return 2; +} + +// GetGuildRosterInfo(index) → name, rank, rankIndex, level, class, zone, note, officerNote, online, status, classId +static int lua_GetGuildRosterInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& roster = gh->getGuildRoster(); + if (index > static_cast(roster.members.size())) { return luaReturnNil(L); } + const auto& m = roster.members[index - 1]; + + lua_pushstring(L, m.name.c_str()); // 1: name + const auto& rankNames = gh->getGuildRankNames(); + lua_pushstring(L, m.rankIndex < rankNames.size() + ? rankNames[m.rankIndex].c_str() : ""); // 2: rank name + lua_pushnumber(L, m.rankIndex); // 3: rankIndex + lua_pushnumber(L, m.level); // 4: level + static const char* kCls[] = {"","Warrior","Paladin","Hunter","Rogue","Priest", + "Death Knight","Shaman","Mage","Warlock","","Druid"}; + lua_pushstring(L, m.classId < 12 ? kCls[m.classId] : "Unknown"); // 5: class + std::string zone; + if (m.zoneId != 0 && m.online) zone = gh->getWhoAreaName(m.zoneId); + lua_pushstring(L, zone.c_str()); // 6: zone + lua_pushstring(L, m.publicNote.c_str()); // 7: note + lua_pushstring(L, m.officerNote.c_str()); // 8: officerNote + lua_pushboolean(L, m.online); // 9: online + lua_pushnumber(L, 0); // 10: status (0=online, 1=AFK, 2=DND) + lua_pushnumber(L, m.classId); // 11: classId (numeric) + return 11; +} + +// GetGuildRosterMOTD() → motd +static int lua_GetGuildRosterMOTD(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, ""); return 1; } + lua_pushstring(L, gh->getGuildRoster().motd.c_str()); + return 1; +} + +// GetNumIgnores() → count +static int lua_GetNumIgnores(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + int count = 0; + for (const auto& c : gh->getContacts()) + if (c.isIgnored()) count++; + lua_pushnumber(L, count); + return 1; +} + +// GetIgnoreName(index) → name +static int lua_GetIgnoreName(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + int found = 0; + for (const auto& c : gh->getContacts()) { + if (!c.isIgnored()) continue; + if (++found == index) { + lua_pushstring(L, c.name.c_str()); + return 1; + } + } + lua_pushnil(L); + return 1; +} + +// --- Talent API --- + +// GetNumTalentTabs() → count (usually 3) +static int lua_GetNumTalentTabs(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + // Count tabs matching the player's class + uint8_t classId = gh->getPlayerClass(); + uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0; + int count = 0; + for (const auto& [tabId, tab] : gh->getAllTalentTabs()) { + if (tab.classMask & classMask) count++; + } + lua_pushnumber(L, count); + return 1; +} + +// GetTalentTabInfo(tabIndex) → name, iconTexture, pointsSpent, background +static int lua_GetTalentTabInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int tabIndex = static_cast(luaL_checknumber(L, 1)); // 1-indexed + if (!gh || tabIndex < 1) { + return luaReturnNil(L); + } + uint8_t classId = gh->getPlayerClass(); + uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0; + // Find the Nth tab for this class (sorted by orderIndex) + std::vector classTabs; + for (const auto& [tabId, tab] : gh->getAllTalentTabs()) { + if (tab.classMask & classMask) classTabs.push_back(&tab); + } + std::sort(classTabs.begin(), classTabs.end(), + [](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; }); + if (tabIndex > static_cast(classTabs.size())) { + return luaReturnNil(L); + } + const auto* tab = classTabs[tabIndex - 1]; + // Count points spent in this tab + int pointsSpent = 0; + const auto& learned = gh->getLearnedTalents(); + for (const auto& [talentId, rank] : learned) { + const auto* entry = gh->getTalentEntry(talentId); + if (entry && entry->tabId == tab->tabId) pointsSpent += rank; + } + lua_pushstring(L, tab->name.c_str()); // 1: name + lua_pushnil(L); // 2: iconTexture (not resolved) + lua_pushnumber(L, pointsSpent); // 3: pointsSpent + lua_pushstring(L, tab->backgroundFile.c_str()); // 4: background + return 4; +} + +// GetNumTalents(tabIndex) → count +static int lua_GetNumTalents(lua_State* L) { + auto* gh = getGameHandler(L); + int tabIndex = static_cast(luaL_checknumber(L, 1)); + if (!gh || tabIndex < 1) { return luaReturnZero(L); } + uint8_t classId = gh->getPlayerClass(); + uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0; + std::vector classTabs; + for (const auto& [tabId, tab] : gh->getAllTalentTabs()) { + if (tab.classMask & classMask) classTabs.push_back(&tab); + } + std::sort(classTabs.begin(), classTabs.end(), + [](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; }); + if (tabIndex > static_cast(classTabs.size())) { + return luaReturnZero(L); + } + uint32_t targetTabId = classTabs[tabIndex - 1]->tabId; + int count = 0; + for (const auto& [talentId, entry] : gh->getAllTalents()) { + if (entry.tabId == targetTabId) count++; + } + lua_pushnumber(L, count); + return 1; +} + +// GetTalentInfo(tabIndex, talentIndex) → name, iconTexture, tier, column, rank, maxRank, isExceptional, available +static int lua_GetTalentInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int tabIndex = static_cast(luaL_checknumber(L, 1)); + int talentIndex = static_cast(luaL_checknumber(L, 2)); + if (!gh || tabIndex < 1 || talentIndex < 1) { + for (int i = 0; i < 8; i++) lua_pushnil(L); + return 8; + } + uint8_t classId = gh->getPlayerClass(); + uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0; + std::vector classTabs; + for (const auto& [tabId, tab] : gh->getAllTalentTabs()) { + if (tab.classMask & classMask) classTabs.push_back(&tab); + } + std::sort(classTabs.begin(), classTabs.end(), + [](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; }); + if (tabIndex > static_cast(classTabs.size())) { + for (int i = 0; i < 8; i++) lua_pushnil(L); + return 8; + } + uint32_t targetTabId = classTabs[tabIndex - 1]->tabId; + // Collect talents for this tab, sorted by row then column + std::vector tabTalents; + for (const auto& [talentId, entry] : gh->getAllTalents()) { + if (entry.tabId == targetTabId) tabTalents.push_back(&entry); + } + std::sort(tabTalents.begin(), tabTalents.end(), + [](const auto* a, const auto* b) { + return (a->row != b->row) ? a->row < b->row : a->column < b->column; + }); + if (talentIndex > static_cast(tabTalents.size())) { + for (int i = 0; i < 8; i++) lua_pushnil(L); + return 8; + } + const auto* talent = tabTalents[talentIndex - 1]; + uint8_t rank = gh->getTalentRank(talent->talentId); + // Get spell name for rank 1 spell + std::string name = gh->getSpellName(talent->rankSpells[0]); + if (name.empty()) name = "Talent " + std::to_string(talent->talentId); + + lua_pushstring(L, name.c_str()); // 1: name + lua_pushnil(L); // 2: iconTexture + lua_pushnumber(L, talent->row + 1); // 3: tier (1-indexed) + lua_pushnumber(L, talent->column + 1); // 4: column (1-indexed) + lua_pushnumber(L, rank); // 5: rank + lua_pushnumber(L, talent->maxRank); // 6: maxRank + lua_pushboolean(L, 0); // 7: isExceptional + lua_pushboolean(L, 1); // 8: available + return 8; +} + +// GetActiveTalentGroup() → 1 or 2 +static int lua_GetActiveTalentGroup(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? (gh->getActiveTalentSpec() + 1) : 1); + return 1; +} + +// --- Loot API --- + +// GetNumLootItems() → count +static int lua_GetNumLootItems(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh || !gh->isLootWindowOpen()) { return luaReturnZero(L); } + lua_pushnumber(L, gh->getCurrentLoot().items.size()); + return 1; +} + +// GetLootSlotInfo(slot) → texture, name, quantity, quality, locked +static int lua_GetLootSlotInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); // 1-indexed + if (!gh || !gh->isLootWindowOpen()) { + return luaReturnNil(L); + } + const auto& loot = gh->getCurrentLoot(); + if (slot < 1 || slot > static_cast(loot.items.size())) { + return luaReturnNil(L); + } + const auto& item = loot.items[slot - 1]; + const auto* info = gh->getItemInfo(item.itemId); + + // texture (icon path from ItemDisplayInfo.dbc) + std::string icon; + if (info && info->displayInfoId != 0) { + icon = gh->getItemIconPath(info->displayInfoId); + } + if (!icon.empty()) lua_pushstring(L, icon.c_str()); + else lua_pushnil(L); + + // name + if (info && !info->name.empty()) lua_pushstring(L, info->name.c_str()); + else lua_pushstring(L, ("Item #" + std::to_string(item.itemId)).c_str()); + + lua_pushnumber(L, item.count); // quantity + lua_pushnumber(L, info ? info->quality : 1); // quality + lua_pushboolean(L, 0); // locked (not tracked) + return 5; +} + +// GetLootSlotLink(slot) → itemLink +static int lua_GetLootSlotLink(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (!gh || !gh->isLootWindowOpen()) { return luaReturnNil(L); } + const auto& loot = gh->getCurrentLoot(); + if (slot < 1 || slot > static_cast(loot.items.size())) { + return luaReturnNil(L); + } + const auto& item = loot.items[slot - 1]; + const auto* info = gh->getItemInfo(item.itemId); + if (!info || info->name.empty()) { return luaReturnNil(L); } + static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"}; + uint32_t qi = info->quality < 8 ? info->quality : 1u; + char link[256]; + snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", + kQH[qi], item.itemId, info->name.c_str()); + lua_pushstring(L, link); + return 1; +} + +// LootSlot(slot) — take item from loot +static int lua_LootSlot(lua_State* L) { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (!gh || !gh->isLootWindowOpen()) return 0; + const auto& loot = gh->getCurrentLoot(); + if (slot < 1 || slot > static_cast(loot.items.size())) return 0; + gh->lootItem(loot.items[slot - 1].slotIndex); + return 0; +} + +// CloseLoot() — close loot window +static int lua_CloseLoot(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh) gh->closeLoot(); + return 0; +} + +// GetLootMethod() → "freeforall"|"roundrobin"|"master"|"group"|"needbeforegreed", partyLoot, raidLoot +static int lua_GetLootMethod(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, "freeforall"); lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 3; } + const auto& pd = gh->getPartyData(); + const char* method = "freeforall"; + switch (pd.lootMethod) { + case 0: method = "freeforall"; break; + case 1: method = "roundrobin"; break; + case 2: method = "master"; break; + case 3: method = "group"; break; + case 4: method = "needbeforegreed"; break; + } + lua_pushstring(L, method); + lua_pushnumber(L, 0); // partyLootMaster (index) + lua_pushnumber(L, 0); // raidLootMaster (index) + return 3; +} + +// --- Additional WoW API --- + +static int lua_UnitAffectingCombat(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr == "player") { + lua_pushboolean(L, gh->isInCombat()); + } else { + // Check UNIT_FLAG_IN_COMBAT (0x00080000) in UNIT_FIELD_FLAGS + uint64_t guid = resolveUnitGuid(gh, uidStr); + bool inCombat = false; + if (guid != 0) { + auto entity = gh->getEntityManager().getEntity(guid); + if (entity) { + uint32_t flags = entity->getField( + game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + inCombat = (flags & 0x00080000) != 0; // UNIT_FLAG_IN_COMBAT + } + } + lua_pushboolean(L, inCombat); + } + return 1; +} + +static int lua_GetNumRaidMembers(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh || !gh->isInGroup()) { return luaReturnZero(L); } + const auto& pd = gh->getPartyData(); + lua_pushnumber(L, (pd.groupType == 1) ? pd.memberCount : 0); + return 1; +} + +static int lua_GetNumPartyMembers(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh || !gh->isInGroup()) { return luaReturnZero(L); } + const auto& pd = gh->getPartyData(); + // In party (not raid), count excludes self + int count = (pd.groupType == 0) ? static_cast(pd.memberCount) : 0; + // memberCount includes self on some servers, subtract 1 if needed + if (count > 0) count = std::max(0, count - 1); + lua_pushnumber(L, count); + return 1; +} + +static int lua_UnitInParty(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr == "player") { + lua_pushboolean(L, gh->isInGroup()); + } else { + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnFalse(L); } + const auto& pd = gh->getPartyData(); + bool found = false; + for (const auto& m : pd.members) { + if (m.guid == guid) { found = true; break; } + } + lua_pushboolean(L, found); + } + return 1; +} + +static int lua_UnitInRaid(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + std::string uidStr(uid); + toLowerInPlace(uidStr); + const auto& pd = gh->getPartyData(); + if (pd.groupType != 1) { return luaReturnFalse(L); } + if (uidStr == "player") { + lua_pushboolean(L, 1); + return 1; + } + uint64_t guid = resolveUnitGuid(gh, uidStr); + bool found = false; + for (const auto& m : pd.members) { + if (m.guid == guid) { found = true; break; } + } + lua_pushboolean(L, found); + return 1; +} + +// GetRaidRosterInfo(index) → name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML +static int lua_GetRaidRosterInfo(lua_State* L) { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& pd = gh->getPartyData(); + if (index > static_cast(pd.members.size())) { return luaReturnNil(L); } + const auto& m = pd.members[index - 1]; + lua_pushstring(L, m.name.c_str()); // name + lua_pushnumber(L, m.guid == pd.leaderGuid ? 2 : (m.flags & 0x01 ? 1 : 0)); // rank (0=member, 1=assist, 2=leader) + lua_pushnumber(L, m.subGroup + 1); // subgroup (1-indexed) + lua_pushnumber(L, m.level); // level + // Class: resolve from entity if available + std::string className = "Unknown"; + auto entity = gh->getEntityManager().getEntity(m.guid); + if (entity) { + uint32_t bytes0 = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_BYTES_0)); + uint8_t classId = static_cast((bytes0 >> 8) & 0xFF); + static const char* kClasses[] = {"","Warrior","Paladin","Hunter","Rogue","Priest", + "Death Knight","Shaman","Mage","Warlock","","Druid"}; + if (classId > 0 && classId < 12) className = kClasses[classId]; + } + lua_pushstring(L, className.c_str()); // class (localized) + lua_pushstring(L, className.c_str()); // fileName + lua_pushstring(L, ""); // zone + lua_pushboolean(L, m.isOnline); // online + lua_pushboolean(L, m.curHealth == 0); // isDead + lua_pushstring(L, "NONE"); // role + lua_pushboolean(L, pd.looterGuid == m.guid ? 1 : 0); // isML + return 11; +} + +// GetThreatStatusColor(statusIndex) → r, g, b +static int lua_GetThreatStatusColor(lua_State* L) { + int status = static_cast(luaL_optnumber(L, 1, 0)); + switch (status) { + case 0: lua_pushnumber(L, 0.69f); lua_pushnumber(L, 0.69f); lua_pushnumber(L, 0.69f); break; // gray (no threat) + case 1: lua_pushnumber(L, 1.0f); lua_pushnumber(L, 1.0f); lua_pushnumber(L, 0.47f); break; // yellow (threat) + case 2: lua_pushnumber(L, 1.0f); lua_pushnumber(L, 0.6f); lua_pushnumber(L, 0.0f); break; // orange (high threat) + case 3: lua_pushnumber(L, 1.0f); lua_pushnumber(L, 0.0f); lua_pushnumber(L, 0.0f); break; // red (tanking) + default: lua_pushnumber(L, 1.0f); lua_pushnumber(L, 1.0f); lua_pushnumber(L, 1.0f); break; + } + return 3; +} + +// GetReadyCheckStatus(unit) → status string +static int lua_GetReadyCheckStatus(lua_State* L) { + (void)L; + lua_pushnil(L); // No ready check in progress + return 1; +} + +// RegisterUnitWatch / UnregisterUnitWatch — secure unit frame stubs +static int lua_RegisterUnitWatch(lua_State* L) { (void)L; return 0; } +static int lua_UnregisterUnitWatch(lua_State* L) { (void)L; return 0; } + +static int lua_UnitIsUnit(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + const char* uid1 = luaL_checkstring(L, 1); + const char* uid2 = luaL_checkstring(L, 2); + std::string u1(uid1), u2(uid2); + toLowerInPlace(u1); + toLowerInPlace(u2); + uint64_t g1 = resolveUnitGuid(gh, u1); + uint64_t g2 = resolveUnitGuid(gh, u2); + lua_pushboolean(L, g1 != 0 && g1 == g2); + return 1; +} + +static int lua_UnitIsFriend(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + lua_pushboolean(L, unit && !unit->isHostile()); + return 1; +} + +static int lua_UnitIsEnemy(lua_State* L) { + const char* uid = luaL_optstring(L, 1, "player"); + auto* unit = resolveUnit(L, uid); + lua_pushboolean(L, unit && unit->isHostile()); + return 1; +} + +static int lua_UnitCreatureType(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, "Unknown"); return 1; } + const char* uid = luaL_optstring(L, 1, "target"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushstring(L, "Unknown"); return 1; } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity) { lua_pushstring(L, "Unknown"); return 1; } + // Player units are always "Humanoid" + if (entity->getType() == game::ObjectType::PLAYER) { + lua_pushstring(L, "Humanoid"); + return 1; + } + auto unit = std::dynamic_pointer_cast(entity); + if (!unit) { lua_pushstring(L, "Unknown"); return 1; } + uint32_t ct = gh->getCreatureType(unit->getEntry()); + static const char* kTypes[] = { + "Unknown", "Beast", "Dragonkin", "Demon", "Elemental", + "Giant", "Undead", "Humanoid", "Critter", "Mechanical", + "Not specified", "Totem", "Non-combat Pet", "Gas Cloud" + }; + lua_pushstring(L, (ct < 14) ? kTypes[ct] : "Unknown"); + return 1; +} + +// GetPlayerInfoByGUID(guid) → localizedClass, englishClass, localizedRace, englishRace, sex, name, realm +static int lua_GetPlayerInfoByGUID(lua_State* L) { + auto* gh = getGameHandler(L); + const char* guidStr = luaL_checkstring(L, 1); + if (!gh || !guidStr) { + for (int i = 0; i < 7; i++) lua_pushnil(L); + return 7; + } + // Parse hex GUID string "0x0000000000000001" + uint64_t guid = 0; + if (guidStr[0] == '0' && (guidStr[1] == 'x' || guidStr[1] == 'X')) + guid = strtoull(guidStr + 2, nullptr, 16); + else + guid = strtoull(guidStr, nullptr, 16); + + if (guid == 0) { for (int i = 0; i < 7; i++) lua_pushnil(L); return 7; } + + // Look up entity name + std::string name = gh->lookupName(guid); + if (name.empty() && guid == gh->getPlayerGuid()) { + const auto& chars = gh->getCharacters(); + for (const auto& c : chars) + if (c.guid == guid) { name = c.name; break; } + } + + // For player GUID, return class/race if it's the local player + const char* className = "Unknown"; + const char* raceName = "Unknown"; + if (guid == gh->getPlayerGuid()) { + static const char* kClasses[] = {"","Warrior","Paladin","Hunter","Rogue","Priest", + "Death Knight","Shaman","Mage","Warlock","","Druid"}; + static const char* kRaces[] = {"","Human","Orc","Dwarf","Night Elf","Undead", + "Tauren","Gnome","Troll","","Blood Elf","Draenei"}; + uint8_t cid = gh->getPlayerClass(); + uint8_t rid = gh->getPlayerRace(); + if (cid < 12) className = kClasses[cid]; + if (rid < 12) raceName = kRaces[rid]; + } + + lua_pushstring(L, className); // 1: localizedClass + lua_pushstring(L, className); // 2: englishClass + lua_pushstring(L, raceName); // 3: localizedRace + lua_pushstring(L, raceName); // 4: englishRace + lua_pushnumber(L, 0); // 5: sex (0=unknown) + lua_pushstring(L, name.c_str()); // 6: name + lua_pushstring(L, ""); // 7: realm + return 7; +} + +// GetItemLink(itemId) → "|cFFxxxxxx|Hitem:ID:...|h[Name]|h|r" +static int lua_GetItemLink(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + uint32_t itemId = static_cast(luaL_checknumber(L, 1)); + if (itemId == 0) { return luaReturnNil(L); } + const auto* info = gh->getItemInfo(itemId); + if (!info || info->name.empty()) { return luaReturnNil(L); } + static const char* kQH[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"}; + uint32_t qi = info->quality < 8 ? info->quality : 1u; + char link[256]; + snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", + kQH[qi], itemId, info->name.c_str()); + lua_pushstring(L, link); + return 1; +} + +// GetSpellLink(spellIdOrName) → "|cFFxxxxxx|Hspell:ID|h[Name]|h|r" +static int lua_GetSpellLink(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + + uint32_t spellId = 0; + if (lua_isnumber(L, 1)) { + spellId = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + const char* name = lua_tostring(L, 1); + if (!name || !*name) { return luaReturnNil(L); } + std::string nameLow(name); + toLowerInPlace(nameLow); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn == nameLow) { spellId = sid; break; } + } + } + if (spellId == 0) { return luaReturnNil(L); } + std::string name = gh->getSpellName(spellId); + if (name.empty()) { return luaReturnNil(L); } + char link[256]; + snprintf(link, sizeof(link), "|cff71d5ff|Hspell:%u|h[%s]|h|r", spellId, name.c_str()); + lua_pushstring(L, link); + return 1; +} + +// IsUsableSpell(spellIdOrName) → usable, noMana +static int lua_IsUsableSpell(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; } + + uint32_t spellId = 0; + if (lua_isnumber(L, 1)) { + spellId = static_cast(lua_tonumber(L, 1)); + } else if (lua_isstring(L, 1)) { + const char* name = lua_tostring(L, 1); + if (!name || !*name) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; } + std::string nameLow(name); + toLowerInPlace(nameLow); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + toLowerInPlace(sn); + if (sn == nameLow) { spellId = sid; break; } + } + } + + if (spellId == 0 || !gh->getKnownSpells().count(spellId)) { + lua_pushboolean(L, 0); + lua_pushboolean(L, 0); + return 2; + } + + // Check if on cooldown + float cd = gh->getSpellCooldown(spellId); + bool onCooldown = (cd > 0.1f); + + // Check mana/power cost + bool noMana = false; + if (!onCooldown) { + auto spellData = gh->getSpellData(spellId); + if (spellData.manaCost > 0) { + auto playerEntity = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (playerEntity) { + auto* unit = dynamic_cast(playerEntity.get()); + if (unit && unit->getPower() < spellData.manaCost) { + noMana = true; + } + } + } + } + lua_pushboolean(L, (onCooldown || noMana) ? 0 : 1); // usable + lua_pushboolean(L, noMana ? 1 : 0); // notEnoughMana + return 2; +} + +// IsInInstance() → isInstance, instanceType +static int lua_IsInInstance(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushboolean(L, 0); lua_pushstring(L, "none"); return 2; } + bool inInstance = gh->isInInstance(); + lua_pushboolean(L, inInstance); + lua_pushstring(L, inInstance ? "party" : "none"); // simplified: "none", "party", "raid", "pvp", "arena" + return 2; +} + +// GetInstanceInfo() → name, type, difficultyIndex, difficultyName, maxPlayers, ... +static int lua_GetInstanceInfo(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { + lua_pushstring(L, ""); lua_pushstring(L, "none"); lua_pushnumber(L, 0); + lua_pushstring(L, "Normal"); lua_pushnumber(L, 0); + return 5; + } + std::string mapName = gh->getMapName(gh->getCurrentMapId()); + lua_pushstring(L, mapName.c_str()); // 1: name + lua_pushstring(L, gh->isInInstance() ? "party" : "none"); // 2: instanceType + lua_pushnumber(L, gh->getInstanceDifficulty()); // 3: difficultyIndex + static const char* kDiff[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"}; + uint32_t diff = gh->getInstanceDifficulty(); + lua_pushstring(L, (diff < 4) ? kDiff[diff] : "Normal"); // 4: difficultyName + lua_pushnumber(L, 5); // 5: maxPlayers (default 5-man) + return 5; +} + +// GetInstanceDifficulty() → difficulty (1=normal, 2=heroic, 3=25normal, 4=25heroic) +static int lua_GetInstanceDifficulty(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? (gh->getInstanceDifficulty() + 1) : 1); // WoW returns 1-based + return 1; +} + +// UnitClassification(unit) → "normal", "elite", "rareelite", "worldboss", "rare" +static int lua_UnitClassification(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, "normal"); return 1; } + const char* uid = luaL_optstring(L, 1, "target"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushstring(L, "normal"); return 1; } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity || entity->getType() == game::ObjectType::PLAYER) { + lua_pushstring(L, "normal"); + return 1; + } + auto unit = std::dynamic_pointer_cast(entity); + if (!unit) { lua_pushstring(L, "normal"); return 1; } + int rank = gh->getCreatureRank(unit->getEntry()); + switch (rank) { + case 1: lua_pushstring(L, "elite"); break; + case 2: lua_pushstring(L, "rareelite"); break; + case 3: lua_pushstring(L, "worldboss"); break; + case 4: lua_pushstring(L, "rare"); break; + default: lua_pushstring(L, "normal"); break; + } + return 1; +} + +// GetComboPoints("player"|"vehicle", "target") → number +static int lua_GetComboPoints(lua_State* L) { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getComboPoints() : 0); + return 1; +} + +// UnitReaction(unit, otherUnit) → 1-8 (hostile to exalted) +// Simplified: hostile=2, neutral=4, friendly=5 +static int lua_UnitReaction(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + const char* uid1 = luaL_checkstring(L, 1); + const char* uid2 = luaL_checkstring(L, 2); + auto* unit2 = resolveUnit(L, uid2); + if (!unit2) { return luaReturnNil(L); } + // If unit2 is the player, always friendly to self + std::string u1(uid1); + toLowerInPlace(u1); + std::string u2(uid2); + toLowerInPlace(u2); + uint64_t g1 = resolveUnitGuid(gh, u1); + uint64_t g2 = resolveUnitGuid(gh, u2); + if (g1 == g2) { lua_pushnumber(L, 5); return 1; } // same unit = friendly + if (unit2->isHostile()) { + lua_pushnumber(L, 2); // hostile + } else { + lua_pushnumber(L, 5); // friendly + } + return 1; +} + +// UnitIsConnected(unit) → boolean +static int lua_UnitIsConnected(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + const char* uid = luaL_optstring(L, 1, "player"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { return luaReturnFalse(L); } + // Player is always connected + if (guid == gh->getPlayerGuid()) { lua_pushboolean(L, 1); return 1; } + // Check party/raid member online status + const auto& pd = gh->getPartyData(); + for (const auto& m : pd.members) { + if (m.guid == guid) { + lua_pushboolean(L, m.isOnline ? 1 : 0); + return 1; + } + } + // Non-party entities that exist are considered connected + auto entity = gh->getEntityManager().getEntity(guid); + lua_pushboolean(L, entity ? 1 : 0); + return 1; +} + +// HasAction(slot) → boolean (1-indexed slot) +static int lua_HasAction(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + int slot = static_cast(luaL_checknumber(L, 1)) - 1; // WoW uses 1-indexed slots + const auto& bar = gh->getActionBar(); + if (slot < 0 || slot >= static_cast(bar.size())) { + lua_pushboolean(L, 0); + return 1; + } + lua_pushboolean(L, !bar[slot].isEmpty()); + return 1; +} + +// GetActionTexture(slot) → texturePath or nil +static int lua_GetActionTexture(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnNil(L); } + 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]; + if (action.type == game::ActionBarSlot::SPELL) { + std::string icon = gh->getSpellIconPath(action.id); + if (!icon.empty()) { + lua_pushstring(L, icon.c_str()); + return 1; + } + } else if (action.type == game::ActionBarSlot::ITEM && action.id != 0) { + const auto* info = gh->getItemInfo(action.id); + if (info && info->displayInfoId != 0) { + std::string icon = gh->getItemIconPath(info->displayInfoId); + if (!icon.empty()) { + lua_pushstring(L, icon.c_str()); + return 1; + } + } + } + lua_pushnil(L); + return 1; +} + +// IsCurrentAction(slot) → boolean +static int lua_IsCurrentAction(lua_State* L) { + // Currently no "active action" tracking; return false + (void)L; + lua_pushboolean(L, 0); + return 1; +} + +// IsUsableAction(slot) → usable, notEnoughMana +static int lua_IsUsableAction(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; } + 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_pushboolean(L, 0); + lua_pushboolean(L, 0); + return 2; + } + const auto& action = bar[slot]; + bool usable = action.isReady(); + bool noMana = false; + if (action.type == game::ActionBarSlot::SPELL) { + usable = usable && gh->getKnownSpells().count(action.id); + // Check power cost + if (usable && action.id != 0) { + auto spellData = gh->getSpellData(action.id); + if (spellData.manaCost > 0) { + auto pe = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (pe) { + auto* unit = dynamic_cast(pe.get()); + if (unit && unit->getPower() < spellData.manaCost) { + noMana = true; + usable = false; + } + } + } + } + } + lua_pushboolean(L, usable ? 1 : 0); + lua_pushboolean(L, noMana ? 1 : 0); + 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) { return luaReturnNil(L); } + 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) { return luaReturnNil(L); } + + 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) { return luaReturnNil(L); } + auto targetEnt = gh->getEntityManager().getEntity(targetGuid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { return luaReturnNil(L); } + + 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) { return luaReturnZero(L); } + 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); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 1); return 3; } + 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); + lua_pushnumber(L, 0); + lua_pushnumber(L, 1); + return 3; + } + const auto& action = bar[slot]; + if (action.cooldownRemaining > 0.0f) { + // WoW returns GetTime()-based start time; approximate + double now = 0; + lua_getglobal(L, "GetTime"); + if (lua_isfunction(L, -1)) { + lua_call(L, 0, 1); + now = lua_tonumber(L, -1); + lua_pop(L, 1); + } else { + lua_pop(L, 1); + } + double start = now - (action.cooldownTotal - action.cooldownRemaining); + lua_pushnumber(L, start); + lua_pushnumber(L, action.cooldownTotal); + lua_pushnumber(L, 1); + } else if (action.type == game::ActionBarSlot::SPELL && gh->isGCDActive()) { + // No individual cooldown but GCD is active — show GCD sweep + float gcdRem = gh->getGCDRemaining(); + float gcdTotal = gh->getGCDTotal(); + double now = 0; + lua_getglobal(L, "GetTime"); + if (lua_isfunction(L, -1)) { lua_call(L, 0, 1); now = lua_tonumber(L, -1); lua_pop(L, 1); } + else lua_pop(L, 1); + double elapsed = gcdTotal - gcdRem; + lua_pushnumber(L, now - elapsed); + lua_pushnumber(L, gcdTotal); + lua_pushnumber(L, 1); + } else { + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + lua_pushnumber(L, 1); + } + return 3; +} + +// UseAction(slot, checkCursor, onSelf) — activate action bar slot (1-indexed) +static int lua_UseAction(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]; + if (action.type == game::ActionBarSlot::SPELL && action.isReady()) { + uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0; + gh->castSpell(action.id, target); + } else if (action.type == game::ActionBarSlot::ITEM && action.id != 0) { + gh->useItemById(action.id); + } + // Macro execution requires GameScreen context; not available from pure Lua API + return 0; +} + +// CancelUnitBuff(unit, index) — cancel a buff by index (1-indexed) +static int lua_CancelUnitBuff(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + const char* uid = luaL_optstring(L, 1, "player"); + std::string uidStr(uid); + toLowerInPlace(uidStr); + if (uidStr != "player") return 0; // Can only cancel own buffs + int index = static_cast(luaL_checknumber(L, 2)); + const auto& auras = gh->getPlayerAuras(); + // Find the Nth buff (non-debuff) + int buffCount = 0; + for (const auto& a : auras) { + if (a.isEmpty()) continue; + if ((a.flags & 0x80) != 0) continue; // skip debuffs + if (++buffCount == index) { + gh->cancelAura(a.spellId); + break; + } + } + return 0; +} + +// CastSpellByID(spellId) — cast spell by numeric ID +static int lua_CastSpellByID(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + uint32_t spellId = static_cast(luaL_checknumber(L, 1)); + if (spellId == 0) return 0; + uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0; + gh->castSpell(spellId, target); + return 0; +} + +// --- Cursor / Drag-Drop System --- +// Tracks what the player is "holding" on the cursor (spell, item, action). + +enum class CursorType { NONE, SPELL, ITEM, ACTION }; +static CursorType s_cursorType = CursorType::NONE; +static uint32_t s_cursorId = 0; // spellId, itemId, or action slot +static int s_cursorSlot = 0; // source slot for placement +static int s_cursorBag = -1; // source bag for container items + +static int lua_ClearCursor(lua_State* L) { + (void)L; + s_cursorType = CursorType::NONE; + s_cursorId = 0; + s_cursorSlot = 0; + s_cursorBag = -1; + return 0; +} + +static int lua_GetCursorInfo(lua_State* L) { + switch (s_cursorType) { + case CursorType::SPELL: + lua_pushstring(L, "spell"); + lua_pushnumber(L, 0); // bookSlotIndex + lua_pushstring(L, "spell"); // bookType + lua_pushnumber(L, s_cursorId); // spellId + return 4; + case CursorType::ITEM: + lua_pushstring(L, "item"); + lua_pushnumber(L, s_cursorId); + return 2; + case CursorType::ACTION: + lua_pushstring(L, "action"); + lua_pushnumber(L, s_cursorSlot); + return 2; + default: + return 0; + } +} + +static int lua_CursorHasItem(lua_State* L) { + lua_pushboolean(L, s_cursorType == CursorType::ITEM ? 1 : 0); + return 1; +} + +static int lua_CursorHasSpell(lua_State* L) { + lua_pushboolean(L, s_cursorType == CursorType::SPELL ? 1 : 0); + return 1; +} + +// PickupAction(slot) — picks up an action from the action bar +static int lua_PickupAction(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + int slot = static_cast(luaL_checknumber(L, 1)); + const auto& bar = gh->getActionBar(); + if (slot < 1 || slot > static_cast(bar.size())) return 0; + const auto& action = bar[slot - 1]; + if (action.isEmpty()) { + // Empty slot — if cursor has something, place it + if (s_cursorType == CursorType::SPELL && s_cursorId != 0) { + gh->setActionBarSlot(slot - 1, game::ActionBarSlot::SPELL, s_cursorId); + s_cursorType = CursorType::NONE; + s_cursorId = 0; + } + } else { + // Pick up existing action + s_cursorType = (action.type == game::ActionBarSlot::SPELL) ? CursorType::SPELL : + (action.type == game::ActionBarSlot::ITEM) ? CursorType::ITEM : + CursorType::ACTION; + s_cursorId = action.id; + s_cursorSlot = slot; + } + return 0; +} + +// PlaceAction(slot) — places cursor content into an action bar slot +static int lua_PlaceAction(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + int slot = static_cast(luaL_checknumber(L, 1)); + if (slot < 1 || slot > static_cast(gh->getActionBar().size())) return 0; + if (s_cursorType == CursorType::SPELL && s_cursorId != 0) { + gh->setActionBarSlot(slot - 1, game::ActionBarSlot::SPELL, s_cursorId); + } else if (s_cursorType == CursorType::ITEM && s_cursorId != 0) { + gh->setActionBarSlot(slot - 1, game::ActionBarSlot::ITEM, s_cursorId); + } + s_cursorType = CursorType::NONE; + s_cursorId = 0; + return 0; +} + +// PickupSpell(bookSlot, bookType) — picks up a spell from the spellbook +static int lua_PickupSpell(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + int slot = static_cast(luaL_checknumber(L, 1)); + const auto& tabs = gh->getSpellBookTabs(); + int idx = slot; + for (const auto& tab : tabs) { + if (idx <= static_cast(tab.spellIds.size())) { + s_cursorType = CursorType::SPELL; + s_cursorId = tab.spellIds[idx - 1]; + return 0; + } + idx -= static_cast(tab.spellIds.size()); + } + return 0; +} + +// PickupSpellBookItem(bookSlot, bookType) — alias for PickupSpell +static int lua_PickupSpellBookItem(lua_State* L) { + return lua_PickupSpell(L); +} + +// PickupContainerItem(bag, slot) — picks up an item from a bag +static int lua_PickupContainerItem(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + int bag = static_cast(luaL_checknumber(L, 1)); + int slot = static_cast(luaL_checknumber(L, 2)); + const auto& inv = gh->getInventory(); + const game::ItemSlot* itemSlot = nullptr; + if (bag == 0 && slot >= 1 && slot <= inv.getBackpackSize()) { + itemSlot = &inv.getBackpackSlot(slot - 1); + } else if (bag >= 1 && bag <= 4) { + int bagSize = inv.getBagSize(bag - 1); + if (slot >= 1 && slot <= bagSize) { + itemSlot = &inv.getBagSlot(bag - 1, slot - 1); + } + } + if (itemSlot && !itemSlot->empty()) { + s_cursorType = CursorType::ITEM; + s_cursorId = itemSlot->item.itemId; + s_cursorBag = bag; + s_cursorSlot = slot; + } + return 0; +} + +// PickupInventoryItem(slot) — picks up an equipped item +static int lua_PickupInventoryItem(lua_State* L) { + auto* gh = getGameHandler(L); + if (!gh) return 0; + int slot = static_cast(luaL_checknumber(L, 1)); + if (slot < 1 || slot > 19) return 0; + const auto& inv = gh->getInventory(); + const auto& eq = inv.getEquipSlot(static_cast(slot - 1)); + if (!eq.empty()) { + s_cursorType = CursorType::ITEM; + s_cursorId = eq.item.itemId; + s_cursorSlot = slot; + s_cursorBag = -1; + } + return 0; +} + +// DeleteCursorItem() — destroys the item on cursor +static int lua_DeleteCursorItem(lua_State* L) { + (void)L; + s_cursorType = CursorType::NONE; + s_cursorId = 0; + return 0; +} + +// AutoEquipCursorItem() — equip item from cursor +static int lua_AutoEquipCursorItem(lua_State* L) { + auto* gh = getGameHandler(L); + if (gh && s_cursorType == CursorType::ITEM && s_cursorId != 0) { + gh->useItemById(s_cursorId); + } + s_cursorType = CursorType::NONE; + s_cursorId = 0; + return 0; +} + +// --- Frame System --- +// Minimal WoW-compatible frame objects with RegisterEvent/SetScript/GetScript. +// Frames are Lua tables with a metatable that provides methods. + +// Frame method: frame:RegisterEvent("EVENT") +static int lua_Frame_RegisterEvent(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); // self + const char* eventName = luaL_checkstring(L, 2); + + // Get frame's registered events table (create if needed) + lua_getfield(L, 1, "__events"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, 1, "__events"); + } + lua_pushboolean(L, 1); + lua_setfield(L, -2, eventName); + lua_pop(L, 1); + + // Also register in global __WoweeFrameEvents for dispatch + lua_getglobal(L, "__WoweeFrameEvents"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setglobal(L, "__WoweeFrameEvents"); + } + lua_getfield(L, -1, eventName); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, -3, eventName); + } + // Append frame reference + int len = static_cast(lua_objlen(L, -1)); + lua_pushvalue(L, 1); // push frame + lua_rawseti(L, -2, len + 1); + lua_pop(L, 2); // pop list + __WoweeFrameEvents + return 0; +} + +// Frame method: frame:UnregisterEvent("EVENT") +static int lua_Frame_UnregisterEvent(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + const char* eventName = luaL_checkstring(L, 2); + + // Remove from frame's own events + lua_getfield(L, 1, "__events"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + lua_setfield(L, -2, eventName); + } + lua_pop(L, 1); + return 0; +} + +// Frame method: frame:SetScript("handler", func) +static int lua_Frame_SetScript(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + const char* scriptType = luaL_checkstring(L, 2); + // arg 3 can be function or nil + lua_getfield(L, 1, "__scripts"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, 1, "__scripts"); + } + lua_pushvalue(L, 3); + lua_setfield(L, -2, scriptType); + lua_pop(L, 1); + + // Track frames with OnUpdate in __WoweeOnUpdateFrames + if (strcmp(scriptType, "OnUpdate") == 0) { + lua_getglobal(L, "__WoweeOnUpdateFrames"); + if (!lua_istable(L, -1)) { lua_pop(L, 1); return 0; } + if (lua_isfunction(L, 3)) { + // Add frame to the list + int len = static_cast(lua_objlen(L, -1)); + lua_pushvalue(L, 1); + lua_rawseti(L, -2, len + 1); + } + lua_pop(L, 1); + } + return 0; +} + +// Frame method: frame:GetScript("handler") +static int lua_Frame_GetScript(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + const char* scriptType = luaL_checkstring(L, 2); + lua_getfield(L, 1, "__scripts"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, scriptType); + } else { + lua_pushnil(L); + } + return 1; +} + +// Frame method: frame:GetName() +static int lua_Frame_GetName(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__name"); + return 1; +} + +// Frame method: frame:Show() / frame:Hide() / frame:IsShown() / frame:IsVisible() +static int lua_Frame_Show(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushboolean(L, 1); + lua_setfield(L, 1, "__visible"); + return 0; +} +static int lua_Frame_Hide(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushboolean(L, 0); + lua_setfield(L, 1, "__visible"); + return 0; +} +static int lua_Frame_IsShown(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__visible"); + lua_pushboolean(L, lua_toboolean(L, -1)); + return 1; +} + +// Frame method: frame:CreateTexture(name, layer) → texture stub +static int lua_Frame_CreateTexture(lua_State* L) { + lua_newtable(L); + // Add noop methods for common texture operations + luaL_dostring(L, + "return function(t) " + "function t:SetTexture() end " + "function t:SetTexCoord() end " + "function t:SetVertexColor() end " + "function t:SetAllPoints() end " + "function t:SetPoint() end " + "function t:SetSize() end " + "function t:SetWidth() end " + "function t:SetHeight() end " + "function t:Show() end " + "function t:Hide() end " + "function t:SetAlpha() end " + "function t:GetTexture() return '' end " + "function t:SetDesaturated() end " + "function t:SetBlendMode() end " + "function t:SetDrawLayer() end " + "end"); + lua_pushvalue(L, -2); // push the table + lua_call(L, 1, 0); // call the function with the table + return 1; +} + +// Frame method: frame:CreateFontString(name, layer, template) → fontstring stub +static int lua_Frame_CreateFontString(lua_State* L) { + lua_newtable(L); + luaL_dostring(L, + "return function(fs) " + "fs._text = '' " + "function fs:SetText(t) self._text = t or '' end " + "function fs:GetText() return self._text end " + "function fs:SetFont() end " + "function fs:SetFontObject() end " + "function fs:SetTextColor() end " + "function fs:SetJustifyH() end " + "function fs:SetJustifyV() end " + "function fs:SetPoint() end " + "function fs:SetAllPoints() end " + "function fs:Show() end " + "function fs:Hide() end " + "function fs:SetAlpha() end " + "function fs:GetStringWidth() return 0 end " + "function fs:GetStringHeight() return 0 end " + "function fs:SetWordWrap() end " + "function fs:SetNonSpaceWrap() end " + "function fs:SetMaxLines() end " + "function fs:SetShadowOffset() end " + "function fs:SetShadowColor() end " + "function fs:SetWidth() end " + "function fs:SetHeight() end " + "end"); + lua_pushvalue(L, -2); + lua_call(L, 1, 0); + return 1; +} + +// GetFramerate() → fps +static int lua_GetFramerate(lua_State* L) { + lua_pushnumber(L, static_cast(ImGui::GetIO().Framerate)); + return 1; +} + +// GetCursorPosition() → x, y (screen coordinates, origin top-left) +static int lua_GetCursorPosition(lua_State* L) { + const auto& io = ImGui::GetIO(); + lua_pushnumber(L, io.MousePos.x); + lua_pushnumber(L, io.MousePos.y); + return 2; +} + +// GetScreenWidth() → width +static int lua_GetScreenWidth(lua_State* L) { + auto* window = core::Application::getInstance().getWindow(); + lua_pushnumber(L, window ? window->getWidth() : 1920); + return 1; +} + +// GetScreenHeight() → height +static int lua_GetScreenHeight(lua_State* L) { + auto* window = core::Application::getInstance().getWindow(); + lua_pushnumber(L, window ? window->getHeight() : 1080); + return 1; +} + +// Modifier key state queries using ImGui IO +static int lua_IsShiftKeyDown(lua_State* L) { + lua_pushboolean(L, ImGui::GetIO().KeyShift ? 1 : 0); + return 1; +} +static int lua_IsControlKeyDown(lua_State* L) { + lua_pushboolean(L, ImGui::GetIO().KeyCtrl ? 1 : 0); + return 1; +} +static int lua_IsAltKeyDown(lua_State* L) { + lua_pushboolean(L, ImGui::GetIO().KeyAlt ? 1 : 0); + return 1; +} + +// IsModifiedClick(action) → boolean +// Checks if a modifier key combo matches a named click action. +// Common actions: "CHATLINK" (shift-click), "DRESSUP" (ctrl-click), +// "SPLITSTACK" (shift-click), "SELFCAST" (alt-click) +static int lua_IsModifiedClick(lua_State* L) { + const char* action = luaL_optstring(L, 1, ""); + std::string act(action); + for (char& c : act) c = static_cast(std::toupper(static_cast(c))); + const auto& io = ImGui::GetIO(); + bool result = false; + if (act == "CHATLINK" || act == "SPLITSTACK") + result = io.KeyShift; + else if (act == "DRESSUP" || act == "COMPAREITEMS") + result = io.KeyCtrl; + else if (act == "SELFCAST" || act == "FOCUSCAST") + result = io.KeyAlt; + else if (act == "STICKYCAMERA") + result = io.KeyCtrl; + else + result = io.KeyShift; // Default: shift for unknown actions + lua_pushboolean(L, result ? 1 : 0); + return 1; +} + +// GetModifiedClick(action) → key name ("SHIFT", "CTRL", "ALT", "NONE") +static int lua_GetModifiedClick(lua_State* L) { + const char* action = luaL_optstring(L, 1, ""); + std::string act(action); + for (char& c : act) c = static_cast(std::toupper(static_cast(c))); + if (act == "CHATLINK" || act == "SPLITSTACK") + lua_pushstring(L, "SHIFT"); + else if (act == "DRESSUP" || act == "COMPAREITEMS") + lua_pushstring(L, "CTRL"); + else if (act == "SELFCAST" || act == "FOCUSCAST") + lua_pushstring(L, "ALT"); + else + lua_pushstring(L, "SHIFT"); + return 1; +} +static int lua_SetModifiedClick(lua_State* L) { (void)L; return 0; } + +// --- Keybinding API --- +// Maps WoW binding names like "ACTIONBUTTON1" to key display strings like "1" + +// GetBindingKey(command) → key1, key2 (or nil) +static int lua_GetBindingKey(lua_State* L) { + const char* cmd = luaL_checkstring(L, 1); + std::string command(cmd); + // Return intuitive default bindings for action buttons + if (command.find("ACTIONBUTTON") == 0) { + std::string num = command.substr(12); + int n = 0; + try { n = std::stoi(num); } catch(...) {} + if (n >= 1 && n <= 9) { + lua_pushstring(L, num.c_str()); + return 1; + } else if (n == 10) { + lua_pushstring(L, "0"); + return 1; + } else if (n == 11) { + lua_pushstring(L, "-"); + return 1; + } else if (n == 12) { + lua_pushstring(L, "="); + return 1; + } + } + lua_pushnil(L); + return 1; +} + +// GetBindingAction(key) → command (or nil) +static int lua_GetBindingAction(lua_State* L) { + const char* key = luaL_checkstring(L, 1); + std::string k(key); + // Simple reverse mapping for number keys + if (k.size() == 1 && k[0] >= '1' && k[0] <= '9') { + lua_pushstring(L, ("ACTIONBUTTON" + k).c_str()); + return 1; + } else if (k == "0") { + lua_pushstring(L, "ACTIONBUTTON10"); + return 1; + } + lua_pushnil(L); + return 1; +} + +static int lua_GetNumBindings(lua_State* L) { return luaReturnZero(L); } +static int lua_GetBinding(lua_State* L) { (void)L; lua_pushnil(L); return 1; } +static int lua_SetBinding(lua_State* L) { (void)L; return 0; } +static int lua_SaveBindings(lua_State* L) { (void)L; return 0; } +static int lua_SetOverrideBindingClick(lua_State* L) { (void)L; return 0; } +static int lua_ClearOverrideBindings(lua_State* L) { (void)L; return 0; } + +// Frame methods: SetPoint, SetSize, SetWidth, SetHeight, GetWidth, GetHeight, GetCenter, SetAlpha, GetAlpha +static int lua_Frame_SetPoint(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + const char* point = luaL_optstring(L, 2, "CENTER"); + // Store point info in frame table + lua_pushstring(L, point); + lua_setfield(L, 1, "__point"); + // Optional x/y offsets (args 4,5 if relativeTo is given, or 3,4 if not) + double xOfs = 0, yOfs = 0; + if (lua_isnumber(L, 4)) { xOfs = lua_tonumber(L, 4); yOfs = lua_tonumber(L, 5); } + else if (lua_isnumber(L, 3)) { xOfs = lua_tonumber(L, 3); yOfs = lua_tonumber(L, 4); } + lua_pushnumber(L, xOfs); + lua_setfield(L, 1, "__xOfs"); + lua_pushnumber(L, yOfs); + lua_setfield(L, 1, "__yOfs"); + return 0; +} + +static int lua_Frame_SetSize(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + double w = luaL_optnumber(L, 2, 0); + double h = luaL_optnumber(L, 3, 0); + lua_pushnumber(L, w); + lua_setfield(L, 1, "__width"); + lua_pushnumber(L, h); + lua_setfield(L, 1, "__height"); + return 0; +} + +static int lua_Frame_SetWidth(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushnumber(L, luaL_checknumber(L, 2)); + lua_setfield(L, 1, "__width"); + return 0; +} + +static int lua_Frame_SetHeight(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushnumber(L, luaL_checknumber(L, 2)); + lua_setfield(L, 1, "__height"); + return 0; +} + +static int lua_Frame_GetWidth(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__width"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 0); } + return 1; +} + +static int lua_Frame_GetHeight(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__height"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 0); } + return 1; +} + +static int lua_Frame_GetCenter(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__xOfs"); + double x = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0; + lua_pop(L, 1); + lua_getfield(L, 1, "__yOfs"); + double y = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0; + lua_pop(L, 1); + lua_pushnumber(L, x); + lua_pushnumber(L, y); + return 2; +} + +static int lua_Frame_SetAlpha(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_pushnumber(L, luaL_checknumber(L, 2)); + lua_setfield(L, 1, "__alpha"); + return 0; +} + +static int lua_Frame_GetAlpha(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__alpha"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 1.0); } + return 1; +} + +static int lua_Frame_SetParent(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (lua_istable(L, 2) || lua_isnil(L, 2)) { + lua_pushvalue(L, 2); + lua_setfield(L, 1, "__parent"); + } + return 0; +} + +static int lua_Frame_GetParent(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_getfield(L, 1, "__parent"); + return 1; +} + +// CreateFrame(frameType, name, parent, template) +static int lua_CreateFrame(lua_State* L) { + const char* frameType = luaL_optstring(L, 1, "Frame"); + const char* name = luaL_optstring(L, 2, nullptr); + (void)frameType; // All frame types use the same table structure for now + + // Create the frame table + lua_newtable(L); + + // Set frame name + if (name && *name) { + lua_pushstring(L, name); + lua_setfield(L, -2, "__name"); + // Also set as a global so other addons can find it by name + lua_pushvalue(L, -1); + lua_setglobal(L, name); + } + + // Set initial visibility + lua_pushboolean(L, 1); + lua_setfield(L, -2, "__visible"); + + // Apply frame metatable with methods + lua_getglobal(L, "__WoweeFrameMT"); + lua_setmetatable(L, -2); + + return 1; +} + +// --- WoW Utility Functions --- + +// strsplit(delimiter, str) — WoW's string split +static int lua_strsplit(lua_State* L) { + const char* delim = luaL_checkstring(L, 1); + const char* str = luaL_checkstring(L, 2); + if (!delim[0]) { lua_pushstring(L, str); return 1; } + int count = 0; + std::string s(str); + size_t pos = 0; + while (pos <= s.size()) { + size_t found = s.find(delim[0], pos); + if (found == std::string::npos) { + lua_pushstring(L, s.substr(pos).c_str()); + count++; + break; + } + lua_pushstring(L, s.substr(pos, found - pos).c_str()); + count++; + pos = found + 1; + } + return count; +} + +// strtrim(str) — remove leading/trailing whitespace +static int lua_strtrim(lua_State* L) { + const char* str = luaL_checkstring(L, 1); + std::string s(str); + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + lua_pushstring(L, (start == std::string::npos) ? "" : s.substr(start, end - start + 1).c_str()); + return 1; +} + +// wipe(table) — clear all entries from a table +static int lua_wipe(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + // Remove all integer keys + int len = static_cast(lua_objlen(L, 1)); + for (int i = len; i >= 1; i--) { + lua_pushnil(L); + lua_rawseti(L, 1, i); + } + // Remove all string keys + lua_pushnil(L); + while (lua_next(L, 1) != 0) { + lua_pop(L, 1); // pop value + lua_pushvalue(L, -1); // copy key + lua_pushnil(L); + lua_rawset(L, 1); // table[key] = nil + } + lua_pushvalue(L, 1); + return 1; +} + +// date(format) — safe date function (os.date was removed) +static int lua_wow_date(lua_State* L) { + const char* fmt = luaL_optstring(L, 1, "%c"); + time_t now = time(nullptr); + struct tm* tm = localtime(&now); + char buf[256]; + strftime(buf, sizeof(buf), fmt, tm); + lua_pushstring(L, buf); + return 1; +} + +// time() — current unix timestamp +static int lua_wow_time(lua_State* L) { + lua_pushnumber(L, static_cast(time(nullptr))); + return 1; +} + +// GetTime() — returns elapsed seconds since engine start (shared epoch) +static int lua_wow_gettime(lua_State* L) { + lua_pushnumber(L, luaGetTimeNow()); + return 1; +} + +LuaEngine::LuaEngine() = default; + +LuaEngine::~LuaEngine() { + shutdown(); +} + +bool LuaEngine::initialize() { + if (L_) return true; + + L_ = luaL_newstate(); + if (!L_) { + LOG_ERROR("LuaEngine: failed to create Lua state"); + return false; + } + + // Open safe standard libraries (no io, os, debug, package) + luaopen_base(L_); + luaopen_table(L_); + luaopen_string(L_); + luaopen_math(L_); + + // Remove unsafe globals from base library + const char* unsafeGlobals[] = { + "dofile", "loadfile", "load", "collectgarbage", "newproxy", nullptr + }; + for (const char** g = unsafeGlobals; *g; ++g) { + lua_pushnil(L_); + lua_setglobal(L_, *g); + } + + registerCoreAPI(); + registerEventAPI(); + + LOG_INFO("LuaEngine: initialized (Lua 5.1)"); + return true; +} + +void LuaEngine::shutdown() { + if (L_) { + lua_close(L_); + L_ = nullptr; + LOG_INFO("LuaEngine: shut down"); + } +} + +void LuaEngine::setGameHandler(game::GameHandler* handler) { + gameHandler_ = handler; + if (L_) { + lua_pushlightuserdata(L_, handler); + lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_game_handler"); + } +} + +void LuaEngine::registerCoreAPI() { + // Override print() to go to chat + lua_pushcfunction(L_, lua_wow_print); + lua_setglobal(L_, "print"); + + // WoW API stubs + lua_pushcfunction(L_, lua_wow_message); + lua_setglobal(L_, "message"); + + lua_pushcfunction(L_, lua_wow_gettime); + lua_setglobal(L_, "GetTime"); + + // Unit API + static const struct { const char* name; lua_CFunction func; } unitAPI[] = { + {"UnitName", lua_UnitName}, + {"UnitFullName", lua_UnitName}, + {"GetUnitName", lua_UnitName}, + {"SpellStopCasting", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->cancelCast(); + return 0; + }}, + {"SpellStopTargeting", [](lua_State* L) -> int { + (void)L; return 0; // No targeting reticle in this client + }}, + {"SpellIsTargeting", [](lua_State* L) -> int { + lua_pushboolean(L, 0); // No AoE targeting reticle + return 1; + }}, + {"UnitHealth", lua_UnitHealth}, + {"UnitHealthMax", lua_UnitHealthMax}, + {"UnitPower", lua_UnitPower}, + {"UnitPowerMax", lua_UnitPowerMax}, + {"UnitMana", lua_UnitPower}, + {"UnitManaMax", lua_UnitPowerMax}, + {"UnitRage", lua_UnitPower}, + {"UnitEnergy", lua_UnitPower}, + {"UnitFocus", lua_UnitPower}, + {"UnitRunicPower", lua_UnitPower}, + {"UnitLevel", lua_UnitLevel}, + {"UnitExists", lua_UnitExists}, + {"UnitIsDead", lua_UnitIsDead}, + {"UnitIsGhost", lua_UnitIsGhost}, + {"UnitIsDeadOrGhost", lua_UnitIsDeadOrGhost}, + {"UnitIsAFK", lua_UnitIsAFK}, + {"UnitIsDND", lua_UnitIsDND}, + {"UnitPlayerControlled", lua_UnitPlayerControlled}, + {"UnitIsTapped", lua_UnitIsTapped}, + {"UnitIsTappedByPlayer", lua_UnitIsTappedByPlayer}, + {"UnitIsTappedByAllThreatList", lua_UnitIsTappedByAllThreatList}, + {"UnitIsVisible", lua_UnitIsVisible}, + {"UnitGroupRolesAssigned", lua_UnitGroupRolesAssigned}, + {"UnitCanAttack", lua_UnitCanAttack}, + {"UnitCanCooperate", lua_UnitCanCooperate}, + {"UnitCreatureFamily", lua_UnitCreatureFamily}, + {"UnitOnTaxi", lua_UnitOnTaxi}, + {"UnitThreatSituation", lua_UnitThreatSituation}, + {"UnitDetailedThreatSituation", lua_UnitDetailedThreatSituation}, + {"UnitSex", lua_UnitSex}, + {"UnitClass", lua_UnitClass}, + {"GetMoney", lua_GetMoney}, + {"GetMerchantNumItems", lua_GetMerchantNumItems}, + {"GetMerchantItemInfo", lua_GetMerchantItemInfo}, + {"GetMerchantItemLink", lua_GetMerchantItemLink}, + {"CanMerchantRepair", lua_CanMerchantRepair}, + {"UnitStat", lua_UnitStat}, + {"UnitArmor", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int32_t armor = gh ? gh->getArmorRating() : 0; + if (armor < 0) armor = 0; + lua_pushnumber(L, armor); // base + lua_pushnumber(L, armor); // effective + lua_pushnumber(L, armor); // armor (again for compat) + lua_pushnumber(L, 0); // posBuff + lua_pushnumber(L, 0); // negBuff + return 5; + }}, + {"UnitResistance", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int school = static_cast(luaL_optnumber(L, 2, 0)); + int32_t val = 0; + if (gh) { + if (school == 0) val = gh->getArmorRating(); // physical = armor + else if (school >= 1 && school <= 6) val = gh->getResistance(school); + } + if (val < 0) val = 0; + lua_pushnumber(L, val); // base + lua_pushnumber(L, val); // effective + lua_pushnumber(L, 0); // posBuff + lua_pushnumber(L, 0); // negBuff + return 4; + }}, + {"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}, + {"GetShapeshiftFormInfo", [](lua_State* L) -> int { + // GetShapeshiftFormInfo(index) → icon, name, isActive, isCastable + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + uint8_t classId = gh->getPlayerClass(); + uint8_t currentForm = gh->getShapeshiftFormId(); + + // Form tables per class: {formId, spellId, name, icon} + struct FormInfo { uint8_t formId; const char* name; const char* icon; }; + static const FormInfo warriorForms[] = { + {17, "Battle Stance", "Interface\\Icons\\Ability_Warrior_OffensiveStance"}, + {18, "Defensive Stance", "Interface\\Icons\\Ability_Warrior_DefensiveStance"}, + {19, "Berserker Stance", "Interface\\Icons\\Ability_Racial_Avatar"}, + }; + static const FormInfo druidForms[] = { + {1, "Bear Form", "Interface\\Icons\\Ability_Racial_BearForm"}, + {4, "Travel Form", "Interface\\Icons\\Ability_Druid_TravelForm"}, + {3, "Cat Form", "Interface\\Icons\\Ability_Druid_CatForm"}, + {27, "Swift Flight Form", "Interface\\Icons\\Ability_Druid_FlightForm"}, + {31, "Moonkin Form", "Interface\\Icons\\Spell_Nature_ForceOfNature"}, + {36, "Tree of Life", "Interface\\Icons\\Ability_Druid_TreeofLife"}, + }; + static const FormInfo dkForms[] = { + {32, "Blood Presence", "Interface\\Icons\\Spell_Deathknight_BloodPresence"}, + {33, "Frost Presence", "Interface\\Icons\\Spell_Deathknight_FrostPresence"}, + {34, "Unholy Presence", "Interface\\Icons\\Spell_Deathknight_UnholyPresence"}, + }; + static const FormInfo rogueForms[] = { + {30, "Stealth", "Interface\\Icons\\Ability_Stealth"}, + }; + + const FormInfo* forms = nullptr; + int numForms = 0; + switch (classId) { + case 1: forms = warriorForms; numForms = 3; break; + case 6: forms = dkForms; numForms = 3; break; + case 4: forms = rogueForms; numForms = 1; break; + case 11: forms = druidForms; numForms = 6; break; + default: lua_pushnil(L); return 1; + } + if (index > numForms) { return luaReturnNil(L); } + const auto& fi = forms[index - 1]; + lua_pushstring(L, fi.icon); // icon + lua_pushstring(L, fi.name); // name + lua_pushboolean(L, currentForm == fi.formId ? 1 : 0); // isActive + lua_pushboolean(L, 1); // isCastable + return 4; + }}, + // --- PvP --- + {"UnitIsPVP", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* uid = luaL_optstring(L, 1, "player"); + if (!gh) { return luaReturnFalse(L); } + uint64_t guid = resolveUnitGuid(gh, std::string(uid)); + if (guid == 0) { return luaReturnFalse(L); } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity) { return luaReturnFalse(L); } + // UNIT_FLAG_PVP = 0x00001000 + uint32_t flags = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + lua_pushboolean(L, (flags & 0x00001000) ? 1 : 0); + return 1; + }}, + {"UnitIsPVPFreeForAll", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* uid = luaL_optstring(L, 1, "player"); + if (!gh) { return luaReturnFalse(L); } + uint64_t guid = resolveUnitGuid(gh, std::string(uid)); + if (guid == 0) { return luaReturnFalse(L); } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity) { return luaReturnFalse(L); } + // UNIT_FLAG_FFA_PVP = 0x00000080 in UNIT_FIELD_BYTES_2 byte 1 + uint32_t flags = entity->getField(game::fieldIndex(game::UF::UNIT_FIELD_FLAGS)); + lua_pushboolean(L, (flags & 0x00080000) ? 1 : 0); // PLAYER_FLAGS_FFA_PVP + return 1; + }}, + {"GetBattlefieldStatus", [](lua_State* L) -> int { + lua_pushstring(L, "none"); + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + return 3; + }}, + {"GetNumBattlefieldScores", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const auto* sb = gh ? gh->getBgScoreboard() : nullptr; + lua_pushnumber(L, sb ? sb->players.size() : 0); + return 1; + }}, + {"GetBattlefieldScore", [](lua_State* L) -> int { + // GetBattlefieldScore(index) → name, killingBlows, honorableKills, deaths, honorGained, faction, rank, race, class, classToken, damageDone, healingDone + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + const auto* sb = gh ? gh->getBgScoreboard() : nullptr; + if (!sb || index < 1 || index > static_cast(sb->players.size())) { + return luaReturnNil(L); + } + const auto& p = sb->players[index - 1]; + lua_pushstring(L, p.name.c_str()); // name + lua_pushnumber(L, p.killingBlows); // killingBlows + lua_pushnumber(L, p.honorableKills); // honorableKills + lua_pushnumber(L, p.deaths); // deaths + lua_pushnumber(L, p.bonusHonor); // honorGained + lua_pushnumber(L, p.team); // faction (0=Horde,1=Alliance) + lua_pushnumber(L, 0); // rank + lua_pushstring(L, ""); // race + lua_pushstring(L, ""); // class + lua_pushstring(L, "WARRIOR"); // classToken + lua_pushnumber(L, 0); // damageDone + lua_pushnumber(L, 0); // healingDone + return 12; + }}, + {"GetBattlefieldWinner", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const auto* sb = gh ? gh->getBgScoreboard() : nullptr; + if (sb && sb->hasWinner) lua_pushnumber(L, sb->winner); + else lua_pushnil(L); + return 1; + }}, + {"RequestBattlefieldScoreData", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->requestPvpLog(); + return 0; + }}, + // --- Network & BG Queue --- + {"GetNetStats", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + uint32_t ms = gh ? gh->getLatencyMs() : 0; + lua_pushnumber(L, 0); // bandwidthIn + lua_pushnumber(L, 0); // bandwidthOut + lua_pushnumber(L, ms); // latencyHome + lua_pushnumber(L, ms); // latencyWorld + return 4; + }}, + {"AcceptBattlefieldPort", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int accept = lua_toboolean(L, 2); + if (gh) { + if (accept) gh->acceptBattlefield(); + else gh->declineBattlefield(); + } + return 0; + }}, + // --- Taxi/Flight Paths --- + {"NumTaxiNodes", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getTaxiNodes().size() : 0); + return 1; + }}, + {"TaxiNodeName", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh) { lua_pushstring(L, ""); return 1; } + int i = 0; + for (const auto& [id, node] : gh->getTaxiNodes()) { + if (++i == index) { + lua_pushstring(L, node.name.c_str()); + return 1; + } + } + lua_pushstring(L, ""); + return 1; + }}, + {"TaxiNodeGetType", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh) { return luaReturnZero(L); } + int i = 0; + for (const auto& [id, node] : gh->getTaxiNodes()) { + if (++i == index) { + bool known = gh->isKnownTaxiNode(id); + lua_pushnumber(L, known ? 1 : 0); // 0=none, 1=reachable, 2=current + return 1; + } + } + lua_pushnumber(L, 0); + return 1; + }}, + {"TakeTaxiNode", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh) return 0; + int i = 0; + for (const auto& [id, node] : gh->getTaxiNodes()) { + if (++i == index) { + gh->activateTaxi(id); + break; + } + } + return 0; + }}, + // --- Quest Interaction --- + {"AcceptQuest", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->acceptQuest(); + return 0; + }}, + {"DeclineQuest", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->declineQuest(); + return 0; + }}, + {"CompleteQuest", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->completeQuest(); + return 0; + }}, + {"AbandonQuest", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + uint32_t questId = static_cast(luaL_checknumber(L, 1)); + if (gh) gh->abandonQuest(questId); + return 0; + }}, + {"GetNumQuestRewards", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + int idx = gh->getSelectedQuestLogIndex(); + if (idx < 1) { return luaReturnZero(L); } + const auto& ql = gh->getQuestLog(); + if (idx > static_cast(ql.size())) { return luaReturnZero(L); } + int count = 0; + for (const auto& r : ql[idx-1].rewardItems) + if (r.itemId != 0) ++count; + lua_pushnumber(L, count); + return 1; + }}, + {"GetNumQuestChoices", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + int idx = gh->getSelectedQuestLogIndex(); + if (idx < 1) { return luaReturnZero(L); } + const auto& ql = gh->getQuestLog(); + if (idx > static_cast(ql.size())) { return luaReturnZero(L); } + int count = 0; + for (const auto& r : ql[idx-1].rewardChoiceItems) + if (r.itemId != 0) ++count; + lua_pushnumber(L, count); + return 1; + }}, + // --- Gossip --- + {"GetNumGossipOptions", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getCurrentGossip().options.size() : 0); + return 1; + }}, + {"GetGossipOptions", [](lua_State* L) -> int { + // Returns pairs of (text, type) for each option + auto* gh = getGameHandler(L); + if (!gh) return 0; + const auto& opts = gh->getCurrentGossip().options; + int n = 0; + static const char* kIcons[] = {"gossip","vendor","taxi","trainer","spiritguide","innkeeper","banker","petition","tabard","battlemaster","auctioneer"}; + for (const auto& o : opts) { + lua_pushstring(L, o.text.c_str()); + lua_pushstring(L, o.icon < 11 ? kIcons[o.icon] : "gossip"); + n += 2; + } + return n; + }}, + {"SelectGossipOption", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) return 0; + const auto& opts = gh->getCurrentGossip().options; + if (index <= static_cast(opts.size())) + gh->selectGossipOption(opts[index - 1].id); + return 0; + }}, + {"GetNumGossipAvailableQuests", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + int count = 0; + for (const auto& q : gh->getCurrentGossip().quests) + if (q.questIcon != 4) ++count; // 4 = active/in-progress + lua_pushnumber(L, count); + return 1; + }}, + {"GetNumGossipActiveQuests", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + int count = 0; + for (const auto& q : gh->getCurrentGossip().quests) + if (q.questIcon == 4) ++count; + lua_pushnumber(L, count); + return 1; + }}, + {"CloseGossip", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->closeGossip(); + return 0; + }}, + // --- Connection & Equipment --- + {"IsConnectedToServer", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->isConnected() ? 1 : 0); + return 1; + }}, + {"UnequipItemSlot", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int slot = static_cast(luaL_checknumber(L, 1)); + if (gh && slot >= 1 && slot <= 19) + gh->unequipToBackpack(static_cast(slot - 1)); + return 0; + }}, + {"HasFocus", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->hasFocus() ? 1 : 0); + return 1; + }}, + {"GetRealmName", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) { + const auto* ac = gh->getActiveCharacter(); + lua_pushstring(L, ac ? "WoWee" : "Unknown"); + } else lua_pushstring(L, "Unknown"); + return 1; + }}, + {"GetNormalizedRealmName", [](lua_State* L) -> int { + lua_pushstring(L, "WoWee"); + return 1; + }}, + // --- Player Commands --- + {"ShowHelm", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->toggleHelm(); // Toggles helm visibility + return 0; + }}, + {"ShowCloak", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->toggleCloak(); + return 0; + }}, + {"TogglePVP", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->togglePvp(); + return 0; + }}, + {"Minimap_Ping", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + float x = static_cast(luaL_optnumber(L, 1, 0)); + float y = static_cast(luaL_optnumber(L, 2, 0)); + if (gh) gh->sendMinimapPing(x, y); + return 0; + }}, + {"RequestTimePlayed", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->requestPlayedTime(); + return 0; + }}, + // --- Chat Channels --- + {"JoinChannelByName", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* name = luaL_checkstring(L, 1); + const char* pw = luaL_optstring(L, 2, ""); + if (gh) gh->joinChannel(name, pw); + return 0; + }}, + {"LeaveChannelByName", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* name = luaL_checkstring(L, 1); + if (gh) gh->leaveChannel(name); + return 0; + }}, + {"GetChannelName", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + std::string name = gh->getChannelByIndex(index - 1); + if (!name.empty()) { + lua_pushstring(L, name.c_str()); + lua_pushstring(L, ""); // header + lua_pushboolean(L, 0); // collapsed + lua_pushnumber(L, index); // channelNumber + lua_pushnumber(L, 0); // count + lua_pushboolean(L, 1); // active + lua_pushstring(L, "CHANNEL_CATEGORY_CUSTOM"); // category + return 7; + } + lua_pushnil(L); + return 1; + }}, + // --- System Commands --- + {"Logout", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->requestLogout(); + return 0; + }}, + {"CancelLogout", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->cancelLogout(); + return 0; + }}, + {"RandomRoll", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int mn = static_cast(luaL_optnumber(L, 1, 1)); + int mx = static_cast(luaL_optnumber(L, 2, 100)); + if (gh) gh->randomRoll(mn, mx); + return 0; + }}, + {"FollowUnit", [](lua_State* L) -> int { + (void)L; // Follow requires movement system integration + return 0; + }}, + // --- Party/Group Management --- + {"InviteUnit", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->inviteToGroup(luaL_checkstring(L, 1)); + return 0; + }}, + {"UninviteUnit", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->uninvitePlayer(luaL_checkstring(L, 1)); + return 0; + }}, + {"LeaveParty", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->leaveGroup(); + return 0; + }}, + // --- Guild Management --- + {"GuildInvite", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->inviteToGuild(luaL_checkstring(L, 1)); + return 0; + }}, + {"GuildUninvite", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->kickGuildMember(luaL_checkstring(L, 1)); + return 0; + }}, + {"GuildPromote", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->promoteGuildMember(luaL_checkstring(L, 1)); + return 0; + }}, + {"GuildDemote", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->demoteGuildMember(luaL_checkstring(L, 1)); + return 0; + }}, + {"GuildLeave", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->leaveGuild(); + return 0; + }}, + {"GuildSetPublicNote", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->setGuildPublicNote(luaL_checkstring(L, 1), luaL_checkstring(L, 2)); + return 0; + }}, + // --- Emotes --- + {"DoEmote", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* token = luaL_checkstring(L, 1); + if (!gh) return 0; + std::string t(token); + for (char& c : t) c = static_cast(std::toupper(static_cast(c))); + // Map common emote tokens to DBC TextEmote IDs + static const std::unordered_map emoteMap = { + {"WAVE", 67}, {"BOW", 2}, {"DANCE", 10}, {"CHEER", 5}, + {"CHICKEN", 6}, {"CRY", 8}, {"EAT", 14}, {"DRINK", 13}, + {"FLEX", 16}, {"KISS", 22}, {"LAUGH", 23}, {"POINT", 30}, + {"ROAR", 34}, {"RUDE", 36}, {"SALUTE", 37}, {"SHY", 40}, + {"SILLY", 41}, {"SIT", 42}, {"SLEEP", 43}, {"SPIT", 44}, + {"THANK", 52}, {"CLAP", 7}, {"KNEEL", 21}, {"LAY", 24}, + {"NO", 28}, {"YES", 70}, {"BEG", 1}, {"ANGRY", 64}, + {"FAREWELL", 15}, {"HELLO", 18}, {"WELCOME", 68}, + }; + auto it = emoteMap.find(t); + uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0; + if (it != emoteMap.end()) { + gh->sendTextEmote(it->second, target); + } + return 0; + }}, + // --- Social (Friend/Ignore) --- + {"AddFriend", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* name = luaL_checkstring(L, 1); + const char* note = luaL_optstring(L, 2, ""); + if (gh) gh->addFriend(name, note); + return 0; + }}, + {"RemoveFriend", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* name = luaL_checkstring(L, 1); + if (gh) gh->removeFriend(name); + return 0; + }}, + {"AddIgnore", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* name = luaL_checkstring(L, 1); + if (gh) gh->addIgnore(name); + return 0; + }}, + {"DelIgnore", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* name = luaL_checkstring(L, 1); + if (gh) gh->removeIgnore(name); + return 0; + }}, + {"ShowFriends", [](lua_State* L) -> int { + (void)L; // Friends panel is shown via ImGui, not Lua + return 0; + }}, + // --- Who --- + {"GetNumWhoResults", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + lua_pushnumber(L, gh->getWhoResults().size()); + lua_pushnumber(L, gh->getWhoOnlineCount()); + return 2; + }}, + {"GetWhoInfo", [](lua_State* L) -> int { + // GetWhoInfo(index) → name, guild, level, race, class, zone, classFileName + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& results = gh->getWhoResults(); + if (index > static_cast(results.size())) { return luaReturnNil(L); } + const auto& w = results[index - 1]; + static const char* kRaces[] = {"","Human","Orc","Dwarf","Night Elf","Undead","Tauren","Gnome","Troll","","Blood Elf","Draenei"}; + static const char* kClasses[] = {"","Warrior","Paladin","Hunter","Rogue","Priest","Death Knight","Shaman","Mage","Warlock","","Druid"}; + const char* raceName = (w.raceId < 12) ? kRaces[w.raceId] : "Unknown"; + const char* className = (w.classId < 12) ? kClasses[w.classId] : "Unknown"; + static const char* kClassFiles[] = {"","WARRIOR","PALADIN","HUNTER","ROGUE","PRIEST","DEATHKNIGHT","SHAMAN","MAGE","WARLOCK","","DRUID"}; + const char* classFile = (w.classId < 12) ? kClassFiles[w.classId] : "WARRIOR"; + lua_pushstring(L, w.name.c_str()); + lua_pushstring(L, w.guildName.c_str()); + lua_pushnumber(L, w.level); + lua_pushstring(L, raceName); + lua_pushstring(L, className); + lua_pushstring(L, ""); // zone name (would need area lookup) + lua_pushstring(L, classFile); + return 7; + }}, + {"SendWho", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* query = luaL_optstring(L, 1, ""); + if (gh) gh->queryWho(query); + return 0; + }}, + {"SetWhoToUI", [](lua_State* L) -> int { + (void)L; return 0; // Stub + }}, + // --- Spell Utility --- + {"IsPlayerSpell", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + uint32_t spellId = static_cast(luaL_checknumber(L, 1)); + lua_pushboolean(L, gh && gh->getKnownSpells().count(spellId) ? 1 : 0); + return 1; + }}, + {"IsSpellOverlayed", [](lua_State* L) -> int { + (void)L; lua_pushboolean(L, 0); return 1; // No proc overlay tracking + }}, + {"IsCurrentSpell", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + uint32_t spellId = static_cast(luaL_checknumber(L, 1)); + lua_pushboolean(L, gh && gh->getCurrentCastSpellId() == spellId ? 1 : 0); + return 1; + }}, + {"IsAutoRepeatSpell", [](lua_State* L) -> int { + (void)L; lua_pushboolean(L, 0); return 1; // Stub + }}, + // --- Titles --- + {"GetCurrentTitle", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getChosenTitleBit() : -1); + return 1; + }}, + {"GetTitleName", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int bit = static_cast(luaL_checknumber(L, 1)); + if (!gh || bit < 0) { return luaReturnNil(L); } + std::string title = gh->getFormattedTitle(static_cast(bit)); + if (title.empty()) { return luaReturnNil(L); } + lua_pushstring(L, title.c_str()); + return 1; + }}, + {"SetCurrentTitle", [](lua_State* L) -> int { + (void)L; // Title changes require CMSG_SET_TITLE which we don't expose yet + return 0; + }}, + // --- Inspect --- + {"GetInspectSpecialization", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const auto* ir = gh ? gh->getInspectResult() : nullptr; + lua_pushnumber(L, ir ? ir->activeTalentGroup : 0); + return 1; + }}, + {"NotifyInspect", [](lua_State* L) -> int { + (void)L; // Inspect is auto-triggered by the C++ side when targeting a player + return 0; + }}, + {"ClearInspectPlayer", [](lua_State* L) -> int { + (void)L; + return 0; + }}, + // --- Player Info --- + {"GetHonorCurrency", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getHonorPoints() : 0); + return 1; + }}, + {"GetArenaCurrency", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getArenaPoints() : 0); + return 1; + }}, + {"GetTimePlayed", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + lua_pushnumber(L, gh->getTotalTimePlayed()); + lua_pushnumber(L, gh->getLevelTimePlayed()); + return 2; + }}, + {"GetBindLocation", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushstring(L, "Unknown"); return 1; } + lua_pushstring(L, gh->getWhoAreaName(gh->getHomeBindZoneId()).c_str()); + return 1; + }}, + // --- Instance Lockouts --- + {"GetNumSavedInstances", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getInstanceLockouts().size() : 0); + return 1; + }}, + {"GetSavedInstanceInfo", [](lua_State* L) -> int { + // GetSavedInstanceInfo(index) → name, id, reset, difficulty, locked, extended, instanceIDMostSig, isRaid, maxPlayers + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& lockouts = gh->getInstanceLockouts(); + if (index > static_cast(lockouts.size())) { return luaReturnNil(L); } + const auto& l = lockouts[index - 1]; + lua_pushstring(L, ("Instance " + std::to_string(l.mapId)).c_str()); // name (would need MapDBC for real names) + lua_pushnumber(L, l.mapId); // id + lua_pushnumber(L, static_cast(l.resetTime - static_cast(time(nullptr)))); // reset (seconds until) + lua_pushnumber(L, l.difficulty); // difficulty + lua_pushboolean(L, l.locked ? 1 : 0); // locked + lua_pushboolean(L, l.extended ? 1 : 0); // extended + lua_pushnumber(L, 0); // instanceIDMostSig + lua_pushboolean(L, l.difficulty >= 2 ? 1 : 0); // isRaid (25-man = raid) + lua_pushnumber(L, l.difficulty >= 2 ? 25 : (l.difficulty >= 1 ? 10 : 5)); // maxPlayers + return 9; + }}, + // --- Calendar --- + {"CalendarGetDate", [](lua_State* L) -> int { + // CalendarGetDate() → weekday, month, day, year + time_t now = time(nullptr); + struct tm* t = localtime(&now); + lua_pushnumber(L, t->tm_wday + 1); // weekday (1=Sun) + lua_pushnumber(L, t->tm_mon + 1); // month (1-12) + lua_pushnumber(L, t->tm_mday); // day + lua_pushnumber(L, t->tm_year + 1900); // year + return 4; + }}, + {"CalendarGetNumPendingInvites", [](lua_State* L) -> int { + return luaReturnZero(L); + }}, + {"CalendarGetNumDayEvents", [](lua_State* L) -> int { + return luaReturnZero(L); + }}, + // --- Instance --- + {"GetDifficultyInfo", [](lua_State* L) -> int { + // GetDifficultyInfo(id) → name, groupType, isHeroic, maxPlayers + int diff = static_cast(luaL_checknumber(L, 1)); + struct DiffInfo { const char* name; const char* group; int heroic; int maxPlayers; }; + static const DiffInfo infos[] = { + {"5 Player", "party", 0, 5}, // 0: Normal 5-man + {"5 Player (Heroic)", "party", 1, 5}, // 1: Heroic 5-man + {"10 Player", "raid", 0, 10}, // 2: 10-man Normal + {"25 Player", "raid", 0, 25}, // 3: 25-man Normal + {"10 Player (Heroic)", "raid", 1, 10}, // 4: 10-man Heroic + {"25 Player (Heroic)", "raid", 1, 25}, // 5: 25-man Heroic + }; + if (diff >= 0 && diff < 6) { + lua_pushstring(L, infos[diff].name); + lua_pushstring(L, infos[diff].group); + lua_pushboolean(L, infos[diff].heroic); + lua_pushnumber(L, infos[diff].maxPlayers); + } else { + lua_pushstring(L, "Unknown"); + lua_pushstring(L, "party"); + lua_pushboolean(L, 0); + lua_pushnumber(L, 5); + } + return 4; + }}, + // --- Weather --- + {"GetWeatherInfo", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + lua_pushnumber(L, gh->getWeatherType()); + lua_pushnumber(L, gh->getWeatherIntensity()); + return 2; + }}, + // --- Vendor Buy/Sell --- + {"BuyMerchantItem", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + int count = static_cast(luaL_optnumber(L, 2, 1)); + if (!gh || index < 1) return 0; + const auto& items = gh->getVendorItems().items; + if (index > static_cast(items.size())) return 0; + const auto& vi = items[index - 1]; + gh->buyItem(gh->getVendorGuid(), vi.itemId, vi.slot, count); + return 0; + }}, + {"SellContainerItem", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int bag = static_cast(luaL_checknumber(L, 1)); + int slot = static_cast(luaL_checknumber(L, 2)); + if (!gh) return 0; + if (bag == 0) gh->sellItemBySlot(slot - 1); + else if (bag >= 1 && bag <= 4) gh->sellItemInBag(bag - 1, slot - 1); + return 0; + }}, + // --- Repair --- + {"RepairAllItems", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->getVendorItems().canRepair) { + bool useGuildBank = lua_toboolean(L, 1) != 0; + gh->repairAll(gh->getVendorGuid(), useGuildBank); + } + return 0; + }}, + // --- Trade API --- + {"AcceptTrade", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->acceptTrade(); + return 0; + }}, + {"CancelTrade", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->isTradeOpen()) gh->cancelTrade(); + return 0; + }}, + {"InitiateTrade", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* uid = luaL_checkstring(L, 1); + if (gh) { + uint64_t guid = resolveUnitGuid(gh, std::string(uid)); + if (guid != 0) gh->initiateTrade(guid); + } + return 0; + }}, + // --- Auction House API --- + {"GetNumAuctionItems", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* listType = luaL_optstring(L, 1, "list"); + if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } + std::string t(listType); + const game::AuctionListResult* r = nullptr; + if (t == "list" || t == "browse") r = &gh->getAuctionBrowseResults(); + else if (t == "owner") r = &gh->getAuctionOwnerResults(); + else if (t == "bidder") r = &gh->getAuctionBidderResults(); + lua_pushnumber(L, r ? r->auctions.size() : 0); + lua_pushnumber(L, r ? r->totalCount : 0); + return 2; + }}, + {"GetAuctionItemInfo", [](lua_State* L) -> int { + // GetAuctionItemInfo(type, index) → name, texture, count, quality, canUse, level, levelColHeader, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, bidderFullName, owner, ownerFullName, saleStatus, itemId + auto* gh = getGameHandler(L); + const char* listType = luaL_checkstring(L, 1); + int index = static_cast(luaL_checknumber(L, 2)); + if (!gh || index < 1) { return luaReturnNil(L); } + std::string t(listType); + const game::AuctionListResult* r = nullptr; + if (t == "list") r = &gh->getAuctionBrowseResults(); + else if (t == "owner") r = &gh->getAuctionOwnerResults(); + else if (t == "bidder") r = &gh->getAuctionBidderResults(); + if (!r || index > static_cast(r->auctions.size())) { return luaReturnNil(L); } + const auto& a = r->auctions[index - 1]; + const auto* info = gh->getItemInfo(a.itemEntry); + std::string name = info ? info->name : "Item #" + std::to_string(a.itemEntry); + std::string icon = (info && info->displayInfoId != 0) ? gh->getItemIconPath(info->displayInfoId) : ""; + uint32_t quality = info ? info->quality : 1; + lua_pushstring(L, name.c_str()); // name + lua_pushstring(L, icon.empty() ? "Interface\\Icons\\INV_Misc_QuestionMark" : icon.c_str()); // texture + lua_pushnumber(L, a.stackCount); // count + lua_pushnumber(L, quality); // quality + lua_pushboolean(L, 1); // canUse + lua_pushnumber(L, info ? info->requiredLevel : 0); // level + lua_pushstring(L, ""); // levelColHeader + lua_pushnumber(L, a.startBid); // minBid + lua_pushnumber(L, a.minBidIncrement); // minIncrement + lua_pushnumber(L, a.buyoutPrice); // buyoutPrice + lua_pushnumber(L, a.currentBid); // bidAmount + lua_pushboolean(L, a.bidderGuid != 0 ? 1 : 0); // highBidder + lua_pushstring(L, ""); // bidderFullName + lua_pushstring(L, ""); // owner + lua_pushstring(L, ""); // ownerFullName + lua_pushnumber(L, 0); // saleStatus + lua_pushnumber(L, a.itemEntry); // itemId + return 17; + }}, + {"GetAuctionItemTimeLeft", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* listType = luaL_checkstring(L, 1); + int index = static_cast(luaL_checknumber(L, 2)); + if (!gh || index < 1) { lua_pushnumber(L, 4); return 1; } + std::string t(listType); + const game::AuctionListResult* r = nullptr; + if (t == "list") r = &gh->getAuctionBrowseResults(); + else if (t == "owner") r = &gh->getAuctionOwnerResults(); + else if (t == "bidder") r = &gh->getAuctionBidderResults(); + if (!r || index > static_cast(r->auctions.size())) { lua_pushnumber(L, 4); return 1; } + // Return 1=short(<30m), 2=medium(<2h), 3=long(<12h), 4=very long(>12h) + uint32_t ms = r->auctions[index - 1].timeLeftMs; + int cat = (ms < 1800000) ? 1 : (ms < 7200000) ? 2 : (ms < 43200000) ? 3 : 4; + lua_pushnumber(L, cat); + return 1; + }}, + {"GetAuctionItemLink", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + const char* listType = luaL_checkstring(L, 1); + int index = static_cast(luaL_checknumber(L, 2)); + if (!gh || index < 1) { return luaReturnNil(L); } + std::string t(listType); + const game::AuctionListResult* r = nullptr; + if (t == "list") r = &gh->getAuctionBrowseResults(); + else if (t == "owner") r = &gh->getAuctionOwnerResults(); + else if (t == "bidder") r = &gh->getAuctionBidderResults(); + if (!r || index > static_cast(r->auctions.size())) { return luaReturnNil(L); } + uint32_t itemId = r->auctions[index - 1].itemEntry; + const auto* info = gh->getItemInfo(itemId); + if (!info) { return luaReturnNil(L); } + static const char* kQH[] = {"ff9d9d9d","ffffffff","ff1eff00","ff0070dd","ffa335ee","ffff8000","ffe6cc80","ff00ccff"}; + const char* ch = (info->quality < 8) ? kQH[info->quality] : "ffffffff"; + char link[256]; + snprintf(link, sizeof(link), "|c%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", ch, itemId, info->name.c_str()); + lua_pushstring(L, link); + return 1; + }}, + // --- Mail API --- + {"GetInboxNumItems", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getMailInbox().size() : 0); + return 1; + }}, + {"GetInboxHeaderInfo", [](lua_State* L) -> int { + // GetInboxHeaderInfo(index) → packageIcon, stationeryIcon, sender, subject, money, COD, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& inbox = gh->getMailInbox(); + if (index > static_cast(inbox.size())) { return luaReturnNil(L); } + const auto& mail = inbox[index - 1]; + lua_pushstring(L, "Interface\\Icons\\INV_Letter_15"); // packageIcon + lua_pushstring(L, "Interface\\Icons\\INV_Letter_15"); // stationeryIcon + lua_pushstring(L, mail.senderName.c_str()); // sender + lua_pushstring(L, mail.subject.c_str()); // subject + lua_pushnumber(L, mail.money); // money (copper) + lua_pushnumber(L, mail.cod); // COD + lua_pushnumber(L, mail.expirationTime / 86400.0f); // daysLeft + lua_pushboolean(L, mail.attachments.empty() ? 0 : 1); // hasItem + lua_pushboolean(L, mail.read ? 1 : 0); // wasRead + lua_pushboolean(L, 0); // wasReturned + lua_pushboolean(L, !mail.body.empty() ? 1 : 0); // textCreated + lua_pushboolean(L, mail.messageType == 0 ? 1 : 0); // canReply (player mail only) + lua_pushboolean(L, 0); // isGM + return 13; + }}, + {"GetInboxText", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) { return luaReturnNil(L); } + const auto& inbox = gh->getMailInbox(); + if (index > static_cast(inbox.size())) { return luaReturnNil(L); } + lua_pushstring(L, inbox[index - 1].body.c_str()); + return 1; + }}, + {"HasNewMail", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnFalse(L); } + bool hasNew = false; + for (const auto& m : gh->getMailInbox()) { + if (!m.read) { hasNew = true; break; } + } + lua_pushboolean(L, hasNew ? 1 : 0); + return 1; + }}, + // --- Glyph API (WotLK) --- + {"GetNumGlyphSockets", [](lua_State* L) -> int { + lua_pushnumber(L, game::GameHandler::MAX_GLYPH_SLOTS); + return 1; + }}, + {"GetGlyphSocketInfo", [](lua_State* L) -> int { + // GetGlyphSocketInfo(index [, talentGroup]) → enabled, glyphType, glyphSpellID, icon + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + int spec = static_cast(luaL_optnumber(L, 2, 0)); + if (!gh || index < 1 || index > game::GameHandler::MAX_GLYPH_SLOTS) { + lua_pushboolean(L, 0); lua_pushnumber(L, 0); lua_pushnil(L); lua_pushnil(L); + return 4; + } + const auto& glyphs = (spec >= 1 && spec <= 2) + ? gh->getGlyphs(static_cast(spec - 1)) : gh->getGlyphs(); + uint16_t glyphId = glyphs[index - 1]; + // Glyph type: slots 1,2,3 = major (1), slots 4,5,6 = minor (2) + int glyphType = (index <= 3) ? 1 : 2; + lua_pushboolean(L, 1); // enabled + lua_pushnumber(L, glyphType); // glyphType (1=major, 2=minor) + if (glyphId != 0) { + lua_pushnumber(L, glyphId); // glyphSpellID + lua_pushstring(L, "Interface\\Icons\\INV_Glyph_MajorWarrior"); // placeholder icon + } else { + lua_pushnil(L); + lua_pushnil(L); + } + return 4; + }}, + // --- Achievement API --- + {"GetNumCompletedAchievements", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getEarnedAchievements().size() : 0); + return 1; + }}, + {"GetAchievementInfo", [](lua_State* L) -> int { + // GetAchievementInfo(id) → id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuildAch + auto* gh = getGameHandler(L); + uint32_t id = static_cast(luaL_checknumber(L, 1)); + if (!gh) { return luaReturnNil(L); } + const std::string& name = gh->getAchievementName(id); + if (name.empty()) { return luaReturnNil(L); } + bool completed = gh->getEarnedAchievements().count(id) > 0; + uint32_t date = gh->getAchievementDate(id); + uint32_t points = gh->getAchievementPoints(id); + const std::string& desc = gh->getAchievementDescription(id); + // Parse date: packed as (month << 24 | day << 16 | year) + int month = completed ? static_cast((date >> 24) & 0xFF) : 0; + int day = completed ? static_cast((date >> 16) & 0xFF) : 0; + int year = completed ? static_cast(date & 0xFFFF) : 0; + lua_pushnumber(L, id); // 1: id + lua_pushstring(L, name.c_str()); // 2: name + lua_pushnumber(L, points); // 3: points + lua_pushboolean(L, completed ? 1 : 0); // 4: completed + lua_pushnumber(L, month); // 5: month + lua_pushnumber(L, day); // 6: day + lua_pushnumber(L, year); // 7: year + lua_pushstring(L, desc.c_str()); // 8: description + lua_pushnumber(L, 0); // 9: flags + lua_pushstring(L, "Interface\\Icons\\Achievement_General"); // 10: icon + lua_pushstring(L, ""); // 11: rewardText + lua_pushboolean(L, 0); // 12: isGuildAchievement + return 12; + }}, + // --- Pet Action Bar --- + {"HasPetUI", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->hasPet() ? 1 : 0); + return 1; + }}, + {"GetPetActionInfo", [](lua_State* L) -> int { + // GetPetActionInfo(index) → name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1 || index > game::GameHandler::PET_ACTION_BAR_SLOTS) { + return luaReturnNil(L); + } + uint32_t packed = gh->getPetActionSlot(index - 1); + uint32_t spellId = packed & 0x00FFFFFF; + uint8_t actionType = static_cast((packed >> 24) & 0xFF); + if (spellId == 0) { return luaReturnNil(L); } + const std::string& name = gh->getSpellName(spellId); + std::string iconPath = gh->getSpellIconPath(spellId); + lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name + lua_pushstring(L, ""); // subtext + lua_pushstring(L, iconPath.empty() ? "Interface\\Icons\\INV_Misc_QuestionMark" : iconPath.c_str()); // texture + lua_pushboolean(L, 0); // isToken + lua_pushboolean(L, (actionType & 0xC0) != 0 ? 1 : 0); // isActive + lua_pushboolean(L, 1); // autoCastAllowed + lua_pushboolean(L, gh->isPetSpellAutocast(spellId) ? 1 : 0); // autoCastEnabled + return 7; + }}, + {"GetPetActionCooldown", [](lua_State* L) -> int { + lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 1); + return 3; + }}, + {"PetAttack", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->hasPet() && gh->hasTarget()) + gh->sendPetAction(0x00000007 | (2u << 24), gh->getTargetGuid()); // CMD_ATTACK + return 0; + }}, + {"PetFollow", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->hasPet()) + gh->sendPetAction(0x00000007 | (1u << 24), 0); // CMD_FOLLOW + return 0; + }}, + {"PetWait", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->hasPet()) + gh->sendPetAction(0x00000007 | (0u << 24), 0); // CMD_STAY + return 0; + }}, + {"PetPassiveMode", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->hasPet()) + gh->sendPetAction(0x00000007 | (0u << 16), 0); // REACT_PASSIVE + return 0; + }}, + {"CastPetAction", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || !gh->hasPet() || index < 1 || index > game::GameHandler::PET_ACTION_BAR_SLOTS) return 0; + uint32_t packed = gh->getPetActionSlot(index - 1); + uint32_t spellId = packed & 0x00FFFFFF; + if (spellId != 0) { + uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : gh->getPetGuid(); + gh->sendPetAction(packed, target); + } + return 0; + }}, + {"TogglePetAutocast", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || !gh->hasPet() || index < 1 || index > game::GameHandler::PET_ACTION_BAR_SLOTS) return 0; + uint32_t packed = gh->getPetActionSlot(index - 1); + uint32_t spellId = packed & 0x00FFFFFF; + if (spellId != 0) gh->togglePetSpellAutocast(spellId); + return 0; + }}, + {"PetDismiss", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->hasPet()) + gh->sendPetAction(0x00000007 | (3u << 24), 0); // CMD_DISMISS + return 0; + }}, + {"IsPetAttackActive", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushboolean(L, gh && gh->getPetCommand() == 2 ? 1 : 0); // 2=attack + return 1; + }}, + {"PetDefensiveMode", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh && gh->hasPet()) + gh->sendPetAction(0x00000007 | (1u << 16), 0); // REACT_DEFENSIVE + return 0; + }}, + {"GetActionBarPage", [](lua_State* L) -> int { + // Return current action bar page (1-6) + lua_getglobal(L, "__WoweeActionBarPage"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 1); } + return 1; + }}, + {"ChangeActionBarPage", [](lua_State* L) -> int { + int page = static_cast(luaL_checknumber(L, 1)); + if (page < 1) page = 1; + if (page > 6) page = 6; + lua_pushnumber(L, page); + lua_setglobal(L, "__WoweeActionBarPage"); + // Fire ACTIONBAR_PAGE_CHANGED via the frame event system + lua_getglobal(L, "__WoweeEvents"); + if (!lua_isnil(L, -1)) { + lua_getfield(L, -1, "ACTIONBAR_PAGE_CHANGED"); + if (!lua_isnil(L, -1)) { + int n = static_cast(lua_objlen(L, -1)); + for (int i = 1; i <= n; i++) { + lua_rawgeti(L, -1, i); + if (lua_isfunction(L, -1)) { + lua_pushstring(L, "ACTIONBAR_PAGE_CHANGED"); + lua_pcall(L, 1, 0, 0); + } else lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + return 0; + }}, + {"CastShapeshiftForm", [](lua_State* L) -> int { + // CastShapeshiftForm(index) — cast the spell for the given form slot + auto* gh = getGameHandler(L); + int index = static_cast(luaL_checknumber(L, 1)); + if (!gh || index < 1) return 0; + uint8_t classId = gh->getPlayerClass(); + // Map class + index to spell IDs + // Warrior stances + static const uint32_t warriorSpells[] = {2457, 71, 2458}; // Battle, Defensive, Berserker + // Druid forms + static const uint32_t druidSpells[] = {5487, 783, 768, 40120, 24858, 33891}; // Bear, Travel, Cat, Swift Flight, Moonkin, Tree + // DK presences + static const uint32_t dkSpells[] = {48266, 48263, 48265}; // Blood, Frost, Unholy + // Rogue + static const uint32_t rogueSpells[] = {1784}; // Stealth + + const uint32_t* spells = nullptr; + int numSpells = 0; + switch (classId) { + case 1: spells = warriorSpells; numSpells = 3; break; + case 6: spells = dkSpells; numSpells = 3; break; + case 4: spells = rogueSpells; numSpells = 1; break; + case 11: spells = druidSpells; numSpells = 6; break; + default: return 0; + } + if (index <= numSpells) { + gh->castSpell(spells[index - 1], 0); + } + return 0; + }}, + {"CancelShapeshiftForm", [](lua_State* L) -> int { + // Cancel current form — cast spell 0 or cancel aura + auto* gh = getGameHandler(L); + if (gh && gh->getShapeshiftFormId() != 0) { + // Cancelling a form is done by re-casting the same form spell + // For simplicity, just note that the server will handle it + } + return 0; + }}, + {"GetShapeshiftFormCooldown", [](lua_State* L) -> int { + // No per-form cooldown tracking — return no cooldown + lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 1); + return 3; + }}, + {"GetShapeshiftForm", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + lua_pushnumber(L, gh ? gh->getShapeshiftFormId() : 0); + return 1; + }}, + {"GetNumShapeshiftForms", [](lua_State* L) -> int { + // Return count based on player class + auto* gh = getGameHandler(L); + if (!gh) { return luaReturnZero(L); } + uint8_t classId = gh->getPlayerClass(); + // Druid: Bear(1), Aquatic(2), Cat(3), Travel(4), Moonkin/Tree(5/6) + // Warrior: Battle(1), Defensive(2), Berserker(3) + // Rogue: Stealth(1) + // Priest: Shadowform(1) + // Paladin: varies by level/talents + // DK: Blood Presence, Frost, Unholy (3) + switch (classId) { + case 1: lua_pushnumber(L, 3); break; // Warrior + case 2: lua_pushnumber(L, 3); break; // Paladin (auras) + case 4: lua_pushnumber(L, 1); break; // Rogue + case 5: lua_pushnumber(L, 1); break; // Priest + case 6: lua_pushnumber(L, 3); break; // Death Knight + case 11: lua_pushnumber(L, 6); break; // Druid + default: lua_pushnumber(L, 0); break; + } + return 1; + }}, + {"GetMaxPlayerLevel", [](lua_State* L) -> int { + auto* reg = core::Application::getInstance().getExpansionRegistry(); + auto* prof = reg ? reg->getActive() : nullptr; + if (prof && prof->id == "wotlk") lua_pushnumber(L, 80); + else if (prof && prof->id == "tbc") lua_pushnumber(L, 70); + else lua_pushnumber(L, 60); + return 1; + }}, + {"GetAccountExpansionLevel", [](lua_State* L) -> int { + auto* reg = core::Application::getInstance().getExpansionRegistry(); + auto* prof = reg ? reg->getActive() : nullptr; + if (prof && prof->id == "wotlk") lua_pushnumber(L, 3); + else if (prof && prof->id == "tbc") lua_pushnumber(L, 2); + else lua_pushnumber(L, 1); + return 1; + }}, + {"PlaySound", lua_PlaySound}, + {"PlaySoundFile", lua_PlaySoundFile}, + {"GetCVar", lua_GetCVar}, + {"SetCVar", lua_SetCVar}, + {"IsShiftKeyDown", lua_IsShiftKeyDown}, + {"IsControlKeyDown", lua_IsControlKeyDown}, + {"IsAltKeyDown", lua_IsAltKeyDown}, + {"IsModifiedClick", lua_IsModifiedClick}, + {"GetModifiedClick", lua_GetModifiedClick}, + {"SetModifiedClick", lua_SetModifiedClick}, + {"GetBindingKey", lua_GetBindingKey}, + {"GetBindingAction", lua_GetBindingAction}, + {"GetNumBindings", lua_GetNumBindings}, + {"GetBinding", lua_GetBinding}, + {"SetBinding", lua_SetBinding}, + {"SaveBindings", lua_SaveBindings}, + {"SetOverrideBindingClick", lua_SetOverrideBindingClick}, + {"ClearOverrideBindings", lua_ClearOverrideBindings}, + {"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}, + {"GetSpellDescription", lua_GetSpellDescription}, + {"GetEnchantInfo", lua_GetEnchantInfo}, + {"IsSpellInRange", lua_IsSpellInRange}, + {"UnitDistanceSquared", lua_UnitDistanceSquared}, + {"CheckInteractDistance", lua_CheckInteractDistance}, + {"HasTarget", lua_HasTarget}, + {"TargetUnit", lua_TargetUnit}, + {"ClearTarget", lua_ClearTarget}, + {"FocusUnit", lua_FocusUnit}, + {"ClearFocus", lua_ClearFocus}, + {"AssistUnit", lua_AssistUnit}, + {"TargetLastTarget", lua_TargetLastTarget}, + {"TargetNearestEnemy", lua_TargetNearestEnemy}, + {"TargetNearestFriend", lua_TargetNearestFriend}, + {"GetRaidTargetIndex", lua_GetRaidTargetIndex}, + {"SetRaidTarget", lua_SetRaidTarget}, + {"UnitRace", lua_UnitRace}, + {"UnitPowerType", lua_UnitPowerType}, + {"GetNumGroupMembers", lua_GetNumGroupMembers}, + {"UnitGUID", lua_UnitGUID}, + {"UnitIsPlayer", lua_UnitIsPlayer}, + {"InCombatLockdown", lua_InCombatLockdown}, + {"UnitBuff", lua_UnitBuff}, + {"UnitDebuff", lua_UnitDebuff}, + {"UnitAura", lua_UnitAuraGeneric}, + {"UnitCastingInfo", lua_UnitCastingInfo}, + {"UnitChannelInfo", lua_UnitChannelInfo}, + {"GetNumAddOns", lua_GetNumAddOns}, + {"GetAddOnInfo", lua_GetAddOnInfo}, + {"GetAddOnMetadata", lua_GetAddOnMetadata}, + {"GetSpellInfo", lua_GetSpellInfo}, + {"GetSpellTexture", lua_GetSpellTexture}, + {"GetItemInfo", lua_GetItemInfo}, + {"GetItemQualityColor", lua_GetItemQualityColor}, + {"_GetItemTooltipData", lua_GetItemTooltipData}, + {"GetItemCount", lua_GetItemCount}, + {"UseContainerItem", lua_UseContainerItem}, + {"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}, + {"GetMinimapZoneText", lua_GetMinimapZoneText}, + // Player state (replaces hardcoded stubs) + {"IsMounted", lua_IsMounted}, + {"IsFlying", lua_IsFlying}, + {"IsSwimming", lua_IsSwimming}, + {"IsResting", lua_IsResting}, + {"IsFalling", lua_IsFalling}, + {"IsStealthed", lua_IsStealthed}, + {"GetUnitSpeed", lua_GetUnitSpeed}, + // Combat/group queries + {"UnitAffectingCombat", lua_UnitAffectingCombat}, + {"GetNumRaidMembers", lua_GetNumRaidMembers}, + {"GetNumPartyMembers", lua_GetNumPartyMembers}, + {"UnitInParty", lua_UnitInParty}, + {"UnitInRaid", lua_UnitInRaid}, + {"GetRaidRosterInfo", lua_GetRaidRosterInfo}, + {"GetThreatStatusColor", lua_GetThreatStatusColor}, + {"GetReadyCheckStatus", lua_GetReadyCheckStatus}, + {"RegisterUnitWatch", lua_RegisterUnitWatch}, + {"UnregisterUnitWatch", lua_UnregisterUnitWatch}, + {"UnitIsUnit", lua_UnitIsUnit}, + {"UnitIsFriend", lua_UnitIsFriend}, + {"UnitIsEnemy", lua_UnitIsEnemy}, + {"UnitCreatureType", lua_UnitCreatureType}, + {"UnitClassification", lua_UnitClassification}, + {"GetPlayerInfoByGUID", lua_GetPlayerInfoByGUID}, + {"GetItemLink", lua_GetItemLink}, + {"GetSpellLink", lua_GetSpellLink}, + {"IsUsableSpell", lua_IsUsableSpell}, + {"IsInInstance", lua_IsInInstance}, + {"GetInstanceInfo", lua_GetInstanceInfo}, + {"GetInstanceDifficulty", lua_GetInstanceDifficulty}, + // Container/bag API + {"GetContainerNumSlots", lua_GetContainerNumSlots}, + {"GetContainerItemInfo", lua_GetContainerItemInfo}, + {"GetContainerItemLink", lua_GetContainerItemLink}, + {"GetContainerNumFreeSlots", lua_GetContainerNumFreeSlots}, + // Equipment slot API + {"GetInventorySlotInfo", lua_GetInventorySlotInfo}, + {"GetInventoryItemLink", lua_GetInventoryItemLink}, + {"GetInventoryItemID", lua_GetInventoryItemID}, + {"GetInventoryItemTexture", lua_GetInventoryItemTexture}, + // Time/XP API + {"GetGameTime", lua_GetGameTime}, + {"GetServerTime", lua_GetServerTime}, + {"UnitXP", lua_UnitXP}, + {"UnitXPMax", lua_UnitXPMax}, + {"GetXPExhaustion", lua_GetXPExhaustion}, + {"GetRestState", lua_GetRestState}, + // Quest log API + {"GetNumQuestLogEntries", lua_GetNumQuestLogEntries}, + {"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}, + // Talent API + {"GetNumTalentTabs", lua_GetNumTalentTabs}, + {"GetTalentTabInfo", lua_GetTalentTabInfo}, + {"GetNumTalents", lua_GetNumTalents}, + {"GetTalentInfo", lua_GetTalentInfo}, + {"GetActiveTalentGroup", lua_GetActiveTalentGroup}, + // Friends/ignore API + // Guild API + {"IsInGuild", lua_IsInGuild}, + {"GetGuildInfo", lua_GetGuildInfoFunc}, + {"GetNumGuildMembers", lua_GetNumGuildMembers}, + {"GuildRoster", [](lua_State* L) -> int { + auto* gh = getGameHandler(L); + if (gh) gh->requestGuildRoster(); + return 0; + }}, + {"SortGuildRoster", [](lua_State* L) -> int { + (void)L; // Sorting is client-side display only + return 0; + }}, + {"GetGuildRosterInfo", lua_GetGuildRosterInfo}, + {"GetGuildRosterMOTD", lua_GetGuildRosterMOTD}, + {"GetNumFriends", lua_GetNumFriends}, + {"GetFriendInfo", lua_GetFriendInfo}, + {"GetNumIgnores", lua_GetNumIgnores}, + {"GetIgnoreName", lua_GetIgnoreName}, + // Reaction/connection queries + {"UnitReaction", lua_UnitReaction}, + {"UnitIsConnected", lua_UnitIsConnected}, + {"GetComboPoints", lua_GetComboPoints}, + // Action bar API + {"HasAction", lua_HasAction}, + {"GetActionTexture", lua_GetActionTexture}, + {"IsCurrentAction", lua_IsCurrentAction}, + {"IsUsableAction", lua_IsUsableAction}, + {"IsActionInRange", lua_IsActionInRange}, + {"GetActionInfo", lua_GetActionInfo}, + {"GetActionCount", lua_GetActionCount}, + {"GetActionCooldown", lua_GetActionCooldown}, + {"UseAction", lua_UseAction}, + {"PickupAction", lua_PickupAction}, + {"PlaceAction", lua_PlaceAction}, + {"PickupSpell", lua_PickupSpell}, + {"PickupSpellBookItem", lua_PickupSpellBookItem}, + {"PickupContainerItem", lua_PickupContainerItem}, + {"PickupInventoryItem", lua_PickupInventoryItem}, + {"ClearCursor", lua_ClearCursor}, + {"GetCursorInfo", lua_GetCursorInfo}, + {"CursorHasItem", lua_CursorHasItem}, + {"CursorHasSpell", lua_CursorHasSpell}, + {"DeleteCursorItem", lua_DeleteCursorItem}, + {"AutoEquipCursorItem", lua_AutoEquipCursorItem}, + {"CancelUnitBuff", lua_CancelUnitBuff}, + {"CastSpellByID", lua_CastSpellByID}, + // Loot API + {"GetNumLootItems", lua_GetNumLootItems}, + {"GetLootSlotInfo", lua_GetLootSlotInfo}, + {"GetLootSlotLink", lua_GetLootSlotLink}, + {"LootSlot", lua_LootSlot}, + {"CloseLoot", lua_CloseLoot}, + {"GetLootMethod", lua_GetLootMethod}, + // Utilities + {"strsplit", lua_strsplit}, + {"strtrim", lua_strtrim}, + {"wipe", lua_wipe}, + {"date", lua_wow_date}, + {"time", lua_wow_time}, + }; + for (const auto& [name, func] : unitAPI) { + lua_pushcfunction(L_, func); + lua_setglobal(L_, name); + } + + // WoW aliases + lua_getglobal(L_, "string"); + lua_getfield(L_, -1, "format"); + lua_setglobal(L_, "format"); + lua_pop(L_, 1); // pop string table + + // tinsert/tremove aliases + lua_getglobal(L_, "table"); + lua_getfield(L_, -1, "insert"); + lua_setglobal(L_, "tinsert"); + lua_getfield(L_, -1, "remove"); + lua_setglobal(L_, "tremove"); + lua_pop(L_, 1); // pop table + + // SlashCmdList table — addons register slash commands here + lua_newtable(L_); + lua_setglobal(L_, "SlashCmdList"); + + // Frame metatable with methods + lua_newtable(L_); // metatable + lua_pushvalue(L_, -1); + lua_setfield(L_, -2, "__index"); // metatable.__index = metatable + + static const struct luaL_Reg frameMethods[] = { + {"RegisterEvent", lua_Frame_RegisterEvent}, + {"UnregisterEvent", lua_Frame_UnregisterEvent}, + {"SetScript", lua_Frame_SetScript}, + {"GetScript", lua_Frame_GetScript}, + {"GetName", lua_Frame_GetName}, + {"Show", lua_Frame_Show}, + {"Hide", lua_Frame_Hide}, + {"IsShown", lua_Frame_IsShown}, + {"IsVisible", lua_Frame_IsShown}, // alias + {"SetPoint", lua_Frame_SetPoint}, + {"SetSize", lua_Frame_SetSize}, + {"SetWidth", lua_Frame_SetWidth}, + {"SetHeight", lua_Frame_SetHeight}, + {"GetWidth", lua_Frame_GetWidth}, + {"GetHeight", lua_Frame_GetHeight}, + {"GetCenter", lua_Frame_GetCenter}, + {"SetAlpha", lua_Frame_SetAlpha}, + {"GetAlpha", lua_Frame_GetAlpha}, + {"SetParent", lua_Frame_SetParent}, + {"GetParent", lua_Frame_GetParent}, + {"CreateTexture", lua_Frame_CreateTexture}, + {"CreateFontString", lua_Frame_CreateFontString}, + {nullptr, nullptr} + }; + for (const luaL_Reg* r = frameMethods; r->name; r++) { + lua_pushcfunction(L_, r->func); + lua_setfield(L_, -2, r->name); + } + lua_setglobal(L_, "__WoweeFrameMT"); + + // Add commonly called no-op frame methods to prevent addon errors + luaL_dostring(L_, + "local mt = __WoweeFrameMT\n" + "function mt:SetFrameLevel(level) self.__frameLevel = level end\n" + "function mt:GetFrameLevel() return self.__frameLevel or 1 end\n" + "function mt:SetFrameStrata(strata) self.__strata = strata end\n" + "function mt:GetFrameStrata() return self.__strata or 'MEDIUM' end\n" + "function mt:EnableMouse(enable) end\n" + "function mt:EnableMouseWheel(enable) end\n" + "function mt:SetMovable(movable) end\n" + "function mt:SetResizable(resizable) end\n" + "function mt:RegisterForDrag(...) end\n" + "function mt:SetClampedToScreen(clamped) end\n" + "function mt:SetBackdrop(backdrop) end\n" + "function mt:SetBackdropColor(...) end\n" + "function mt:SetBackdropBorderColor(...) end\n" + "function mt:ClearAllPoints() end\n" + "function mt:SetID(id) self.__id = id end\n" + "function mt:GetID() return self.__id or 0 end\n" + "function mt:SetScale(scale) self.__scale = scale end\n" + "function mt:GetScale() return self.__scale or 1.0 end\n" + "function mt:GetEffectiveScale() return self.__scale or 1.0 end\n" + "function mt:SetToplevel(top) end\n" + "function mt:Raise() end\n" + "function mt:Lower() end\n" + "function mt:GetLeft() return 0 end\n" + "function mt:GetRight() return 0 end\n" + "function mt:GetTop() return 0 end\n" + "function mt:GetBottom() return 0 end\n" + "function mt:GetNumPoints() return 0 end\n" + "function mt:GetPoint(n) return 'CENTER', nil, 'CENTER', 0, 0 end\n" + "function mt:SetHitRectInsets(...) end\n" + "function mt:RegisterForClicks(...) end\n" + "function mt:SetAttribute(name, value) self['attr_'..name] = value end\n" + "function mt:GetAttribute(name) return self['attr_'..name] end\n" + "function mt:HookScript(scriptType, fn)\n" + " local orig = self.__scripts and self.__scripts[scriptType]\n" + " if orig then\n" + " self:SetScript(scriptType, function(...) orig(...); fn(...) end)\n" + " else\n" + " self:SetScript(scriptType, fn)\n" + " end\n" + "end\n" + "function mt:SetMinResize(...) end\n" + "function mt:SetMaxResize(...) end\n" + "function mt:StartMoving() end\n" + "function mt:StopMovingOrSizing() end\n" + "function mt:IsMouseOver() return false end\n" + "function mt:GetObjectType() return 'Frame' end\n" + ); + + // CreateFrame function + lua_pushcfunction(L_, lua_CreateFrame); + lua_setglobal(L_, "CreateFrame"); + + // Cursor/screen/FPS functions + lua_pushcfunction(L_, lua_GetCursorPosition); + lua_setglobal(L_, "GetCursorPosition"); + lua_pushcfunction(L_, lua_GetScreenWidth); + lua_setglobal(L_, "GetScreenWidth"); + lua_pushcfunction(L_, lua_GetScreenHeight); + lua_setglobal(L_, "GetScreenHeight"); + lua_pushcfunction(L_, lua_GetFramerate); + lua_setglobal(L_, "GetFramerate"); + + // Frame event dispatch table + lua_newtable(L_); + lua_setglobal(L_, "__WoweeFrameEvents"); + + // OnUpdate frame tracking table + lua_newtable(L_); + lua_setglobal(L_, "__WoweeOnUpdateFrames"); + + // C_Timer implementation via Lua (uses OnUpdate internally) + luaL_dostring(L_, + "C_Timer = {}\n" + "local timers = {}\n" + "local timerFrame = CreateFrame('Frame', '__WoweeTimerFrame')\n" + "timerFrame:SetScript('OnUpdate', function(self, elapsed)\n" + " local i = 1\n" + " while i <= #timers do\n" + " timers[i].remaining = timers[i].remaining - elapsed\n" + " if timers[i].remaining <= 0 then\n" + " local cb = timers[i].callback\n" + " table.remove(timers, i)\n" + " cb()\n" + " else\n" + " i = i + 1\n" + " end\n" + " end\n" + " if #timers == 0 then self:Hide() end\n" + "end)\n" + "timerFrame:Hide()\n" + "function C_Timer.After(seconds, callback)\n" + " tinsert(timers, {remaining = seconds, callback = callback})\n" + " timerFrame:Show()\n" + "end\n" + "function C_Timer.NewTicker(seconds, callback, iterations)\n" + " local count = 0\n" + " local maxIter = iterations or -1\n" + " local ticker = {cancelled = false}\n" + " local function tick()\n" + " if ticker.cancelled then return end\n" + " count = count + 1\n" + " callback(ticker)\n" + " if maxIter > 0 and count >= maxIter then return end\n" + " C_Timer.After(seconds, tick)\n" + " end\n" + " C_Timer.After(seconds, tick)\n" + " function ticker:Cancel() self.cancelled = true end\n" + " return ticker\n" + "end\n" + ); + + // DEFAULT_CHAT_FRAME with AddMessage method (used by many addons) + luaL_dostring(L_, + "DEFAULT_CHAT_FRAME = {}\n" + "function DEFAULT_CHAT_FRAME:AddMessage(text, r, g, b)\n" + " if r and g and b then\n" + " local hex = format('|cff%02x%02x%02x', " + " math.floor(r*255), math.floor(g*255), math.floor(b*255))\n" + " print(hex .. tostring(text) .. '|r')\n" + " else\n" + " print(tostring(text))\n" + " end\n" + "end\n" + "ChatFrame1 = DEFAULT_CHAT_FRAME\n" + ); + + // hooksecurefunc — hook a function to run additional code after it + luaL_dostring(L_, + "function hooksecurefunc(tblOrName, nameOrFunc, funcOrNil)\n" + " local tbl, name, hook\n" + " if type(tblOrName) == 'table' then\n" + " tbl, name, hook = tblOrName, nameOrFunc, funcOrNil\n" + " else\n" + " tbl, name, hook = _G, tblOrName, nameOrFunc\n" + " end\n" + " local orig = tbl[name]\n" + " if type(orig) ~= 'function' then return end\n" + " tbl[name] = function(...)\n" + " local r = {orig(...)}\n" + " hook(...)\n" + " return unpack(r)\n" + " end\n" + "end\n" + ); + + // LibStub — universal library version management used by Ace3 and virtually all addon libs. + // This is the standard WoW LibStub implementation that addons embed/expect globally. + luaL_dostring(L_, + "local LibStub = LibStub or {}\n" + "LibStub.libs = LibStub.libs or {}\n" + "LibStub.minors = LibStub.minors or {}\n" + "function LibStub:NewLibrary(major, minor)\n" + " assert(type(major) == 'string', 'LibStub:NewLibrary: bad argument #1 (string expected)')\n" + " minor = assert(tonumber(minor or (type(minor) == 'string' and minor:match('(%d+)'))), 'LibStub:NewLibrary: bad argument #2 (number expected)')\n" + " local oldMinor = self.minors[major]\n" + " if oldMinor and oldMinor >= minor then return nil end\n" + " local lib = self.libs[major] or {}\n" + " self.libs[major] = lib\n" + " self.minors[major] = minor\n" + " return lib, oldMinor\n" + "end\n" + "function LibStub:GetLibrary(major, silent)\n" + " if not self.libs[major] and not silent then\n" + " error('Cannot find a library instance of \"' .. tostring(major) .. '\".')\n" + " end\n" + " return self.libs[major], self.minors[major]\n" + "end\n" + "function LibStub:IterateLibraries() return pairs(self.libs) end\n" + "setmetatable(LibStub, { __call = LibStub.GetLibrary })\n" + "_G['LibStub'] = LibStub\n" + ); + + // CallbackHandler-1.0 — minimal implementation for Ace3-based addons + luaL_dostring(L_, + "if LibStub then\n" + " local CBH = LibStub:NewLibrary('CallbackHandler-1.0', 7)\n" + " if CBH then\n" + " CBH.mixins = { 'RegisterCallback', 'UnregisterCallback', 'UnregisterAllCallbacks', 'Fire' }\n" + " function CBH:New(target, regName, unregName, unregAllName, onUsed)\n" + " local registry = setmetatable({}, { __index = CBH })\n" + " registry.callbacks = {}\n" + " target = target or {}\n" + " target[regName or 'RegisterCallback'] = function(self, event, method, ...)\n" + " if not registry.callbacks[event] then registry.callbacks[event] = {} end\n" + " local handler = type(method) == 'function' and method or self[method]\n" + " registry.callbacks[event][self] = handler\n" + " end\n" + " target[unregName or 'UnregisterCallback'] = function(self, event)\n" + " if registry.callbacks[event] then registry.callbacks[event][self] = nil end\n" + " end\n" + " target[unregAllName or 'UnregisterAllCallbacks'] = function(self)\n" + " for event, handlers in pairs(registry.callbacks) do handlers[self] = nil end\n" + " end\n" + " registry.Fire = function(self, event, ...)\n" + " if not self.callbacks[event] then return end\n" + " for obj, handler in pairs(self.callbacks[event]) do\n" + " handler(obj, event, ...)\n" + " end\n" + " end\n" + " return registry\n" + " end\n" + " end\n" + "end\n" + ); + + // Noop stubs for commonly called functions that don't need implementation + luaL_dostring(L_, + "function SetDesaturation() end\n" + "function SetPortraitTexture() end\n" + "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" + "GameTooltip.__lines = {}\n" + "function GameTooltip:SetOwner(owner, anchor) self.__owner = owner; self.__anchor = anchor end\n" + "function GameTooltip:ClearLines() self.__lines = {} end\n" + "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()\n" + " if self.__itemId and self.__itemId > 0 then\n" + " local name = GetItemInfo(self.__itemId)\n" + " local _, itemLink = GetItemInfo(self.__itemId)\n" + " return name, itemLink or ('|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(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" + " _WoweePopulateItemTooltip(self, tonumber(id))\n" + " return\n" + " end\n" + " id = link:match('spell:(%d+)')\n" + " if id then\n" + " self:SetSpellByID(tonumber(id))\n" + " return\n" + " end\n" + "end\n" + // Shared item tooltip builder using GetItemInfo return values + "function _WoweePopulateItemTooltip(self, itemId)\n" + " local name, itemLink, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, sellPrice = GetItemInfo(itemId)\n" + " if not name then return false end\n" + " local qColors = {[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},[7]={0,0.8,1}}\n" + " local c = qColors[quality or 1] or {1,1,1}\n" + " self:SetText(name, c[1], c[2], c[3])\n" + " -- Item level for equipment\n" + " if equipSlot and equipSlot ~= '' and iLevel and iLevel > 0 then\n" + " self:AddLine('Item Level '..iLevel, 1, 0.82, 0)\n" + " end\n" + " -- Equip slot and subclass on same line\n" + " if equipSlot and equipSlot ~= '' then\n" + " local slotNames = {INVTYPE_HEAD='Head',INVTYPE_NECK='Neck',INVTYPE_SHOULDER='Shoulder',\n" + " INVTYPE_CHEST='Chest',INVTYPE_WAIST='Waist',INVTYPE_LEGS='Legs',INVTYPE_FEET='Feet',\n" + " INVTYPE_WRIST='Wrist',INVTYPE_HAND='Hands',INVTYPE_FINGER='Finger',\n" + " INVTYPE_TRINKET='Trinket',INVTYPE_CLOAK='Back',INVTYPE_WEAPON='One-Hand',\n" + " INVTYPE_SHIELD='Off Hand',INVTYPE_2HWEAPON='Two-Hand',INVTYPE_RANGED='Ranged',\n" + " INVTYPE_WEAPONMAINHAND='Main Hand',INVTYPE_WEAPONOFFHAND='Off Hand',\n" + " INVTYPE_HOLDABLE='Held In Off-Hand',INVTYPE_TABARD='Tabard',INVTYPE_ROBE='Chest'}\n" + " local slotText = slotNames[equipSlot] or ''\n" + " local subText = (subclass and subclass ~= '') and subclass or ''\n" + " if slotText ~= '' or subText ~= '' then\n" + " self:AddDoubleLine(slotText, subText, 1,1,1, 1,1,1)\n" + " end\n" + " elseif class and class ~= '' then\n" + " self:AddLine(class, 1, 1, 1)\n" + " end\n" + " -- Fetch detailed stats from C side\n" + " local data = _GetItemTooltipData(itemId)\n" + " if data then\n" + " -- Bind type\n" + " if data.isHeroic then self:AddLine('Heroic', 0, 1, 0) end\n" + " if data.isUnique then self:AddLine('Unique', 1, 1, 1)\n" + " elseif data.isUniqueEquipped then self:AddLine('Unique-Equipped', 1, 1, 1) end\n" + " if data.bindType == 1 then self:AddLine('Binds when picked up', 1, 1, 1)\n" + " elseif data.bindType == 2 then self:AddLine('Binds when equipped', 1, 1, 1)\n" + " elseif data.bindType == 3 then self:AddLine('Binds when used', 1, 1, 1) end\n" + " -- Armor\n" + " if data.armor and data.armor > 0 then\n" + " self:AddLine(data.armor..' Armor', 1, 1, 1)\n" + " end\n" + " -- Weapon damage and speed\n" + " if data.damageMin and data.damageMax and data.damageMin > 0 then\n" + " local speed = (data.speed or 0) / 1000\n" + " if speed > 0 then\n" + " self:AddDoubleLine(string.format('%.0f - %.0f Damage', data.damageMin, data.damageMax), string.format('Speed %.2f', speed), 1,1,1, 1,1,1)\n" + " local dps = (data.damageMin + data.damageMax) / 2 / speed\n" + " self:AddLine(string.format('(%.1f damage per second)', dps), 1, 1, 1)\n" + " end\n" + " end\n" + " -- Stats\n" + " if data.stamina then self:AddLine('+'..data.stamina..' Stamina', 0, 1, 0) end\n" + " if data.strength then self:AddLine('+'..data.strength..' Strength', 0, 1, 0) end\n" + " if data.agility then self:AddLine('+'..data.agility..' Agility', 0, 1, 0) end\n" + " if data.intellect then self:AddLine('+'..data.intellect..' Intellect', 0, 1, 0) end\n" + " if data.spirit then self:AddLine('+'..data.spirit..' Spirit', 0, 1, 0) end\n" + " -- Extra stats (hit, crit, haste, AP, SP, etc.)\n" + " if data.extraStats then\n" + " local statNames = {[3]='Agility',[4]='Strength',[5]='Intellect',[6]='Spirit',[7]='Stamina',\n" + " [12]='Defense Rating',[13]='Dodge Rating',[14]='Parry Rating',[15]='Block Rating',\n" + " [16]='Melee Hit Rating',[17]='Ranged Hit Rating',[18]='Spell Hit Rating',\n" + " [19]='Melee Crit Rating',[20]='Ranged Crit Rating',[21]='Spell Crit Rating',\n" + " [28]='Melee Haste Rating',[29]='Ranged Haste Rating',[30]='Spell Haste Rating',\n" + " [31]='Hit Rating',[32]='Crit Rating',[36]='Haste Rating',\n" + " [33]='Resilience Rating',[34]='Attack Power',[35]='Spell Power',\n" + " [37]='Expertise Rating',[38]='Attack Power',[39]='Ranged Attack Power',\n" + " [43]='Mana per 5 sec.',[44]='Armor Penetration Rating',\n" + " [45]='Spell Power',[46]='Health per 5 sec.',[47]='Spell Penetration'}\n" + " for _, stat in ipairs(data.extraStats) do\n" + " local name = statNames[stat.type]\n" + " if name and stat.value ~= 0 then\n" + " local prefix = stat.value > 0 and '+' or ''\n" + " self:AddLine(prefix..stat.value..' '..name, 0, 1, 0)\n" + " end\n" + " end\n" + " end\n" + " -- Resistances\n" + " if data.fireRes and data.fireRes ~= 0 then self:AddLine('+'..data.fireRes..' Fire Resistance', 0, 1, 0) end\n" + " if data.natureRes and data.natureRes ~= 0 then self:AddLine('+'..data.natureRes..' Nature Resistance', 0, 1, 0) end\n" + " if data.frostRes and data.frostRes ~= 0 then self:AddLine('+'..data.frostRes..' Frost Resistance', 0, 1, 0) end\n" + " if data.shadowRes and data.shadowRes ~= 0 then self:AddLine('+'..data.shadowRes..' Shadow Resistance', 0, 1, 0) end\n" + " if data.arcaneRes and data.arcaneRes ~= 0 then self:AddLine('+'..data.arcaneRes..' Arcane Resistance', 0, 1, 0) end\n" + " -- Item spell effects (Use: / Equip: / Chance on Hit:)\n" + " if data.itemSpells then\n" + " local triggerLabels = {[0]='Use: ',[1]='Equip: ',[2]='Chance on hit: ',[5]=''}\n" + " for _, sp in ipairs(data.itemSpells) do\n" + " local label = triggerLabels[sp.trigger] or ''\n" + " local text = sp.description or sp.name or ''\n" + " if text ~= '' then\n" + " self:AddLine(label .. text, 0, 1, 0)\n" + " end\n" + " end\n" + " end\n" + " -- Gem sockets\n" + " if data.sockets then\n" + " local socketNames = {[1]='Meta',[2]='Red',[4]='Yellow',[8]='Blue'}\n" + " for _, sock in ipairs(data.sockets) do\n" + " local colorName = socketNames[sock.color] or 'Prismatic'\n" + " self:AddLine('[' .. colorName .. ' Socket]', 0.5, 0.5, 0.5)\n" + " end\n" + " end\n" + " -- Required level\n" + " if data.requiredLevel and data.requiredLevel > 1 then\n" + " self:AddLine('Requires Level '..data.requiredLevel, 1, 1, 1)\n" + " end\n" + " -- Flavor text\n" + " if data.description then self:AddLine('\"'..data.description..'\"', 1, 0.82, 0) end\n" + " if data.startsQuest then self:AddLine('This Item Begins a Quest', 1, 0.82, 0) end\n" + " end\n" + " -- Sell price from GetItemInfo\n" + " if sellPrice and sellPrice > 0 then\n" + " local gold = math.floor(sellPrice / 10000)\n" + " local silver = math.floor((sellPrice % 10000) / 100)\n" + " local copper = sellPrice % 100\n" + " local parts = {}\n" + " if gold > 0 then table.insert(parts, gold..'g') end\n" + " if silver > 0 then table.insert(parts, silver..'s') end\n" + " if copper > 0 then table.insert(parts, copper..'c') end\n" + " if #parts > 0 then self:AddLine('Sell Price: '..table.concat(parts, ' '), 1, 1, 1) end\n" + " end\n" + " self.__itemId = itemId\n" + " return true\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 ok = _WoweePopulateItemTooltip(self, tonumber(id))\n" + " return ok or false, 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" + " _WoweePopulateItemTooltip(self, tonumber(id))\n" + " if count and count > 1 then self:AddLine('Count: '..count, 0.5, 0.5, 0.5) 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, castTime, minRange, maxRange = 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" + " -- Mana cost\n" + " local cost, costType = GetSpellPowerCost(spellId)\n" + " if cost and cost > 0 then\n" + " local powerNames = {[0]='Mana',[1]='Rage',[2]='Focus',[3]='Energy',[6]='Runic Power'}\n" + " self:AddLine(cost..' '..(powerNames[costType] or 'Mana'), 1, 1, 1)\n" + " end\n" + " -- Range\n" + " if maxRange and maxRange > 0 then\n" + " self:AddDoubleLine(string.format('%.0f yd range', maxRange), '', 1,1,1, 1,1,1)\n" + " end\n" + " -- Cast time\n" + " if castTime and castTime > 0 then\n" + " self:AddDoubleLine(string.format('%.1f sec cast', castTime / 1000), '', 1,1,1, 1,1,1)\n" + " else\n" + " self:AddDoubleLine('Instant', '', 1,1,1, 1,1,1)\n" + " end\n" + " -- Description\n" + " local desc = GetSpellDescription(spellId)\n" + " if desc and desc ~= '' then\n" + " self:AddLine(desc, 1, 0.82, 0)\n" + " end\n" + " -- Cooldown\n" + " local start, dur = GetSpellCooldown(spellId)\n" + " if dur and dur > 0 then\n" + " local rem = start + dur - GetTime()\n" + " if rem > 0.1 then self:AddLine(string.format('%.0f sec cooldown', rem), 1, 0, 0) end\n" + " 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" + " _WoweePopulateItemTooltip(self, id)\n" + " end\n" + "end\n" + "function GameTooltip:FadeOut() end\n" + "function GameTooltip:SetFrameStrata(...) end\n" + "function GameTooltip:SetClampedToScreen(...) end\n" + "function GameTooltip:IsOwned(f) return self.__owner == f end\n" + // ShoppingTooltip: used by comparison tooltips + "ShoppingTooltip1 = CreateFrame('Frame', 'ShoppingTooltip1')\n" + "ShoppingTooltip2 = CreateFrame('Frame', 'ShoppingTooltip2')\n" + // Error handling stubs (used by many addons) + "local _errorHandler = function(err) return err end\n" + "function geterrorhandler() return _errorHandler end\n" + "function seterrorhandler(fn) if type(fn)=='function' then _errorHandler=fn end end\n" + "function debugstack(start, count1, count2) return '' end\n" + "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" + // 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 + "function GetNetStats() return 0, 0, 0, 0 end\n" + "function IsLoggedIn() return true end\n" + "function StaticPopup_Show() end\n" + "function StaticPopup_Hide() end\n" + // UI Panel management — Show/Hide standard WoW panels + "UIPanelWindows = {}\n" + "function ShowUIPanel(frame, force)\n" + " if frame and frame.Show then frame:Show() end\n" + "end\n" + "function HideUIPanel(frame)\n" + " if frame and frame.Hide then frame:Hide() end\n" + "end\n" + "function ToggleFrame(frame)\n" + " if frame then\n" + " if frame:IsShown() then frame:Hide() else frame:Show() end\n" + " end\n" + "end\n" + "function GetUIPanel(which) return nil end\n" + "function CloseWindows(ignoreCenter) return false end\n" + // TEXT localization stub — returns input string unchanged + "function TEXT(text) return text end\n" + // Faux scroll frame helpers (used by many list UIs) + "function FauxScrollFrame_GetOffset(frame)\n" + " return frame and frame.offset or 0\n" + "end\n" + "function FauxScrollFrame_Update(frame, numItems, numVisible, valueStep, button, smallWidth, bigWidth, highlightFrame, smallHighlightWidth, bigHighlightWidth)\n" + " if not frame then return false end\n" + " frame.offset = frame.offset or 0\n" + " local showScrollBar = numItems > numVisible\n" + " return showScrollBar\n" + "end\n" + "function FauxScrollFrame_SetOffset(frame, offset)\n" + " if frame then frame.offset = offset or 0 end\n" + "end\n" + "function FauxScrollFrame_OnVerticalScroll(frame, value, itemHeight, updateFunction)\n" + " if not frame then return end\n" + " frame.offset = math.floor(value / (itemHeight or 1) + 0.5)\n" + " if updateFunction then updateFunction() end\n" + "end\n" + // SecureCmdOptionParse — parses conditional macros like [target=focus] + "function SecureCmdOptionParse(options)\n" + " if not options then return nil end\n" + " -- Simple: return the unconditional fallback (text after last semicolon or the whole string)\n" + " local result = options:match(';%s*(.-)$') or options:match('^%[.*%]%s*(.-)$') or options\n" + " return result\n" + "end\n" + // ChatFrame message group stubs + "function ChatFrame_AddMessageGroup(frame, group) end\n" + "function ChatFrame_RemoveMessageGroup(frame, group) end\n" + "function ChatFrame_AddChannel(frame, channel) end\n" + "function ChatFrame_RemoveChannel(frame, channel) end\n" + // CreateTexture/CreateFontString are now C frame methods in the metatable + "do\n" + " local function cc(r,g,b)\n" + " local t = {r=r, g=g, b=b}\n" + " t.colorStr = string.format('%02x%02x%02x', math.floor(r*255), math.floor(g*255), math.floor(b*255))\n" + " function t:GenerateHexColor() return '|cff' .. self.colorStr end\n" + " function t:GenerateHexColorMarkup() return '|cff' .. self.colorStr end\n" + " return t\n" + " end\n" + " RAID_CLASS_COLORS = {\n" + " WARRIOR=cc(0.78,0.61,0.43), PALADIN=cc(0.96,0.55,0.73),\n" + " HUNTER=cc(0.67,0.83,0.45), ROGUE=cc(1.0,0.96,0.41),\n" + " PRIEST=cc(1.0,1.0,1.0), DEATHKNIGHT=cc(0.77,0.12,0.23),\n" + " SHAMAN=cc(0.0,0.44,0.87), MAGE=cc(0.41,0.80,0.94),\n" + " WARLOCK=cc(0.58,0.51,0.79), DRUID=cc(1.0,0.49,0.04),\n" + " }\n" + "end\n" + // GetClassColor(className) — returns r, g, b, colorString + "function GetClassColor(className)\n" + " local c = RAID_CLASS_COLORS[className]\n" + " if c then return c.r, c.g, c.b, c.colorStr end\n" + " return 1, 1, 1, 'ffffffff'\n" + "end\n" + // QuestDifficultyColors table for quest level coloring + "QuestDifficultyColors = {\n" + " impossible = {r=1.0,g=0.1,b=0.1,font='QuestDifficulty_Impossible'},\n" + " verydifficult = {r=1.0,g=0.5,b=0.25,font='QuestDifficulty_VeryDifficult'},\n" + " difficult = {r=1.0,g=1.0,b=0.0,font='QuestDifficulty_Difficult'},\n" + " standard = {r=0.25,g=0.75,b=0.25,font='QuestDifficulty_Standard'},\n" + " trivial = {r=0.5,g=0.5,b=0.5,font='QuestDifficulty_Trivial'},\n" + " header = {r=1.0,g=0.82,b=0.0,font='QuestDifficulty_Header'},\n" + "}\n" + // Money formatting utility + "function GetCoinTextureString(copper)\n" + " if not copper or copper == 0 then return '0c' end\n" + " copper = math.floor(copper)\n" + " local g = math.floor(copper / 10000)\n" + " local s = math.floor(math.fmod(copper, 10000) / 100)\n" + " local c = math.fmod(copper, 100)\n" + " local r = ''\n" + " if g > 0 then r = r .. g .. 'g ' end\n" + " if s > 0 then r = r .. s .. 's ' end\n" + " if c > 0 or r == '' then r = r .. c .. 'c' end\n" + " return r\n" + "end\n" + "GetCoinText = GetCoinTextureString\n" + ); + + // UIDropDownMenu framework — minimal compat for addons using dropdown menus + luaL_dostring(L_, + "UIDROPDOWNMENU_MENU_LEVEL = 1\n" + "UIDROPDOWNMENU_MENU_VALUE = nil\n" + "UIDROPDOWNMENU_OPEN_MENU = nil\n" + "local _ddMenuList = {}\n" + "function UIDropDownMenu_Initialize(frame, initFunc, displayMode, level, menuList)\n" + " if frame then frame.__initFunc = initFunc end\n" + "end\n" + "function UIDropDownMenu_CreateInfo() return {} end\n" + "function UIDropDownMenu_AddButton(info, level) table.insert(_ddMenuList, info) end\n" + "function UIDropDownMenu_SetWidth(frame, width) end\n" + "function UIDropDownMenu_SetButtonWidth(frame, width) end\n" + "function UIDropDownMenu_SetText(frame, text)\n" + " if frame then frame.__text = text end\n" + "end\n" + "function UIDropDownMenu_GetText(frame)\n" + " return frame and frame.__text or ''\n" + "end\n" + "function UIDropDownMenu_SetSelectedID(frame, id) end\n" + "function UIDropDownMenu_SetSelectedValue(frame, value) end\n" + "function UIDropDownMenu_GetSelectedID(frame) return 1 end\n" + "function UIDropDownMenu_GetSelectedValue(frame) return nil end\n" + "function UIDropDownMenu_JustifyText(frame, justify) end\n" + "function UIDropDownMenu_EnableDropDown(frame) end\n" + "function UIDropDownMenu_DisableDropDown(frame) end\n" + "function CloseDropDownMenus() end\n" + "function ToggleDropDownMenu(level, value, frame, anchor, xOfs, yOfs) end\n" + ); + + // UISpecialFrames: frames in this list close on Escape key + luaL_dostring(L_, + "UISpecialFrames = {}\n" + // Font object stubs — addons reference these for CreateFontString templates + "GameFontNormal = {}\n" + "GameFontNormalSmall = {}\n" + "GameFontNormalLarge = {}\n" + "GameFontHighlight = {}\n" + "GameFontHighlightSmall = {}\n" + "GameFontHighlightLarge = {}\n" + "GameFontDisable = {}\n" + "GameFontDisableSmall = {}\n" + "GameFontWhite = {}\n" + "GameFontRed = {}\n" + "GameFontGreen = {}\n" + "NumberFontNormal = {}\n" + "ChatFontNormal = {}\n" + "SystemFont = {}\n" + // InterfaceOptionsFrame: addons register settings panels here + "InterfaceOptionsFrame = CreateFrame('Frame', 'InterfaceOptionsFrame')\n" + "InterfaceOptionsFramePanelContainer = CreateFrame('Frame', 'InterfaceOptionsFramePanelContainer')\n" + "function InterfaceOptions_AddCategory(panel) end\n" + "function InterfaceOptionsFrame_OpenToCategory(panel) end\n" + // Commonly expected global tables + "SLASH_RELOAD1 = '/reload'\n" + "SLASH_RELOADUI1 = '/reloadui'\n" + "GRAY_FONT_COLOR = {r=0.5,g=0.5,b=0.5}\n" + "NORMAL_FONT_COLOR = {r=1.0,g=0.82,b=0.0}\n" + "HIGHLIGHT_FONT_COLOR = {r=1.0,g=1.0,b=1.0}\n" + "GREEN_FONT_COLOR = {r=0.1,g=1.0,b=0.1}\n" + "RED_FONT_COLOR = {r=1.0,g=0.1,b=0.1}\n" + // C_ChatInfo — addon message prefix API used by some addons + "C_ChatInfo = C_ChatInfo or {}\n" + "C_ChatInfo.RegisterAddonMessagePrefix = RegisterAddonMessagePrefix\n" + "C_ChatInfo.IsAddonMessagePrefixRegistered = IsAddonMessagePrefixRegistered\n" + "C_ChatInfo.SendAddonMessage = SendAddonMessage\n" + ); + + // Action bar constants and functions used by action bar addons + luaL_dostring(L_, + "NUM_ACTIONBAR_BUTTONS = 12\n" + "NUM_ACTIONBAR_PAGES = 6\n" + "ACTION_BUTTON_SHOW_GRID_REASON_CVAR = 1\n" + "ACTION_BUTTON_SHOW_GRID_REASON_EVENT = 2\n" + // Action bar page tracking + "local _actionBarPage = 1\n" + "function GetActionBarPage() return _actionBarPage end\n" + "function ChangeActionBarPage(page) _actionBarPage = page end\n" + "function GetBonusBarOffset() return 0 end\n" + // Action type query + "function GetActionText(slot) return nil end\n" + "function GetActionCount(slot) return 0 end\n" + // Binding functions + "function GetBindingKey(action) return nil end\n" + "function GetBindingAction(key) return nil end\n" + "function SetBinding(key, action) end\n" + "function SaveBindings(which) end\n" + "function GetCurrentBindingSet() return 1 end\n" + // Macro functions + "function GetNumMacros() return 0, 0 end\n" + "function GetMacroInfo(id) return nil end\n" + "function GetMacroBody(id) return nil end\n" + "function GetMacroIndexByName(name) return 0 end\n" + // Stance bar + "function GetNumShapeshiftForms() return 0 end\n" + "function GetShapeshiftFormInfo(index) return nil, nil, nil, nil end\n" + // Pet action bar + "NUM_PET_ACTION_SLOTS = 10\n" + // Common WoW constants used by many addons + "MAX_TALENT_TABS = 3\n" + "MAX_NUM_TALENTS = 100\n" + "BOOKTYPE_SPELL = 0\n" + "BOOKTYPE_PET = 1\n" + "MAX_PARTY_MEMBERS = 4\n" + "MAX_RAID_MEMBERS = 40\n" + "MAX_ARENA_TEAMS = 3\n" + "INVSLOT_FIRST_EQUIPPED = 1\n" + "INVSLOT_LAST_EQUIPPED = 19\n" + "NUM_BAG_SLOTS = 4\n" + "NUM_BANKBAGSLOTS = 7\n" + "CONTAINER_BAG_OFFSET = 0\n" + "MAX_SKILLLINE_TABS = 8\n" + "TRADE_ENCHANT_SLOT = 7\n" + "function GetPetActionInfo(slot) return nil end\n" + "function GetPetActionsUsable() return false end\n" + ); + + // WoW table/string utility functions used by many addons + luaL_dostring(L_, + // Table utilities + "function tContains(tbl, item)\n" + " for _, v in pairs(tbl) do if v == item then return true end end\n" + " return false\n" + "end\n" + "function tInvert(tbl)\n" + " local inv = {}\n" + " for k, v in pairs(tbl) do inv[v] = k end\n" + " return inv\n" + "end\n" + "function CopyTable(src)\n" + " if type(src) ~= 'table' then return src end\n" + " local copy = {}\n" + " for k, v in pairs(src) do copy[k] = CopyTable(v) end\n" + " return setmetatable(copy, getmetatable(src))\n" + "end\n" + "function tDeleteItem(tbl, item)\n" + " for i = #tbl, 1, -1 do if tbl[i] == item then table.remove(tbl, i) end end\n" + "end\n" + // Mixin pattern — used by modern addons for OOP-style object creation + "function Mixin(obj, ...)\n" + " for i = 1, select('#', ...) do\n" + " local mixin = select(i, ...)\n" + " for k, v in pairs(mixin) do obj[k] = v end\n" + " end\n" + " return obj\n" + "end\n" + "function CreateFromMixins(...)\n" + " return Mixin({}, ...)\n" + "end\n" + "function CreateAndInitFromMixin(mixin, ...)\n" + " local obj = CreateFromMixins(mixin)\n" + " if obj.Init then obj:Init(...) end\n" + " return obj\n" + "end\n" + "function MergeTable(dest, src)\n" + " for k, v in pairs(src) do dest[k] = v end\n" + " return dest\n" + "end\n" + // String utilities (WoW globals that alias Lua string functions) + "strupper = string.upper\n" + "strlower = string.lower\n" + "strfind = string.find\n" + "strsub = string.sub\n" + "strlen = string.len\n" + "strrep = string.rep\n" + "strbyte = string.byte\n" + "strchar = string.char\n" + "strgfind = string.gmatch\n" + "function tostringall(...)\n" + " local n = select('#', ...)\n" + " if n == 0 then return end\n" + " local r = {}\n" + " for i = 1, n do r[i] = tostring(select(i, ...)) end\n" + " return unpack(r, 1, n)\n" + "end\n" + "strrev = string.reverse\n" + "gsub = string.gsub\n" + "gmatch = string.gmatch\n" + "strjoin = function(delim, ...)\n" + " return table.concat({...}, delim)\n" + "end\n" + // Math utilities + "function Clamp(val, lo, hi) return math.min(math.max(val, lo), hi) end\n" + "function Round(val) return math.floor(val + 0.5) end\n" + // Bit operations (WoW provides these; Lua 5.1 doesn't have native bit ops) + "bit = bit or {}\n" + "bit.band = bit.band or function(a, b) local r,m=0,1 for i=0,31 do if a%2==1 and b%2==1 then r=r+m end a=math.floor(a/2) b=math.floor(b/2) m=m*2 end return r end\n" + "bit.bor = bit.bor or function(a, b) local r,m=0,1 for i=0,31 do if a%2==1 or b%2==1 then r=r+m end a=math.floor(a/2) b=math.floor(b/2) m=m*2 end return r end\n" + "bit.bxor = bit.bxor or function(a, b) local r,m=0,1 for i=0,31 do if (a%2==1)~=(b%2==1) then r=r+m end a=math.floor(a/2) b=math.floor(b/2) m=m*2 end return r end\n" + "bit.bnot = bit.bnot or function(a) return 4294967295 - a end\n" + "bit.lshift = bit.lshift or function(a, n) return a * (2^n) end\n" + "bit.rshift = bit.rshift or function(a, n) return math.floor(a / (2^n)) end\n" + ); +} + +// ---- Event System ---- +// Lua-side: WoweeEvents table holds { ["EVENT_NAME"] = { handler1, handler2, ... } } +// RegisterEvent("EVENT", handler) adds a handler function +// UnregisterEvent("EVENT", handler) removes it + +static int lua_RegisterEvent(lua_State* L) { + const char* eventName = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + // Get or create the WoweeEvents table + lua_getglobal(L, "__WoweeEvents"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setglobal(L, "__WoweeEvents"); + } + + // Get or create the handler list for this event + lua_getfield(L, -1, eventName); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, -3, eventName); + } + + // Append the handler function to the list + int len = static_cast(lua_objlen(L, -1)); + lua_pushvalue(L, 2); // push the handler function + lua_rawseti(L, -2, len + 1); + + lua_pop(L, 2); // pop handler list + WoweeEvents + return 0; +} + +static int lua_UnregisterEvent(lua_State* L) { + const char* eventName = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + lua_getglobal(L, "__WoweeEvents"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); return 0; } + + lua_getfield(L, -1, eventName); + if (lua_isnil(L, -1)) { lua_pop(L, 2); return 0; } + + // Remove matching handler from the list + int len = static_cast(lua_objlen(L, -1)); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, -1, i); + if (lua_rawequal(L, -1, 2)) { + lua_pop(L, 1); + // Shift remaining elements down + for (int j = i; j < len; j++) { + lua_rawgeti(L, -1, j + 1); + lua_rawseti(L, -2, j); + } + lua_pushnil(L); + lua_rawseti(L, -2, len); + break; + } + lua_pop(L, 1); + } + lua_pop(L, 2); + return 0; +} + +void LuaEngine::registerEventAPI() { + lua_pushcfunction(L_, lua_RegisterEvent); + lua_setglobal(L_, "RegisterEvent"); + + lua_pushcfunction(L_, lua_UnregisterEvent); + lua_setglobal(L_, "UnregisterEvent"); + + // Create the events table + lua_newtable(L_); + lua_setglobal(L_, "__WoweeEvents"); +} + +void LuaEngine::fireEvent(const std::string& eventName, + const std::vector& args) { + if (!L_) return; + + lua_getglobal(L_, "__WoweeEvents"); + if (lua_isnil(L_, -1)) { lua_pop(L_, 1); return; } + + lua_getfield(L_, -1, eventName.c_str()); + if (lua_isnil(L_, -1)) { lua_pop(L_, 2); return; } + + int handlerCount = static_cast(lua_objlen(L_, -1)); + for (int i = 1; i <= handlerCount; i++) { + lua_rawgeti(L_, -1, i); + if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); continue; } + + // Push arguments: event name first, then extra args + lua_pushstring(L_, eventName.c_str()); + for (const auto& arg : args) { + lua_pushstring(L_, arg.c_str()); + } + + int nargs = 1 + static_cast(args.size()); + if (lua_pcall(L_, nargs, 0, 0) != 0) { + const char* err = lua_tostring(L_, -1); + std::string errStr = err ? err : "(unknown)"; + LOG_ERROR("LuaEngine: event '", eventName, "' handler error: ", errStr); + if (luaErrorCallback_) luaErrorCallback_(errStr); + lua_pop(L_, 1); + } + } + lua_pop(L_, 2); // pop handler list + WoweeEvents + + // Also dispatch to frames that registered for this event via frame:RegisterEvent() + lua_getglobal(L_, "__WoweeFrameEvents"); + if (lua_istable(L_, -1)) { + lua_getfield(L_, -1, eventName.c_str()); + if (lua_istable(L_, -1)) { + int frameCount = static_cast(lua_objlen(L_, -1)); + for (int i = 1; i <= frameCount; i++) { + lua_rawgeti(L_, -1, i); + if (!lua_istable(L_, -1)) { lua_pop(L_, 1); continue; } + + // Get the frame's OnEvent script + lua_getfield(L_, -1, "__scripts"); + if (lua_istable(L_, -1)) { + lua_getfield(L_, -1, "OnEvent"); + if (lua_isfunction(L_, -1)) { + lua_pushvalue(L_, -3); // self (frame) + lua_pushstring(L_, eventName.c_str()); + for (const auto& arg : args) lua_pushstring(L_, arg.c_str()); + int nargs = 2 + static_cast(args.size()); + if (lua_pcall(L_, nargs, 0, 0) != 0) { + const char* ferr = lua_tostring(L_, -1); + std::string ferrStr = ferr ? ferr : "(unknown)"; + LOG_ERROR("LuaEngine: frame OnEvent error: ", ferrStr); + if (luaErrorCallback_) luaErrorCallback_(ferrStr); + lua_pop(L_, 1); + } + } else { + lua_pop(L_, 1); // pop non-function + } + } + lua_pop(L_, 2); // pop __scripts + frame + } + } + lua_pop(L_, 1); // pop event frame list + } + lua_pop(L_, 1); // pop __WoweeFrameEvents +} + +void LuaEngine::dispatchOnUpdate(float elapsed) { + if (!L_) return; + + lua_getglobal(L_, "__WoweeOnUpdateFrames"); + if (!lua_istable(L_, -1)) { lua_pop(L_, 1); return; } + + int count = static_cast(lua_objlen(L_, -1)); + for (int i = 1; i <= count; i++) { + lua_rawgeti(L_, -1, i); + if (!lua_istable(L_, -1)) { lua_pop(L_, 1); continue; } + + // Check if frame is visible + lua_getfield(L_, -1, "__visible"); + bool visible = lua_toboolean(L_, -1); + lua_pop(L_, 1); + if (!visible) { lua_pop(L_, 1); continue; } + + // Get OnUpdate script + lua_getfield(L_, -1, "__scripts"); + if (lua_istable(L_, -1)) { + lua_getfield(L_, -1, "OnUpdate"); + if (lua_isfunction(L_, -1)) { + lua_pushvalue(L_, -3); // self (frame) + lua_pushnumber(L_, static_cast(elapsed)); + if (lua_pcall(L_, 2, 0, 0) != 0) { + const char* uerr = lua_tostring(L_, -1); + std::string uerrStr = uerr ? uerr : "(unknown)"; + LOG_ERROR("LuaEngine: OnUpdate error: ", uerrStr); + if (luaErrorCallback_) luaErrorCallback_(uerrStr); + lua_pop(L_, 1); + } + } else { + lua_pop(L_, 1); + } + } + lua_pop(L_, 2); // pop __scripts + frame + } + lua_pop(L_, 1); // pop __WoweeOnUpdateFrames +} + +bool LuaEngine::dispatchSlashCommand(const std::string& command, const std::string& args) { + if (!L_) return false; + + // Check each SlashCmdList entry: for key NAME, check SLASH_NAME1, SLASH_NAME2, etc. + lua_getglobal(L_, "SlashCmdList"); + if (!lua_istable(L_, -1)) { lua_pop(L_, 1); return false; } + + std::string cmdLower = command; + toLowerInPlace(cmdLower); + + lua_pushnil(L_); + while (lua_next(L_, -2) != 0) { + // Stack: SlashCmdList, key, handler + if (!lua_isfunction(L_, -1) || !lua_isstring(L_, -2)) { + lua_pop(L_, 1); + continue; + } + const char* name = lua_tostring(L_, -2); + + // Check SLASH_1 through SLASH_9 + for (int i = 1; i <= 9; i++) { + std::string globalName = "SLASH_" + std::string(name) + std::to_string(i); + lua_getglobal(L_, globalName.c_str()); + if (lua_isstring(L_, -1)) { + std::string slashStr = lua_tostring(L_, -1); + toLowerInPlace(slashStr); + if (slashStr == cmdLower) { + lua_pop(L_, 1); // pop global + // Call the handler with args + lua_pushvalue(L_, -1); // copy handler + lua_pushstring(L_, args.c_str()); + if (lua_pcall(L_, 1, 0, 0) != 0) { + LOG_ERROR("LuaEngine: SlashCmdList['", name, "'] error: ", + lua_tostring(L_, -1)); + lua_pop(L_, 1); + } + lua_pop(L_, 3); // pop handler, key, SlashCmdList + return true; + } + } + lua_pop(L_, 1); // pop global + } + lua_pop(L_, 1); // pop handler, keep key for next iteration + } + lua_pop(L_, 1); // pop SlashCmdList + return false; +} + +// ---- SavedVariables serialization ---- + +static void serializeLuaValue(lua_State* L, int idx, std::string& out, int indent); + +static void serializeLuaTable(lua_State* L, int idx, std::string& out, int indent) { + out += "{\n"; + std::string pad(indent + 2, ' '); + lua_pushnil(L); + while (lua_next(L, idx) != 0) { + out += pad; + // Key + if (lua_type(L, -2) == LUA_TSTRING) { + const char* k = lua_tostring(L, -2); + out += "[\""; + for (const char* p = k; *p; ++p) { + if (*p == '"' || *p == '\\') out += '\\'; + out += *p; + } + out += "\"] = "; + } else if (lua_type(L, -2) == LUA_TNUMBER) { + out += "[" + std::to_string(static_cast(lua_tonumber(L, -2))) + "] = "; + } else { + lua_pop(L, 1); + continue; + } + // Value + serializeLuaValue(L, lua_gettop(L), out, indent + 2); + out += ",\n"; + lua_pop(L, 1); + } + out += std::string(indent, ' ') + "}"; +} + +static void serializeLuaValue(lua_State* L, int idx, std::string& out, int indent) { + switch (lua_type(L, idx)) { + case LUA_TNIL: out += "nil"; break; + case LUA_TBOOLEAN: out += lua_toboolean(L, idx) ? "true" : "false"; break; + case LUA_TNUMBER: { + double v = lua_tonumber(L, idx); + char buf[64]; + snprintf(buf, sizeof(buf), "%.17g", v); + out += buf; + break; + } + case LUA_TSTRING: { + const char* s = lua_tostring(L, idx); + out += "\""; + for (const char* p = s; *p; ++p) { + if (*p == '"' || *p == '\\') out += '\\'; + else if (*p == '\n') { out += "\\n"; continue; } + else if (*p == '\r') continue; + out += *p; + } + out += "\""; + break; + } + case LUA_TTABLE: + serializeLuaTable(L, idx, out, indent); + break; + default: + out += "nil"; // Functions, userdata, etc. can't be serialized + break; + } +} + +void LuaEngine::setAddonList(const std::vector& addons) { + if (!L_) return; + lua_pushnumber(L_, static_cast(addons.size())); + lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_addon_count"); + + lua_newtable(L_); + for (size_t i = 0; i < addons.size(); i++) { + lua_newtable(L_); + lua_pushstring(L_, addons[i].addonName.c_str()); + lua_setfield(L_, -2, "name"); + lua_pushstring(L_, addons[i].getTitle().c_str()); + lua_setfield(L_, -2, "title"); + auto notesIt = addons[i].directives.find("Notes"); + lua_pushstring(L_, notesIt != addons[i].directives.end() ? notesIt->second.c_str() : ""); + lua_setfield(L_, -2, "notes"); + // Store all TOC directives for GetAddOnMetadata + lua_newtable(L_); + for (const auto& [key, val] : addons[i].directives) { + lua_pushstring(L_, val.c_str()); + lua_setfield(L_, -2, key.c_str()); + } + lua_setfield(L_, -2, "metadata"); + lua_rawseti(L_, -2, static_cast(i + 1)); + } + lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_addon_info"); +} + +bool LuaEngine::loadSavedVariables(const std::string& path) { + if (!L_) return false; + std::ifstream f(path); + if (!f.is_open()) return false; // No saved data yet — not an error + std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + if (content.empty()) return true; + int err = luaL_dostring(L_, content.c_str()); + if (err != 0) { + LOG_WARNING("LuaEngine: error loading saved variables from '", path, "': ", + lua_tostring(L_, -1)); + lua_pop(L_, 1); + return false; + } + return true; +} + +bool LuaEngine::saveSavedVariables(const std::string& path, const std::vector& varNames) { + if (!L_ || varNames.empty()) return false; + std::string output; + for (const auto& name : varNames) { + lua_getglobal(L_, name.c_str()); + if (!lua_isnil(L_, -1)) { + output += name + " = "; + serializeLuaValue(L_, lua_gettop(L_), output, 0); + output += "\n"; + } + lua_pop(L_, 1); + } + if (output.empty()) return true; + + // Ensure directory exists + size_t lastSlash = path.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + std::error_code ec; + std::filesystem::create_directories(path.substr(0, lastSlash), ec); + } + + std::ofstream f(path); + if (!f.is_open()) { + LOG_WARNING("LuaEngine: cannot write saved variables to '", path, "'"); + return false; + } + f << output; + LOG_INFO("LuaEngine: saved variables to '", path, "' (", output.size(), " bytes)"); + return true; +} + +bool LuaEngine::executeFile(const std::string& path) { + if (!L_) return false; + + int err = luaL_dofile(L_, path.c_str()); + if (err != 0) { + const char* errMsg = lua_tostring(L_, -1); + std::string msg = errMsg ? errMsg : "(unknown error)"; + LOG_ERROR("LuaEngine: error loading '", path, "': ", msg); + if (luaErrorCallback_) luaErrorCallback_(msg); + if (gameHandler_) { + game::MessageChatData errChat; + errChat.type = game::ChatType::SYSTEM; + errChat.language = game::ChatLanguage::UNIVERSAL; + errChat.message = "|cffff4040[Lua Error] " + msg + "|r"; + gameHandler_->addLocalChatMessage(errChat); + } + lua_pop(L_, 1); + return false; + } + return true; +} + +bool LuaEngine::executeString(const std::string& code) { + if (!L_) return false; + + int err = luaL_dostring(L_, code.c_str()); + if (err != 0) { + const char* errMsg = lua_tostring(L_, -1); + std::string msg = errMsg ? errMsg : "(unknown error)"; + LOG_ERROR("LuaEngine: script error: ", msg); + if (luaErrorCallback_) luaErrorCallback_(msg); + if (gameHandler_) { + game::MessageChatData errChat; + errChat.type = game::ChatType::SYSTEM; + errChat.language = game::ChatLanguage::UNIVERSAL; + errChat.message = "|cffff4040[Lua Error] " + msg + "|r"; + gameHandler_->addLocalChatMessage(errChat); + } + lua_pop(L_, 1); + return false; + } + return true; +} + +} // namespace wowee::addons diff --git a/src/addons/toc_parser.cpp b/src/addons/toc_parser.cpp new file mode 100644 index 00000000..523a164a --- /dev/null +++ b/src/addons/toc_parser.cpp @@ -0,0 +1,110 @@ +#include "addons/toc_parser.hpp" +#include +#include + +namespace wowee::addons { + +std::string TocFile::getTitle() const { + auto it = directives.find("Title"); + return (it != directives.end()) ? it->second : addonName; +} + +std::string TocFile::getInterface() const { + auto it = directives.find("Interface"); + return (it != directives.end()) ? it->second : ""; +} + +bool TocFile::isLoadOnDemand() const { + auto it = directives.find("LoadOnDemand"); + return (it != directives.end()) && it->second == "1"; +} + +static std::vector parseVarList(const std::string& val) { + std::vector result; + size_t pos = 0; + while (pos <= val.size()) { + size_t comma = val.find(',', pos); + std::string name = (comma != std::string::npos) ? val.substr(pos, comma - pos) : val.substr(pos); + size_t start = name.find_first_not_of(" \t"); + size_t end = name.find_last_not_of(" \t"); + if (start != std::string::npos) + result.push_back(name.substr(start, end - start + 1)); + if (comma == std::string::npos) break; + pos = comma + 1; + } + return result; +} + +std::vector TocFile::getSavedVariables() const { + auto it = directives.find("SavedVariables"); + return (it != directives.end()) ? parseVarList(it->second) : std::vector{}; +} + +std::vector TocFile::getSavedVariablesPerCharacter() const { + auto it = directives.find("SavedVariablesPerCharacter"); + return (it != directives.end()) ? parseVarList(it->second) : std::vector{}; +} + +std::optional parseTocFile(const std::string& tocPath) { + std::ifstream f(tocPath); + if (!f.is_open()) return std::nullopt; + + TocFile toc; + toc.basePath = tocPath; + // Strip filename to get directory + size_t lastSlash = tocPath.find_last_of("/\\"); + if (lastSlash != std::string::npos) { + toc.basePath = tocPath.substr(0, lastSlash); + toc.addonName = tocPath.substr(lastSlash + 1); + } + // Strip .toc extension from addon name + size_t dotPos = toc.addonName.rfind(".toc"); + if (dotPos != std::string::npos) toc.addonName.resize(dotPos); + + std::string line; + while (std::getline(f, line)) { + // Strip trailing CR (Windows line endings) + if (!line.empty() && line.back() == '\r') line.pop_back(); + + // Skip empty lines + if (line.empty()) continue; + + // ## directives + if (line.size() >= 3 && line[0] == '#' && line[1] == '#') { + std::string directive = line.substr(2); + size_t colon = directive.find(':'); + if (colon != std::string::npos) { + std::string key = directive.substr(0, colon); + std::string val = directive.substr(colon + 1); + // Trim whitespace + auto trim = [](std::string& s) { + size_t start = s.find_first_not_of(" \t"); + size_t end = s.find_last_not_of(" \t"); + s = (start == std::string::npos) ? "" : s.substr(start, end - start + 1); + }; + trim(key); + trim(val); + if (!key.empty()) toc.directives[key] = val; + } + continue; + } + + // Single # comment + if (line[0] == '#') continue; + + // Whitespace-only line + size_t firstNonSpace = line.find_first_not_of(" \t"); + if (firstNonSpace == std::string::npos) continue; + + // File entry — normalize backslashes to forward slashes + std::string filename = line.substr(firstNonSpace); + size_t lastNonSpace = filename.find_last_not_of(" \t"); + if (lastNonSpace != std::string::npos) filename.resize(lastNonSpace + 1); + std::replace(filename.begin(), filename.end(), '\\', '/'); + toc.files.push_back(std::move(filename)); + } + + return toc; +} + +} // namespace wowee::addons diff --git a/src/audio/activity_sound_manager.cpp b/src/audio/activity_sound_manager.cpp index 4f02b35e..3a0bfe54 100644 --- a/src/audio/activity_sound_manager.cpp +++ b/src/audio/activity_sound_manager.cpp @@ -52,18 +52,14 @@ bool ActivitySoundManager::initialize(pipeline::AssetManager* assets) { preloadLandingSet(FootstepSurface::SNOW, "Snow"); preloadCandidates(meleeSwingClips, { - "Sound\\Item\\Weapons\\Sword\\SwordSwing1.wav", - "Sound\\Item\\Weapons\\Sword\\SwordSwing2.wav", - "Sound\\Item\\Weapons\\Sword\\SwordSwing3.wav", - "Sound\\Item\\Weapons\\Sword\\SwordHit1.wav", - "Sound\\Item\\Weapons\\Sword\\SwordHit2.wav", - "Sound\\Item\\Weapons\\Sword\\SwordHit3.wav", - "Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing1.wav", - "Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing2.wav", - "Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing3.wav", - "Sound\\Item\\Weapons\\Melee\\MeleeSwing1.wav", - "Sound\\Item\\Weapons\\Melee\\MeleeSwing2.wav", - "Sound\\Item\\Weapons\\Melee\\MeleeSwing3.wav" + "Sound\\Item\\Weapons\\WeaponSwings\\mWooshMedium1.wav", + "Sound\\Item\\Weapons\\WeaponSwings\\mWooshMedium2.wav", + "Sound\\Item\\Weapons\\WeaponSwings\\mWooshMedium3.wav", + "Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge1.wav", + "Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge2.wav", + "Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge3.wav", + "Sound\\Item\\Weapons\\MissSwings\\MissWhoosh1Handed.wav", + "Sound\\Item\\Weapons\\MissSwings\\MissWhoosh2Handed.wav" }); initialized = true; diff --git a/src/audio/npc_voice_manager.cpp b/src/audio/npc_voice_manager.cpp index 1027d165..6f6c3b67 100644 --- a/src/audio/npc_voice_manager.cpp +++ b/src/audio/npc_voice_manager.cpp @@ -178,6 +178,30 @@ void NpcVoiceManager::loadVoiceSounds() { loadCategory(vendorLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Vendor", 2); loadCategory(pissedLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Pissed", 6); + // Blood Elf Male (TBC+ NPCBloodElfMaleStandard, sparse numbering up to 12) + loadCategory(greetingLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Greeting", 12); + loadCategory(farewellLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Farewell", 12); + loadCategory(vendorLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Vendor", 6); + loadCategory(pissedLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Pissed", 10); + + // Blood Elf Female + loadCategory(greetingLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Greeting", 12); + loadCategory(farewellLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Farewell", 12); + loadCategory(vendorLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Vendor", 6); + loadCategory(pissedLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Pissed", 10); + + // Draenei Male + loadCategory(greetingLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Greeting", 12); + loadCategory(farewellLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Farewell", 12); + loadCategory(vendorLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Vendor", 6); + loadCategory(pissedLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Pissed", 10); + + // Draenei Female + loadCategory(greetingLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Greeting", 12); + loadCategory(farewellLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Farewell", 12); + loadCategory(vendorLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Vendor", 6); + loadCategory(pissedLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Pissed", 10); + // Load combat sounds from Character vocal files // These use a different path structure: Sound\Character\{Race}\{Race}Vocal{Gender}\{Race}{Gender}{Sound}.wav auto loadCombatCategory = [this]( @@ -251,6 +275,38 @@ void NpcVoiceManager::loadVoiceSounds() { loadCombatCategory(aggroLibrary_, VoiceType::TROLL_FEMALE, "Troll", "TrollFemale", "AttackMyTarget", 3); loadCombatCategory(fleeLibrary_, VoiceType::TROLL_FEMALE, "Troll", "TrollFemale", "Flee", 2); + + // Blood Elf and Draenei combat sounds (flat folder structure, no VocalMale/Female subfolder) + auto loadCombatFlat = [this]( + std::unordered_map>& library, + VoiceType type, + const std::string& raceFolder, + const std::string& raceGender, + const std::string& soundType, + int count) { + + auto& samples = library[type]; + for (int i = 1; i <= count; ++i) { + std::string num = (i < 10) ? ("0" + std::to_string(i)) : std::to_string(i); + std::string path = "Sound\\Character\\" + raceFolder + "\\" + raceGender + soundType + num + ".wav"; + VoiceSample sample; + if (loadSound(path, sample)) samples.push_back(std::move(sample)); + } + }; + + // Blood Elf combat sounds + loadCombatFlat(aggroLibrary_, VoiceType::BLOODELF_MALE, "BloodElf", "BloodElfMale", "AttackMyTarget", 3); + loadCombatFlat(fleeLibrary_, VoiceType::BLOODELF_MALE, "BloodElf", "BloodElfMale", "Flee", 3); + + loadCombatFlat(aggroLibrary_, VoiceType::BLOODELF_FEMALE, "BloodElf", "BloodElfFemale", "AttackMyTarget", 3); + loadCombatFlat(fleeLibrary_, VoiceType::BLOODELF_FEMALE, "BloodElf", "BloodElfFemale", "Flee", 3); + + // Draenei combat sounds + loadCombatFlat(aggroLibrary_, VoiceType::DRAENEI_MALE, "Draenei", "DraeneiMale", "AttackMyTarget", 3); + loadCombatFlat(fleeLibrary_, VoiceType::DRAENEI_MALE, "Draenei", "DraeneiMale", "Flee", 3); + + loadCombatFlat(aggroLibrary_, VoiceType::DRAENEI_FEMALE, "Draenei", "DraeneiFemale", "AttackMyTarget", 3); + loadCombatFlat(fleeLibrary_, VoiceType::DRAENEI_FEMALE, "Draenei", "DraeneiFemale", "Flee", 3); } bool NpcVoiceManager::loadSound(const std::string& path, VoiceSample& sample) { diff --git a/src/audio/ui_sound_manager.cpp b/src/audio/ui_sound_manager.cpp index f32f0d9b..6518259e 100644 --- a/src/audio/ui_sound_manager.cpp +++ b/src/audio/ui_sound_manager.cpp @@ -122,6 +122,20 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) { deselectTargetSounds_.resize(1); loadSound("Sound\\Interface\\iDeselectTarget.wav", deselectTargetSounds_[0], assets); + // Whisper notification (falls back to iSelectTarget if the dedicated file is absent) + whisperSounds_.resize(1); + if (!loadSound("Sound\\Interface\\Whisper_TellMale.wav", whisperSounds_[0], assets)) { + if (!loadSound("Sound\\Interface\\Whisper_TellFemale.wav", whisperSounds_[0], assets)) { + whisperSounds_ = selectTargetSounds_; + } + } + + // Minimap ping sound + minimapPingSounds_.resize(1); + if (!loadSound("Sound\\Interface\\MapPing.wav", minimapPingSounds_[0], assets)) { + minimapPingSounds_ = selectTargetSounds_; // fallback to target select sound + } + LOG_INFO("UISoundManager: Window sounds - Bag: ", (bagOpenLoaded && bagCloseLoaded) ? "YES" : "NO", ", QuestLog: ", (questLogOpenLoaded && questLogCloseLoaded) ? "YES" : "NO", ", CharSheet: ", (charSheetOpenLoaded && charSheetCloseLoaded) ? "YES" : "NO"); @@ -225,5 +239,11 @@ void UiSoundManager::playError() { playSound(errorSounds_); } void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); } void UiSoundManager::playTargetDeselect() { playSound(deselectTargetSounds_); } +// Chat notifications +void UiSoundManager::playWhisperReceived() { playSound(whisperSounds_); } + +// Minimap ping +void UiSoundManager::playMinimapPing() { playSound(minimapPingSounds_); } + } // namespace audio } // namespace wowee diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index a6ad394a..77794365 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -82,7 +82,7 @@ void AuthHandler::requestRealmList() { return; } if (state != AuthState::AUTHENTICATED && state != AuthState::REALM_LIST_RECEIVED) { - LOG_ERROR("Cannot request realm list: not authenticated (state: ", (int)state, ")"); + LOG_ERROR("Cannot request realm list: not authenticated (state: ", static_cast(state), ")"); return; } @@ -182,11 +182,11 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { if (response.result == AuthResult::BUILD_INVALID || response.result == AuthResult::BUILD_UPDATE) { std::ostringstream ss; ss << "LOGON_CHALLENGE failed: version mismatch (client v" - << (int)clientInfo.majorVersion << "." - << (int)clientInfo.minorVersion << "." - << (int)clientInfo.patchVersion + << static_cast(clientInfo.majorVersion) << "." + << static_cast(clientInfo.minorVersion) << "." + << static_cast(clientInfo.patchVersion) << " build " << clientInfo.build - << ", auth protocol " << (int)clientInfo.protocolVersion << ")"; + << ", auth protocol " << static_cast(clientInfo.protocolVersion) << ")"; fail(ss.str()); } else { fail(std::string("LOGON_CHALLENGE failed: ") + getAuthResultString(response.result)); @@ -195,14 +195,14 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) { } if (response.securityFlags != 0) { - LOG_WARNING("Server sent security flags: 0x", std::hex, (int)response.securityFlags, std::dec); + LOG_WARNING("Server sent security flags: 0x", std::hex, static_cast(response.securityFlags), std::dec); if (response.securityFlags & 0x01) LOG_WARNING(" PIN required"); if (response.securityFlags & 0x02) LOG_WARNING(" Matrix card required (not supported)"); if (response.securityFlags & 0x04) LOG_WARNING(" Authenticator required (not supported)"); } LOG_INFO("Challenge: N=", response.N.size(), "B g=", response.g.size(), "B salt=", - response.salt.size(), "B secFlags=0x", std::hex, (int)response.securityFlags, std::dec); + response.salt.size(), "B secFlags=0x", std::hex, static_cast(response.securityFlags), std::dec); // Feed SRP with server challenge data srp->feed(response.B, response.g, response.N, response.salt); @@ -389,12 +389,12 @@ void AuthHandler::handleRealmListResponse(network::Packet& packet) { const auto& realm = realms[i]; LOG_INFO("Realm ", (i + 1), ": ", realm.name); LOG_INFO(" Address: ", realm.address); - LOG_INFO(" ID: ", (int)realm.id); + LOG_INFO(" ID: ", static_cast(realm.id)); LOG_INFO(" Population: ", realm.population); - LOG_INFO(" Characters: ", (int)realm.characters); + LOG_INFO(" Characters: ", static_cast(realm.characters)); if (realm.hasVersionInfo()) { - LOG_INFO(" Version: ", (int)realm.majorVersion, ".", - (int)realm.minorVersion, ".", (int)realm.patchVersion, + LOG_INFO(" Version: ", static_cast(realm.majorVersion), ".", + static_cast(realm.minorVersion), ".", static_cast(realm.patchVersion), " (build ", realm.build, ")"); } } @@ -421,9 +421,9 @@ void AuthHandler::handlePacket(network::Packet& packet) { const auto& raw = packet.getData(); std::ostringstream hs; for (size_t i = 0; i < std::min(raw.size(), 40); ++i) - hs << std::hex << std::setfill('0') << std::setw(2) << (int)raw[i]; + hs << std::hex << std::setfill('0') << std::setw(2) << static_cast(raw[i]); if (raw.size() > 40) hs << "..."; - LOG_INFO("Auth pkt 0x", std::hex, (int)opcodeValue, std::dec, + LOG_INFO("Auth pkt 0x", std::hex, static_cast(opcodeValue), std::dec, " (", raw.size(), "B): ", hs.str()); } @@ -442,11 +442,11 @@ void AuthHandler::handlePacket(network::Packet& packet) { } if (response.result == AuthResult::BUILD_INVALID || response.result == AuthResult::BUILD_UPDATE) { ss << ": version mismatch (client v" - << (int)clientInfo.majorVersion << "." - << (int)clientInfo.minorVersion << "." - << (int)clientInfo.patchVersion + << static_cast(clientInfo.majorVersion) << "." + << static_cast(clientInfo.minorVersion) << "." + << static_cast(clientInfo.patchVersion) << " build " << clientInfo.build - << ", auth protocol " << (int)clientInfo.protocolVersion << ")"; + << ", auth protocol " << static_cast(clientInfo.protocolVersion) << ")"; } else { ss << ": " << getAuthResultString(response.result) << " (code 0x" << std::hex << std::setw(2) << std::setfill('0') @@ -454,7 +454,7 @@ void AuthHandler::handlePacket(network::Packet& packet) { } fail(ss.str()); } else { - LOG_WARNING("Unexpected LOGON_CHALLENGE response in state: ", (int)state); + LOG_WARNING("Unexpected LOGON_CHALLENGE response in state: ", static_cast(state)); } } break; @@ -463,7 +463,7 @@ void AuthHandler::handlePacket(network::Packet& packet) { if (state == AuthState::PROOF_SENT) { handleLogonProofResponse(packet); } else { - LOG_WARNING("Unexpected LOGON_PROOF response in state: ", (int)state); + LOG_WARNING("Unexpected LOGON_PROOF response in state: ", static_cast(state)); } break; @@ -471,12 +471,12 @@ void AuthHandler::handlePacket(network::Packet& packet) { if (state == AuthState::REALM_LIST_REQUESTED) { handleRealmListResponse(packet); } else { - LOG_WARNING("Unexpected REALM_LIST response in state: ", (int)state); + LOG_WARNING("Unexpected REALM_LIST response in state: ", static_cast(state)); } break; default: - LOG_WARNING("Unhandled auth opcode: 0x", std::hex, (int)opcodeValue, std::dec); + LOG_WARNING("Unhandled auth opcode: 0x", std::hex, static_cast(opcodeValue), std::dec); break; } } @@ -503,7 +503,7 @@ void AuthHandler::update(float /*deltaTime*/) { void AuthHandler::setState(AuthState newState) { if (state != newState) { - LOG_DEBUG("Auth state: ", (int)state, " -> ", (int)newState); + LOG_DEBUG("Auth state: ", static_cast(state), " -> ", static_cast(newState)); state = newState; } } diff --git a/src/auth/auth_packets.cpp b/src/auth/auth_packets.cpp index f95a5344..258490ac 100644 --- a/src/auth/auth_packets.cpp +++ b/src/auth/auth_packets.cpp @@ -207,7 +207,7 @@ bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallenge LOG_DEBUG(" g size: ", response.g.size(), " bytes"); LOG_DEBUG(" N size: ", response.N.size(), " bytes"); LOG_DEBUG(" salt size: ", response.salt.size(), " bytes"); - LOG_DEBUG(" Security flags: ", (int)response.securityFlags); + LOG_DEBUG(" Security flags: ", static_cast(response.securityFlags)); if (response.securityFlags & 0x01) { LOG_DEBUG(" PIN grid seed: ", response.pinGridSeed); } @@ -317,10 +317,10 @@ bool LogonProofResponseParser::parse(network::Packet& packet, LogonProofResponse // Status response.status = packet.readUInt8(); - LOG_INFO("LOGON_PROOF response status: ", (int)response.status); + LOG_INFO("LOGON_PROOF response status: ", static_cast(response.status)); if (response.status != 0) { - LOG_ERROR("LOGON_PROOF failed with status: ", (int)response.status); + LOG_ERROR("LOGON_PROOF failed with status: ", static_cast(response.status)); return true; // Valid packet, but proof failed } @@ -418,23 +418,23 @@ bool RealmListResponseParser::parse(network::Packet& packet, RealmListResponse& realm.patchVersion = packet.readUInt8(); realm.build = packet.readUInt16(); - LOG_DEBUG(" Realm ", (int)i, " (", realm.name, ") version: ", - (int)realm.majorVersion, ".", (int)realm.minorVersion, ".", - (int)realm.patchVersion, " (", realm.build, ")"); + LOG_DEBUG(" Realm ", static_cast(i), " (", realm.name, ") version: ", + static_cast(realm.majorVersion), ".", static_cast(realm.minorVersion), ".", + static_cast(realm.patchVersion), " (", realm.build, ")"); } else { - LOG_DEBUG(" Realm ", (int)i, " (", realm.name, ") - no version info"); + LOG_DEBUG(" Realm ", static_cast(i), " (", realm.name, ") - no version info"); } - LOG_DEBUG(" Realm ", (int)i, " details:"); + LOG_DEBUG(" Realm ", static_cast(i), " details:"); LOG_DEBUG(" Name: ", realm.name); LOG_DEBUG(" Address: ", realm.address); - LOG_DEBUG(" ID: ", (int)realm.id); - LOG_DEBUG(" Icon: ", (int)realm.icon); - LOG_DEBUG(" Lock: ", (int)realm.lock); - LOG_DEBUG(" Flags: ", (int)realm.flags); + LOG_DEBUG(" ID: ", static_cast(realm.id)); + LOG_DEBUG(" Icon: ", static_cast(realm.icon)); + LOG_DEBUG(" Lock: ", static_cast(realm.lock)); + LOG_DEBUG(" Flags: ", static_cast(realm.flags)); LOG_DEBUG(" Population: ", realm.population); - LOG_DEBUG(" Characters: ", (int)realm.characters); - LOG_DEBUG(" Timezone: ", (int)realm.timezone); + LOG_DEBUG(" Characters: ", static_cast(realm.characters)); + LOG_DEBUG(" Timezone: ", static_cast(realm.timezone)); response.realms.push_back(realm); } diff --git a/src/auth/srp.cpp b/src/auth/srp.cpp index 48438ce3..6d741920 100644 --- a/src/auth/srp.cpp +++ b/src/auth/srp.cpp @@ -100,7 +100,7 @@ void SRP::feed(const std::vector& B_bytes, auto hexStr = [](const std::vector& v, size_t maxBytes = 8) -> std::string { std::ostringstream ss; for (size_t i = 0; i < std::min(v.size(), maxBytes); ++i) - ss << std::hex << std::setfill('0') << std::setw(2) << (int)v[i]; + ss << std::hex << std::setfill('0') << std::setw(2) << static_cast(v[i]); if (v.size() > maxBytes) ss << "..."; return ss.str(); }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 9ad75cc6..5f980e5c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -31,6 +31,7 @@ #include "audio/footstep_manager.hpp" #include "audio/activity_sound_manager.hpp" #include "audio/audio_engine.hpp" +#include "addons/addon_manager.hpp" #include #include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" @@ -86,6 +87,17 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) { } // namespace +const char* Application::mapDisplayName(uint32_t mapId) { + // Friendly display names for the loading screen + switch (mapId) { + case 0: return "Eastern Kingdoms"; + case 1: return "Kalimdor"; + case 530: return "Outland"; + case 571: return "Northrend"; + default: return nullptr; + } +} + const char* Application::mapIdToName(uint32_t mapId) { // Fallback when Map.dbc is unavailable. Names must match WDT directory names // (case-insensitive — AssetManager lowercases all paths). @@ -329,6 +341,263 @@ bool Application::initialize() { } } + // Initialize addon system + addonManager_ = std::make_unique(); + if (addonManager_->initialize(gameHandler.get())) { + std::string addonsDir = assetPath + "/interface/AddOns"; + addonManager_->scanAddons(addonsDir); + // Wire Lua errors to UI error display + addonManager_->getLuaEngine()->setLuaErrorCallback([gh = gameHandler.get()](const std::string& err) { + if (gh) gh->addUIError(err); + }); + // Wire chat messages to addon event dispatch + gameHandler->setAddonChatCallback([this](const game::MessageChatData& msg) { + if (!addonManager_ || !addonsLoaded_) return; + // Map ChatType to WoW event name + const char* eventName = nullptr; + switch (msg.type) { + case game::ChatType::SAY: eventName = "CHAT_MSG_SAY"; break; + case game::ChatType::YELL: eventName = "CHAT_MSG_YELL"; break; + case game::ChatType::WHISPER: eventName = "CHAT_MSG_WHISPER"; break; + case game::ChatType::PARTY: eventName = "CHAT_MSG_PARTY"; break; + case game::ChatType::GUILD: eventName = "CHAT_MSG_GUILD"; break; + case game::ChatType::OFFICER: eventName = "CHAT_MSG_OFFICER"; break; + case game::ChatType::RAID: eventName = "CHAT_MSG_RAID"; break; + case game::ChatType::RAID_WARNING: eventName = "CHAT_MSG_RAID_WARNING"; break; + case game::ChatType::BATTLEGROUND: eventName = "CHAT_MSG_BATTLEGROUND"; break; + case game::ChatType::SYSTEM: eventName = "CHAT_MSG_SYSTEM"; break; + case game::ChatType::CHANNEL: eventName = "CHAT_MSG_CHANNEL"; break; + case game::ChatType::EMOTE: + case game::ChatType::TEXT_EMOTE: eventName = "CHAT_MSG_EMOTE"; break; + case game::ChatType::ACHIEVEMENT: eventName = "CHAT_MSG_ACHIEVEMENT"; break; + case game::ChatType::GUILD_ACHIEVEMENT: eventName = "CHAT_MSG_GUILD_ACHIEVEMENT"; break; + case game::ChatType::WHISPER_INFORM: eventName = "CHAT_MSG_WHISPER_INFORM"; break; + case game::ChatType::RAID_LEADER: eventName = "CHAT_MSG_RAID_LEADER"; break; + case game::ChatType::BATTLEGROUND_LEADER: eventName = "CHAT_MSG_BATTLEGROUND_LEADER"; break; + case game::ChatType::MONSTER_SAY: eventName = "CHAT_MSG_MONSTER_SAY"; break; + case game::ChatType::MONSTER_YELL: eventName = "CHAT_MSG_MONSTER_YELL"; break; + case game::ChatType::MONSTER_EMOTE: eventName = "CHAT_MSG_MONSTER_EMOTE"; break; + case game::ChatType::MONSTER_WHISPER: eventName = "CHAT_MSG_MONSTER_WHISPER"; break; + case game::ChatType::RAID_BOSS_EMOTE: eventName = "CHAT_MSG_RAID_BOSS_EMOTE"; break; + case game::ChatType::RAID_BOSS_WHISPER: eventName = "CHAT_MSG_RAID_BOSS_WHISPER"; break; + case game::ChatType::BG_SYSTEM_NEUTRAL: eventName = "CHAT_MSG_BG_SYSTEM_NEUTRAL"; break; + case game::ChatType::BG_SYSTEM_ALLIANCE: eventName = "CHAT_MSG_BG_SYSTEM_ALLIANCE"; break; + case game::ChatType::BG_SYSTEM_HORDE: eventName = "CHAT_MSG_BG_SYSTEM_HORDE"; break; + case game::ChatType::MONSTER_PARTY: eventName = "CHAT_MSG_MONSTER_PARTY"; break; + case game::ChatType::AFK: eventName = "CHAT_MSG_AFK"; break; + case game::ChatType::DND: eventName = "CHAT_MSG_DND"; break; + case game::ChatType::LOOT: eventName = "CHAT_MSG_LOOT"; break; + case game::ChatType::SKILL: eventName = "CHAT_MSG_SKILL"; break; + default: break; + } + if (eventName) { + addonManager_->fireEvent(eventName, {msg.message, msg.senderName}); + } + }); + // Wire generic game events to addon dispatch + gameHandler->setAddonEventCallback([this](const std::string& event, const std::vector& args) { + if (addonManager_ && addonsLoaded_) { + addonManager_->fireEvent(event, args); + } + }); + // Wire spell icon path resolver for Lua API (GetSpellInfo, UnitBuff icon, etc.) + { + auto spellIconPaths = std::make_shared>(); + auto spellIconIds = std::make_shared>(); + auto loaded = std::make_shared(false); + auto* am = assetManager.get(); + gameHandler->setSpellIconPathResolver([spellIconPaths, spellIconIds, loaded, am](uint32_t spellId) -> std::string { + if (!am) return {}; + // Lazy-load SpellIcon.dbc + Spell.dbc icon IDs on first call + if (!*loaded) { + *loaded = true; + auto iconDbc = am->loadDBC("SpellIcon.dbc"); + const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr; + if (iconDbc && iconDbc->isLoaded()) { + for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) { + uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0); + std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1); + if (!path.empty() && id > 0) (*spellIconPaths)[id] = path; + } + } + auto spellDbc = am->loadDBC("Spell.dbc"); + const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; + if (spellDbc && spellDbc->isLoaded()) { + uint32_t fieldCount = spellDbc->getFieldCount(); + uint32_t iconField = 133; // WotLK default + uint32_t idField = 0; + if (spellL) { + uint32_t layoutIcon = (*spellL)["IconID"]; + if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) { + iconField = layoutIcon; + idField = (*spellL)["ID"]; + } + } + for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) { + uint32_t id = spellDbc->getUInt32(i, idField); + uint32_t iconId = spellDbc->getUInt32(i, iconField); + if (id > 0 && iconId > 0) (*spellIconIds)[id] = iconId; + } + } + } + auto iit = spellIconIds->find(spellId); + if (iit == spellIconIds->end()) return {}; + auto pit = spellIconPaths->find(iit->second); + if (pit == spellIconPaths->end()) return {}; + return pit->second; + }); + } + // Wire item icon path resolver: displayInfoId -> "Interface\\Icons\\INV_..." + { + auto iconNames = std::make_shared>(); + auto loaded = std::make_shared(false); + auto* am = assetManager.get(); + gameHandler->setItemIconPathResolver([iconNames, loaded, am](uint32_t displayInfoId) -> std::string { + if (!am || displayInfoId == 0) return {}; + if (!*loaded) { + *loaded = true; + auto dbc = am->loadDBC("ItemDisplayInfo.dbc"); + const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + if (dbc && dbc->isLoaded()) { + uint32_t iconField = dispL ? (*dispL)["InventoryIcon"] : 5; + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + uint32_t id = dbc->getUInt32(i, 0); // field 0 = ID + std::string name = dbc->getString(i, iconField); + if (id > 0 && !name.empty()) (*iconNames)[id] = name; + } + LOG_INFO("Loaded ", iconNames->size(), " item icon names from ItemDisplayInfo.dbc"); + } + } + auto it = iconNames->find(displayInfoId); + if (it == iconNames->end()) return {}; + return "Interface\\Icons\\" + it->second; + }); + } + // Wire spell data resolver: spellId -> {castTimeMs, minRange, maxRange} + { + auto castTimeMap = std::make_shared>(); + auto rangeMap = std::make_shared>>(); + auto spellCastIdx = std::make_shared>(); // spellId→castTimeIdx + auto spellRangeIdx = std::make_shared>(); // spellId→rangeIdx + struct SpellCostEntry { uint32_t manaCost = 0; uint8_t powerType = 0; }; + auto spellCostMap = std::make_shared>(); + auto loaded = std::make_shared(false); + auto* am = assetManager.get(); + gameHandler->setSpellDataResolver([castTimeMap, rangeMap, spellCastIdx, spellRangeIdx, spellCostMap, loaded, am](uint32_t spellId) -> game::GameHandler::SpellDataInfo { + if (!am) return {}; + if (!*loaded) { + *loaded = true; + // Load SpellCastTimes.dbc + auto ctDbc = am->loadDBC("SpellCastTimes.dbc"); + if (ctDbc && ctDbc->isLoaded()) { + for (uint32_t i = 0; i < ctDbc->getRecordCount(); ++i) { + uint32_t id = ctDbc->getUInt32(i, 0); + int32_t base = static_cast(ctDbc->getUInt32(i, 1)); + if (id > 0 && base > 0) (*castTimeMap)[id] = static_cast(base); + } + } + // Load SpellRange.dbc + const auto* srL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellRange") : nullptr; + uint32_t minRField = srL ? (*srL)["MinRange"] : 1; + uint32_t maxRField = srL ? (*srL)["MaxRange"] : 4; + auto rDbc = am->loadDBC("SpellRange.dbc"); + if (rDbc && rDbc->isLoaded()) { + for (uint32_t i = 0; i < rDbc->getRecordCount(); ++i) { + uint32_t id = rDbc->getUInt32(i, 0); + float minR = rDbc->getFloat(i, minRField); + float maxR = rDbc->getFloat(i, maxRField); + if (id > 0) (*rangeMap)[id] = {minR, maxR}; + } + } + // Load Spell.dbc: extract castTimeIndex and rangeIndex per spell + auto sDbc = am->loadDBC("Spell.dbc"); + const auto* spL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; + if (sDbc && sDbc->isLoaded()) { + uint32_t idF = spL ? (*spL)["ID"] : 0; + uint32_t ctF = spL ? (*spL)["CastingTimeIndex"] : 134; // WotLK default + uint32_t rF = spL ? (*spL)["RangeIndex"] : 132; + uint32_t ptF = UINT32_MAX, mcF = UINT32_MAX; + if (spL) { + try { ptF = (*spL)["PowerType"]; } catch (...) {} + try { mcF = (*spL)["ManaCost"]; } catch (...) {} + } + uint32_t fc = sDbc->getFieldCount(); + for (uint32_t i = 0; i < sDbc->getRecordCount(); ++i) { + uint32_t id = sDbc->getUInt32(i, idF); + if (id == 0) continue; + uint32_t ct = sDbc->getUInt32(i, ctF); + uint32_t ri = sDbc->getUInt32(i, rF); + if (ct > 0) (*spellCastIdx)[id] = ct; + if (ri > 0) (*spellRangeIdx)[id] = ri; + // Extract power cost + uint32_t mc = (mcF < fc) ? sDbc->getUInt32(i, mcF) : 0; + uint8_t pt = (ptF < fc) ? static_cast(sDbc->getUInt32(i, ptF)) : 0; + if (mc > 0) (*spellCostMap)[id] = {mc, pt}; + } + } + LOG_INFO("SpellDataResolver: loaded ", spellCastIdx->size(), " cast indices, ", + spellRangeIdx->size(), " range indices"); + } + game::GameHandler::SpellDataInfo info; + auto ciIt = spellCastIdx->find(spellId); + if (ciIt != spellCastIdx->end()) { + auto ctIt = castTimeMap->find(ciIt->second); + if (ctIt != castTimeMap->end()) info.castTimeMs = ctIt->second; + } + auto riIt = spellRangeIdx->find(spellId); + if (riIt != spellRangeIdx->end()) { + auto rIt = rangeMap->find(riIt->second); + if (rIt != rangeMap->end()) { + info.minRange = rIt->second.first; + info.maxRange = rIt->second.second; + } + } + auto mcIt = spellCostMap->find(spellId); + if (mcIt != spellCostMap->end()) { + info.manaCost = mcIt->second.manaCost; + info.powerType = mcIt->second.powerType; + } + return info; + }); + } + // Wire random property/suffix name resolver for item display + { + auto propNames = std::make_shared>(); + auto propLoaded = std::make_shared(false); + auto* amPtr = assetManager.get(); + gameHandler->setRandomPropertyNameResolver([propNames, propLoaded, amPtr](int32_t id) -> std::string { + if (!amPtr || id == 0) return {}; + if (!*propLoaded) { + *propLoaded = true; + // ItemRandomProperties.dbc: ID=0, Name=4 (string) + if (auto dbc = amPtr->loadDBC("ItemRandomProperties.dbc"); dbc && dbc->isLoaded()) { + uint32_t nameField = (dbc->getFieldCount() > 4) ? 4 : 1; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + int32_t rid = static_cast(dbc->getUInt32(r, 0)); + std::string name = dbc->getString(r, nameField); + if (!name.empty() && rid > 0) (*propNames)[rid] = name; + } + } + // ItemRandomSuffix.dbc: ID=0, Name=4 (string) — stored as negative IDs + if (auto dbc = amPtr->loadDBC("ItemRandomSuffix.dbc"); dbc && dbc->isLoaded()) { + uint32_t nameField = (dbc->getFieldCount() > 4) ? 4 : 1; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + int32_t rid = static_cast(dbc->getUInt32(r, 0)); + std::string name = dbc->getString(r, nameField); + if (!name.empty() && rid > 0) (*propNames)[-rid] = name; + } + } + } + auto it = propNames->find(id); + return (it != propNames->end()) ? it->second : std::string{}; + }); + } + LOG_INFO("Addon system initialized, found ", addonManager_->getAddons().size(), " addon(s)"); + } else { + LOG_WARNING("Failed to initialize addon system"); + addonManager_.reset(); + } + } else { LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable"); LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory"); @@ -391,143 +660,195 @@ void Application::run() { } auto lastTime = std::chrono::high_resolution_clock::now(); + std::atomic watchdogRunning{true}; + std::atomic watchdogHeartbeatMs{ + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count() + }; + std::thread watchdogThread([this, &watchdogRunning, &watchdogHeartbeatMs]() { + bool releasedForCurrentStall = false; + while (watchdogRunning.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + const int64_t nowMs = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + const int64_t lastBeatMs = watchdogHeartbeatMs.load(std::memory_order_acquire); + const int64_t stallMs = nowMs - lastBeatMs; - while (running && !window->shouldClose()) { - // Calculate delta time - auto currentTime = std::chrono::high_resolution_clock::now(); - std::chrono::duration deltaTimeDuration = currentTime - lastTime; - float deltaTime = deltaTimeDuration.count(); - lastTime = currentTime; - - // Cap delta time to prevent large jumps - if (deltaTime > 0.1f) { - deltaTime = 0.1f; + // Failsafe: if the main loop stalls while relative mouse mode is active, + // forcibly release grab so the user can move the cursor and close the app. + if (stallMs > 1500) { + if (!releasedForCurrentStall) { + SDL_SetRelativeMouseMode(SDL_FALSE); + SDL_ShowCursor(SDL_ENABLE); + if (window && window->getSDLWindow()) { + SDL_SetWindowGrab(window->getSDLWindow(), SDL_FALSE); + } + LOG_WARNING("Main-loop stall detected (", stallMs, + "ms) — force-released mouse capture failsafe"); + releasedForCurrentStall = true; + } + } else { + releasedForCurrentStall = false; + } } + }); - // Poll events - SDL_Event event; - while (SDL_PollEvent(&event)) { - // Pass event to UI manager first - if (uiManager) { - uiManager->processEvent(event); + try { + while (running && !window->shouldClose()) { + watchdogHeartbeatMs.store( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(), + std::memory_order_release); + + // Calculate delta time + auto currentTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration deltaTimeDuration = currentTime - lastTime; + float deltaTime = deltaTimeDuration.count(); + lastTime = currentTime; + + // Cap delta time to prevent large jumps + if (deltaTime > 0.1f) { + deltaTime = 0.1f; } - // Pass mouse events to camera controller (skip when UI has mouse focus) - if (renderer && renderer->getCameraController() && !ImGui::GetIO().WantCaptureMouse) { - if (event.type == SDL_MOUSEMOTION) { - renderer->getCameraController()->processMouseMotion(event.motion); + // Poll events + SDL_Event event; + while (SDL_PollEvent(&event)) { + // Pass event to UI manager first + if (uiManager) { + uiManager->processEvent(event); } - else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { - renderer->getCameraController()->processMouseButton(event.button); + + // Pass mouse events to camera controller (skip when UI has mouse focus) + if (renderer && renderer->getCameraController() && !ImGui::GetIO().WantCaptureMouse) { + if (event.type == SDL_MOUSEMOTION) { + renderer->getCameraController()->processMouseMotion(event.motion); + } + else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { + renderer->getCameraController()->processMouseButton(event.button); + } + else if (event.type == SDL_MOUSEWHEEL) { + renderer->getCameraController()->processMouseWheel(static_cast(event.wheel.y)); + } } - else if (event.type == SDL_MOUSEWHEEL) { - renderer->getCameraController()->processMouseWheel(static_cast(event.wheel.y)); + + // Handle window events + if (event.type == SDL_QUIT) { + window->setShouldClose(true); + } + else if (event.type == SDL_WINDOWEVENT) { + if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + int newWidth = event.window.data1; + int newHeight = event.window.data2; + window->setSize(newWidth, newHeight); + // Vulkan viewport set in command buffer, not globally + if (renderer && renderer->getCamera()) { + renderer->getCamera()->setAspectRatio(static_cast(newWidth) / newHeight); + } + // Notify addons so UI layouts can adapt to the new size + if (addonManager_) + addonManager_->fireEvent("DISPLAY_SIZE_CHANGED"); + } + } + // Debug controls + else if (event.type == SDL_KEYDOWN) { + // Skip non-function-key input when UI (chat) has keyboard focus + bool uiHasKeyboard = ImGui::GetIO().WantCaptureKeyboard; + auto sc = event.key.keysym.scancode; + bool isFKey = (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12); + if (uiHasKeyboard && !isFKey) { + continue; // Let ImGui handle the keystroke + } + + // F1: Toggle performance HUD + if (event.key.keysym.scancode == SDL_SCANCODE_F1) { + if (renderer && renderer->getPerformanceHUD()) { + renderer->getPerformanceHUD()->toggle(); + bool enabled = renderer->getPerformanceHUD()->isEnabled(); + LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF"); + } + } + // F4: Toggle shadows + else if (event.key.keysym.scancode == SDL_SCANCODE_F4) { + if (renderer) { + bool enabled = !renderer->areShadowsEnabled(); + renderer->setShadowsEnabled(enabled); + LOG_INFO("Shadows: ", enabled ? "ON" : "OFF"); + } + } + // F8: Debug WMO floor at current position + else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) { + if (renderer && renderer->getWMORenderer()) { + glm::vec3 pos = renderer->getCharacterPosition(); + LOG_WARNING("F8: WMO floor debug at render pos (", pos.x, ", ", pos.y, ", ", pos.z, ")"); + renderer->getWMORenderer()->debugDumpGroupsAtPosition(pos.x, pos.y, pos.z); + } + } } } - // Handle window events - if (event.type == SDL_QUIT) { + // Update input + Input::getInstance().update(); + + // Update application state + try { + update(deltaTime); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM during Application::update (state=", static_cast(state), + ", dt=", deltaTime, "): ", e.what()); + throw; + } catch (const std::exception& e) { + LOG_ERROR("Exception during Application::update (state=", static_cast(state), + ", dt=", deltaTime, "): ", e.what()); + throw; + } + // Render + try { + render(); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM during Application::render (state=", static_cast(state), "): ", e.what()); + throw; + } catch (const std::exception& e) { + LOG_ERROR("Exception during Application::render (state=", static_cast(state), "): ", e.what()); + throw; + } + // Swap buffers + try { + window->swapBuffers(); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM during swapBuffers: ", e.what()); + throw; + } catch (const std::exception& e) { + LOG_ERROR("Exception during swapBuffers: ", e.what()); + throw; + } + + // Exit gracefully on GPU device lost (unrecoverable) + if (renderer && renderer->getVkContext() && renderer->getVkContext()->isDeviceLost()) { + LOG_ERROR("GPU device lost — exiting application"); window->setShouldClose(true); } - else if (event.type == SDL_WINDOWEVENT) { - if (event.window.event == SDL_WINDOWEVENT_RESIZED) { - int newWidth = event.window.data1; - int newHeight = event.window.data2; - window->setSize(newWidth, newHeight); - // Vulkan viewport set in command buffer, not globally - if (renderer && renderer->getCamera()) { - renderer->getCamera()->setAspectRatio(static_cast(newWidth) / newHeight); - } - } - } - // Debug controls - else if (event.type == SDL_KEYDOWN) { - // Skip non-function-key input when UI (chat) has keyboard focus - bool uiHasKeyboard = ImGui::GetIO().WantCaptureKeyboard; - auto sc = event.key.keysym.scancode; - bool isFKey = (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12); - if (uiHasKeyboard && !isFKey) { - continue; // Let ImGui handle the keystroke - } - // F1: Toggle performance HUD - if (event.key.keysym.scancode == SDL_SCANCODE_F1) { - if (renderer && renderer->getPerformanceHUD()) { - renderer->getPerformanceHUD()->toggle(); - bool enabled = renderer->getPerformanceHUD()->isEnabled(); - LOG_INFO("Performance HUD: ", enabled ? "ON" : "OFF"); - } - } - // F4: Toggle shadows - else if (event.key.keysym.scancode == SDL_SCANCODE_F4) { - if (renderer) { - bool enabled = !renderer->areShadowsEnabled(); - renderer->setShadowsEnabled(enabled); - LOG_INFO("Shadows: ", enabled ? "ON" : "OFF"); - } - } - // F7: Test level-up effect (ignore key repeat) - else if (event.key.keysym.scancode == SDL_SCANCODE_F7 && event.key.repeat == 0) { - if (renderer) { - renderer->triggerLevelUpEffect(renderer->getCharacterPosition()); - LOG_INFO("Triggered test level-up effect"); - } - if (uiManager) { - uiManager->getGameScreen().triggerDing(99); - } - } - // F8: Debug WMO floor at current position - else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) { - if (renderer && renderer->getWMORenderer()) { - glm::vec3 pos = renderer->getCharacterPosition(); - LOG_WARNING("F8: WMO floor debug at render pos (", pos.x, ", ", pos.y, ", ", pos.z, ")"); - renderer->getWMORenderer()->debugDumpGroupsAtPosition(pos.x, pos.y, pos.z); - } - } + // Soft frame rate cap when vsync is off to prevent 100% CPU usage. + // Target ~240 FPS max (~4.2ms per frame); vsync handles its own pacing. + if (!window->isVsyncEnabled() && deltaTime < 0.004f) { + float sleepMs = (0.004f - deltaTime) * 1000.0f; + if (sleepMs > 0.5f) + std::this_thread::sleep_for(std::chrono::microseconds( + static_cast(sleepMs * 900.0f))); // 90% of target to account for sleep overshoot } } + } catch (...) { + watchdogRunning.store(false, std::memory_order_release); + if (watchdogThread.joinable()) { + watchdogThread.join(); + } + throw; + } - // Update input - Input::getInstance().update(); - - // Update application state - try { - update(deltaTime); - } catch (const std::bad_alloc& e) { - LOG_ERROR("OOM during Application::update (state=", static_cast(state), - ", dt=", deltaTime, "): ", e.what()); - throw; - } catch (const std::exception& e) { - LOG_ERROR("Exception during Application::update (state=", static_cast(state), - ", dt=", deltaTime, "): ", e.what()); - throw; - } - // Render - try { - render(); - } catch (const std::bad_alloc& e) { - LOG_ERROR("OOM during Application::render (state=", static_cast(state), "): ", e.what()); - throw; - } catch (const std::exception& e) { - LOG_ERROR("Exception during Application::render (state=", static_cast(state), "): ", e.what()); - throw; - } - // Swap buffers - try { - window->swapBuffers(); - } catch (const std::bad_alloc& e) { - LOG_ERROR("OOM during swapBuffers: ", e.what()); - throw; - } catch (const std::exception& e) { - LOG_ERROR("Exception during swapBuffers: ", e.what()); - throw; - } - - // Exit gracefully on GPU device lost (unrecoverable) - if (renderer && renderer->getVkContext() && renderer->getVkContext()->isDeviceLost()) { - LOG_ERROR("GPU device lost — exiting application"); - window->setShouldClose(true); - } + watchdogRunning.store(false, std::memory_order_release); + if (watchdogThread.joinable()) { + watchdogThread.join(); } LOG_INFO("Main loop ended"); @@ -536,6 +857,12 @@ void Application::run() { void Application::shutdown() { LOG_WARNING("Shutting down application..."); + // Hide the window immediately so the OS doesn't think the app is frozen + // during the (potentially slow) resource cleanup below. + if (window && window->getSDLWindow()) { + SDL_HideWindow(window->getSDLWindow()); + } + // Stop background world preloader before destroying AssetManager cancelWorldPreload(); @@ -602,8 +929,13 @@ void Application::setState(AppState newState) { } // Ensure no stale in-world player model leaks into the next login attempt. // If we reuse a previously spawned instance without forcing a respawn, appearance (notably hair) can desync. + if (addonManager_ && addonsLoaded_) { + addonManager_->fireEvent("PLAYER_LEAVING_WORLD"); + addonManager_->saveAllSavedVariables(); + } npcsSpawned = false; playerCharacterSpawned = false; + addonsLoaded_ = false; weaponsSheathed_ = false; wasAutoAttacking_ = false; loadedMapId_ = 0xFFFFFFFF; @@ -646,6 +978,11 @@ void Application::setState(AppState newState) { renderer->getCameraController()->applyKnockBack(vcos, vsin, hspeed, vspeed); } }); + gameHandler->setCameraShakeCallback([this](float magnitude, float frequency, float duration) { + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->triggerShake(magnitude, frequency, duration); + } + }); } // Load quest marker models loadQuestMarkerModels(); @@ -779,6 +1116,7 @@ void Application::logoutToLogin() { if (load.future.valid()) load.future.wait(); } asyncCreatureLoads_.clear(); + asyncCreatureDisplayLoads_.clear(); // --- Creature spawn queues --- pendingCreatureSpawns_.clear(); @@ -797,11 +1135,13 @@ void Application::logoutToLogin() { gameObjectInstances_.clear(); pendingGameObjectSpawns_.clear(); pendingTransportMoves_.clear(); + pendingTransportRegistrations_.clear(); pendingTransportDoodadBatches_.clear(); world.reset(); if (renderer) { + renderer->resetCombatVisualState(); // Remove old player model so it doesn't persist into next session if (auto* charRenderer = renderer->getCharacterRenderer()) { charRenderer->removeInstance(1); @@ -901,6 +1241,9 @@ void Application::update(float deltaTime) { gameHandler->update(deltaTime); } }); + if (addonManager_ && addonsLoaded_) { + addonManager_->update(deltaTime); + } // Always unsheath on combat engage. inGameStep = "auto-unsheathe"; updateCheckpoint = "in_game: auto-unsheathe"; @@ -1007,6 +1350,7 @@ void Application::update(float deltaTime) { updateCheckpoint = "in_game: gameobject/transport queues"; runInGameStage("gameobject/transport queues", [&] { processGameObjectSpawnQueue(); + processPendingTransportRegistrations(); processPendingTransportDoodads(); }); inGameStep = "pending mount"; @@ -1069,6 +1413,15 @@ void Application::update(float deltaTime) { gameHandler->isTaxiMountActive() || gameHandler->isTaxiActivationPending()); bool onTransportNow = gameHandler && gameHandler->isOnTransport(); + // Clear stale client-side transport state when the tracked transport no longer exists. + if (onTransportNow && gameHandler->getTransportManager()) { + auto* currentTracked = gameHandler->getTransportManager()->getTransport( + gameHandler->getPlayerTransportGuid()); + if (!currentTracked) { + gameHandler->clearPlayerTransport(); + onTransportNow = false; + } + } // M2 transports (trams) use position-delta approach: player keeps normal // movement and the transport's frame-to-frame delta is applied on top. // Only WMO transports (ships) use full external-driven mode. @@ -1304,23 +1657,29 @@ void Application::update(float deltaTime) { } else { glm::vec3 renderPos = renderer->getCharacterPosition(); - // M2 transport riding: apply transport's frame-to-frame position delta - // so the player moves with the tram while retaining normal movement input. + // M2 transport riding: resolve in canonical space and lock once per frame. + // This avoids visible jitter from mixed render/canonical delta application. if (isM2Transport && gameHandler->getTransportManager()) { auto* tr = gameHandler->getTransportManager()->getTransport( gameHandler->getPlayerTransportGuid()); if (tr) { - static glm::vec3 lastTransportCanonical(0); - static uint64_t lastTransportGuid = 0; - if (lastTransportGuid == gameHandler->getPlayerTransportGuid()) { - glm::vec3 deltaCanonical = tr->position - lastTransportCanonical; - glm::vec3 deltaRender = core::coords::canonicalToRender(deltaCanonical) - - core::coords::canonicalToRender(glm::vec3(0)); - renderPos += deltaRender; - renderer->getCharacterPosition() = renderPos; + // Keep passenger locked to elevator vertical motion while grounded. + // Without this, floor clamping can hold world-Z static unless the + // player is jumping, which makes lifts appear to not move vertically. + glm::vec3 tentativeCanonical = core::coords::renderToCanonical(renderPos); + glm::vec3 localOffset = gameHandler->getPlayerTransportOffset(); + localOffset.x = tentativeCanonical.x - tr->position.x; + localOffset.y = tentativeCanonical.y - tr->position.y; + if (renderer->getCameraController() && + !renderer->getCameraController()->isGrounded()) { + // While airborne (jump/fall), allow local Z offset to change. + localOffset.z = tentativeCanonical.z - tr->position.z; } - lastTransportCanonical = tr->position; - lastTransportGuid = gameHandler->getPlayerTransportGuid(); + gameHandler->setPlayerTransportOffset(localOffset); + + glm::vec3 lockedCanonical = tr->position + localOffset; + renderPos = core::coords::canonicalToRender(lockedCanonical); + renderer->getCharacterPosition() = renderPos; } } @@ -1357,21 +1716,45 @@ void Application::update(float deltaTime) { } // Client-side transport boarding detection (for M2 transports like trams - // where the server doesn't send transport attachment data). - // Use a generous AABB around each transport's current position. + // and lifts where the server doesn't send transport attachment data). + // Thunder Bluff elevators use model origins that can be far from the deck + // the player stands on, so they need wider attachment bounds. if (gameHandler->getTransportManager() && !gameHandler->isOnTransport()) { auto* tm = gameHandler->getTransportManager(); glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos); + constexpr float kM2BoardHorizDistSq = 12.0f * 12.0f; + constexpr float kM2BoardVertDist = 15.0f; + constexpr float kTbLiftBoardHorizDistSq = 22.0f * 22.0f; + constexpr float kTbLiftBoardVertDist = 14.0f; + uint64_t bestGuid = 0; + float bestScore = 1e30f; for (auto& [guid, transport] : tm->getTransports()) { if (!transport.isM2) continue; + const bool isThunderBluffLift = + (transport.entry >= 20649u && transport.entry <= 20657u); + const float maxHorizDistSq = isThunderBluffLift + ? kTbLiftBoardHorizDistSq + : kM2BoardHorizDistSq; + const float maxVertDist = isThunderBluffLift + ? kTbLiftBoardVertDist + : kM2BoardVertDist; glm::vec3 diff = playerCanonical - transport.position; float horizDistSq = diff.x * diff.x + diff.y * diff.y; float vertDist = std::abs(diff.z); - if (horizDistSq < 144.0f && vertDist < 15.0f) { - gameHandler->setPlayerOnTransport(guid, playerCanonical - transport.position); - LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, guid, std::dec); - break; + if (horizDistSq < maxHorizDistSq && vertDist < maxVertDist) { + float score = horizDistSq + vertDist * vertDist; + if (score < bestScore) { + bestScore = score; + bestGuid = guid; + } + } + } + if (bestGuid != 0) { + auto* tr = tm->getTransport(bestGuid); + if (tr) { + gameHandler->setPlayerOnTransport(bestGuid, playerCanonical - tr->position); + LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, bestGuid, std::dec); } } } @@ -1384,7 +1767,19 @@ void Application::update(float deltaTime) { glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos); glm::vec3 diff = playerCanonical - tr->position; float horizDistSq = diff.x * diff.x + diff.y * diff.y; - if (horizDistSq > 225.0f) { + const bool isThunderBluffLift = + (tr->entry >= 20649u && tr->entry <= 20657u); + constexpr float kM2DisembarkHorizDistSq = 15.0f * 15.0f; + constexpr float kTbLiftDisembarkHorizDistSq = 28.0f * 28.0f; + constexpr float kM2DisembarkVertDist = 18.0f; + constexpr float kTbLiftDisembarkVertDist = 16.0f; + const float disembarkHorizDistSq = isThunderBluffLift + ? kTbLiftDisembarkHorizDistSq + : kM2DisembarkHorizDistSq; + const float disembarkVertDist = isThunderBluffLift + ? kTbLiftDisembarkVertDist + : kM2DisembarkVertDist; + if (horizDistSq > disembarkHorizDistSq || std::abs(diff.z) > disembarkVertDist) { gameHandler->clearPlayerTransport(); LOG_DEBUG("M2 transport disembark"); } @@ -1541,7 +1936,11 @@ void Application::update(float deltaTime) { // startMoveTo() in handleMonsterMove, regardless of distance-cull. // This correctly detects movement for distant creatures (> 150u) // where updateMovement() is not called and getX/Y/Z() stays stale. - const bool entityIsMoving = entity->isEntityMoving(); + // Use isActivelyMoving() (not isEntityMoving()) so the + // Run/Walk animation stops when the creature reaches its + // destination, rather than persisting through the dead- + // reckoning overrun window. + const bool entityIsMoving = entity->isActivelyMoving(); const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f); if (deadOrCorpse || largeCorrection) { charRenderer->setInstancePosition(instanceId, renderPos); @@ -1610,6 +2009,110 @@ void Application::update(float deltaTime) { } } + // --- Online player render sync (position, orientation, animation) --- + // Mirrors the creature sync loop above but without collision guard or + // weapon-attach logic. Without this, online players never transition + // back to Stand after movement stops ("run in place" bug). + auto playerSyncStart = std::chrono::steady_clock::now(); + if (renderer && gameHandler && renderer->getCharacterRenderer()) { + auto* charRenderer = renderer->getCharacterRenderer(); + glm::vec3 pPos(0.0f); + bool havePPos = false; + if (auto pe = gameHandler->getEntityManager().getEntity(gameHandler->getPlayerGuid())) { + pPos = glm::vec3(pe->getX(), pe->getY(), pe->getZ()); + havePPos = true; + } + const float pSyncRadiusSq = 320.0f * 320.0f; + + for (const auto& [guid, instanceId] : playerInstances_) { + auto entity = gameHandler->getEntityManager().getEntity(guid); + if (!entity || entity->getType() != game::ObjectType::PLAYER) continue; + + // Distance cull + if (havePPos) { + glm::vec3 latestCanonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); + glm::vec3 d = latestCanonical - pPos; + if (glm::dot(d, d) > pSyncRadiusSq) continue; + } + + // Position sync + glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ()); + glm::vec3 renderPos = core::coords::canonicalToRender(canonical); + + auto posIt = creatureRenderPosCache_.find(guid); + if (posIt == creatureRenderPosCache_.end()) { + charRenderer->setInstancePosition(instanceId, renderPos); + creatureRenderPosCache_[guid] = renderPos; + } else { + const glm::vec3 prevPos = posIt->second; + const glm::vec2 delta2(renderPos.x - prevPos.x, renderPos.y - prevPos.y); + float planarDist = glm::length(delta2); + float dz = std::abs(renderPos.z - prevPos.z); + + auto unitPtr = std::static_pointer_cast(entity); + const bool deadOrCorpse = unitPtr->getHealth() == 0; + const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f); + const bool entityIsMoving = entity->isActivelyMoving(); + const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f); + + if (deadOrCorpse || largeCorrection) { + charRenderer->setInstancePosition(instanceId, renderPos); + } else if (planarDist > 0.03f || dz > 0.08f) { + float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f); + charRenderer->moveInstanceTo(instanceId, renderPos, duration); + } + posIt->second = renderPos; + + // Drive movement animation (same logic as creatures) + const bool isSwimmingNow = creatureSwimmingState_.count(guid) > 0; + const bool isWalkingNow = creatureWalkingState_.count(guid) > 0; + const bool isFlyingNow = creatureFlyingState_.count(guid) > 0; + bool prevMoving = creatureWasMoving_[guid]; + bool prevSwimming = creatureWasSwimming_[guid]; + bool prevFlying = creatureWasFlying_[guid]; + bool prevWalking = creatureWasWalking_[guid]; + const bool stateChanged = (isMovingNow != prevMoving) || + (isSwimmingNow != prevSwimming) || + (isFlyingNow != prevFlying) || + (isWalkingNow != prevWalking && isMovingNow); + if (stateChanged) { + creatureWasMoving_[guid] = isMovingNow; + creatureWasSwimming_[guid] = isSwimmingNow; + creatureWasFlying_[guid] = isFlyingNow; + creatureWasWalking_[guid] = isWalkingNow; + uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; + bool gotState = charRenderer->getAnimationState(instanceId, curAnimId, curT, curDur); + if (!gotState || curAnimId != 1 /*Death*/) { + uint32_t targetAnim; + if (isMovingNow) { + if (isFlyingNow) targetAnim = 159u; // FlyForward + else if (isSwimmingNow) targetAnim = 42u; // Swim + else if (isWalkingNow) targetAnim = 4u; // Walk + else targetAnim = 5u; // Run + } else { + if (isFlyingNow) targetAnim = 158u; // FlyIdle (hover) + else if (isSwimmingNow) targetAnim = 41u; // SwimIdle + else targetAnim = 0u; // Stand + } + charRenderer->playAnimation(instanceId, targetAnim, /*loop=*/true); + } + } + } + + // Orientation sync + float renderYaw = entity->getOrientation() + glm::radians(90.0f); + charRenderer->setInstanceRotation(instanceId, glm::vec3(0.0f, 0.0f, renderYaw)); + } + } + { + float psMs = std::chrono::duration( + std::chrono::steady_clock::now() - playerSyncStart).count(); + if (psMs > 5.0f) { + LOG_WARNING("SLOW update stage 'player render sync': ", psMs, "ms (", + playerInstances_.size(), " players)"); + } + } + // Movement heartbeat is sent from GameHandler::update() to avoid // duplicate packets from multiple update loops. @@ -1628,6 +2131,19 @@ void Application::update(float deltaTime) { break; } + if (pendingWorldEntry_ && !loadingWorld_ && state != AppState::DISCONNECTED) { + auto entry = *pendingWorldEntry_; + pendingWorldEntry_.reset(); + worldEntryMovementGraceTimer_ = 2.0f; + taxiLandingClampTimer_ = 0.0f; + lastTaxiFlight_ = false; + if (renderer && renderer->getCameraController()) { + renderer->getCameraController()->clearMovementInputs(); + renderer->getCameraController()->suppressMovementFor(1.0f); + } + loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z); + } + // Update renderer (camera, etc.) only when in-game updateCheckpoint = "renderer update"; if (renderer && state == AppState::IN_GAME) { @@ -1817,6 +2333,9 @@ void Application::setupUICallbacks() { gameHandler->setWorldEntryCallback([this](uint32_t mapId, float x, float y, float z, bool isInitialEntry) { LOG_INFO("Online world entry: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")" " initial=", isInitialEntry); + if (renderer) { + renderer->resetCombatVisualState(); + } // Reconnect to the same map: terrain stays loaded but all online entities are stale. // Despawn them properly so the server's fresh CREATE_OBJECTs will re-populate the world. @@ -1868,7 +2387,7 @@ void Application::setupUICallbacks() { worldEntryMovementGraceTimer_ = 2.0f; taxiLandingClampTimer_ = 0.0f; lastTaxiFlight_ = false; - renderer->getTerrainManager()->processAllReadyTiles(); + renderer->getTerrainManager()->processReadyTiles(); { auto [tileX, tileY] = core::coords::worldToTile(renderPos.x, renderPos.y); std::vector> nearbyTiles; @@ -1901,10 +2420,12 @@ void Application::setupUICallbacks() { renderer->getCameraController()->clearMovementInputs(); renderer->getCameraController()->suppressMovementFor(0.5f); } - // Flush any tiles that finished background parsing during the cast - // (e.g. Hearthstone pre-loaded them) so they're GPU-uploaded before - // the first frame at the new position. - renderer->getTerrainManager()->processAllReadyTiles(); + // Kick off async upload for any tiles that finished background + // parsing. Use the bounded processReadyTiles() instead of + // processAllReadyTiles() to avoid multi-second main-thread stalls + // when many tiles are ready (the rest will finalize over subsequent + // frames via the normal terrain update loop). + renderer->getTerrainManager()->processReadyTiles(); // Queue all remaining tiles within the load radius (8 tiles = 17x17) // at the new position. precacheTiles skips already-loaded/pending tiles, @@ -1925,24 +2446,19 @@ void Application::setupUICallbacks() { // If a world load is already in progress (re-entrant call from // gameHandler->update() processing SMSG_NEW_WORLD during warmup), - // defer this entry. The current load will pick it up when it finishes. + // defer this entry. The current load will pick it up when it finishes. if (loadingWorld_) { LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)"); pendingWorldEntry_ = {mapId, x, y, z}; return; } - worldEntryMovementGraceTimer_ = 2.0f; - taxiLandingClampTimer_ = 0.0f; - lastTaxiFlight_ = false; - // Stop any movement that was active before the teleport - if (renderer && renderer->getCameraController()) { - renderer->getCameraController()->clearMovementInputs(); - renderer->getCameraController()->suppressMovementFor(1.0f); - } - loadOnlineWorldTerrain(mapId, x, y, z); - // loadedMapId_ is set inside loadOnlineWorldTerrain (including - // any deferred entries it processes), so we must NOT override it here. + // Full world loads are expensive and `loadOnlineWorldTerrain()` itself + // drives `gameHandler->update()` during warmup. Queue the load here so + // it runs after the current packet handler returns instead of recursing + // from `SMSG_LOGIN_VERIFY_WORLD` / `SMSG_NEW_WORLD`. + LOG_WARNING("Queued world entry: map ", mapId, " pos=(", x, ", ", y, ", ", z, ")"); + pendingWorldEntry_ = {mapId, x, y, z}; }); auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional { @@ -2416,10 +2932,12 @@ void Application::setupUICallbacks() { if (name.empty()) continue; std::string path = dir.empty() ? name : dir + "\\" + name; - // Play as 3D sound if source entity position is available + // Play as 3D sound if source entity position is available. + // Entity stores canonical coords; listener uses render coords (camera). auto entity = gameHandler->getEntityManager().getEntity(sourceGuid); if (entity) { - glm::vec3 pos{entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()}; + glm::vec3 canonical{entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()}; + glm::vec3 pos = core::coords::canonicalToRender(canonical); audio::AudioEngine::instance().playSound3D(path, pos); } else { audio::AudioEngine::instance().playSound2D(path); @@ -2545,6 +3063,11 @@ void Application::setupUICallbacks() { } }); + // Open dungeon finder callback — server sends SMSG_OPEN_LFG_DUNGEON_FINDER + gameHandler->setOpenLfgCallback([this]() { + if (uiManager) uiManager->getGameScreen().openDungeonFinder(); + }); + // Creature move callback (online mode) - update creature positions gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) { if (!renderer || !renderer->getCharacterRenderer()) return; @@ -2607,133 +3130,28 @@ void Application::setupUICallbacks() { // Transport spawn callback (online mode) - register transports with TransportManager gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) { - auto* transportManager = gameHandler->getTransportManager(); - if (!transportManager || !renderer) return; + if (!renderer) return; - // Get the WMO instance ID from the GameObject spawn + // Get the GameObject instance now so late queue processing can rely on stable IDs. auto it = gameObjectInstances_.find(guid); if (it == gameObjectInstances_.end()) { LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec); return; } - uint32_t wmoInstanceId = it->second.instanceId; - LOG_WARNING("Registering server transport: GUID=0x", std::hex, guid, std::dec, - " entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId, - " pos=(", x, ", ", y, ", ", z, ")"); - - // TransportAnimation.dbc is indexed by GameObject entry - uint32_t pathId = entry; - const bool preferServerData = gameHandler && gameHandler->hasServerTransportUpdate(guid); - - bool clientAnim = transportManager->isClientSideAnimation(); - LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim, - " guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId, - " preferServer=", preferServerData); - - // Coordinates are already canonical (converted in game_handler.cpp when entity was created) - glm::vec3 canonicalSpawnPos(x, y, z); - - // Check if we have a real path from TransportAnimation.dbc (indexed by entry). - // AzerothCore transport entries are not always 1:1 with DBC path ids. - const bool shipOrZeppelinDisplay = - (displayId == 3015 || displayId == 3031 || displayId == 7546 || - displayId == 7446 || displayId == 1587 || displayId == 2454 || - displayId == 807 || displayId == 808); - bool hasUsablePath = transportManager->hasPathForEntry(entry); - if (shipOrZeppelinDisplay) { - // For true transports, reject tiny XY tracks that effectively look stationary. - hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f); - } - - LOG_WARNING("Transport path check: entry=", entry, " hasUsablePath=", hasUsablePath, - " preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay); - - if (preferServerData) { - // Strict server-authoritative mode: do not infer/remap fallback routes. - if (!hasUsablePath) { - std::vector path = { canonicalSpawnPos }; - transportManager->loadPathFromNodes(pathId, path, false, 0.0f); - LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x", - std::hex, guid, std::dec, " entry=", entry); - } else { - LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", entry); - } - } else if (!hasUsablePath) { - // Remap/infer path by spawn position when entry doesn't map 1:1 to DBC ids. - // For elevators (TB lift platforms), we must allow z-only paths here. - bool allowZOnly = (displayId == 455 || displayId == 462); - uint32_t inferredPath = transportManager->inferDbcPathForSpawn( - canonicalSpawnPos, 1200.0f, allowZOnly); - if (inferredPath != 0) { - pathId = inferredPath; - LOG_WARNING("Using inferred transport path ", pathId, " for entry ", entry); - } else { - uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId); - if (remappedPath != 0) { - pathId = remappedPath; - LOG_WARNING("Using remapped fallback transport path ", pathId, - " for entry ", entry, " displayId=", displayId, - " (usableEntryPath=", transportManager->hasPathForEntry(entry), ")"); - } else { - LOG_WARNING("No TransportAnimation.dbc path for entry ", entry, - " - transport will be stationary"); - - // Fallback: Stationary at spawn point (wait for server to send real position) - std::vector path = { canonicalSpawnPos }; - transportManager->loadPathFromNodes(pathId, path, false, 0.0f); - } - } + auto pendingIt = std::find_if( + pendingTransportRegistrations_.begin(), pendingTransportRegistrations_.end(), + [guid](const PendingTransportRegistration& pending) { return pending.guid == guid; }); + if (pendingIt != pendingTransportRegistrations_.end()) { + pendingIt->entry = entry; + pendingIt->displayId = displayId; + pendingIt->x = x; + pendingIt->y = y; + pendingIt->z = z; + pendingIt->orientation = orientation; } else { - LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", entry); - } - - // Register the transport with spawn position (prevents rendering at origin until server update) - transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry); - - // Mark M2 transports (e.g. Deeprun Tram cars) so TransportManager uses M2Renderer - if (!it->second.isWmo) { - if (auto* tr = transportManager->getTransport(guid)) { - tr->isM2 = true; - } - } - - // Server-authoritative movement - set initial position from spawn data - glm::vec3 canonicalPos(x, y, z); - transportManager->updateServerTransport(guid, canonicalPos, orientation); - - // If a move packet arrived before registration completed, replay latest now. - auto pendingIt = pendingTransportMoves_.find(guid); - if (pendingIt != pendingTransportMoves_.end()) { - const PendingTransportMove pending = pendingIt->second; - transportManager->updateServerTransport(guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation); - LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, guid, std::dec, - " pos=(", pending.x, ", ", pending.y, ", ", pending.z, ") orientation=", pending.orientation); - pendingTransportMoves_.erase(pendingIt); - } - - // For MO_TRANSPORT at (0,0,0): check if GO data is already cached with a taxiPathId - if (glm::length(canonicalSpawnPos) < 1.0f && gameHandler) { - auto goData = gameHandler->getCachedGameObjectInfo(entry); - if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) { - uint32_t taxiPathId = goData->data[0]; - if (transportManager->hasTaxiPath(taxiPathId)) { - transportManager->assignTaxiPathToTransport(entry, taxiPathId); - LOG_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", entry, - " taxiPathId=", taxiPathId); - } - } - } - - if (auto* tr = transportManager->getTransport(guid); tr) { - LOG_WARNING("Transport registered: guid=0x", std::hex, guid, std::dec, - " entry=", entry, " displayId=", displayId, - " pathId=", tr->pathId, - " mode=", (tr->useClientAnimation ? "client" : "server"), - " serverUpdates=", tr->serverUpdateCount); - } else { - LOG_DEBUG("Transport registered: guid=0x", std::hex, guid, std::dec, - " entry=", entry, " displayId=", displayId, " (TransportManager instance missing)"); + pendingTransportRegistrations_.push_back( + PendingTransportRegistration{guid, entry, displayId, x, y, z, orientation}); } }); @@ -2748,6 +3166,15 @@ void Application::setupUICallbacks() { return; } + auto pendingRegIt = std::find_if( + pendingTransportRegistrations_.begin(), pendingTransportRegistrations_.end(), + [guid](const PendingTransportRegistration& pending) { return pending.guid == guid; }); + if (pendingRegIt != pendingTransportRegistrations_.end()) { + pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation}; + LOG_DEBUG("Queued transport move for pending registration GUID=0x", std::hex, guid, std::dec); + return; + } + // Check if transport exists - if not, treat this as a late spawn (reconnection/server restart) if (!transportManager->getTransport(guid)) { LOG_DEBUG("Received position update for unregistered transport 0x", std::hex, guid, std::dec, @@ -2822,6 +3249,12 @@ void Application::setupUICallbacks() { } transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry); + // Keep type in sync with the spawned instance; needed for M2 lift boarding/motion. + if (!it->second.isWmo) { + if (auto* tr = transportManager->getTransport(guid)) { + tr->isM2 = true; + } + } } else { pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation}; LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, @@ -2857,29 +3290,50 @@ void Application::setupUICallbacks() { } }); - // NPC death callback (online mode) - play death animation + // NPC/player death callback (online mode) - play death animation gameHandler->setNpcDeathCallback([this](uint64_t guid) { deadCreatureGuids_.insert(guid); + if (!renderer || !renderer->getCharacterRenderer()) return; + uint32_t instanceId = 0; auto it = creatureInstances_.find(guid); - if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { - renderer->getCharacterRenderer()->playAnimation(it->second, 1, false); // Death + if (it != creatureInstances_.end()) instanceId = it->second; + else { + auto pit = playerInstances_.find(guid); + if (pit != playerInstances_.end()) instanceId = pit->second; + } + if (instanceId != 0) { + renderer->getCharacterRenderer()->playAnimation(instanceId, 1, false); // Death } }); - // NPC respawn callback (online mode) - reset to idle animation + // NPC/player respawn callback (online mode) - reset to idle animation gameHandler->setNpcRespawnCallback([this](uint64_t guid) { deadCreatureGuids_.erase(guid); + if (!renderer || !renderer->getCharacterRenderer()) return; + uint32_t instanceId = 0; auto it = creatureInstances_.find(guid); - if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { - renderer->getCharacterRenderer()->playAnimation(it->second, 0, true); // Idle + if (it != creatureInstances_.end()) instanceId = it->second; + else { + auto pit = playerInstances_.find(guid); + if (pit != playerInstances_.end()) instanceId = pit->second; + } + if (instanceId != 0) { + renderer->getCharacterRenderer()->playAnimation(instanceId, 0, true); // Idle } }); - // NPC swing callback (online mode) - play attack animation + // NPC/player swing callback (online mode) - play attack animation gameHandler->setNpcSwingCallback([this](uint64_t guid) { + if (!renderer || !renderer->getCharacterRenderer()) return; + uint32_t instanceId = 0; auto it = creatureInstances_.find(guid); - if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) { - renderer->getCharacterRenderer()->playAnimation(it->second, 16, false); // Attack + if (it != creatureInstances_.end()) instanceId = it->second; + else { + auto pit = playerInstances_.find(guid); + if (pit != playerInstances_.end()) instanceId = pit->second; + } + if (instanceId != 0) { + renderer->getCharacterRenderer()->playAnimation(instanceId, 16, false); // Attack } }); @@ -3001,9 +3455,11 @@ void Application::setupUICallbacks() { if (charInstId == 0) return; // WoW stand state → M2 animation ID mapping // 0=Stand→0, 1-6=Sit variants→27 (SitGround), 7=Dead→1, 8=Kneel→72 + // Do not force Stand(0) here: locomotion state machine already owns standing/running. + // Forcing Stand on packet timing causes visible run-cycle hitching while steering. uint32_t animId = 0; if (standState == 0) { - animId = 0; // Stand + return; } else if (standState >= 1 && standState <= 6) { animId = 27; // SitGround (covers sit-chair too; correct visual differs by chair height) } else if (standState == 7) { @@ -3212,8 +3668,8 @@ void Application::spawnPlayerCharacter() { charFaceId = (activeChar->appearanceBytes >> 8) & 0xFF; charHairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF; charHairColorId = (activeChar->appearanceBytes >> 24) & 0xFF; - LOG_INFO("Appearance: skin=", (int)charSkinId, " face=", (int)charFaceId, - " hairStyle=", (int)charHairStyleId, " hairColor=", (int)charHairColorId); + LOG_INFO("Appearance: skin=", static_cast(charSkinId), " face=", static_cast(charFaceId), + " hairStyle=", static_cast(charHairStyleId), " hairColor=", static_cast(charHairColorId)); } } @@ -3223,45 +3679,45 @@ void Application::spawnPlayerCharacter() { if (charSectionsDbc) { LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records"); const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL); bool foundSkin = false; bool foundUnderwear = false; bool foundFaceLower = false; bool foundHair = false; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); - uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId); + uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId); + uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex); if (raceId != targetRaceId || sexId != targetSexId) continue; // Section 0 = skin: match by colorIndex = skin byte - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) { - std::string tex1 = charSectionsDbc->getString(r, csTex1); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); if (!tex1.empty()) { bodySkinPath = tex1; foundSkin = true; - LOG_INFO(" DBC body skin: ", bodySkinPath, " (skin=", (int)charSkinId, ")"); + LOG_INFO(" DBC body skin: ", bodySkinPath, " (skin=", static_cast(charSkinId), ")"); } } // Section 3 = hair: match variation=hairStyle, color=hairColor else if (baseSection == 3 && !foundHair && variationIndex == charHairStyleId && colorIndex == charHairColorId) { - hairTexturePath = charSectionsDbc->getString(r, csTex1); + hairTexturePath = charSectionsDbc->getString(r, csF.texture1); if (!hairTexturePath.empty()) { foundHair = true; LOG_INFO(" DBC hair texture: ", hairTexturePath, - " (style=", (int)charHairStyleId, " color=", (int)charHairColorId, ")"); + " (style=", static_cast(charHairStyleId), " color=", static_cast(charHairColorId), ")"); } } // Section 1 = face: match variation=faceId, colorIndex=skinId // Texture1 = face lower, Texture2 = face upper else if (baseSection == 1 && !foundFaceLower && variationIndex == charFaceId && colorIndex == charSkinId) { - std::string tex1 = charSectionsDbc->getString(r, csTex1); - std::string tex2 = charSectionsDbc->getString(r, csTex1 + 1); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); + std::string tex2 = charSectionsDbc->getString(r, csF.texture2); if (!tex1.empty()) { faceLowerTexturePath = tex1; LOG_INFO(" DBC face lower: ", faceLowerTexturePath); @@ -3274,7 +3730,7 @@ void Application::spawnPlayerCharacter() { } // Section 4 = underwear else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) { - for (uint32_t f = csTex1; f <= csTex1 + 2; f++) { + for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) { underwearPaths.push_back(tex); @@ -3288,8 +3744,8 @@ void Application::spawnPlayerCharacter() { } if (!foundHair) { - LOG_WARNING("No DBC hair match for style=", (int)charHairStyleId, - " color=", (int)charHairColorId, + LOG_WARNING("No DBC hair match for style=", static_cast(charHairStyleId), + " color=", static_cast(charHairColorId), " race=", targetRaceId, " sex=", targetSexId); } } else { @@ -3493,7 +3949,7 @@ void Application::spawnPlayerCharacter() { // Facial hair geoset: group 2 = 200 + variation + 1 activeGeosets.insert(static_cast(200 + facialId + 1)); activeGeosets.insert(401); // Bare forearms (no gloves) — group 4 - activeGeosets.insert(502); // Bare shins (no boots) — group 5 + activeGeosets.insert(503); // Bare shins (no boots) — group 5 activeGeosets.insert(702); // Ears: default activeGeosets.insert(801); // Bare wrists (no chest armor sleeves) — group 8 activeGeosets.insert(902); // Kneepads: default — group 9 @@ -3560,6 +4016,21 @@ void Application::spawnPlayerCharacter() { } } +bool Application::loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel) { + auto m2Data = assetManager->readFile(m2Path); + if (m2Data.empty()) return false; + outModel = pipeline::M2Loader::load(m2Data); + // Load skin (WotLK+ M2 format): strip .m2, append 00.skin + std::string skinPath = m2Path; + size_t dotPos = skinPath.rfind('.'); + if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos); + skinPath += "00.skin"; + auto skinData = assetManager->readFile(skinPath); + if (!skinData.empty() && outModel.version >= 264) + pipeline::M2Loader::loadSkin(skinData, outModel); + return outModel.isValid(); +} + void Application::loadEquippedWeapons() { if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return; @@ -3634,39 +4105,15 @@ void Application::loadEquippedWeapons() { // Try Weapon directory first, then Shield std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile; - auto m2Data = assetManager->readFile(m2Path); - if (m2Data.empty()) { + pipeline::M2Model weaponModel; + if (!loadWeaponM2(m2Path, weaponModel)) { m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile; - m2Data = assetManager->readFile(m2Path); - } - if (m2Data.empty()) { - LOG_WARNING("loadEquippedWeapons: failed to read ", modelFile); - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - continue; - } - - auto weaponModel = pipeline::M2Loader::load(m2Data); - - // Load skin file - std::string skinFile = modelFile; - { - size_t dotPos = skinFile.rfind('.'); - if (dotPos != std::string::npos) { - skinFile = skinFile.substr(0, dotPos) + "00.skin"; + if (!loadWeaponM2(m2Path, weaponModel)) { + LOG_WARNING("loadEquippedWeapons: failed to load ", modelFile); + charRenderer->detachWeapon(charInstanceId, ws.attachmentId); + continue; } } - // Try same directory as m2 - std::string skinDir = m2Path.substr(0, m2Path.rfind('\\') + 1); - auto skinData = assetManager->readFile(skinDir + skinFile); - if (!skinData.empty() && weaponModel.version >= 264) { - pipeline::M2Loader::loadSkin(skinData, weaponModel); - } - - if (!weaponModel.isValid()) { - LOG_WARNING("loadEquippedWeapons: invalid weapon model from ", m2Path); - charRenderer->detachWeapon(charInstanceId, ws.attachmentId); - continue; - } // Build texture path std::string texturePath; @@ -3772,22 +4219,9 @@ bool Application::tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instan modelFile += ".m2"; // Main-hand NPC weapon path: only use actual weapon models. - // This avoids shields/placeholder hilts being attached incorrectly. std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile; - auto m2Data = assetManager->readFile(m2Path); - if (m2Data.empty()) return false; - - auto weaponModel = pipeline::M2Loader::load(m2Data); - std::string skinFile = modelFile; - size_t skinDot = skinFile.rfind('.'); - if (skinDot != std::string::npos) skinFile = skinFile.substr(0, skinDot); - skinFile += "00.skin"; - std::string skinDir = m2Path.substr(0, m2Path.rfind('\\') + 1); - auto skinData = assetManager->readFile(skinDir + skinFile); - if (!skinData.empty() && weaponModel.version >= 264) { - pipeline::M2Loader::loadSkin(skinData, weaponModel); - } - if (!weaponModel.isValid()) return false; + pipeline::M2Model weaponModel; + if (!loadWeaponM2(m2Path, weaponModel)) return false; std::string texturePath; if (!textureName.empty()) { @@ -3907,7 +4341,7 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) { } } } - LOG_INFO("Faction.dbc: ", hostileParentFactions.size(), " factions hostile to race ", (int)playerRace); + LOG_INFO("Faction.dbc: ", hostileParentFactions.size(), " factions hostile to race ", static_cast(playerRace)); } // Get player faction template data @@ -3975,7 +4409,7 @@ void Application::buildFactionHostilityMap(uint8_t playerRace) { uint32_t hostileCount = 0; for (const auto& [fid, h] : factionMap) { if (h) hostileCount++; } gameHandler->setFactionHostileMap(std::move(factionMap)); - LOG_INFO("Faction hostility for race ", (int)playerRace, " (FT ", playerFtId, "): ", + LOG_INFO("Faction hostility for race ", static_cast(playerRace), " (FT ", playerFtId, "): ", hostileCount, "/", ftDbc->getRecordCount(), " hostile (friendGroup=0x", std::hex, playerFriendGroup, ", enemyGroup=0x", playerEnemyGroup, std::dec, ")"); } @@ -4023,6 +4457,20 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float window->swapBuffers(); }; + // Set zone name on loading screen — prefer friendly display name, then DBC + { + const char* friendly = mapDisplayName(mapId); + if (friendly) { + loadingScreen.setZoneName(friendly); + } else if (gameHandler) { + std::string dbcName = gameHandler->getMapName(mapId); + if (!dbcName.empty()) + loadingScreen.setZoneName(dbcName); + else + loadingScreen.setZoneName("Loading..."); + } + } + showProgress("Entering world...", 0.0f); // --- Clean up previous map's state on map change --- @@ -4042,6 +4490,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float deferredEquipmentQueue_.clear(); pendingGameObjectSpawns_.clear(); pendingTransportMoves_.clear(); + pendingTransportRegistrations_.clear(); pendingTransportDoodadBatches_.clear(); if (renderer) { @@ -4097,12 +4546,15 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float if (load.future.valid()) load.future.wait(); } asyncCreatureLoads_.clear(); + asyncCreatureDisplayLoads_.clear(); playerInstances_.clear(); onlinePlayerAppearance_.clear(); gameObjectInstances_.clear(); gameObjectDisplayIdModelCache_.clear(); + gameObjectDisplayIdWmoCache_.clear(); + gameObjectDisplayIdFailedCache_.clear(); // Force player character re-spawn on new map playerCharacterSpawned = false; @@ -4453,7 +4905,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float glm::vec3 worldPos = glm::vec3(worldMatrix[3]); uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); - m2Renderer->loadModel(m2Model, doodadModelId); + if (!m2Renderer->loadModel(m2Model, doodadModelId)) continue; uint32_t doodadInstId = m2Renderer->createInstanceWithMatrix(doodadModelId, worldMatrix, worldPos); if (doodadInstId) m2Renderer->setSkipCollision(doodadInstId, true); loadedDoodads++; @@ -4685,24 +5137,42 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float gameHandler->setNpcDeathCallback([cr, app](uint64_t guid) { app->deadCreatureGuids_.insert(guid); + uint32_t instanceId = 0; auto it = app->creatureInstances_.find(guid); - if (it != app->creatureInstances_.end() && cr) { - cr->playAnimation(it->second, 1, false); // animation ID 1 = Death + if (it != app->creatureInstances_.end()) instanceId = it->second; + else { + auto pit = app->playerInstances_.find(guid); + if (pit != app->playerInstances_.end()) instanceId = pit->second; + } + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death } }); gameHandler->setNpcRespawnCallback([cr, app](uint64_t guid) { app->deadCreatureGuids_.erase(guid); + uint32_t instanceId = 0; auto it = app->creatureInstances_.find(guid); - if (it != app->creatureInstances_.end() && cr) { - cr->playAnimation(it->second, 0, true); // animation ID 0 = Idle + if (it != app->creatureInstances_.end()) instanceId = it->second; + else { + auto pit = app->playerInstances_.find(guid); + if (pit != app->playerInstances_.end()) instanceId = pit->second; + } + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 0, true); // animation ID 0 = Idle } }); gameHandler->setNpcSwingCallback([cr, app](uint64_t guid) { + uint32_t instanceId = 0; auto it = app->creatureInstances_.find(guid); - if (it != app->creatureInstances_.end() && cr) { - cr->playAnimation(it->second, 16, false); // animation ID 16 = Attack1 + if (it != app->creatureInstances_.end()) instanceId = it->second; + else { + auto pit = app->playerInstances_.find(guid); + if (pit != app->playerInstances_.end()) instanceId = pit->second; + } + if (instanceId != 0 && cr) { + cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1 } }); } @@ -4751,25 +5221,23 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float if (world) world->update(1.0f / 60.0f); processPlayerSpawnQueue(); - // During load screen warmup: lift per-frame budgets so GPU uploads - // and spawns happen in bulk while the loading screen is still visible. - processCreatureSpawnQueue(true); - processAsyncNpcCompositeResults(true); - // Process equipment queue more aggressively during warmup (multiple per iteration) - for (int i = 0; i < 8 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) { + // Keep warmup bounded: unbounded queue draining can stall the main thread + // long enough to trigger socket timeouts. + processCreatureSpawnQueue(false); + processAsyncNpcCompositeResults(false); + // Process equipment queue with a small bounded burst during warmup. + for (int i = 0; i < 2 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) { processDeferredEquipmentQueue(); } if (auto* cr = renderer ? renderer->getCharacterRenderer() : nullptr) { - cr->processPendingNormalMaps(INT_MAX); + cr->processPendingNormalMaps(4); } - // Process ALL pending game object spawns. - while (!pendingGameObjectSpawns_.empty()) { - auto& s = pendingGameObjectSpawns_.front(); - spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale); - pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin()); - } + // Keep warmup responsive: process gameobject queue with the same bounded + // budget logic used in-world instead of draining everything in one tick. + processGameObjectSpawnQueue(); + processPendingTransportRegistrations(); processPendingTransportDoodads(); processPendingMount(); updateQuestMarkers(); @@ -4821,6 +5289,14 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float showProgress("Entering world...", 1.0f); + // Ensure all GPU resources (textures, buffers, pipelines) created during + // world load are fully flushed before the first render frame. Without this, + // vkCmdBeginRenderPass can crash on NVIDIA 590.x when resources from async + // uploads haven't completed their queue operations. + if (renderer && renderer->getVkContext()) { + vkDeviceWaitIdle(renderer->getVkContext()->getDevice()); + } + if (loadingScreenOk) { loadingScreen.shutdown(); } @@ -4846,6 +5322,33 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Only enter IN_GAME when this is the final map (no deferred entry pending). setState(AppState::IN_GAME); + + // Load addons once per session on first world entry + if (addonManager_ && !addonsLoaded_) { + // Set character name for per-character SavedVariables + if (gameHandler) { + const std::string& charName = gameHandler->lookupName(gameHandler->getPlayerGuid()); + if (!charName.empty()) { + addonManager_->setCharacterName(charName); + } else { + // Fallback: find name from character list + for (const auto& c : gameHandler->getCharacters()) { + if (c.guid == gameHandler->getPlayerGuid()) { + addonManager_->setCharacterName(c.name); + break; + } + } + } + } + addonManager_->loadAllAddons(); + addonsLoaded_ = true; + addonManager_->fireEvent("VARIABLES_LOADED"); + addonManager_->fireEvent("PLAYER_LOGIN"); + addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); + } else if (addonManager_ && addonsLoaded_) { + // Subsequent world entries (e.g. teleport, instance entry) + addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); + } } void Application::buildCharSectionsCache() { @@ -4854,22 +5357,17 @@ void Application::buildCharSectionsCache() { if (!dbc) return; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; - 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; + auto csF = pipeline::detectCharSectionsFields(dbc.get(), csL); for (uint32_t r = 0; r < dbc->getRecordCount(); r++) { - uint32_t race = dbc->getUInt32(r, raceF); - uint32_t sex = dbc->getUInt32(r, sexF); - uint32_t section = dbc->getUInt32(r, secF); - uint32_t variation = dbc->getUInt32(r, varF); - uint32_t color = dbc->getUInt32(r, colF); + uint32_t race = dbc->getUInt32(r, csF.raceId); + uint32_t sex = dbc->getUInt32(r, csF.sexId); + uint32_t section = dbc->getUInt32(r, csF.baseSection); + uint32_t variation = dbc->getUInt32(r, csF.variationIndex); + uint32_t color = dbc->getUInt32(r, csF.colorIndex); // We only cache sections 0 (skin), 1 (face), 3 (hair), 4 (underwear) if (section != 0 && section != 1 && section != 3 && section != 4) continue; for (int ti = 0; ti < 3; ti++) { - std::string tex = dbc->getString(r, tex1F + ti); + std::string tex = dbc->getString(r, csF.texture1 + ti); if (tex.empty()) continue; // Key: race(8)|sex(4)|section(4)|variation(8)|color(8)|texIndex(2) packed into 64 bits uint64_t key = (static_cast(race) << 26) | @@ -5154,10 +5652,12 @@ audio::VoiceType Application::detectVoiceTypeFromDisplayId(uint32_t displayId) c case 6: raceName = "Tauren"; result = (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE; break; case 7: raceName = "Gnome"; result = (sexId == 0) ? audio::VoiceType::GNOME_MALE : audio::VoiceType::GNOME_FEMALE; break; case 8: raceName = "Troll"; result = (sexId == 0) ? audio::VoiceType::TROLL_MALE : audio::VoiceType::TROLL_FEMALE; break; + case 10: raceName = "BloodElf"; result = (sexId == 0) ? audio::VoiceType::BLOODELF_MALE : audio::VoiceType::BLOODELF_FEMALE; break; + case 11: raceName = "Draenei"; result = (sexId == 0) ? audio::VoiceType::DRAENEI_MALE : audio::VoiceType::DRAENEI_FEMALE; break; default: result = audio::VoiceType::GENERIC; break; } - LOG_INFO("Voice detection: displayId ", displayId, " -> ", raceName, " ", sexName, " (race=", (int)raceId, ", sex=", (int)sexId, ")"); + LOG_INFO("Voice detection: displayId ", displayId, " -> ", raceName, " ", sexName, " (race=", static_cast(raceId), ", sex=", static_cast(sexId), ")"); return result; } @@ -5404,8 +5904,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x auto itExtra = humanoidExtraMap_.find(dispData.extraDisplayId); if (itExtra != humanoidExtraMap_.end()) { const auto& extra = itExtra->second; - LOG_DEBUG(" Found humanoid extra: raceId=", (int)extra.raceId, " sexId=", (int)extra.sexId, - " hairStyle=", (int)extra.hairStyleId, " hairColor=", (int)extra.hairColorId, + LOG_DEBUG(" Found humanoid extra: raceId=", static_cast(extra.raceId), " sexId=", static_cast(extra.sexId), + " hairStyle=", static_cast(extra.hairStyleId), " hairColor=", static_cast(extra.hairColorId), " bakeName='", extra.bakeName, "'"); // Collect model texture slot info (type 1 = skin, type 6 = hair) @@ -5453,6 +5953,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (csDbc) { const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(csDbc.get(), csL); uint32_t npcRace = static_cast(extraCopy.raceId); uint32_t npcSex = static_cast(extraCopy.sexId); uint32_t npcSkin = static_cast(extraCopy.skinId); @@ -5461,23 +5962,22 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x std::vector npcUnderwear; for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) { - uint32_t rId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + uint32_t rId = csDbc->getUInt32(r, csF.raceId); + uint32_t sId = csDbc->getUInt32(r, csF.sexId); 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 section = csDbc->getUInt32(r, csF.baseSection); + uint32_t variation = csDbc->getUInt32(r, csF.variationIndex); + uint32_t color = csDbc->getUInt32(r, csF.colorIndex); if (section == 0 && def.basePath.empty() && color == npcSkin) { - def.basePath = csDbc->getString(r, tex1F); + def.basePath = csDbc->getString(r, csF.texture1); } else if (section == 1 && npcFaceLower.empty() && variation == npcFace && color == npcSkin) { - npcFaceLower = csDbc->getString(r, tex1F); - npcFaceUpper = csDbc->getString(r, tex1F + 1); + npcFaceLower = csDbc->getString(r, csF.texture1); + npcFaceUpper = csDbc->getString(r, csF.texture2); } else if (section == 4 && npcUnderwear.empty() && color == npcSkin) { - for (uint32_t f = tex1F; f <= tex1F + 2; f++) { + for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = csDbc->getString(r, f); if (!tex.empty()) npcUnderwear.push_back(tex); } @@ -5575,20 +6075,21 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (csDbc) { const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(csDbc.get(), csL); uint32_t targetRace = static_cast(extraCopy.raceId); uint32_t targetSex = static_cast(extraCopy.sexId); for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) { - uint32_t raceId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sexId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + uint32_t raceId = csDbc->getUInt32(r, csF.raceId); + uint32_t sexId = csDbc->getUInt32(r, csF.sexId); if (raceId != targetRace || sexId != targetSex) continue; - uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + uint32_t section = csDbc->getUInt32(r, csF.baseSection); 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, csF.variationIndex); + uint32_t colorIdx = csDbc->getUInt32(r, csF.colorIndex); 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, csF.texture1); break; } @@ -5936,9 +6437,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (itFacial != facialHairGeosetMap_.end()) { const auto& fhg = itFacial->second; // DBC values are variation indices within each group; add group base - activeGeosets.insert(static_cast(100 + std::max(fhg.geoset100, (uint16_t)1))); - activeGeosets.insert(static_cast(300 + std::max(fhg.geoset300, (uint16_t)1))); - activeGeosets.insert(static_cast(200 + std::max(fhg.geoset200, (uint16_t)1))); + activeGeosets.insert(static_cast(100 + std::max(fhg.geoset100, static_cast(1)))); + activeGeosets.insert(static_cast(300 + std::max(fhg.geoset300, static_cast(1)))); + activeGeosets.insert(static_cast(200 + std::max(fhg.geoset200, static_cast(1)))); } else { activeGeosets.insert(101); // Default group 1: no extra activeGeosets.insert(201); // Default group 2: no facial hair @@ -5946,7 +6447,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } // Default equipment geosets (bare/no armor) - // CharGeosets: group 4=gloves(forearm), 5=boots(shin), 8=sleeves, 9=kneepads, 13=pants + // CharGeosets: group 4=gloves(forearm), 5=boots(shin), 8=sleeves, 12=tabard, 13=pants std::unordered_set modelGeosets; std::unordered_map firstByGroup; if (const auto* md = charRenderer->getModelData(modelId)) { @@ -5967,9 +6468,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x return preferred; }; - uint16_t geosetGloves = pickGeoset(301, 3); // Bare gloves/forearms (group 3) - uint16_t geosetBoots = pickGeoset(401, 4); // Bare boots/shins (group 4) - uint16_t geosetTorso = pickGeoset(501, 5); // Base torso/waist (group 5) + uint16_t geosetGloves = pickGeoset(401, 4); // Bare gloves/forearms (group 4) + uint16_t geosetBoots = pickGeoset(503, 5); // Bare boots/shins (group 5) uint16_t geosetSleeves = pickGeoset(801, 8); // Bare wrists (group 8, controlled by chest) uint16_t geosetPants = pickGeoset(1301, 13); // Bare legs (group 13) uint16_t geosetCape = 0; // Group 15 disabled unless cape is equipped @@ -5997,10 +6497,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x return gg; }; - // Chest (slot 3) → group 5 (torso) + group 8 (sleeves/wristbands) + // Chest (slot 3) → group 8 (sleeves/wristbands) { uint32_t gg = readGeosetGroup(3, "chest"); - if (gg > 0) geosetTorso = pickGeoset(static_cast(501 + gg), 5); if (gg > 0) geosetSleeves = pickGeoset(static_cast(801 + gg), 8); } @@ -6010,16 +6509,16 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (gg > 0) geosetPants = pickGeoset(static_cast(1301 + gg), 13); } - // Feet (slot 6) → group 4 (boots/shins) + // Feet (slot 6) → group 5 (boots/shins) { uint32_t gg = readGeosetGroup(6, "feet"); - if (gg > 0) geosetBoots = pickGeoset(static_cast(401 + gg), 4); + if (gg > 0) geosetBoots = pickGeoset(static_cast(501 + gg), 5); } - // Hands (slot 8) → group 3 (gloves/forearms) + // Hands (slot 8) → group 4 (gloves/forearms) { uint32_t gg = readGeosetGroup(8, "hands"); - if (gg > 0) geosetGloves = pickGeoset(static_cast(301 + gg), 3); + if (gg > 0) geosetGloves = pickGeoset(static_cast(401 + gg), 4); } // Tabard (slot 9) → group 12 (tabard/robe mesh) @@ -6096,7 +6595,6 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Apply equipment geosets activeGeosets.insert(geosetGloves); activeGeosets.insert(geosetBoots); - activeGeosets.insert(geosetTorso); activeGeosets.insert(geosetSleeves); activeGeosets.insert(geosetPants); if (geosetCape != 0) { @@ -6138,7 +6636,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } } - LOG_DEBUG("Set humanoid geosets: hair=", (int)hairGeoset, + LOG_DEBUG("Set humanoid geosets: hair=", static_cast(hairGeoset), " sleeves=", geosetSleeves, " pants=", geosetPants, " boots=", geosetBoots, " gloves=", geosetGloves); @@ -6575,7 +7073,7 @@ void Application::spawnOnlinePlayer(uint64_t guid, std::string m2Path = game::getPlayerModelPath(race, gender); if (m2Path.empty()) { LOG_WARNING("spawnOnlinePlayer: unknown race/gender for guid 0x", std::hex, guid, std::dec, - " race=", (int)raceId, " gender=", (int)genderId); + " race=", static_cast(raceId), " gender=", static_cast(genderId)); return; } @@ -6601,7 +7099,7 @@ void Application::spawnOnlinePlayer(uint64_t guid, } pipeline::M2Model model = pipeline::M2Loader::load(m2Data); - if (!model.isValid() || model.vertices.empty()) { + if (model.vertices.empty()) { LOG_WARNING("spawnOnlinePlayer: failed to parse M2: ", m2Path); return; } @@ -6613,6 +7111,12 @@ void Application::spawnOnlinePlayer(uint64_t guid, pipeline::M2Loader::loadSkin(skinData, model); } + // After skin loading, full model must be valid (vertices + indices) + if (!model.isValid()) { + LOG_WARNING("spawnOnlinePlayer: failed to load skin for M2: ", m2Path); + return; + } + // Load only core external animations (stand/walk/run) to avoid stalls for (uint32_t si = 0; si < model.sequences.size(); si++) { if (!(model.sequences[si].flags & 0x20)) { @@ -6647,9 +7151,9 @@ void Application::spawnOnlinePlayer(uint64_t guid, if (const auto* md = charRenderer->getModelData(modelId)) { for (size_t ti = 0; ti < md->textures.size(); ti++) { uint32_t t = md->textures[ti].type; - if (t == 1 && slots.skin < 0) slots.skin = (int)ti; - else if (t == 6 && slots.hair < 0) slots.hair = (int)ti; - else if (t == 8 && slots.underwear < 0) slots.underwear = (int)ti; + if (t == 1 && slots.skin < 0) slots.skin = static_cast(ti); + else if (t == 6 && slots.hair < 0) slots.hair = static_cast(ti); + else if (t == 8 && slots.underwear < 0) slots.underwear = static_cast(ti); } } playerTextureSlotsByModelId_[modelId] = slots; @@ -6692,9 +7196,9 @@ void Application::spawnOnlinePlayer(uint64_t guid, if (auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); charSectionsDbc && charSectionsDbc->isLoaded()) { const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL); uint32_t targetRaceId = raceId; uint32_t targetSexId = genderId; - const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6; bool foundSkin = false; bool foundUnderwear = false; @@ -6702,31 +7206,31 @@ void Application::spawnOnlinePlayer(uint64_t guid, bool foundFaceLower = false; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - 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 rRace = charSectionsDbc->getUInt32(r, csF.raceId); + uint32_t rSex = charSectionsDbc->getUInt32(r, csF.sexId); + uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex); if (rRace != targetRaceId || rSex != targetSexId) continue; if (baseSection == 0 && !foundSkin && colorIndex == skinId) { - std::string tex1 = charSectionsDbc->getString(r, csTex1); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); if (!tex1.empty()) { bodySkinPath = tex1; foundSkin = true; } } else if (baseSection == 3 && !foundHair && variationIndex == hairStyleId && colorIndex == hairColorId) { - hairTexturePath = charSectionsDbc->getString(r, csTex1); + hairTexturePath = charSectionsDbc->getString(r, csF.texture1); if (!hairTexturePath.empty()) foundHair = true; } else if (baseSection == 4 && !foundUnderwear && colorIndex == skinId) { - for (uint32_t f = csTex1; f <= csTex1 + 2; f++) { + for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) underwearPaths.push_back(tex); } foundUnderwear = true; } else if (baseSection == 1 && !foundFaceLower && variationIndex == faceId && colorIndex == skinId) { - std::string tex1 = charSectionsDbc->getString(r, csTex1); - std::string tex2 = charSectionsDbc->getString(r, csTex1 + 1); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); + std::string tex2 = charSectionsDbc->getString(r, csF.texture2); if (!tex1.empty()) faceLowerPath = tex1; if (!tex2.empty()) faceUpperPath = tex2; foundFaceLower = true; @@ -6777,7 +7281,7 @@ void Application::spawnOnlinePlayer(uint64_t guid, activeGeosets.insert(static_cast(100 + hairStyleId + 1)); activeGeosets.insert(static_cast(200 + facialFeatures + 1)); activeGeosets.insert(401); // Bare forearms (no gloves) — group 4 - activeGeosets.insert(502); // Bare shins (no boots) — group 5 + activeGeosets.insert(503); // Bare shins (no boots) — group 5 activeGeosets.insert(702); // Ears activeGeosets.insert(801); // Bare wrists (no sleeves) — group 8 activeGeosets.insert(902); // Kneepads — group 9 @@ -6866,6 +7370,10 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, }; // --- Geosets --- + // Mirror the same group-range logic as CharacterPreview::applyEquipment to + // keep other-player rendering consistent with the local character preview. + // Group 4 (4xx) = forearms/gloves, 5 (5xx) = shins/boots, 8 (8xx) = wrists/sleeves, + // 13 (13xx) = legs/trousers. Missing defaults caused the shin-mesh gap (status.md). std::unordered_set geosets; // Body parts (group 0: IDs 0-99, some models use up to 27) for (uint16_t i = 0; i <= 99; i++) geosets.insert(i); @@ -6873,8 +7381,6 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, uint8_t hairStyleId = static_cast((st.appearanceBytes >> 16) & 0xFF); geosets.insert(static_cast(100 + hairStyleId + 1)); geosets.insert(static_cast(200 + st.facialFeatures + 1)); - geosets.insert(401); // Body joint patches (knees) - geosets.insert(402); // Body joint patches (elbows) geosets.insert(701); // Ears geosets.insert(902); // Kneepads geosets.insert(2002); // Bare feet mesh @@ -6882,39 +7388,47 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, const uint32_t geosetGroup1Field = idiL ? (*idiL)["GeosetGroup1"] : 7; const uint32_t geosetGroup3Field = idiL ? (*idiL)["GeosetGroup3"] : 9; - // Chest/Shirt/Robe (invType 4,5,20) + // Per-group defaults — overridden below when equipment provides a geoset value. + uint16_t geosetGloves = 401; // Bare forearms (group 4, no gloves) + uint16_t geosetBoots = 503; // Bare shins (group 5, no boots) + uint16_t geosetSleeves = 801; // Bare wrists (group 8, no chest/sleeves) + uint16_t geosetPants = 1301; // Bare legs (group 13, no leggings) + + // Chest/Shirt/Robe (invType 4,5,20) → wrist/sleeve group 8 { uint32_t did = findDisplayIdByInvType({4, 5, 20}); uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field); - geosets.insert(static_cast(gg1 > 0 ? 501 + gg1 : 501)); - + if (gg1 > 0) geosetSleeves = static_cast(801 + gg1); + // Robe kilt → leg group 13 uint32_t gg3 = getGeosetGroup(did, geosetGroup3Field); - if (gg3 > 0) geosets.insert(static_cast(1301 + gg3)); + if (gg3 > 0) geosetPants = static_cast(1301 + gg3); } - // Legs (invType 7) + // Legs (invType 7) → leg group 13 { uint32_t did = findDisplayIdByInvType({7}); uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field); - if (geosets.count(1302) == 0 && geosets.count(1303) == 0) { - geosets.insert(static_cast(gg1 > 0 ? 1301 + gg1 : 1301)); - } + if (gg1 > 0) geosetPants = static_cast(1301 + gg1); } - // Feet (invType 8): 401/402 are body patches (always on), 403+ are boot meshes + // Feet/Boots (invType 8) → shin group 5 { uint32_t did = findDisplayIdByInvType({8}); uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field); - if (gg1 > 0) geosets.insert(static_cast(402 + gg1)); + if (gg1 > 0) geosetBoots = static_cast(501 + gg1); } - // Hands (invType 10) + // Hands/Gloves (invType 10) → forearm group 4 { uint32_t did = findDisplayIdByInvType({10}); uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field); - geosets.insert(static_cast(gg1 > 0 ? 301 + gg1 : 301)); + if (gg1 > 0) geosetGloves = static_cast(401 + gg1); } + geosets.insert(geosetGloves); + geosets.insert(geosetBoots); + geosets.insert(geosetSleeves); + geosets.insert(geosetPants); // Back/Cloak (invType 16) geosets.insert(hasInvType({16}) ? 1502 : 1501); // Tabard (invType 19) @@ -6992,6 +7506,7 @@ void Application::despawnOnlinePlayer(uint64_t guid) { playerInstances_.erase(it); onlinePlayerAppearance_.erase(guid); pendingOnlinePlayerEquipment_.erase(guid); + creatureRenderPosCache_.erase(guid); creatureSwimmingState_.erase(guid); creatureWalkingState_.erase(guid); creatureFlyingState_.erase(guid); @@ -7098,8 +7613,15 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t auto itCache = gameObjectDisplayIdWmoCache_.find(displayId); if (itCache != gameObjectDisplayIdWmoCache_.end()) { modelId = itCache->second; - loadedAsWmo = true; - } else { + // Only use cached entry if the model is still resident in the renderer + if (wmoRenderer->isModelLoaded(modelId)) { + loadedAsWmo = true; + } else { + gameObjectDisplayIdWmoCache_.erase(itCache); + modelId = 0; + } + } + if (!loadedAsWmo && modelId == 0) { auto wmoData = assetManager->readFile(modelPath); if (!wmoData.empty()) { pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); @@ -7224,6 +7746,11 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t auto* m2Renderer = renderer->getM2Renderer(); if (!m2Renderer) return; + // Skip displayIds that permanently failed to load (e.g. empty/unsupported M2s). + // Without this guard the same empty model is re-parsed every frame, causing + // sustained log spam and wasted CPU. + if (gameObjectDisplayIdFailedCache_.count(displayId)) return; + uint32_t modelId = 0; auto itCache = gameObjectDisplayIdModelCache_.find(displayId); if (itCache != gameObjectDisplayIdModelCache_.end()) { @@ -7242,12 +7769,14 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t auto m2Data = assetManager->readFile(modelPath); if (m2Data.empty()) { LOG_WARNING("Failed to read gameobject M2: ", modelPath); + gameObjectDisplayIdFailedCache_.insert(displayId); return; } pipeline::M2Model model = pipeline::M2Loader::load(m2Data); if (model.vertices.empty()) { LOG_WARNING("Failed to parse gameobject M2: ", modelPath); + gameObjectDisplayIdFailedCache_.insert(displayId); return; } @@ -7259,6 +7788,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t if (!m2Renderer->loadModel(model, modelId)) { LOG_WARNING("Failed to load gameobject model: ", modelPath); + gameObjectDisplayIdFailedCache_.insert(displayId); return; } @@ -7301,12 +7831,23 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t void Application::processAsyncCreatureResults(bool unlimited) { // Check completed async model loads and finalize on main thread (GPU upload + instance creation). - // Limit GPU model uploads per frame to avoid spikes, but always drain cheap bookkeeping. - // In unlimited mode (load screen), process all pending uploads without cap. - static constexpr int kMaxModelUploadsPerFrame = 1; + // Limit GPU model uploads per tick to avoid long main-thread stalls that can starve socket updates. + // Even in unlimited mode (load screen), keep a small cap and budget to prevent multi-second stalls. + static constexpr int kMaxModelUploadsPerTick = 1; + static constexpr int kMaxModelUploadsPerTickWarmup = 1; + static constexpr float kFinalizeBudgetMs = 2.0f; + static constexpr float kFinalizeBudgetWarmupMs = 2.0f; + const int maxUploadsThisTick = unlimited ? kMaxModelUploadsPerTickWarmup : kMaxModelUploadsPerTick; + const float budgetMs = unlimited ? kFinalizeBudgetWarmupMs : kFinalizeBudgetMs; + const auto tickStart = std::chrono::steady_clock::now(); int modelUploads = 0; for (auto it = asyncCreatureLoads_.begin(); it != asyncCreatureLoads_.end(); ) { + if (std::chrono::duration( + std::chrono::steady_clock::now() - tickStart).count() >= budgetMs) { + break; + } + if (!it->future.valid() || it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) { ++it; @@ -7315,12 +7856,13 @@ void Application::processAsyncCreatureResults(bool unlimited) { // Peek: if this result needs a NEW model upload (not cached) and we've hit // the upload budget, defer to next frame without consuming the future. - if (!unlimited && modelUploads >= kMaxModelUploadsPerFrame) { + if (modelUploads >= maxUploadsThisTick) { break; } auto result = it->future.get(); it = asyncCreatureLoads_.erase(it); + asyncCreatureDisplayLoads_.erase(result.displayId); if (result.permanent_failure) { nonRenderableCreatureDisplayIds_.insert(result.displayId); @@ -7335,6 +7877,27 @@ void Application::processAsyncCreatureResults(bool unlimited) { continue; } + // Another async result may have already uploaded this displayId while this + // task was still running; in that case, skip duplicate GPU upload. + if (displayIdModelCache_.find(result.displayId) != displayIdModelCache_.end()) { + pendingCreatureSpawnGuids_.erase(result.guid); + creatureSpawnRetryCounts_.erase(result.guid); + if (!creatureInstances_.count(result.guid) && + !creaturePermanentFailureGuids_.count(result.guid)) { + PendingCreatureSpawn s{}; + s.guid = result.guid; + s.displayId = result.displayId; + s.x = result.x; + s.y = result.y; + s.z = result.z; + s.orientation = result.orientation; + s.scale = result.scale; + pendingCreatureSpawns_.push_back(s); + pendingCreatureSpawnGuids_.insert(result.guid); + } + continue; + } + // Model parsed on background thread — upload to GPU on main thread. auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr; if (!charRenderer) { @@ -7342,6 +7905,10 @@ void Application::processAsyncCreatureResults(bool unlimited) { continue; } + // Count upload attempts toward the frame budget even if upload fails. + // Otherwise repeated failures can consume an unbounded amount of frame time. + modelUploads++; + // Upload model to GPU (must happen on main thread) // Use pre-decoded BLP cache to skip main-thread texture decode auto uploadStart = std::chrono::steady_clock::now(); @@ -7368,8 +7935,6 @@ void Application::processAsyncCreatureResults(bool unlimited) { displayIdPredecodedTextures_[result.displayId] = std::move(result.predecodedTextures); } displayIdModelCache_[result.displayId] = result.modelId; - modelUploads++; - pendingCreatureSpawnGuids_.erase(result.guid); creatureSpawnRetryCounts_.erase(result.guid); @@ -7523,6 +8088,14 @@ void Application::processCreatureSpawnQueue(bool unlimited) { // For new models: launch async load on background thread instead of blocking. if (needsNewModel) { + // Keep exactly one background load per displayId. Additional spawns for + // the same displayId stay queued and will spawn once cache is populated. + if (asyncCreatureDisplayLoads_.count(s.displayId)) { + pendingCreatureSpawns_.push_back(s); + rotationsLeft--; + continue; + } + const int maxAsync = unlimited ? (MAX_ASYNC_CREATURE_LOADS * 4) : MAX_ASYNC_CREATURE_LOADS; if (static_cast(asyncCreatureLoads_.size()) + asyncLaunched >= maxAsync) { // Too many in-flight — defer to next frame @@ -7612,32 +8185,32 @@ void Application::processCreatureSpawnQueue(bool unlimited) { if (csDbc) { const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(csDbc.get(), csL); uint32_t nRace = static_cast(he.raceId); uint32_t nSex = static_cast(he.sexId); uint32_t nSkin = static_cast(he.skinId); uint32_t nFace = static_cast(he.faceId); for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) { - uint32_t rId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + uint32_t rId = csDbc->getUInt32(r, csF.raceId); + uint32_t sId = csDbc->getUInt32(r, csF.sexId); 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 section = csDbc->getUInt32(r, csF.baseSection); + uint32_t variation = csDbc->getUInt32(r, csF.variationIndex); + uint32_t color = csDbc->getUInt32(r, csF.colorIndex); if (section == 0 && color == nSkin) { - std::string t = csDbc->getString(r, tex1F); + std::string t = csDbc->getString(r, csF.texture1); if (!t.empty()) displaySkinPaths.push_back(t); } else if (section == 1 && variation == nFace && color == nSkin) { - std::string t1 = csDbc->getString(r, tex1F); - std::string t2 = csDbc->getString(r, tex1F + 1); + std::string t1 = csDbc->getString(r, csF.texture1); + std::string t2 = csDbc->getString(r, csF.texture2); if (!t1.empty()) displaySkinPaths.push_back(t1); if (!t2.empty()) displaySkinPaths.push_back(t2); } else if (section == 3 && variation == static_cast(he.hairStyleId) && color == static_cast(he.hairColorId)) { - std::string t = csDbc->getString(r, tex1F); + std::string t = csDbc->getString(r, csF.texture1); if (!t.empty()) displaySkinPaths.push_back(t); } else if (section == 4 && color == nSkin) { - for (uint32_t f = tex1F; f <= tex1F + 2; f++) { + for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string t = csDbc->getString(r, f); if (!t.empty()) displaySkinPaths.push_back(t); } @@ -7768,6 +8341,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) { return result; }); asyncCreatureLoads_.push_back(std::move(load)); + asyncCreatureDisplayLoads_.insert(s.displayId); asyncLaunched++; // Don't erase from pendingCreatureSpawnGuids_ — the async result handler will do it rotationsLeft = pendingCreatureSpawns_.size(); @@ -8168,6 +8742,151 @@ void Application::processGameObjectSpawnQueue() { } } +void Application::processPendingTransportRegistrations() { + if (pendingTransportRegistrations_.empty()) return; + if (!gameHandler || !renderer) return; + + auto* transportManager = gameHandler->getTransportManager(); + if (!transportManager) return; + + auto startTime = std::chrono::steady_clock::now(); + static constexpr int kMaxRegistrationsPerFrame = 2; + static constexpr float kRegistrationBudgetMs = 2.0f; + int processed = 0; + + for (auto it = pendingTransportRegistrations_.begin(); + it != pendingTransportRegistrations_.end() && processed < kMaxRegistrationsPerFrame;) { + float elapsedMs = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + if (elapsedMs >= kRegistrationBudgetMs) break; + + const PendingTransportRegistration pending = *it; + auto goIt = gameObjectInstances_.find(pending.guid); + if (goIt == gameObjectInstances_.end()) { + it = pendingTransportRegistrations_.erase(it); + continue; + } + + if (transportManager->getTransport(pending.guid)) { + transportManager->updateServerTransport( + pending.guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation); + it = pendingTransportRegistrations_.erase(it); + continue; + } + + const uint32_t wmoInstanceId = goIt->second.instanceId; + LOG_WARNING("Registering server transport: GUID=0x", std::hex, pending.guid, std::dec, + " entry=", pending.entry, " displayId=", pending.displayId, " wmoInstance=", wmoInstanceId, + " pos=(", pending.x, ", ", pending.y, ", ", pending.z, ")"); + + // TransportAnimation.dbc is indexed by GameObject entry. + uint32_t pathId = pending.entry; + const bool preferServerData = gameHandler->hasServerTransportUpdate(pending.guid); + + bool clientAnim = transportManager->isClientSideAnimation(); + LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim, + " guid=0x", std::hex, pending.guid, std::dec, + " entry=", pending.entry, " pathId=", pathId, + " preferServer=", preferServerData); + + glm::vec3 canonicalSpawnPos(pending.x, pending.y, pending.z); + const bool shipOrZeppelinDisplay = + (pending.displayId == 3015 || pending.displayId == 3031 || pending.displayId == 7546 || + pending.displayId == 7446 || pending.displayId == 1587 || pending.displayId == 2454 || + pending.displayId == 807 || pending.displayId == 808); + bool hasUsablePath = transportManager->hasPathForEntry(pending.entry); + if (shipOrZeppelinDisplay) { + hasUsablePath = transportManager->hasUsableMovingPathForEntry(pending.entry, 25.0f); + } + + LOG_WARNING("Transport path check: entry=", pending.entry, " hasUsablePath=", hasUsablePath, + " preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay); + + if (preferServerData) { + if (!hasUsablePath) { + std::vector path = { canonicalSpawnPos }; + transportManager->loadPathFromNodes(pathId, path, false, 0.0f); + LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x", + std::hex, pending.guid, std::dec, " entry=", pending.entry); + } else { + LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", pending.entry); + } + } else if (!hasUsablePath) { + bool allowZOnly = (pending.displayId == 455 || pending.displayId == 462); + uint32_t inferredPath = transportManager->inferDbcPathForSpawn( + canonicalSpawnPos, 1200.0f, allowZOnly); + if (inferredPath != 0) { + pathId = inferredPath; + LOG_WARNING("Using inferred transport path ", pathId, " for entry ", pending.entry); + } else { + uint32_t remappedPath = transportManager->pickFallbackMovingPath(pending.entry, pending.displayId); + if (remappedPath != 0) { + pathId = remappedPath; + LOG_WARNING("Using remapped fallback transport path ", pathId, + " for entry ", pending.entry, " displayId=", pending.displayId, + " (usableEntryPath=", transportManager->hasPathForEntry(pending.entry), ")"); + } else { + LOG_WARNING("No TransportAnimation.dbc path for entry ", pending.entry, + " - transport will be stationary"); + std::vector path = { canonicalSpawnPos }; + transportManager->loadPathFromNodes(pathId, path, false, 0.0f); + } + } + } else { + LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", pending.entry); + } + + transportManager->registerTransport(pending.guid, wmoInstanceId, pathId, canonicalSpawnPos, pending.entry); + + if (!goIt->second.isWmo) { + if (auto* tr = transportManager->getTransport(pending.guid)) { + tr->isM2 = true; + } + } + + transportManager->updateServerTransport( + pending.guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation); + + auto moveIt = pendingTransportMoves_.find(pending.guid); + if (moveIt != pendingTransportMoves_.end()) { + const PendingTransportMove latestMove = moveIt->second; + transportManager->updateServerTransport( + pending.guid, glm::vec3(latestMove.x, latestMove.y, latestMove.z), latestMove.orientation); + LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, pending.guid, std::dec, + " pos=(", latestMove.x, ", ", latestMove.y, ", ", latestMove.z, + ") orientation=", latestMove.orientation); + pendingTransportMoves_.erase(moveIt); + } + + if (glm::length(canonicalSpawnPos) < 1.0f) { + auto goData = gameHandler->getCachedGameObjectInfo(pending.entry); + if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) { + uint32_t taxiPathId = goData->data[0]; + if (transportManager->hasTaxiPath(taxiPathId)) { + transportManager->assignTaxiPathToTransport(pending.entry, taxiPathId); + LOG_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", pending.entry, + " taxiPathId=", taxiPathId); + } + } + } + + if (auto* tr = transportManager->getTransport(pending.guid); tr) { + LOG_WARNING("Transport registered: guid=0x", std::hex, pending.guid, std::dec, + " entry=", pending.entry, " displayId=", pending.displayId, + " pathId=", tr->pathId, + " mode=", (tr->useClientAnimation ? "client" : "server"), + " serverUpdates=", tr->serverUpdateCount); + } else { + LOG_DEBUG("Transport registered: guid=0x", std::hex, pending.guid, std::dec, + " entry=", pending.entry, " displayId=", pending.displayId, + " (TransportManager instance missing)"); + } + + ++processed; + it = pendingTransportRegistrations_.erase(it); + } +} + void Application::processPendingTransportDoodads() { if (pendingTransportDoodadBatches_.empty()) return; if (!renderer || !assetManager) return; @@ -8179,6 +8898,13 @@ void Application::processPendingTransportDoodads() { auto startTime = std::chrono::steady_clock::now(); static constexpr float kDoodadBudgetMs = 4.0f; + // Batch all GPU uploads into a single async command buffer submission so that + // N doodads with multiple textures each don't each block on vkQueueSubmit + + // vkWaitForFences. Without batching, 30+ doodads × several textures = hundreds + // of sync GPU submits → the 490ms stall that preceded the VK_ERROR_DEVICE_LOST. + auto* vkCtx = renderer->getVkContext(); + if (vkCtx) vkCtx->beginUploadBatch(); + size_t budgetLeft = MAX_TRANSPORT_DOODADS_PER_FRAME; for (auto it = pendingTransportDoodadBatches_.begin(); it != pendingTransportDoodadBatches_.end() && budgetLeft > 0;) { @@ -8222,7 +8948,7 @@ void Application::processPendingTransportDoodads() { } if (!m2Model.isValid()) continue; - m2Renderer->loadModel(m2Model, doodadModelId); + if (!m2Renderer->loadModel(m2Model, doodadModelId)) continue; uint32_t m2InstanceId = m2Renderer->createInstance(doodadModelId, glm::vec3(0.0f), glm::vec3(0.0f), 1.0f); if (m2InstanceId == 0) continue; m2Renderer->setSkipCollision(m2InstanceId, true); @@ -8246,6 +8972,9 @@ void Application::processPendingTransportDoodads() { ++it; } } + + // Finalize the upload batch — submit all GPU copies in one shot (async, no wait). + if (vkCtx) vkCtx->endUploadBatch(); } void Application::processPendingMount() { diff --git a/src/core/window.cpp b/src/core/window.cpp index 9f74a81c..318e5408 100644 --- a/src/core/window.cpp +++ b/src/core/window.cpp @@ -38,6 +38,15 @@ bool Window::initialize() { // clear error and avoids the misleading "not configured in SDL" message. // SDL 2.28+ uses LoadLibraryExW(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) which does // not search System32, so fall back to the explicit path on Windows if needed. + // + // On macOS, MoltenVK is a Vulkan "portability" driver. The Vulkan loader + // hides portability drivers (and their extensions like VK_KHR_surface) from + // pre-instance enumeration unless told otherwise. Setting this env var + // makes the loader include portability ICDs so SDL's VK_KHR_surface check + // succeeds. +#ifdef __APPLE__ + setenv("VK_LOADER_ENABLE_PORTABILITY_DRIVERS", "1", 0 /*don't overwrite*/); +#endif bool vulkanLoaded = (SDL_Vulkan_LoadLibrary(nullptr) == 0); #ifdef _WIN32 if (!vulkanLoaded) { diff --git a/src/game/expansion_profile.cpp b/src/game/expansion_profile.cpp index 9b14e0b7..5910ff0d 100644 --- a/src/game/expansion_profile.cpp +++ b/src/game/expansion_profile.cpp @@ -85,7 +85,7 @@ namespace game { std::string ExpansionProfile::versionString() const { std::ostringstream ss; - ss << (int)majorVersion << "." << (int)minorVersion << "." << (int)patchVersion; + ss << static_cast(majorVersion) << "." << static_cast(minorVersion) << "." << static_cast(patchVersion); // Append letter suffix for known builds if (majorVersion == 3 && minorVersion == 3 && patchVersion == 5) ss << "a"; else if (majorVersion == 2 && minorVersion == 4 && patchVersion == 3) ss << ""; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b3e57ccc..265a23c3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -80,6 +80,28 @@ bool isAuthCharPipelineOpcode(LogicalOpcode op) { } } +// Build a WoW-format item link for use in system chat messages. +// The chat renderer in game_screen.cpp parses this format and draws the +// item name in its quality colour with a small icon and tooltip. +// Format: |cff|Hitem::0:0:0:0:0:0:0:0|h[]|h|r +std::string buildItemLink(uint32_t itemId, uint32_t quality, const std::string& name) { + static const char* kQualHex[] = { + "9d9d9d", // 0 Poor + "ffffff", // 1 Common + "1eff00", // 2 Uncommon + "0070dd", // 3 Rare + "a335ee", // 4 Epic + "ff8000", // 5 Legendary + "e6cc80", // 6 Artifact + "e6cc80", // 7 Heirloom + }; + uint32_t qi = quality < 8 ? quality : 1u; + char buf[512]; + snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r", + kQualHex[qi], itemId, name.c_str()); + return buf; +} + bool isActiveExpansion(const char* expansionId) { auto& app = core::Application::getInstance(); auto* registry = app.getExpansionRegistry(); @@ -93,6 +115,10 @@ bool isClassicLikeExpansion() { return isActiveExpansion("classic") || isActiveExpansion("turtle"); } +bool isPreWotlk() { + return isPreWotlk(); +} + bool envFlagEnabled(const char* key, bool defaultValue = false) { const char* raw = std::getenv(key); if (!raw || !*raw) return defaultValue; @@ -100,6 +126,72 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) { raw[0] == 'n' || raw[0] == 'N'); } +int parseEnvIntClamped(const char* key, int defaultValue, int minValue, int maxValue) { + const char* raw = std::getenv(key); + if (!raw || !*raw) return defaultValue; + char* end = nullptr; + long parsed = std::strtol(raw, &end, 10); + if (end == raw) return defaultValue; + return static_cast(std::clamp(parsed, minValue, maxValue)); +} + +int incomingPacketsBudgetPerUpdate(WorldState state) { + static const int inWorldBudget = + parseEnvIntClamped("WOWEE_NET_MAX_GAMEHANDLER_PACKETS", 24, 1, 512); + static const int loginBudget = + parseEnvIntClamped("WOWEE_NET_MAX_GAMEHANDLER_PACKETS_LOGIN", 96, 1, 512); + return state == WorldState::IN_WORLD ? inWorldBudget : loginBudget; +} + +float incomingPacketBudgetMs(WorldState state) { + static const int inWorldBudgetMs = + parseEnvIntClamped("WOWEE_NET_MAX_GAMEHANDLER_PACKET_MS", 2, 1, 50); + static const int loginBudgetMs = + parseEnvIntClamped("WOWEE_NET_MAX_GAMEHANDLER_PACKET_MS_LOGIN", 8, 1, 50); + return static_cast(state == WorldState::IN_WORLD ? inWorldBudgetMs : loginBudgetMs); +} + +int updateObjectBlocksBudgetPerUpdate(WorldState state) { + static const int inWorldBudget = + parseEnvIntClamped("WOWEE_NET_MAX_UPDATE_OBJECT_BLOCKS", 24, 1, 2048); + static const int loginBudget = + parseEnvIntClamped("WOWEE_NET_MAX_UPDATE_OBJECT_BLOCKS_LOGIN", 128, 1, 4096); + return state == WorldState::IN_WORLD ? inWorldBudget : loginBudget; +} + +float slowPacketLogThresholdMs() { + static const int thresholdMs = + parseEnvIntClamped("WOWEE_NET_SLOW_PACKET_LOG_MS", 10, 1, 60000); + return static_cast(thresholdMs); +} + +float slowUpdateObjectBlockLogThresholdMs() { + static const int thresholdMs = + parseEnvIntClamped("WOWEE_NET_SLOW_UPDATE_BLOCK_LOG_MS", 10, 1, 60000); + return static_cast(thresholdMs); +} + +constexpr size_t kMaxQueuedInboundPackets = 4096; + +CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) { + switch (missInfo) { + case 0: return CombatTextEntry::MISS; + case 1: return CombatTextEntry::DODGE; + case 2: return CombatTextEntry::PARRY; + case 3: return CombatTextEntry::BLOCK; + case 4: return CombatTextEntry::EVADE; + case 5: return CombatTextEntry::IMMUNE; + case 6: return CombatTextEntry::DEFLECT; + case 7: return CombatTextEntry::ABSORB; + case 8: return CombatTextEntry::RESIST; + case 9: // Some cores encode SPELL_MISS_IMMUNE2 as 9. + case 10: // Others encode SPELL_MISS_IMMUNE2 as 10. + return CombatTextEntry::IMMUNE; + case 11: return CombatTextEntry::REFLECT; + default: return CombatTextEntry::MISS; + } +} + std::string formatCopperAmount(uint32_t amount) { uint32_t gold = amount / 10000; uint32_t silver = (amount / 100) % 100; @@ -123,6 +215,40 @@ std::string formatCopperAmount(uint32_t amount) { return oss.str(); } +std::string displaySpellName(GameHandler& handler, uint32_t spellId) { + if (spellId == 0) return {}; + const std::string& name = handler.getSpellName(spellId); + if (!name.empty()) return name; + return "spell " + std::to_string(spellId); +} + +std::string formatSpellNameList(GameHandler& handler, + const std::vector& spellIds, + size_t maxShown = 3) { + if (spellIds.empty()) return {}; + + const size_t shownCount = std::min(spellIds.size(), maxShown); + std::ostringstream oss; + for (size_t i = 0; i < shownCount; ++i) { + if (i > 0) { + if (shownCount == 2) { + oss << " and "; + } else if (i == shownCount - 1) { + oss << ", and "; + } else { + oss << ", "; + } + } + oss << displaySpellName(handler, spellIds[i]); + } + + if (spellIds.size() > shownCount) { + oss << ", and " << (spellIds.size() - shownCount) << " more"; + } + + return oss.str(); +} + bool readCStringAt(const std::vector& data, size_t start, std::string& out, size_t& nextPos) { out.clear(); if (start >= data.size()) return false; @@ -259,6 +385,16 @@ bool isPlaceholderQuestTitle(const std::string& s) { return s.rfind("Quest #", 0) == 0; } +float mergeCooldownSeconds(float current, float incoming) { + constexpr float kEpsilon = 0.05f; + if (incoming <= 0.0f) return 0.0f; + if (current <= 0.0f) return incoming; + // Cooldowns should normally tick down. If a duplicate/late packet reports a + // larger value, keep the local remaining time to avoid visible timer resets. + if (incoming > current + kEpsilon) return current; + return incoming; +} + bool looksLikeQuestDescriptionText(const std::string& s) { int spaces = 0; int commas = 0; @@ -483,6 +619,31 @@ static QuestQueryRewards tryParseQuestRewards(const std::vector& data, } // namespace +template +void GameHandler::withSoundManager(ManagerGetter getter, Callback cb) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* mgr = (renderer->*getter)()) cb(mgr); + } +} + +// Registration helpers for common dispatch table patterns +void GameHandler::registerSkipHandler(LogicalOpcode op) { + dispatchTable_[op] = [](network::Packet& packet) { packet.skipAll(); }; +} +void GameHandler::registerErrorHandler(LogicalOpcode op, const char* msg) { + dispatchTable_[op] = [this, msg](network::Packet&) { + addUIError(msg); + addSystemChatMessage(msg); + }; +} +void GameHandler::registerHandler(LogicalOpcode op, void (GameHandler::*handler)(network::Packet&)) { + dispatchTable_[op] = [this, handler](network::Packet& packet) { (this->*handler)(packet); }; +} +void GameHandler::registerWorldHandler(LogicalOpcode op, void (GameHandler::*handler)(network::Packet&)) { + dispatchTable_[op] = [this, handler](network::Packet& packet) { + if (state == WorldState::IN_WORLD) (this->*handler)(packet); + }; +} GameHandler::GameHandler() { LOG_DEBUG("GameHandler created"); @@ -508,6 +669,9 @@ GameHandler::GameHandler() { actionBar[0].id = 6603; // Attack in slot 1 actionBar[11].type = ActionBarSlot::SPELL; actionBar[11].id = 8690; // Hearthstone in slot 12 + + // Build the opcode dispatch table (replaces switch(*logicalOp) in handlePacket) + registerOpcodeHandlers(); } GameHandler::~GameHandler() { @@ -546,11 +710,8 @@ bool GameHandler::connect(const std::string& host, this->realmId_ = realmId; // Diagnostic: dump session key for AUTH_REJECT debugging - { - std::string hex; - for (uint8_t b : sessionKey) { char buf[4]; snprintf(buf, sizeof(buf), "%02x", b); hex += buf; } - LOG_INFO("GameHandler session key (", sessionKey.size(), "): ", hex); - } + LOG_INFO("GameHandler session key (", sessionKey.size(), "): ", + core::toHexString(sessionKey.data(), sessionKey.size())); requiresWarden_ = false; wardenGateSeen_ = false; wardenGateElapsed_ = 0.0f; @@ -574,8 +735,7 @@ bool GameHandler::connect(const std::string& host, // Set up packet callback socket->setPacketCallback([this](const network::Packet& packet) { - network::Packet mutablePacket = packet; - handlePacket(mutablePacket); + enqueueIncomingPacket(packet); }); // Connect to world server @@ -606,6 +766,8 @@ void GameHandler::disconnect() { activeCharacterGuid_ = 0; playerNameCache.clear(); pendingNameQueries.clear(); + guildNameCache_.clear(); + pendingGuildNameQueries_.clear(); friendGuids_.clear(); contacts_.clear(); transportAttachments_.clear(); @@ -627,7 +789,24 @@ void GameHandler::disconnect() { wardenModuleSize_ = 0; wardenModuleData_.clear(); wardenLoadedModule_.reset(); - // Clear entity state so reconnect sees fresh CREATE_OBJECT for all visible objects. + pendingIncomingPackets_.clear(); + pendingUpdateObjectWork_.clear(); + // Fire despawn callbacks so the renderer releases M2/character model resources. + for (const auto& [guid, entity] : entityManager.getEntities()) { + if (guid == playerGuid) continue; + if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) + creatureDespawnCallback_(guid); + else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) + playerDespawnCallback_(guid); + else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) + gameObjectDespawnCallback_(guid); + } + otherPlayerVisibleItemEntries_.clear(); + otherPlayerVisibleDirty_.clear(); + otherPlayerMoveTimeMs_.clear(); + unitCastStates_.clear(); + unitAurasCache_.clear(); + combatText.clear(); entityManager.clear(); setState(WorldState::DISCONNECTED); LOG_INFO("Disconnected from world server"); @@ -665,18 +844,10 @@ bool GameHandler::isConnected() const { return socket && socket->isConnected(); } -void GameHandler::update(float deltaTime) { - // Fire deferred char-create callback (outside ImGui render) - if (pendingCharCreateResult_) { - pendingCharCreateResult_ = false; - if (charCreateCallback_) { - charCreateCallback_(pendingCharCreateSuccess_, pendingCharCreateMsg_); - } - } - - if (!socket) { - return; - } +void GameHandler::updateNetworking(float deltaTime) { + // Reset per-tick monster-move budget tracking (Classic/Turtle flood protection). + monsterMovePacketsThisTick_ = 0; + monsterMovePacketsDroppedThisTick_ = 0; // Update socket (processes incoming data and triggers callbacks) if (socket) { @@ -689,11 +860,60 @@ void GameHandler::update(float deltaTime) { } } + { + auto packetStart = std::chrono::steady_clock::now(); + processQueuedIncomingPackets(); + float packetMs = std::chrono::duration( + std::chrono::steady_clock::now() - packetStart).count(); + if (packetMs > 3.0f) { + LOG_WARNING("SLOW queued packet handling: ", packetMs, "ms"); + } + } + + // Drain pending async Warden response (built on background thread to avoid 5s stalls) + if (wardenResponsePending_) { + auto status = wardenPendingEncrypted_.wait_for(std::chrono::milliseconds(0)); + if (status == std::future_status::ready) { + auto plaintext = wardenPendingEncrypted_.get(); + wardenResponsePending_ = false; + if (!plaintext.empty() && wardenCrypto_) { + std::vector encrypted = wardenCrypto_->encrypt(plaintext); + network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA)); + for (uint8_t byte : encrypted) { + response.writeUInt8(byte); + } + if (socket && socket->isConnected()) { + socket->send(response); + LOG_WARNING("Warden: Sent async CHEAT_CHECKS_RESULT (", plaintext.size(), " bytes plaintext)"); + } + } + } + } + + // Detect RX silence (server stopped sending packets but TCP still open) + if (isInWorld() && socket->isConnected() && + lastRxTime_.time_since_epoch().count() > 0) { + auto silenceMs = std::chrono::duration_cast( + std::chrono::steady_clock::now() - lastRxTime_).count(); + if (silenceMs > 10000 && !rxSilenceLogged_) { + rxSilenceLogged_ = true; + LOG_WARNING("RX SILENCE: No packets from server for ", silenceMs, "ms — possible soft disconnect"); + } + if (silenceMs > 15000 && silenceMs < 15500) { + LOG_WARNING("RX SILENCE: 15s — server appears to have stopped sending"); + } + } + // Detect server-side disconnect (socket closed during update) if (socket && !socket->isConnected() && state != WorldState::DISCONNECTED) { - LOG_WARNING("Server closed connection in state: ", worldStateName(state)); - disconnect(); - return; + if (pendingIncomingPackets_.empty() && pendingUpdateObjectWork_.empty()) { + LOG_WARNING("Server closed connection in state: ", worldStateName(state)); + disconnect(); + return; + } + LOG_DEBUG("World socket closed with ", pendingIncomingPackets_.size(), + " queued packet(s) and ", pendingUpdateObjectWork_.size(), + " update-object batch(es) pending dispatch"); } // Post-gate visibility: determine whether server goes silent or closes after Warden requirement. @@ -706,12 +926,272 @@ void GameHandler::update(float deltaTime) { wardenGateNextStatusLog_ += 30.0f; } } +} - // Validate target still exists - if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) { - clearTarget(); +void GameHandler::updateTaxiAndMountState(float deltaTime) { +// Update taxi landing cooldown +if (taxiLandingCooldown_ > 0.0f) { + taxiLandingCooldown_ -= deltaTime; +} +if (taxiStartGrace_ > 0.0f) { + taxiStartGrace_ -= deltaTime; +} +if (playerTransportStickyTimer_ > 0.0f) { + playerTransportStickyTimer_ -= deltaTime; + if (playerTransportStickyTimer_ <= 0.0f) { + playerTransportStickyTimer_ = 0.0f; + playerTransportStickyGuid_ = 0; + } +} + +// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared +if (onTaxiFlight_) { + updateClientTaxi(deltaTime); + auto playerEntity = entityManager.getEntity(playerGuid); + auto unit = std::dynamic_pointer_cast(playerEntity); + if (unit && + (unit->getUnitFlags() & 0x00000100) == 0 && + !taxiClientActive_ && + !taxiActivatePending_ && + taxiStartGrace_ <= 0.0f) { + onTaxiFlight_ = false; + taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering + if (taxiMountActive_ && mountCallback_) { + mountCallback_(0); + } + taxiMountActive_ = false; + taxiMountDisplayId_ = 0; + currentMountDisplayId_ = 0; + taxiClientActive_ = false; + taxiClientPath_.clear(); + taxiRecoverPending_ = false; + movementInfo.flags = 0; + movementInfo.flags2 = 0; + if (socket) { + sendMovement(Opcode::MSG_MOVE_STOP); + sendMovement(Opcode::MSG_MOVE_HEARTBEAT); + } + LOG_INFO("Taxi flight landed"); + } +} + +// Safety: if taxi flight ended but mount is still active, force dismount. +// Guard against transient taxi-state flicker. +if (!onTaxiFlight_ && taxiMountActive_) { + bool serverStillTaxi = false; + auto playerEntity = entityManager.getEntity(playerGuid); + auto playerUnit = std::dynamic_pointer_cast(playerEntity); + if (playerUnit) { + serverStillTaxi = (playerUnit->getUnitFlags() & 0x00000100) != 0; } + if (taxiStartGrace_ > 0.0f || serverStillTaxi || taxiClientActive_ || taxiActivatePending_) { + onTaxiFlight_ = true; + } else { + if (mountCallback_) mountCallback_(0); + taxiMountActive_ = false; + taxiMountDisplayId_ = 0; + currentMountDisplayId_ = 0; + movementInfo.flags = 0; + movementInfo.flags2 = 0; + if (socket) { + sendMovement(Opcode::MSG_MOVE_STOP); + sendMovement(Opcode::MSG_MOVE_HEARTBEAT); + } + LOG_INFO("Taxi dismount cleanup"); + } +} + +// Keep non-taxi mount state server-authoritative. +// Some server paths don't emit explicit mount field updates in lockstep +// with local visual state changes, so reconcile continuously. +if (!onTaxiFlight_ && !taxiMountActive_) { + auto playerEntity = entityManager.getEntity(playerGuid); + auto playerUnit = std::dynamic_pointer_cast(playerEntity); + if (playerUnit) { + uint32_t serverMountDisplayId = playerUnit->getMountDisplayId(); + if (serverMountDisplayId != currentMountDisplayId_) { + LOG_INFO("Mount reconcile: server=", serverMountDisplayId, + " local=", currentMountDisplayId_); + currentMountDisplayId_ = serverMountDisplayId; + if (mountCallback_) { + mountCallback_(serverMountDisplayId); + } + } + } +} + +if (taxiRecoverPending_ && state == WorldState::IN_WORLD) { + auto playerEntity = entityManager.getEntity(playerGuid); + if (playerEntity) { + playerEntity->setPosition(taxiRecoverPos_.x, taxiRecoverPos_.y, + taxiRecoverPos_.z, movementInfo.orientation); + movementInfo.x = taxiRecoverPos_.x; + movementInfo.y = taxiRecoverPos_.y; + movementInfo.z = taxiRecoverPos_.z; + if (socket) { + sendMovement(Opcode::MSG_MOVE_HEARTBEAT); + } + taxiRecoverPending_ = false; + LOG_INFO("Taxi recovery applied"); + } +} + +if (taxiActivatePending_) { + taxiActivateTimer_ += deltaTime; + if (taxiActivateTimer_ > 5.0f) { + // If client taxi simulation is already active, server reply may be missing/late. + // Do not cancel the flight in that case; clear pending state and continue. + if (onTaxiFlight_ || taxiClientActive_ || taxiMountActive_) { + taxiActivatePending_ = false; + taxiActivateTimer_ = 0.0f; + } else { + taxiActivatePending_ = false; + taxiActivateTimer_ = 0.0f; + if (taxiMountActive_ && mountCallback_) { + mountCallback_(0); + } + taxiMountActive_ = false; + taxiMountDisplayId_ = 0; + taxiClientActive_ = false; + taxiClientPath_.clear(); + onTaxiFlight_ = false; + LOG_WARNING("Taxi activation timed out"); + } + } +} +} + +void GameHandler::updateAutoAttack(float deltaTime) { +// Leave combat if auto-attack target is too far away (leash range) +// and keep melee intent tightly synced while stationary. +if (autoAttackRequested_ && autoAttackTarget != 0) { + auto targetEntity = entityManager.getEntity(autoAttackTarget); + if (targetEntity) { + // Use latest server-authoritative target position to avoid stale + // interpolation snapshots masking out-of-range states. + const float targetX = targetEntity->getLatestX(); + const float targetY = targetEntity->getLatestY(); + const float targetZ = targetEntity->getLatestZ(); + float dx = movementInfo.x - targetX; + float dy = movementInfo.y - targetY; + float dz = movementInfo.z - targetZ; + float dist = std::sqrt(dx * dx + dy * dy); + float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz); + const bool classicLike = isPreWotlk(); + if (dist > 40.0f) { + stopAutoAttack(); + LOG_INFO("Left combat: target too far (", dist, " yards)"); + } else if (isInWorld()) { + bool allowResync = true; + const float meleeRange = classicLike ? 5.25f : 5.75f; + if (dist3d > meleeRange) { + autoAttackOutOfRange_ = true; + autoAttackOutOfRangeTime_ += deltaTime; + if (autoAttackRangeWarnCooldown_ <= 0.0f) { + addSystemChatMessage("Target is too far away."); + addUIError("Target is too far away."); + autoAttackRangeWarnCooldown_ = 1.25f; + } + // Stop chasing stale swings when the target remains out of range. + if (autoAttackOutOfRangeTime_ > 2.0f && dist3d > 9.0f) { + stopAutoAttack(); + addSystemChatMessage("Auto-attack stopped: target out of range."); + allowResync = false; + } + } else { + autoAttackOutOfRange_ = false; + autoAttackOutOfRangeTime_ = 0.0f; + } + + if (allowResync) { + autoAttackResendTimer_ += deltaTime; + autoAttackFacingSyncTimer_ += deltaTime; + + // Classic/Turtle servers do not tolerate steady attack-start + // reissues well. Only retry once after local start or an + // explicit server-side attack stop while intent is still set. + const float resendInterval = classicLike ? 1.0f : 0.50f; + if (!autoAttacking && !autoAttackOutOfRange_ && autoAttackRetryPending_ && + autoAttackResendTimer_ >= resendInterval) { + autoAttackResendTimer_ = 0.0f; + autoAttackRetryPending_ = false; + auto pkt = AttackSwingPacket::build(autoAttackTarget); + socket->send(pkt); + } + + // Keep server-facing aligned while trying to acquire melee. + // Once the server confirms auto-attack, rely on explicit + // bad-facing feedback instead of periodic steady-state facing spam. + const float facingSyncInterval = classicLike ? 0.25f : 0.20f; + const bool allowPeriodicFacingSync = !classicLike || !autoAttacking; + if (allowPeriodicFacingSync && + autoAttackFacingSyncTimer_ >= facingSyncInterval) { + autoAttackFacingSyncTimer_ = 0.0f; + float toTargetX = targetX - movementInfo.x; + float toTargetY = targetY - movementInfo.y; + if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) { + float desired = std::atan2(-toTargetY, toTargetX); + float diff = desired - movementInfo.orientation; + while (diff > static_cast(M_PI)) diff -= 2.0f * static_cast(M_PI); + while (diff < -static_cast(M_PI)) diff += 2.0f * static_cast(M_PI); + const float facingThreshold = classicLike ? 0.035f : 0.12f; // ~2deg / ~7deg + if (std::abs(diff) > facingThreshold) { + movementInfo.orientation = desired; + sendMovement(Opcode::MSG_MOVE_SET_FACING); + } + } + } + } + } + } +} + +// Keep active melee attackers visually facing the player as positions change. +// Some servers don't stream frequent orientation updates during combat. +if (!hostileAttackers_.empty()) { + for (uint64_t attackerGuid : hostileAttackers_) { + auto attacker = entityManager.getEntity(attackerGuid); + if (!attacker) continue; + float dx = movementInfo.x - attacker->getX(); + float dy = movementInfo.y - attacker->getY(); + if (std::abs(dx) < 0.01f && std::abs(dy) < 0.01f) continue; + attacker->setOrientation(std::atan2(-dy, dx)); + } +} + +// Close NPC windows if player walks too far (15 units) +} + +void GameHandler::updateEntityInterpolation(float deltaTime) { +// Update entity movement interpolation (keeps targeting in sync with visuals) +// Only update entities within reasonable distance for performance +const float updateRadiusSq = 150.0f * 150.0f; // 150 unit radius +auto playerEntity = entityManager.getEntity(playerGuid); +glm::vec3 playerPos = playerEntity ? glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()) : glm::vec3(0.0f); + +for (auto& [guid, entity] : entityManager.getEntities()) { + // Always update player + if (guid == playerGuid) { + entity->updateMovement(deltaTime); + continue; + } + // Keep selected/engaged target interpolation exact for UI targeting circle. + if (guid == targetGuid || guid == autoAttackTarget) { + entity->updateMovement(deltaTime); + continue; + } + + // Distance cull other entities (use latest position to avoid culling by stale origin) + glm::vec3 entityPos(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); + float distSq = glm::dot(entityPos - playerPos, entityPos - playerPos); + if (distSq < updateRadiusSq) { + entity->updateMovement(deltaTime); + } +} +} + +void GameHandler::updateTimers(float deltaTime) { if (auctionSearchDelayTimer_ > 0.0f) { auctionSearchDelayTimer_ -= deltaTime; if (auctionSearchDelayTimer_ < 0.0f) auctionSearchDelayTimer_ = 0.0f; @@ -757,7 +1237,7 @@ void GameHandler::update(float deltaTime) { for (auto it = pendingGameObjectLootRetries_.begin(); it != pendingGameObjectLootRetries_.end();) { it->timer -= deltaTime; if (it->timer <= 0.0f) { - if (it->remainingRetries > 0 && state == WorldState::IN_WORLD && socket) { + if (it->remainingRetries > 0 && isInWorld()) { // Keep server-side position/facing fresh before retrying GO use. sendMovement(Opcode::MSG_MOVE_HEARTBEAT); auto usePacket = GameObjectUsePacket::build(it->guid); @@ -780,7 +1260,14 @@ void GameHandler::update(float deltaTime) { for (auto it = pendingGameObjectLootOpens_.begin(); it != pendingGameObjectLootOpens_.end();) { it->timer -= deltaTime; if (it->timer <= 0.0f) { - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { + // Avoid sending CMSG_LOOT while a timed cast is active (e.g. gathering). + // handleSpellGo will trigger loot after the cast completes. + if (casting && currentCastSpellId != 0) { + it->timer = 0.20f; + ++it; + continue; + } lootTarget(it->guid); } it = pendingGameObjectLootOpens_.erase(it); @@ -792,7 +1279,7 @@ void GameHandler::update(float deltaTime) { // Periodically re-query names for players whose initial CMSG_NAME_QUERY was // lost (server didn't respond) or whose entity was recreated while the query // was still pending. Runs every 5 seconds to keep overhead minimal. - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { static float nameResyncTimer = 0.0f; nameResyncTimer += deltaTime; if (nameResyncTimer >= 5.0f) { @@ -859,7 +1346,7 @@ void GameHandler::update(float deltaTime) { if (inspectRateLimit_ > 0.0f) { inspectRateLimit_ = std::max(0.0f, inspectRateLimit_ - deltaTime); } - if (state == WorldState::IN_WORLD && socket && inspectRateLimit_ <= 0.0f && !pendingAutoInspect_.empty()) { + if (isInWorld() && inspectRateLimit_ <= 0.0f && !pendingAutoInspect_.empty()) { uint64_t guid = *pendingAutoInspect_.begin(); pendingAutoInspect_.erase(pendingAutoInspect_.begin()); if (guid != 0 && guid != playerGuid && entityManager.hasEntity(guid)) { @@ -869,13 +1356,49 @@ void GameHandler::update(float deltaTime) { LOG_DEBUG("Sent CMSG_INSPECT for player 0x", std::hex, guid, std::dec); } } +} + +void GameHandler::update(float deltaTime) { + // Fire deferred char-create callback (outside ImGui render) + if (pendingCharCreateResult_) { + pendingCharCreateResult_ = false; + if (charCreateCallback_) { + charCreateCallback_(pendingCharCreateSuccess_, pendingCharCreateMsg_); + } + } + + if (!socket) { + return; + } + + updateNetworking(deltaTime); + if (!socket) return; // disconnect() may have been called + + // Validate target still exists + if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) { + clearTarget(); + } + + // Detect combat state transitions → fire PLAYER_REGEN_DISABLED / PLAYER_REGEN_ENABLED + { + bool combatNow = isInCombat(); + if (combatNow != wasCombat_) { + wasCombat_ = combatNow; + fireAddonEvent(combatNow ? "PLAYER_REGEN_DISABLED" : "PLAYER_REGEN_ENABLED", {}); + } + } + + updateTimers(deltaTime); + // Send periodic heartbeat if in world if (state == WorldState::IN_WORLD) { timeSinceLastPing += deltaTime; timeSinceLastMoveHeartbeat_ += deltaTime; - if (timeSinceLastPing >= pingInterval) { + const float currentPingInterval = + (isPreWotlk()) ? 10.0f : pingInterval; + if (timeSinceLastPing >= currentPingInterval) { if (socket) { sendPing(); } @@ -883,10 +1406,28 @@ void GameHandler::update(float deltaTime) { } const bool classicLikeCombatSync = - autoAttackRequested_ && (isClassicLikeExpansion() || isActiveExpansion("tbc")); + autoAttackRequested_ && (isPreWotlk()); + const uint32_t locomotionFlags = + static_cast(MovementFlags::FORWARD) | + static_cast(MovementFlags::BACKWARD) | + static_cast(MovementFlags::STRAFE_LEFT) | + static_cast(MovementFlags::STRAFE_RIGHT) | + static_cast(MovementFlags::TURN_LEFT) | + static_cast(MovementFlags::TURN_RIGHT) | + static_cast(MovementFlags::ASCENDING) | + static_cast(MovementFlags::FALLING) | + static_cast(MovementFlags::FALLINGFAR); + const bool classicLikeStationaryCombatSync = + classicLikeCombatSync && + !onTaxiFlight_ && + !taxiActivatePending_ && + !taxiClientActive_ && + (movementInfo.flags & locomotionFlags) == 0; float heartbeatInterval = (onTaxiFlight_ || taxiActivatePending_ || taxiClientActive_) ? 0.25f - : (classicLikeCombatSync ? 0.05f : moveHeartbeatInterval_); + : (classicLikeStationaryCombatSync ? 0.75f + : (classicLikeCombatSync ? 0.20f + : moveHeartbeatInterval_)); if (timeSinceLastMoveHeartbeat_ >= heartbeatInterval) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); timeSinceLastMoveHeartbeat_ = 0.0f; @@ -960,137 +1501,14 @@ void GameHandler::update(float deltaTime) { updateCombatText(deltaTime); tickMinimapPings(deltaTime); - // Update taxi landing cooldown - if (taxiLandingCooldown_ > 0.0f) { - taxiLandingCooldown_ -= deltaTime; - } - if (taxiStartGrace_ > 0.0f) { - taxiStartGrace_ -= deltaTime; - } - if (playerTransportStickyTimer_ > 0.0f) { - playerTransportStickyTimer_ -= deltaTime; - if (playerTransportStickyTimer_ <= 0.0f) { - playerTransportStickyTimer_ = 0.0f; - playerTransportStickyGuid_ = 0; - } + // Tick logout countdown + if (loggingOut_ && logoutCountdown_ > 0.0f) { + logoutCountdown_ -= deltaTime; + if (logoutCountdown_ < 0.0f) logoutCountdown_ = 0.0f; } - // Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared - if (onTaxiFlight_) { - updateClientTaxi(deltaTime); - auto playerEntity = entityManager.getEntity(playerGuid); - auto unit = std::dynamic_pointer_cast(playerEntity); - if (unit && - (unit->getUnitFlags() & 0x00000100) == 0 && - !taxiClientActive_ && - !taxiActivatePending_ && - taxiStartGrace_ <= 0.0f) { - onTaxiFlight_ = false; - taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering - if (taxiMountActive_ && mountCallback_) { - mountCallback_(0); - } - taxiMountActive_ = false; - taxiMountDisplayId_ = 0; - currentMountDisplayId_ = 0; - taxiClientActive_ = false; - taxiClientPath_.clear(); - taxiRecoverPending_ = false; - movementInfo.flags = 0; - movementInfo.flags2 = 0; - if (socket) { - sendMovement(Opcode::MSG_MOVE_STOP); - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - } - LOG_INFO("Taxi flight landed"); - } - } + updateTaxiAndMountState(deltaTime); - // Safety: if taxi flight ended but mount is still active, force dismount. - // Guard against transient taxi-state flicker. - if (!onTaxiFlight_ && taxiMountActive_) { - bool serverStillTaxi = false; - auto playerEntity = entityManager.getEntity(playerGuid); - auto playerUnit = std::dynamic_pointer_cast(playerEntity); - if (playerUnit) { - serverStillTaxi = (playerUnit->getUnitFlags() & 0x00000100) != 0; - } - - if (taxiStartGrace_ > 0.0f || serverStillTaxi || taxiClientActive_ || taxiActivatePending_) { - onTaxiFlight_ = true; - } else { - if (mountCallback_) mountCallback_(0); - taxiMountActive_ = false; - taxiMountDisplayId_ = 0; - currentMountDisplayId_ = 0; - movementInfo.flags = 0; - movementInfo.flags2 = 0; - if (socket) { - sendMovement(Opcode::MSG_MOVE_STOP); - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - } - LOG_INFO("Taxi dismount cleanup"); - } - } - - // Keep non-taxi mount state server-authoritative. - // Some server paths don't emit explicit mount field updates in lockstep - // with local visual state changes, so reconcile continuously. - if (!onTaxiFlight_ && !taxiMountActive_) { - auto playerEntity = entityManager.getEntity(playerGuid); - auto playerUnit = std::dynamic_pointer_cast(playerEntity); - if (playerUnit) { - uint32_t serverMountDisplayId = playerUnit->getMountDisplayId(); - if (serverMountDisplayId != currentMountDisplayId_) { - LOG_INFO("Mount reconcile: server=", serverMountDisplayId, - " local=", currentMountDisplayId_); - currentMountDisplayId_ = serverMountDisplayId; - if (mountCallback_) { - mountCallback_(serverMountDisplayId); - } - } - } - } - - if (taxiRecoverPending_ && state == WorldState::IN_WORLD) { - auto playerEntity = entityManager.getEntity(playerGuid); - if (playerEntity) { - playerEntity->setPosition(taxiRecoverPos_.x, taxiRecoverPos_.y, - taxiRecoverPos_.z, movementInfo.orientation); - movementInfo.x = taxiRecoverPos_.x; - movementInfo.y = taxiRecoverPos_.y; - movementInfo.z = taxiRecoverPos_.z; - if (socket) { - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - } - taxiRecoverPending_ = false; - LOG_INFO("Taxi recovery applied"); - } - } - - if (taxiActivatePending_) { - taxiActivateTimer_ += deltaTime; - if (taxiActivateTimer_ > 5.0f) { - // If client taxi simulation is already active, server reply may be missing/late. - // Do not cancel the flight in that case; clear pending state and continue. - if (onTaxiFlight_ || taxiClientActive_ || taxiMountActive_) { - taxiActivatePending_ = false; - taxiActivateTimer_ = 0.0f; - } else { - taxiActivatePending_ = false; - taxiActivateTimer_ = 0.0f; - if (taxiMountActive_ && mountCallback_) { - mountCallback_(0); - } - taxiMountActive_ = false; - taxiMountDisplayId_ = 0; - taxiClientActive_ = false; - taxiClientPath_.clear(); - onTaxiFlight_ = false; - LOG_WARNING("Taxi activation timed out"); - } - } - } // Update transport manager if (transportManager_) { @@ -1098,191 +1516,6199 @@ void GameHandler::update(float deltaTime) { updateAttachedTransportChildren(deltaTime); } - // Leave combat if auto-attack target is too far away (leash range) - // and keep melee intent tightly synced while stationary. + updateAutoAttack(deltaTime); + auto closeIfTooFar = [&](bool windowOpen, uint64_t npcGuid, auto closeFn, const char* label) { + if (!windowOpen || npcGuid == 0) return; + auto npc = entityManager.getEntity(npcGuid); + if (!npc) return; + float dx = movementInfo.x - npc->getX(); + float dy = movementInfo.y - npc->getY(); + if (std::sqrt(dx * dx + dy * dy) > 15.0f) { + closeFn(); + LOG_INFO(label, " closed: walked too far from NPC"); + } + }; + closeIfTooFar(vendorWindowOpen, currentVendorItems.vendorGuid, [this]{ closeVendor(); }, "Vendor"); + closeIfTooFar(gossipWindowOpen, currentGossip.npcGuid, [this]{ closeGossip(); }, "Gossip"); + closeIfTooFar(taxiWindowOpen_, taxiNpcGuid_, [this]{ closeTaxi(); }, "Taxi window"); + closeIfTooFar(trainerWindowOpen_, currentTrainerList_.trainerGuid, [this]{ closeTrainer(); }, "Trainer"); + + updateEntityInterpolation(deltaTime); + + } +} + +void GameHandler::registerOpcodeHandlers() { + // ----------------------------------------------------------------------- + // Auth / session / pre-world handshake + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_AUTH_CHALLENGE] = [this](network::Packet& packet) { + if (state == WorldState::CONNECTED) + handleAuthChallenge(packet); + else + LOG_WARNING("Unexpected SMSG_AUTH_CHALLENGE in state: ", worldStateName(state)); + }; + dispatchTable_[Opcode::SMSG_AUTH_RESPONSE] = [this](network::Packet& packet) { + if (state == WorldState::AUTH_SENT) + handleAuthResponse(packet); + else + LOG_WARNING("Unexpected SMSG_AUTH_RESPONSE in state: ", worldStateName(state)); + }; + dispatchTable_[Opcode::SMSG_CHAR_CREATE] = [this](network::Packet& packet) { + handleCharCreateResponse(packet); + }; + dispatchTable_[Opcode::SMSG_CHAR_DELETE] = [this](network::Packet& packet) { + uint8_t result = packet.readUInt8(); + lastCharDeleteResult_ = result; + bool success = (result == 0x00 || result == 0x47); + LOG_INFO("SMSG_CHAR_DELETE result: ", static_cast(result), success ? " (success)" : " (failed)"); + requestCharacterList(); + if (charDeleteCallback_) charDeleteCallback_(success); + }; + dispatchTable_[Opcode::SMSG_CHAR_ENUM] = [this](network::Packet& packet) { + if (state == WorldState::CHAR_LIST_REQUESTED) + handleCharEnum(packet); + else + LOG_WARNING("Unexpected SMSG_CHAR_ENUM in state: ", worldStateName(state)); + }; + registerHandler(Opcode::SMSG_CHARACTER_LOGIN_FAILED, &GameHandler::handleCharLoginFailed); + dispatchTable_[Opcode::SMSG_LOGIN_VERIFY_WORLD] = [this](network::Packet& packet) { + if (state == WorldState::ENTERING_WORLD || state == WorldState::IN_WORLD) + handleLoginVerifyWorld(packet); + else + LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", worldStateName(state)); + }; + registerHandler(Opcode::SMSG_LOGIN_SETTIMESPEED, &GameHandler::handleLoginSetTimeSpeed); + registerHandler(Opcode::SMSG_CLIENTCACHE_VERSION, &GameHandler::handleClientCacheVersion); + registerHandler(Opcode::SMSG_TUTORIAL_FLAGS, &GameHandler::handleTutorialFlags); + registerHandler(Opcode::SMSG_WARDEN_DATA, &GameHandler::handleWardenData); + registerHandler(Opcode::SMSG_ACCOUNT_DATA_TIMES, &GameHandler::handleAccountDataTimes); + registerHandler(Opcode::SMSG_MOTD, &GameHandler::handleMotd); + registerHandler(Opcode::SMSG_NOTIFICATION, &GameHandler::handleNotification); + registerHandler(Opcode::SMSG_PONG, &GameHandler::handlePong); + + // ----------------------------------------------------------------------- + // World object updates + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_UPDATE_OBJECT] = [this](network::Packet& packet) { + LOG_DEBUG("Received SMSG_UPDATE_OBJECT, state=", static_cast(state), " size=", packet.getSize()); + if (state == WorldState::IN_WORLD) handleUpdateObject(packet); + }; + dispatchTable_[Opcode::SMSG_COMPRESSED_UPDATE_OBJECT] = [this](network::Packet& packet) { + LOG_DEBUG("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast(state), " size=", packet.getSize()); + if (state == WorldState::IN_WORLD) handleCompressedUpdateObject(packet); + }; + dispatchTable_[Opcode::SMSG_DESTROY_OBJECT] = [this](network::Packet& packet) { + if (state == WorldState::IN_WORLD) handleDestroyObject(packet); + }; + + // ----------------------------------------------------------------------- + // Chat + // ----------------------------------------------------------------------- + registerWorldHandler(Opcode::SMSG_MESSAGECHAT, &GameHandler::handleMessageChat); + registerWorldHandler(Opcode::SMSG_GM_MESSAGECHAT, &GameHandler::handleMessageChat); + registerWorldHandler(Opcode::SMSG_TEXT_EMOTE, &GameHandler::handleTextEmote); + dispatchTable_[Opcode::SMSG_EMOTE] = [this](network::Packet& packet) { + if (state != WorldState::IN_WORLD) return; + if (!packet.hasRemaining(12)) return; + uint32_t emoteAnim = packet.readUInt32(); + uint64_t sourceGuid = packet.readUInt64(); + if (emoteAnimCallback_ && sourceGuid != 0) emoteAnimCallback_(sourceGuid, emoteAnim); + }; + dispatchTable_[Opcode::SMSG_CHANNEL_NOTIFY] = [this](network::Packet& packet) { + if (state == WorldState::IN_WORLD || state == WorldState::ENTERING_WORLD) + handleChannelNotify(packet); + }; + dispatchTable_[Opcode::SMSG_CHAT_PLAYER_NOT_FOUND] = [this](network::Packet& packet) { + std::string name = packet.readString(); + if (!name.empty()) addSystemChatMessage("No player named '" + name + "' is currently playing."); + }; + dispatchTable_[Opcode::SMSG_CHAT_PLAYER_AMBIGUOUS] = [this](network::Packet& packet) { + std::string name = packet.readString(); + if (!name.empty()) addSystemChatMessage("Player name '" + name + "' is ambiguous."); + }; + registerErrorHandler(Opcode::SMSG_CHAT_WRONG_FACTION, "You cannot send messages to members of that faction."); + registerErrorHandler(Opcode::SMSG_CHAT_NOT_IN_PARTY, "You are not in a party."); + registerErrorHandler(Opcode::SMSG_CHAT_RESTRICTED, "You cannot send chat messages in this area."); + + // ----------------------------------------------------------------------- + // Player info queries / social + // ----------------------------------------------------------------------- + registerWorldHandler(Opcode::SMSG_QUERY_TIME_RESPONSE, &GameHandler::handleQueryTimeResponse); + registerWorldHandler(Opcode::SMSG_PLAYED_TIME, &GameHandler::handlePlayedTime); + registerWorldHandler(Opcode::SMSG_WHO, &GameHandler::handleWho); + dispatchTable_[Opcode::SMSG_WHOIS] = [this](network::Packet& packet) { + if (packet.hasData()) { + std::string whoisText = packet.readString(); + if (!whoisText.empty()) { + std::string line; + for (char c : whoisText) { + if (c == '\n') { if (!line.empty()) addSystemChatMessage("[Whois] " + line); line.clear(); } + else line += c; + } + if (!line.empty()) addSystemChatMessage("[Whois] " + line); + LOG_INFO("SMSG_WHOIS: ", whoisText); + } + } + }; + registerWorldHandler(Opcode::SMSG_FRIEND_STATUS, &GameHandler::handleFriendStatus); + registerHandler(Opcode::SMSG_CONTACT_LIST, &GameHandler::handleContactList); + registerHandler(Opcode::SMSG_FRIEND_LIST, &GameHandler::handleFriendList); + dispatchTable_[Opcode::SMSG_IGNORE_LIST] = [this](network::Packet& packet) { + if (!packet.hasRemaining(1)) return; + uint8_t ignCount = packet.readUInt8(); + for (uint8_t i = 0; i < ignCount; ++i) { + if (!packet.hasRemaining(8)) break; + uint64_t ignGuid = packet.readUInt64(); + std::string ignName = packet.readString(); + if (!ignName.empty() && ignGuid != 0) ignoreCache[ignName] = ignGuid; + } + LOG_DEBUG("SMSG_IGNORE_LIST: loaded ", static_cast(ignCount), " ignored players"); + }; + registerWorldHandler(Opcode::MSG_RANDOM_ROLL, &GameHandler::handleRandomRoll); + + // ----------------------------------------------------------------------- + // Item push / logout / entity queries + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_ITEM_PUSH_RESULT] = [this](network::Packet& packet) { + constexpr size_t kMinSize = 8 + 1 + 1 + 1 + 1 + 4 + 4 + 4 + 4 + 4 + 4; + if (packet.hasRemaining(kMinSize)) { + /*uint64_t recipientGuid =*/ packet.readUInt64(); + /*uint8_t received =*/ packet.readUInt8(); + /*uint8_t created =*/ packet.readUInt8(); + uint8_t showInChat = packet.readUInt8(); + /*uint8_t bagSlot =*/ packet.readUInt8(); + /*uint32_t itemSlot =*/ packet.readUInt32(); + uint32_t itemId = packet.readUInt32(); + /*uint32_t suffixFactor =*/ packet.readUInt32(); + int32_t randomProp = static_cast(packet.readUInt32()); + uint32_t count = packet.readUInt32(); + /*uint32_t totalCount =*/ packet.readUInt32(); + queryItemInfo(itemId, 0); + if (showInChat) { + if (const ItemQueryResponseData* info = getItemInfo(itemId)) { + std::string itemName = info->name.empty() ? ("item #" + std::to_string(itemId)) : info->name; + if (randomProp != 0) { + std::string suffix = getRandomPropertyName(randomProp); + if (!suffix.empty()) itemName += " " + suffix; + } + uint32_t quality = info->quality; + std::string link = buildItemLink(itemId, quality, itemName); + std::string msg = "Received: " + link; + if (count > 1) msg += " x" + std::to_string(count); + addSystemChatMessage(msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLootItem(); }); + if (itemLootCallback_) itemLootCallback_(itemId, count, quality, itemName); + fireAddonEvent("CHAT_MSG_LOOT", {msg, "", std::to_string(itemId), std::to_string(count)}); + } else { + pendingItemPushNotifs_.push_back({itemId, count}); + } + } + fireAddonEvent("BAG_UPDATE", {}); + fireAddonEvent("UNIT_INVENTORY_CHANGED", {"player"}); + LOG_INFO("Item push: itemId=", itemId, " count=", count, " showInChat=", static_cast(showInChat)); + } + }; + registerHandler(Opcode::SMSG_LOGOUT_RESPONSE, &GameHandler::handleLogoutResponse); + registerHandler(Opcode::SMSG_LOGOUT_COMPLETE, &GameHandler::handleLogoutComplete); + registerHandler(Opcode::SMSG_NAME_QUERY_RESPONSE, &GameHandler::handleNameQueryResponse); + registerHandler(Opcode::SMSG_CREATURE_QUERY_RESPONSE, &GameHandler::handleCreatureQueryResponse); + registerHandler(Opcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE, &GameHandler::handleItemQueryResponse); + registerHandler(Opcode::SMSG_INSPECT_TALENT, &GameHandler::handleInspectResults); + registerSkipHandler(Opcode::SMSG_ADDON_INFO); + registerSkipHandler(Opcode::SMSG_EXPECTED_SPAM_RECORDS); + + // ----------------------------------------------------------------------- + // XP / exploration + // ----------------------------------------------------------------------- + registerHandler(Opcode::SMSG_LOG_XPGAIN, &GameHandler::handleXpGain); + dispatchTable_[Opcode::SMSG_EXPLORATION_EXPERIENCE] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint32_t areaId = packet.readUInt32(); + uint32_t xpGained = packet.readUInt32(); + if (xpGained > 0) { + std::string areaName = getAreaName(areaId); + std::string msg; + if (!areaName.empty()) { + msg = "Discovered " + areaName + "! Gained " + std::to_string(xpGained) + " experience."; + } else { + char buf[128]; + std::snprintf(buf, sizeof(buf), "Discovered new area! Gained %u experience.", xpGained); + msg = buf; + } + addSystemChatMessage(msg); + addCombatText(CombatTextEntry::XP_GAIN, static_cast(xpGained), 0, true); + if (areaDiscoveryCallback_) areaDiscoveryCallback_(areaName, xpGained); + fireAddonEvent("CHAT_MSG_COMBAT_XP_GAIN", {msg, std::to_string(xpGained)}); + } + } + }; + + // ----------------------------------------------------------------------- + // Pet feedback (pre-main pet block) + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_PET_TAME_FAILURE] = [this](network::Packet& packet) { + static const char* reasons[] = { + "Invalid creature", "Too many pets", "Already tamed", + "Wrong faction", "Level too low", "Creature not tameable", + "Can't control", "Can't command" + }; + if (packet.hasRemaining(1)) { + uint8_t reason = packet.readUInt8(); + const char* msg = (reason < 8) ? reasons[reason] : "Unknown reason"; + std::string s = std::string("Failed to tame: ") + msg; + addUIError(s); + addSystemChatMessage(s); + } + }; + dispatchTable_[Opcode::SMSG_PET_ACTION_FEEDBACK] = [this](network::Packet& packet) { + static const char* kPetFeedback[] = { + nullptr, + "Your pet is dead.", "Your pet has nothing to attack.", + "Your pet cannot attack that target.", "That target is too far away.", + "Your pet cannot find a path to the target.", + "Your pet cannot attack an immune target.", + }; + if (!packet.hasRemaining(1)) return; + uint8_t msg = packet.readUInt8(); + if (msg > 0 && msg < 7 && kPetFeedback[msg]) addSystemChatMessage(kPetFeedback[msg]); + packet.skipAll(); + }; + registerSkipHandler(Opcode::SMSG_PET_NAME_QUERY_RESPONSE); + + // ----------------------------------------------------------------------- + // Quest failures + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_QUESTUPDATE_FAILED] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t questId = packet.readUInt32(); + auto questTitle = getQuestTitle(questId); + addSystemChatMessage(questTitle.empty() ? std::string("Quest failed!") + : ('"' + questTitle + "\" failed!")); + } + }; + dispatchTable_[Opcode::SMSG_QUESTUPDATE_FAILEDTIMER] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t questId = packet.readUInt32(); + auto questTitle = getQuestTitle(questId); + addSystemChatMessage(questTitle.empty() ? std::string("Quest timed out!") + : ('"' + questTitle + "\" has timed out.")); + } + }; + + // ----------------------------------------------------------------------- + // Entity delta updates: health / power / world state / combo / timers / PvP + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_HEALTH_UPDATE] = [this](network::Packet& packet) { + const bool huTbc = isActiveExpansion("tbc"); + if (!packet.hasRemaining(huTbc ? 8u : 2u) ) return; + uint64_t guid = huTbc ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + uint32_t hp = packet.readUInt32(); + if (auto* unit = getUnitByGuid(guid)) unit->setHealth(hp); + if (guid != 0) { + auto unitId = guidToUnitId(guid); + if (!unitId.empty()) fireAddonEvent("UNIT_HEALTH", {unitId}); + } + }; + dispatchTable_[Opcode::SMSG_POWER_UPDATE] = [this](network::Packet& packet) { + const bool puTbc = isActiveExpansion("tbc"); + if (!packet.hasRemaining(puTbc ? 8u : 2u) ) return; + uint64_t guid = puTbc ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(5)) return; + uint8_t powerType = packet.readUInt8(); + uint32_t value = packet.readUInt32(); + if (auto* unit = getUnitByGuid(guid)) unit->setPowerByType(powerType, value); + if (guid != 0) { + auto unitId = guidToUnitId(guid); + if (!unitId.empty()) { + fireAddonEvent("UNIT_POWER", {unitId}); + if (guid == playerGuid) { + fireAddonEvent("ACTIONBAR_UPDATE_USABLE", {}); + fireAddonEvent("SPELL_UPDATE_USABLE", {}); + } + } + } + }; + dispatchTable_[Opcode::SMSG_UPDATE_WORLD_STATE] = [this](network::Packet& packet) { + if (!packet.hasRemaining(8)) return; + uint32_t field = packet.readUInt32(); + uint32_t value = packet.readUInt32(); + worldStates_[field] = value; + LOG_DEBUG("SMSG_UPDATE_WORLD_STATE: field=", field, " value=", value); + fireAddonEvent("UPDATE_WORLD_STATES", {}); + }; + dispatchTable_[Opcode::SMSG_WORLD_STATE_UI_TIMER_UPDATE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t serverTime = packet.readUInt32(); + LOG_DEBUG("SMSG_WORLD_STATE_UI_TIMER_UPDATE: serverTime=", serverTime); + } + }; + dispatchTable_[Opcode::SMSG_PVP_CREDIT] = [this](network::Packet& packet) { + if (packet.hasRemaining(16)) { + uint32_t honor = packet.readUInt32(); + uint64_t victimGuid = packet.readUInt64(); + uint32_t rank = packet.readUInt32(); + LOG_INFO("SMSG_PVP_CREDIT: honor=", honor, " victim=0x", std::hex, victimGuid, std::dec, " rank=", rank); + std::string msg = "You gain " + std::to_string(honor) + " honor points."; + addSystemChatMessage(msg); + if (honor > 0) addCombatText(CombatTextEntry::HONOR_GAIN, static_cast(honor), 0, true); + if (pvpHonorCallback_) pvpHonorCallback_(honor, victimGuid, rank); + fireAddonEvent("CHAT_MSG_COMBAT_HONOR_GAIN", {msg}); + } + }; + dispatchTable_[Opcode::SMSG_UPDATE_COMBO_POINTS] = [this](network::Packet& packet) { + const bool cpTbc = isActiveExpansion("tbc"); + if (!packet.hasRemaining(cpTbc ? 8u : 2u) ) return; + uint64_t target = cpTbc ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(1)) return; + comboPoints_ = packet.readUInt8(); + comboTarget_ = target; + LOG_DEBUG("SMSG_UPDATE_COMBO_POINTS: target=0x", std::hex, target, + std::dec, " points=", static_cast(comboPoints_)); + fireAddonEvent("PLAYER_COMBO_POINTS", {}); + }; + dispatchTable_[Opcode::SMSG_START_MIRROR_TIMER] = [this](network::Packet& packet) { + if (!packet.hasRemaining(21)) return; + uint32_t type = packet.readUInt32(); + int32_t value = static_cast(packet.readUInt32()); + int32_t maxV = static_cast(packet.readUInt32()); + int32_t scale = static_cast(packet.readUInt32()); + /*uint32_t tracker =*/ packet.readUInt32(); + uint8_t paused = packet.readUInt8(); + if (type < 3) { + mirrorTimers_[type].value = value; + mirrorTimers_[type].maxValue = maxV; + mirrorTimers_[type].scale = scale; + mirrorTimers_[type].paused = (paused != 0); + mirrorTimers_[type].active = true; + fireAddonEvent("MIRROR_TIMER_START", { + std::to_string(type), std::to_string(value), + std::to_string(maxV), std::to_string(scale), + paused ? "1" : "0"}); + } + }; + dispatchTable_[Opcode::SMSG_STOP_MIRROR_TIMER] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t type = packet.readUInt32(); + if (type < 3) { + mirrorTimers_[type].active = false; + mirrorTimers_[type].value = 0; + fireAddonEvent("MIRROR_TIMER_STOP", {std::to_string(type)}); + } + }; + dispatchTable_[Opcode::SMSG_PAUSE_MIRROR_TIMER] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint32_t type = packet.readUInt32(); + uint8_t paused = packet.readUInt8(); + if (type < 3) { + mirrorTimers_[type].paused = (paused != 0); + fireAddonEvent("MIRROR_TIMER_PAUSE", {paused ? "1" : "0"}); + } + }; + + // ----------------------------------------------------------------------- + // Cast result / spell proc + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_CAST_RESULT] = [this](network::Packet& packet) { + uint32_t castResultSpellId = 0; + uint8_t castResult = 0; + if (packetParsers_->parseCastResult(packet, castResultSpellId, castResult)) { + if (castResult != 0) { + casting = false; castIsChannel = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; + lastInteractedGoGuid_ = 0; + craftQueueSpellId_ = 0; craftQueueRemaining_ = 0; + queuedSpellId_ = 0; queuedSpellTarget_ = 0; + int playerPowerType = -1; + if (auto pe = entityManager.getEntity(playerGuid)) { + if (auto pu = std::dynamic_pointer_cast(pe)) + playerPowerType = static_cast(pu->getPowerType()); + } + const char* reason = getSpellCastResultString(castResult, playerPowerType); + std::string errMsg = reason ? reason + : ("Spell cast failed (error " + std::to_string(castResult) + ")"); + addUIError(errMsg); + if (spellCastFailedCallback_) spellCastFailedCallback_(castResultSpellId); + fireAddonEvent("UNIT_SPELLCAST_FAILED", {"player", std::to_string(castResultSpellId)}); + fireAddonEvent("UNIT_SPELLCAST_STOP", {"player", std::to_string(castResultSpellId)}); + MessageChatData msg; + msg.type = ChatType::SYSTEM; + msg.language = ChatLanguage::UNIVERSAL; + msg.message = errMsg; + addLocalChatMessage(msg); + } + } + }; + dispatchTable_[Opcode::SMSG_SPELL_FAILED_OTHER] = [this](network::Packet& packet) { + const bool tbcLike2 = isPreWotlk(); + uint64_t failOtherGuid = tbcLike2 + ? (packet.hasRemaining(8) ? packet.readUInt64() : 0) + : packet.readPackedGuid(); + if (failOtherGuid != 0 && failOtherGuid != playerGuid) { + unitCastStates_.erase(failOtherGuid); + if (addonEventCallback_) { + std::string unitId; + if (failOtherGuid == targetGuid) unitId = "target"; + else if (failOtherGuid == focusGuid) unitId = "focus"; + if (!unitId.empty()) { + fireAddonEvent("UNIT_SPELLCAST_FAILED", {unitId}); + fireAddonEvent("UNIT_SPELLCAST_STOP", {unitId}); + } + } + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_PROCRESIST] = [this](network::Packet& packet) { + const bool prUsesFullGuid = isActiveExpansion("tbc"); + auto readPrGuid = [&]() -> uint64_t { + if (prUsesFullGuid) + return (packet.hasRemaining(8)) ? packet.readUInt64() : 0; + return packet.readPackedGuid(); + }; + if (!packet.hasRemaining(prUsesFullGuid ? 8u : 1u) || (!prUsesFullGuid && !packet.hasFullPackedGuid())) { packet.skipAll(); return; } + uint64_t caster = readPrGuid(); + if (!packet.hasRemaining(prUsesFullGuid ? 8u : 1u) || (!prUsesFullGuid && !packet.hasFullPackedGuid())) { packet.skipAll(); return; } + uint64_t victim = readPrGuid(); + if (!packet.hasRemaining(4)) return; + uint32_t spellId = packet.readUInt32(); + if (victim == playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim); + else if (caster == playerGuid) addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim); + packet.skipAll(); + }; + + // ----------------------------------------------------------------------- + // Loot roll + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_LOOT_START_ROLL] = [this](network::Packet& packet) { + const bool isWotLK = isActiveExpansion("wotlk"); + const size_t minSize = isWotLK ? 33u : 25u; + if (!packet.hasRemaining(minSize)) return; + uint64_t objectGuid = packet.readUInt64(); + /*uint32_t mapId =*/ packet.readUInt32(); + uint32_t slot = packet.readUInt32(); + uint32_t itemId = packet.readUInt32(); + int32_t rollRandProp = 0; + if (isWotLK) { + /*uint32_t randSuffix =*/ packet.readUInt32(); + rollRandProp = static_cast(packet.readUInt32()); + } + uint32_t countdown = packet.readUInt32(); + uint8_t voteMask = packet.readUInt8(); + pendingLootRollActive_ = true; + pendingLootRoll_.objectGuid = objectGuid; + pendingLootRoll_.slot = slot; + pendingLootRoll_.itemId = itemId; + queryItemInfo(itemId, 0); + auto* info = getItemInfo(itemId); + std::string rollItemName = info ? info->name : std::to_string(itemId); + if (rollRandProp != 0) { + std::string suffix = getRandomPropertyName(rollRandProp); + if (!suffix.empty()) rollItemName += " " + suffix; + } + pendingLootRoll_.itemName = rollItemName; + pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; + pendingLootRoll_.rollCountdownMs = (countdown > 0 && countdown <= 120000) ? countdown : 60000; + pendingLootRoll_.voteMask = voteMask; + pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now(); + LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName, + ") slot=", slot, " voteMask=0x", std::hex, static_cast(voteMask), std::dec); + fireAddonEvent("START_LOOT_ROLL", {std::to_string(slot), std::to_string(countdown)}); + }; + + // ----------------------------------------------------------------------- + // Pet stable + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::MSG_LIST_STABLED_PETS] = [this](network::Packet& packet) { + if (state == WorldState::IN_WORLD) handleListStabledPets(packet); + }; + dispatchTable_[Opcode::SMSG_STABLE_RESULT] = [this](network::Packet& packet) { + if (!packet.hasRemaining(1)) return; + uint8_t result = packet.readUInt8(); + const char* msg = nullptr; + switch (result) { + case 0x01: msg = "Pet stored in stable."; break; + case 0x06: msg = "Pet retrieved from stable."; break; + case 0x07: msg = "Stable slot purchased."; break; + case 0x08: msg = "Stable list updated."; break; + case 0x09: msg = "Stable failed: not enough money or other error."; addUIError(msg); break; + default: break; + } + if (msg) addSystemChatMessage(msg); + LOG_INFO("SMSG_STABLE_RESULT: result=", static_cast(result)); + if (stableWindowOpen_ && stableMasterGuid_ != 0 && socket && result <= 0x08) { + auto refreshPkt = ListStabledPetsPacket::build(stableMasterGuid_); + socket->send(refreshPkt); + } + }; + + // ----------------------------------------------------------------------- + // Titles / achievements / character services + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_TITLE_EARNED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(8)) return; + uint32_t titleBit = packet.readUInt32(); + uint32_t isLost = packet.readUInt32(); + loadTitleNameCache(); + std::string titleStr; + auto tit = titleNameCache_.find(titleBit); + if (tit != titleNameCache_.end() && !tit->second.empty()) { + const auto& ln = lookupName(playerGuid); + const std::string& pName = ln.empty() ? std::string("you") : ln; + const std::string& fmt = tit->second; + size_t pos = fmt.find("%s"); + if (pos != std::string::npos) + titleStr = fmt.substr(0, pos) + pName + fmt.substr(pos + 2); + else + titleStr = fmt; + } + std::string msg; + if (!titleStr.empty()) { + msg = isLost ? ("Title removed: " + titleStr + ".") : ("Title earned: " + titleStr + "!"); + } else { + char buf[64]; + std::snprintf(buf, sizeof(buf), isLost ? "Title removed (bit %u)." : "Title earned (bit %u)!", titleBit); + msg = buf; + } + if (isLost) knownTitleBits_.erase(titleBit); + else knownTitleBits_.insert(titleBit); + addSystemChatMessage(msg); + LOG_INFO("SMSG_TITLE_EARNED: bit=", titleBit, " lost=", isLost, " title='", titleStr, "'"); + }; + dispatchTable_[Opcode::SMSG_LEARNED_DANCE_MOVES] = [this](network::Packet& packet) { + LOG_DEBUG("SMSG_LEARNED_DANCE_MOVES: ignored (size=", packet.getSize(), ")"); + }; + dispatchTable_[Opcode::SMSG_CHAR_RENAME] = [this](network::Packet& packet) { + if (packet.hasRemaining(13)) { + uint32_t result = packet.readUInt32(); + /*uint64_t guid =*/ packet.readUInt64(); + std::string newName = packet.readString(); + if (result == 0) { + addSystemChatMessage("Character name changed to: " + newName); + } else { + static const char* kRenameErrors[] = { + nullptr, "Name already in use.", "Name too short.", "Name too long.", + "Name contains invalid characters.", "Name contains a profanity.", + "Name is reserved.", "Character name does not meet requirements.", + }; + const char* errMsg = (result < 8) ? kRenameErrors[result] : nullptr; + std::string renameErr = errMsg ? std::string("Rename failed: ") + errMsg : "Character rename failed."; + addUIError(renameErr); addSystemChatMessage(renameErr); + } + LOG_INFO("SMSG_CHAR_RENAME: result=", result, " newName=", newName); + } + }; + + // ----------------------------------------------------------------------- + // Bind / heartstone / phase / barber / corpse + // ----------------------------------------------------------------------- + dispatchTable_[Opcode::SMSG_PLAYERBOUND] = [this](network::Packet& packet) { + if (!packet.hasRemaining(16)) return; + /*uint64_t binderGuid =*/ packet.readUInt64(); + uint32_t mapId = packet.readUInt32(); + uint32_t zoneId = packet.readUInt32(); + homeBindMapId_ = mapId; + homeBindZoneId_ = zoneId; + std::string pbMsg = "Your home location has been set"; + std::string zoneName = getAreaName(zoneId); + if (!zoneName.empty()) pbMsg += " to " + zoneName; + pbMsg += '.'; + addSystemChatMessage(pbMsg); + }; + registerSkipHandler(Opcode::SMSG_BINDER_CONFIRM); + registerSkipHandler(Opcode::SMSG_SET_PHASE_SHIFT); + dispatchTable_[Opcode::SMSG_TOGGLE_XP_GAIN] = [this](network::Packet& packet) { + if (!packet.hasRemaining(1)) return; + uint8_t enabled = packet.readUInt8(); + addSystemChatMessage(enabled ? "XP gain enabled." : "XP gain disabled."); + }; + dispatchTable_[Opcode::SMSG_GOSSIP_POI] = [this](network::Packet& packet) { + if (!packet.hasRemaining(20)) return; + /*uint32_t flags =*/ packet.readUInt32(); + float poiX = packet.readFloat(); + float poiY = packet.readFloat(); + uint32_t icon = packet.readUInt32(); + uint32_t data = packet.readUInt32(); + std::string name = packet.readString(); + GossipPoi poi; poi.x = poiX; poi.y = poiY; poi.icon = icon; poi.data = data; poi.name = std::move(name); + if (gossipPois_.size() >= 200) gossipPois_.erase(gossipPois_.begin()); + gossipPois_.push_back(std::move(poi)); + LOG_DEBUG("SMSG_GOSSIP_POI: x=", poiX, " y=", poiY, " icon=", icon); + }; + dispatchTable_[Opcode::SMSG_BINDZONEREPLY] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t result = packet.readUInt32(); + if (result == 0) addSystemChatMessage("Your home is now set to this location."); + else { addUIError("You are too far from the innkeeper."); addSystemChatMessage("You are too far from the innkeeper."); } + } + }; + dispatchTable_[Opcode::SMSG_CHANGEPLAYER_DIFFICULTY_RESULT] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t result = packet.readUInt32(); + if (result == 0) { + addSystemChatMessage("Difficulty changed."); + } else { + static const char* reasons[] = { + "", "Error", "Too many members", "Already in dungeon", + "You are in a battleground", "Raid not allowed in heroic", + "You must be in a raid group", "Player not in group" + }; + const char* msg = (result < 8) ? reasons[result] : "Difficulty change failed."; + addUIError(std::string("Cannot change difficulty: ") + msg); + addSystemChatMessage(std::string("Cannot change difficulty: ") + msg); + } + } + }; + dispatchTable_[Opcode::SMSG_CORPSE_NOT_IN_INSTANCE] = [this](network::Packet& /*packet*/) { + addUIError("Your corpse is outside this instance."); + addSystemChatMessage("Your corpse is outside this instance. Release spirit to retrieve it."); + }; + dispatchTable_[Opcode::SMSG_CROSSED_INEBRIATION_THRESHOLD] = [this](network::Packet& packet) { + if (packet.hasRemaining(12)) { + uint64_t guid = packet.readUInt64(); + uint32_t threshold = packet.readUInt32(); + if (guid == playerGuid && threshold > 0) addSystemChatMessage("You feel rather drunk."); + LOG_DEBUG("SMSG_CROSSED_INEBRIATION_THRESHOLD: guid=0x", std::hex, guid, std::dec, " threshold=", threshold); + } + }; + dispatchTable_[Opcode::SMSG_CLEAR_FAR_SIGHT_IMMEDIATE] = [this](network::Packet& /*packet*/) { + LOG_DEBUG("SMSG_CLEAR_FAR_SIGHT_IMMEDIATE"); + }; + registerSkipHandler(Opcode::SMSG_COMBAT_EVENT_FAILED); + dispatchTable_[Opcode::SMSG_FORCE_ANIM] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint64_t animGuid = packet.readPackedGuid(); + if (packet.hasRemaining(4)) { + uint32_t animId = packet.readUInt32(); + if (emoteAnimCallback_) emoteAnimCallback_(animGuid, animId); + } + } + }; + // Consume silently — opcodes we receive but don't need to act on + for (auto op : { + Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM, Opcode::SMSG_GAMEOBJECT_RESET_STATE, + Opcode::SMSG_FLIGHT_SPLINE_SYNC, Opcode::SMSG_FORCE_DISPLAY_UPDATE, + Opcode::SMSG_FORCE_SEND_QUEUED_PACKETS, Opcode::SMSG_FORCE_SET_VEHICLE_REC_ID, + Opcode::SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE, Opcode::SMSG_DAMAGE_CALC_LOG, + Opcode::SMSG_DYNAMIC_DROP_ROLL_RESULT, Opcode::SMSG_DESTRUCTIBLE_BUILDING_DAMAGE, + }) { registerSkipHandler(op); } + dispatchTable_[Opcode::SMSG_FORCED_DEATH_UPDATE] = [this](network::Packet& packet) { + playerDead_ = true; + if (ghostStateCallback_) ghostStateCallback_(false); + fireAddonEvent("PLAYER_DEAD", {}); + addSystemChatMessage("You have been killed."); + LOG_INFO("SMSG_FORCED_DEATH_UPDATE: player force-killed"); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_DEFENSE_MESSAGE] = [this](network::Packet& packet) { + if (packet.hasRemaining(5)) { + /*uint32_t zoneId =*/ packet.readUInt32(); + std::string defMsg = packet.readString(); + if (!defMsg.empty()) addSystemChatMessage("[Defense] " + defMsg); + } + }; + dispatchTable_[Opcode::SMSG_CORPSE_RECLAIM_DELAY] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t delayMs = packet.readUInt32(); + auto nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + corpseReclaimAvailableMs_ = nowMs + delayMs; + LOG_INFO("SMSG_CORPSE_RECLAIM_DELAY: ", delayMs, "ms"); + } + }; + dispatchTable_[Opcode::SMSG_DEATH_RELEASE_LOC] = [this](network::Packet& packet) { + if (packet.hasRemaining(16)) { + uint32_t relMapId = packet.readUInt32(); + float relX = packet.readFloat(), relY = packet.readFloat(), relZ = packet.readFloat(); + LOG_INFO("SMSG_DEATH_RELEASE_LOC (graveyard spawn): map=", relMapId, " x=", relX, " y=", relY, " z=", relZ); + } + }; + dispatchTable_[Opcode::SMSG_ENABLE_BARBER_SHOP] = [this](network::Packet& /*packet*/) { + LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); + barberShopOpen_ = true; + fireAddonEvent("BARBER_SHOP_OPEN", {}); + }; + + // ---- Batch 3: Corpse/gametime, combat clearing, mount, loot notify, + // movement/speed/flags, attack, spells, group ---- + + dispatchTable_[Opcode::MSG_CORPSE_QUERY] = [this](network::Packet& packet) { + if (!packet.hasRemaining(1)) return; + uint8_t found = packet.readUInt8(); + if (found && packet.hasRemaining(20)) { + /*uint32_t mapId =*/ packet.readUInt32(); + float cx = packet.readFloat(); + float cy = packet.readFloat(); + float cz = packet.readFloat(); + uint32_t corpseMapId = packet.readUInt32(); + corpseX_ = cx; + corpseY_ = cy; + corpseZ_ = cz; + corpseMapId_ = corpseMapId; + LOG_INFO("MSG_CORPSE_QUERY: corpse at (", cx, ",", cy, ",", cz, ") map=", corpseMapId); + } + }; + dispatchTable_[Opcode::SMSG_FEIGN_DEATH_RESISTED] = [this](network::Packet& /*packet*/) { + addUIError("Your Feign Death was resisted."); + addSystemChatMessage("Your Feign Death attempt was resisted."); + }; + dispatchTable_[Opcode::SMSG_CHANNEL_MEMBER_COUNT] = [this](network::Packet& packet) { + std::string chanName = packet.readString(); + if (packet.hasRemaining(5)) { + /*uint8_t flags =*/ packet.readUInt8(); + uint32_t count = packet.readUInt32(); + LOG_DEBUG("SMSG_CHANNEL_MEMBER_COUNT: channel=", chanName, " members=", count); + } + }; + for (auto op : { Opcode::SMSG_GAMETIME_SET, Opcode::SMSG_GAMETIME_UPDATE }) { + dispatchTable_[op] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t gameTimePacked = packet.readUInt32(); + gameTime_ = static_cast(gameTimePacked); + } + packet.skipAll(); + }; + } + dispatchTable_[Opcode::SMSG_GAMESPEED_SET] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint32_t gameTimePacked = packet.readUInt32(); + float timeSpeed = packet.readFloat(); + gameTime_ = static_cast(gameTimePacked); + timeSpeed_ = timeSpeed; + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_GAMETIMEBIAS_SET] = [this](network::Packet& packet) { + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_ACHIEVEMENT_DELETED] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t achId = packet.readUInt32(); + earnedAchievements_.erase(achId); + achievementDates_.erase(achId); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_CRITERIA_DELETED] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t critId = packet.readUInt32(); + criteriaProgress_.erase(critId); + } + packet.skipAll(); + }; + + // Combat clearing + dispatchTable_[Opcode::SMSG_ATTACKSWING_DEADTARGET] = [this](network::Packet& /*packet*/) { + autoAttacking = false; + autoAttackTarget = 0; + }; + dispatchTable_[Opcode::SMSG_THREAT_CLEAR] = [this](network::Packet& /*packet*/) { + threatLists_.clear(); + fireAddonEvent("UNIT_THREAT_LIST_UPDATE", {}); + }; + dispatchTable_[Opcode::SMSG_THREAT_REMOVE] = [this](network::Packet& packet) { + if (!packet.hasRemaining(1)) return; + uint64_t unitGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(1)) return; + uint64_t victimGuid = packet.readPackedGuid(); + auto it = threatLists_.find(unitGuid); + if (it != threatLists_.end()) { + auto& list = it->second; + list.erase(std::remove_if(list.begin(), list.end(), + [victimGuid](const ThreatEntry& e){ return e.victimGuid == victimGuid; }), + list.end()); + if (list.empty()) threatLists_.erase(it); + } + }; + dispatchTable_[Opcode::SMSG_CANCEL_COMBAT] = [this](network::Packet& /*packet*/) { + autoAttacking = false; + autoAttackTarget = 0; + autoAttackRequested_ = false; + }; + dispatchTable_[Opcode::SMSG_BREAK_TARGET] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint64_t bGuid = packet.readUInt64(); + if (bGuid == targetGuid) targetGuid = 0; + } + }; + dispatchTable_[Opcode::SMSG_CLEAR_TARGET] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint64_t cGuid = packet.readUInt64(); + if (cGuid == 0 || cGuid == targetGuid) targetGuid = 0; + } + }; + + // Mount/dismount + dispatchTable_[Opcode::SMSG_DISMOUNT] = [this](network::Packet& /*packet*/) { + currentMountDisplayId_ = 0; + if (mountCallback_) mountCallback_(0); + }; + dispatchTable_[Opcode::SMSG_MOUNTRESULT] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t result = packet.readUInt32(); + if (result != 4) { + const char* msgs[] = { "Cannot mount here.", "Invalid mount spell.", + "Too far away to mount.", "Already mounted." }; + std::string mountErr = result < 4 ? msgs[result] : "Cannot mount."; + addUIError(mountErr); + addSystemChatMessage(mountErr); + } + }; + dispatchTable_[Opcode::SMSG_DISMOUNTRESULT] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t result = packet.readUInt32(); + if (result != 0) { + addUIError("Cannot dismount here."); + addSystemChatMessage("Cannot dismount here."); + } + }; + + // Loot notifications + dispatchTable_[Opcode::SMSG_LOOT_ALL_PASSED] = [this](network::Packet& packet) { + const bool isWotLK = isActiveExpansion("wotlk"); + const size_t minSize = isWotLK ? 24u : 16u; + if (!packet.hasRemaining(minSize)) return; + /*uint64_t objGuid =*/ packet.readUInt64(); + /*uint32_t slot =*/ packet.readUInt32(); + uint32_t itemId = packet.readUInt32(); + if (isWotLK) { + /*uint32_t randSuffix =*/ packet.readUInt32(); + /*uint32_t randProp =*/ packet.readUInt32(); + } + auto* info = getItemInfo(itemId); + std::string allPassName = info && !info->name.empty() ? info->name : std::to_string(itemId); + uint32_t allPassQuality = info ? info->quality : 1u; + addSystemChatMessage("Everyone passed on " + buildItemLink(itemId, allPassQuality, allPassName) + "."); + pendingLootRollActive_ = false; + }; + dispatchTable_[Opcode::SMSG_LOOT_ITEM_NOTIFY] = [this](network::Packet& packet) { + if (!packet.hasRemaining(24)) { + packet.skipAll(); return; + } + uint64_t looterGuid = packet.readUInt64(); + /*uint64_t lootGuid =*/ packet.readUInt64(); + uint32_t itemId = packet.readUInt32(); + uint32_t count = packet.readUInt32(); + if (isInGroup() && looterGuid != playerGuid) { + const auto& looterName = lookupName(looterGuid); + if (!looterName.empty()) { + queryItemInfo(itemId, 0); + std::string itemName = "item #" + std::to_string(itemId); + uint32_t notifyQuality = 1; + if (const ItemQueryResponseData* info = getItemInfo(itemId)) { + if (!info->name.empty()) itemName = info->name; + notifyQuality = info->quality; + } + std::string itemLink2 = buildItemLink(itemId, notifyQuality, itemName); + std::string lootMsg = looterName + " loots " + itemLink2; + if (count > 1) lootMsg += " x" + std::to_string(count); + lootMsg += "."; + addSystemChatMessage(lootMsg); + } + } + }; + dispatchTable_[Opcode::SMSG_LOOT_SLOT_CHANGED] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t slotIndex = packet.readUInt8(); + for (auto it = currentLoot.items.begin(); it != currentLoot.items.end(); ++it) { + if (it->slotIndex == slotIndex) { + currentLoot.items.erase(it); + break; + } + } + } + }; + + // Creature movement + registerHandler(Opcode::SMSG_MONSTER_MOVE, &GameHandler::handleMonsterMove); + registerHandler(Opcode::SMSG_COMPRESSED_MOVES, &GameHandler::handleCompressedMoves); + registerHandler(Opcode::SMSG_MONSTER_MOVE_TRANSPORT, &GameHandler::handleMonsterMoveTransport); + + // Spline move: consume-only (no state change) + for (auto op : { Opcode::SMSG_SPLINE_MOVE_FEATHER_FALL, + Opcode::SMSG_SPLINE_MOVE_GRAVITY_DISABLE, + Opcode::SMSG_SPLINE_MOVE_GRAVITY_ENABLE, + Opcode::SMSG_SPLINE_MOVE_LAND_WALK, + Opcode::SMSG_SPLINE_MOVE_NORMAL_FALL, + Opcode::SMSG_SPLINE_MOVE_ROOT, + Opcode::SMSG_SPLINE_MOVE_SET_HOVER }) { + dispatchTable_[op] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) + (void)packet.readPackedGuid(); + }; + } + + // Spline move: synth flags (each opcode produces different flags) + { + auto makeSynthHandler = [this](uint32_t synthFlags) { + return [this, synthFlags](network::Packet& packet) { + if (!packet.hasRemaining(1)) return; + uint64_t guid = packet.readPackedGuid(); + if (guid == 0 || guid == playerGuid || !unitMoveFlagsCallback_) return; + unitMoveFlagsCallback_(guid, synthFlags); + }; + }; + dispatchTable_[Opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE] = makeSynthHandler(0x00000100u); + dispatchTable_[Opcode::SMSG_SPLINE_MOVE_SET_RUN_MODE] = makeSynthHandler(0u); + dispatchTable_[Opcode::SMSG_SPLINE_MOVE_SET_FLYING] = makeSynthHandler(0x01000000u | 0x00800000u); + dispatchTable_[Opcode::SMSG_SPLINE_MOVE_START_SWIM] = makeSynthHandler(0x00200000u); + dispatchTable_[Opcode::SMSG_SPLINE_MOVE_STOP_SWIM] = makeSynthHandler(0u); + } + + // Spline speed: each opcode updates a different speed member + dispatchTable_[Opcode::SMSG_SPLINE_SET_RUN_SPEED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t guid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float speed = packet.readFloat(); + if (guid == playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f) + serverRunSpeed_ = speed; + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t guid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float speed = packet.readFloat(); + if (guid == playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f) + serverRunBackSpeed_ = speed; + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_SWIM_SPEED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t guid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float speed = packet.readFloat(); + if (guid == playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f) + serverSwimSpeed_ = speed; + }; + + // Force speed changes + registerHandler(Opcode::SMSG_FORCE_RUN_SPEED_CHANGE, &GameHandler::handleForceRunSpeedChange); + dispatchTable_[Opcode::SMSG_FORCE_MOVE_ROOT] = [this](network::Packet& packet) { handleForceMoveRootState(packet, true); }; + dispatchTable_[Opcode::SMSG_FORCE_MOVE_UNROOT] = [this](network::Packet& packet) { handleForceMoveRootState(packet, false); }; + dispatchTable_[Opcode::SMSG_FORCE_WALK_SPEED_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "WALK_SPEED", Opcode::CMSG_FORCE_WALK_SPEED_CHANGE_ACK, &serverWalkSpeed_); + }; + dispatchTable_[Opcode::SMSG_FORCE_RUN_BACK_SPEED_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "RUN_BACK_SPEED", Opcode::CMSG_FORCE_RUN_BACK_SPEED_CHANGE_ACK, &serverRunBackSpeed_); + }; + dispatchTable_[Opcode::SMSG_FORCE_SWIM_SPEED_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "SWIM_SPEED", Opcode::CMSG_FORCE_SWIM_SPEED_CHANGE_ACK, &serverSwimSpeed_); + }; + dispatchTable_[Opcode::SMSG_FORCE_SWIM_BACK_SPEED_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "SWIM_BACK_SPEED", Opcode::CMSG_FORCE_SWIM_BACK_SPEED_CHANGE_ACK, &serverSwimBackSpeed_); + }; + dispatchTable_[Opcode::SMSG_FORCE_FLIGHT_SPEED_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "FLIGHT_SPEED", Opcode::CMSG_FORCE_FLIGHT_SPEED_CHANGE_ACK, &serverFlightSpeed_); + }; + dispatchTable_[Opcode::SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "FLIGHT_BACK_SPEED", Opcode::CMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE_ACK, &serverFlightBackSpeed_); + }; + dispatchTable_[Opcode::SMSG_FORCE_TURN_RATE_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "TURN_RATE", Opcode::CMSG_FORCE_TURN_RATE_CHANGE_ACK, &serverTurnRate_); + }; + dispatchTable_[Opcode::SMSG_FORCE_PITCH_RATE_CHANGE] = [this](network::Packet& packet) { + handleForceSpeedChange(packet, "PITCH_RATE", Opcode::CMSG_FORCE_PITCH_RATE_CHANGE_ACK, &serverPitchRate_); + }; + + // Movement flag toggles + dispatchTable_[Opcode::SMSG_MOVE_SET_CAN_FLY] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "SET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK, + static_cast(MovementFlags::CAN_FLY), true); + }; + dispatchTable_[Opcode::SMSG_MOVE_UNSET_CAN_FLY] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "UNSET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK, + static_cast(MovementFlags::CAN_FLY), false); + }; + dispatchTable_[Opcode::SMSG_MOVE_FEATHER_FALL] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "FEATHER_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK, + static_cast(MovementFlags::FEATHER_FALL), true); + }; + dispatchTable_[Opcode::SMSG_MOVE_WATER_WALK] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "WATER_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK, + static_cast(MovementFlags::WATER_WALK), true); + }; + dispatchTable_[Opcode::SMSG_MOVE_SET_HOVER] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "SET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK, + static_cast(MovementFlags::HOVER), true); + }; + dispatchTable_[Opcode::SMSG_MOVE_UNSET_HOVER] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "UNSET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK, + static_cast(MovementFlags::HOVER), false); + }; + registerHandler(Opcode::SMSG_MOVE_KNOCK_BACK, &GameHandler::handleMoveKnockBack); + + // Camera shake + dispatchTable_[Opcode::SMSG_CAMERA_SHAKE] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint32_t shakeId = packet.readUInt32(); + uint32_t shakeType = packet.readUInt32(); + (void)shakeType; + float magnitude = (shakeId < 50) ? 0.04f : 0.08f; + if (cameraShakeCallback_) + cameraShakeCallback_(magnitude, 18.0f, 0.5f); + } + }; + + // Attack/combat delegates + registerHandler(Opcode::SMSG_ATTACKSTART, &GameHandler::handleAttackStart); + registerHandler(Opcode::SMSG_ATTACKSTOP, &GameHandler::handleAttackStop); + dispatchTable_[Opcode::SMSG_ATTACKSWING_NOTINRANGE] = [this](network::Packet& /*packet*/) { + autoAttackOutOfRange_ = true; + if (autoAttackRangeWarnCooldown_ <= 0.0f) { + addSystemChatMessage("Target is too far away."); + autoAttackRangeWarnCooldown_ = 1.25f; + } + }; + dispatchTable_[Opcode::SMSG_ATTACKSWING_BADFACING] = [this](network::Packet& /*packet*/) { if (autoAttackRequested_ && autoAttackTarget != 0) { auto targetEntity = entityManager.getEntity(autoAttackTarget); if (targetEntity) { - // Use latest server-authoritative target position to avoid stale - // interpolation snapshots masking out-of-range states. - const float targetX = targetEntity->getLatestX(); - const float targetY = targetEntity->getLatestY(); - const float targetZ = targetEntity->getLatestZ(); - float dx = movementInfo.x - targetX; - float dy = movementInfo.y - targetY; - float dz = movementInfo.z - targetZ; - float dist = std::sqrt(dx * dx + dy * dy); - float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz); - const bool classicLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (dist > 40.0f) { - stopAutoAttack(); - LOG_INFO("Left combat: target too far (", dist, " yards)"); - } else if (state == WorldState::IN_WORLD && socket) { - bool allowResync = true; - const float meleeRange = classicLike ? 5.25f : 5.75f; - if (dist3d > meleeRange) { - autoAttackOutOfRange_ = true; - autoAttackOutOfRangeTime_ += deltaTime; - if (autoAttackRangeWarnCooldown_ <= 0.0f) { - addSystemChatMessage("Target is too far away."); - addUIError("Target is too far away."); - autoAttackRangeWarnCooldown_ = 1.25f; - } - // Stop chasing stale swings when the target remains out of range. - if (autoAttackOutOfRangeTime_ > 2.0f && dist3d > 9.0f) { - stopAutoAttack(); - addSystemChatMessage("Auto-attack stopped: target out of range."); - allowResync = false; - } - } else { - autoAttackOutOfRange_ = false; - autoAttackOutOfRangeTime_ = 0.0f; - } - - if (allowResync) { - autoAttackResendTimer_ += deltaTime; - autoAttackFacingSyncTimer_ += deltaTime; - - // Re-request swing more aggressively until server confirms active loop. - float resendInterval = 1.0f; - if (!autoAttacking || autoAttackOutOfRange_) { - resendInterval = classicLike ? 0.25f : 0.50f; - } - if (autoAttackResendTimer_ >= resendInterval) { - autoAttackResendTimer_ = 0.0f; - auto pkt = AttackSwingPacket::build(autoAttackTarget); - socket->send(pkt); - } - - // Keep server-facing aligned with our current melee target. - // Some vanilla-family realms become strict about front-arc checks unless - // the client sends explicit facing updates while stationary. - const float facingSyncInterval = classicLike ? 0.10f : 0.20f; - if (autoAttackFacingSyncTimer_ >= facingSyncInterval) { - autoAttackFacingSyncTimer_ = 0.0f; - float toTargetX = targetX - movementInfo.x; - float toTargetY = targetY - movementInfo.y; - bool sentMovement = false; - if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) { - float desired = std::atan2(-toTargetY, toTargetX); - float diff = desired - movementInfo.orientation; - while (diff > static_cast(M_PI)) diff -= 2.0f * static_cast(M_PI); - while (diff < -static_cast(M_PI)) diff += 2.0f * static_cast(M_PI); - const float facingThreshold = classicLike ? 0.035f : 0.12f; // ~2deg / ~7deg - if (std::abs(diff) > facingThreshold) { - movementInfo.orientation = desired; - sendMovement(Opcode::MSG_MOVE_SET_FACING); - // Follow facing update with a heartbeat to tighten server range/facing checks. - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - sentMovement = true; - } - } else if (classicLike) { - // Keep stationary melee position/facing fresh for strict vanilla-family checks. - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - sentMovement = true; - } - - // Even when facing is already correct, keep position fresh while - // trying to connect melee hits so servers don't require a step. - if (!sentMovement && (!autoAttacking || autoAttackOutOfRange_)) { - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - } - } - } + float toTargetX = targetEntity->getX() - movementInfo.x; + float toTargetY = targetEntity->getY() - movementInfo.y; + if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) { + movementInfo.orientation = std::atan2(-toTargetY, toTargetX); + sendMovement(Opcode::MSG_MOVE_SET_FACING); } } } + }; + dispatchTable_[Opcode::SMSG_ATTACKSWING_NOTSTANDING] = [this](network::Packet& /*packet*/) { + autoAttackOutOfRange_ = false; + autoAttackOutOfRangeTime_ = 0.0f; + if (autoAttackRangeWarnCooldown_ <= 0.0f) { + addSystemChatMessage("You need to stand up to fight."); + autoAttackRangeWarnCooldown_ = 1.25f; + } + }; + dispatchTable_[Opcode::SMSG_ATTACKSWING_CANT_ATTACK] = [this](network::Packet& /*packet*/) { + stopAutoAttack(); + if (autoAttackRangeWarnCooldown_ <= 0.0f) { + addSystemChatMessage("You can't attack that."); + autoAttackRangeWarnCooldown_ = 1.25f; + } + }; + registerHandler(Opcode::SMSG_ATTACKERSTATEUPDATE, &GameHandler::handleAttackerStateUpdate); + dispatchTable_[Opcode::SMSG_AI_REACTION] = [this](network::Packet& packet) { + if (!packet.hasRemaining(12)) return; + uint64_t guid = packet.readUInt64(); + uint32_t reaction = packet.readUInt32(); + if (reaction == 2 && npcAggroCallback_) { + auto entity = entityManager.getEntity(guid); + if (entity) + npcAggroCallback_(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + } + }; + registerHandler(Opcode::SMSG_SPELLNONMELEEDAMAGELOG, &GameHandler::handleSpellDamageLog); + dispatchTable_[Opcode::SMSG_PLAY_SPELL_VISUAL] = [this](network::Packet& packet) { + if (!packet.hasRemaining(12)) return; + uint64_t casterGuid = packet.readUInt64(); + uint32_t visualId = packet.readUInt32(); + if (visualId == 0) return; + auto* renderer = core::Application::getInstance().getRenderer(); + if (!renderer) return; + glm::vec3 spawnPos; + if (casterGuid == playerGuid) { + spawnPos = renderer->getCharacterPosition(); + } else { + auto entity = entityManager.getEntity(casterGuid); + if (!entity) return; + glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); + spawnPos = core::coords::canonicalToRender(canonical); + } + renderer->playSpellVisual(visualId, spawnPos); + }; + registerHandler(Opcode::SMSG_SPELLHEALLOG, &GameHandler::handleSpellHealLog); - // Keep active melee attackers visually facing the player as positions change. - // Some servers don't stream frequent orientation updates during combat. - if (!hostileAttackers_.empty()) { - for (uint64_t attackerGuid : hostileAttackers_) { - auto attacker = entityManager.getEntity(attackerGuid); - if (!attacker) continue; - float dx = movementInfo.x - attacker->getX(); - float dy = movementInfo.y - attacker->getY(); - if (std::abs(dx) < 0.01f && std::abs(dy) < 0.01f) continue; - attacker->setOrientation(std::atan2(-dy, dx)); + // Spell delegates + registerHandler(Opcode::SMSG_INITIAL_SPELLS, &GameHandler::handleInitialSpells); + registerHandler(Opcode::SMSG_CAST_FAILED, &GameHandler::handleCastFailed); + registerHandler(Opcode::SMSG_SPELL_START, &GameHandler::handleSpellStart); + registerHandler(Opcode::SMSG_SPELL_GO, &GameHandler::handleSpellGo); + registerHandler(Opcode::SMSG_SPELL_COOLDOWN, &GameHandler::handleSpellCooldown); + registerHandler(Opcode::SMSG_COOLDOWN_EVENT, &GameHandler::handleCooldownEvent); + dispatchTable_[Opcode::SMSG_CLEAR_COOLDOWN] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t spellId = packet.readUInt32(); + spellCooldowns.erase(spellId); + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) + slot.cooldownRemaining = 0.0f; } } - - // Close vendor/gossip/taxi window if player walks too far from NPC - if (vendorWindowOpen && currentVendorItems.vendorGuid != 0) { - auto npc = entityManager.getEntity(currentVendorItems.vendorGuid); - if (npc) { - float dx = movementInfo.x - npc->getX(); - float dy = movementInfo.y - npc->getY(); - float dist = std::sqrt(dx * dx + dy * dy); - if (dist > 15.0f) { - closeVendor(); - LOG_INFO("Vendor closed: walked too far from NPC"); + }; + dispatchTable_[Opcode::SMSG_MODIFY_COOLDOWN] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint32_t spellId = packet.readUInt32(); + int32_t diffMs = static_cast(packet.readUInt32()); + float diffSec = diffMs / 1000.0f; + auto it = spellCooldowns.find(spellId); + if (it != spellCooldowns.end()) { + it->second = std::max(0.0f, it->second + diffSec); + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) + slot.cooldownRemaining = std::max(0.0f, slot.cooldownRemaining + diffSec); } } } - if (gossipWindowOpen && currentGossip.npcGuid != 0) { - auto npc = entityManager.getEntity(currentGossip.npcGuid); - if (npc) { - float dx = movementInfo.x - npc->getX(); - float dy = movementInfo.y - npc->getY(); - float dist = std::sqrt(dx * dx + dy * dy); - if (dist > 15.0f) { - closeGossip(); - LOG_INFO("Gossip closed: walked too far from NPC"); + }; + registerHandler(Opcode::SMSG_LEARNED_SPELL, &GameHandler::handleLearnedSpell); + registerHandler(Opcode::SMSG_SUPERCEDED_SPELL, &GameHandler::handleSupercededSpell); + registerHandler(Opcode::SMSG_REMOVED_SPELL, &GameHandler::handleRemovedSpell); + registerHandler(Opcode::SMSG_SEND_UNLEARN_SPELLS, &GameHandler::handleUnlearnSpells); + registerHandler(Opcode::SMSG_TALENTS_INFO, &GameHandler::handleTalentsInfo); + + // Group + registerHandler(Opcode::SMSG_GROUP_INVITE, &GameHandler::handleGroupInvite); + registerHandler(Opcode::SMSG_GROUP_DECLINE, &GameHandler::handleGroupDecline); + registerHandler(Opcode::SMSG_GROUP_LIST, &GameHandler::handleGroupList); + dispatchTable_[Opcode::SMSG_GROUP_DESTROYED] = [this](network::Packet& /*packet*/) { + partyData.members.clear(); + partyData.memberCount = 0; + partyData.leaderGuid = 0; + addUIError("Your party has been disbanded."); + addSystemChatMessage("Your party has been disbanded."); + fireAddonEvent("GROUP_ROSTER_UPDATE", {}); + fireAddonEvent("PARTY_MEMBERS_CHANGED", {}); + }; + dispatchTable_[Opcode::SMSG_GROUP_CANCEL] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("Group invite cancelled."); + }; + registerHandler(Opcode::SMSG_GROUP_UNINVITE, &GameHandler::handleGroupUninvite); + registerHandler(Opcode::SMSG_PARTY_COMMAND_RESULT, &GameHandler::handlePartyCommandResult); + dispatchTable_[Opcode::SMSG_PARTY_MEMBER_STATS] = [this](network::Packet& packet) { handlePartyMemberStats(packet, false); }; + dispatchTable_[Opcode::SMSG_PARTY_MEMBER_STATS_FULL] = [this](network::Packet& packet) { handlePartyMemberStats(packet, true); }; + + // ---- Batch 4: Ready check, duels, guild, loot/gossip/vendor, factions, spell mods ---- + + // Ready check + dispatchTable_[Opcode::MSG_RAID_READY_CHECK] = [this](network::Packet& packet) { + pendingReadyCheck_ = true; + readyCheckReadyCount_ = 0; + readyCheckNotReadyCount_ = 0; + readyCheckInitiator_.clear(); + readyCheckResults_.clear(); + if (packet.hasRemaining(8)) { + uint64_t initiatorGuid = packet.readUInt64(); + if (auto* unit = getUnitByGuid(initiatorGuid)) + readyCheckInitiator_ = unit->getName(); + } + if (readyCheckInitiator_.empty() && partyData.leaderGuid != 0) { + for (const auto& member : partyData.members) { + if (member.guid == partyData.leaderGuid) { readyCheckInitiator_ = member.name; break; } + } + } + addSystemChatMessage(readyCheckInitiator_.empty() + ? "Ready check initiated!" + : readyCheckInitiator_ + " initiated a ready check!"); + fireAddonEvent("READY_CHECK", {readyCheckInitiator_}); + }; + dispatchTable_[Opcode::MSG_RAID_READY_CHECK_CONFIRM] = [this](network::Packet& packet) { + if (!packet.hasRemaining(9)) { packet.skipAll(); return; } + uint64_t respGuid = packet.readUInt64(); + uint8_t isReady = packet.readUInt8(); + if (isReady) ++readyCheckReadyCount_; else ++readyCheckNotReadyCount_; + const auto& rname = lookupName(respGuid); + if (!rname.empty()) { + bool found = false; + for (auto& r : readyCheckResults_) { + if (r.name == rname) { r.ready = (isReady != 0); found = true; break; } + } + if (!found) readyCheckResults_.push_back({ rname, isReady != 0 }); + char rbuf[128]; + std::snprintf(rbuf, sizeof(rbuf), "%s is %s.", rname.c_str(), isReady ? "Ready" : "Not Ready"); + addSystemChatMessage(rbuf); + } + if (addonEventCallback_) { + char guidBuf[32]; + snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)respGuid); + fireAddonEvent("READY_CHECK_CONFIRM", {guidBuf, isReady ? "1" : "0"}); + } + }; + dispatchTable_[Opcode::MSG_RAID_READY_CHECK_FINISHED] = [this](network::Packet& /*packet*/) { + char fbuf[128]; + std::snprintf(fbuf, sizeof(fbuf), "Ready check complete: %u ready, %u not ready.", + readyCheckReadyCount_, readyCheckNotReadyCount_); + addSystemChatMessage(fbuf); + pendingReadyCheck_ = false; + readyCheckReadyCount_ = 0; + readyCheckNotReadyCount_ = 0; + readyCheckResults_.clear(); + fireAddonEvent("READY_CHECK_FINISHED", {}); + }; + registerHandler(Opcode::SMSG_RAID_INSTANCE_INFO, &GameHandler::handleRaidInstanceInfo); + + // Duels + registerHandler(Opcode::SMSG_DUEL_REQUESTED, &GameHandler::handleDuelRequested); + registerHandler(Opcode::SMSG_DUEL_COMPLETE, &GameHandler::handleDuelComplete); + registerHandler(Opcode::SMSG_DUEL_WINNER, &GameHandler::handleDuelWinner); + dispatchTable_[Opcode::SMSG_DUEL_OUTOFBOUNDS] = [this](network::Packet& /*packet*/) { + addUIError("You are out of the duel area!"); + addSystemChatMessage("You are out of the duel area!"); + }; + dispatchTable_[Opcode::SMSG_DUEL_INBOUNDS] = [this](network::Packet& /*packet*/) {}; + dispatchTable_[Opcode::SMSG_DUEL_COUNTDOWN] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t ms = packet.readUInt32(); + duelCountdownMs_ = (ms > 0 && ms <= 30000) ? ms : 3000; + duelCountdownStartedAt_ = std::chrono::steady_clock::now(); + } + }; + dispatchTable_[Opcode::SMSG_PARTYKILLLOG] = [this](network::Packet& packet) { + if (!packet.hasRemaining(16)) return; + uint64_t killerGuid = packet.readUInt64(); + uint64_t victimGuid = packet.readUInt64(); + const auto& killerName = lookupName(killerGuid); + const auto& victimName = lookupName(victimGuid); + if (!killerName.empty() && !victimName.empty()) { + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s killed %s.", killerName.c_str(), victimName.c_str()); + addSystemChatMessage(buf); + } + }; + + // Guild + registerHandler(Opcode::SMSG_GUILD_INFO, &GameHandler::handleGuildInfo); + registerHandler(Opcode::SMSG_GUILD_ROSTER, &GameHandler::handleGuildRoster); + registerHandler(Opcode::SMSG_GUILD_QUERY_RESPONSE, &GameHandler::handleGuildQueryResponse); + registerHandler(Opcode::SMSG_GUILD_EVENT, &GameHandler::handleGuildEvent); + registerHandler(Opcode::SMSG_GUILD_INVITE, &GameHandler::handleGuildInvite); + registerHandler(Opcode::SMSG_GUILD_COMMAND_RESULT, &GameHandler::handleGuildCommandResult); + registerHandler(Opcode::SMSG_PET_SPELLS, &GameHandler::handlePetSpells); + registerHandler(Opcode::SMSG_PETITION_SHOWLIST, &GameHandler::handlePetitionShowlist); + registerHandler(Opcode::SMSG_TURN_IN_PETITION_RESULTS, &GameHandler::handleTurnInPetitionResults); + + // Loot/gossip/vendor delegates + registerHandler(Opcode::SMSG_LOOT_RESPONSE, &GameHandler::handleLootResponse); + registerHandler(Opcode::SMSG_LOOT_RELEASE_RESPONSE, &GameHandler::handleLootReleaseResponse); + registerHandler(Opcode::SMSG_LOOT_REMOVED, &GameHandler::handleLootRemoved); + registerHandler(Opcode::SMSG_QUEST_CONFIRM_ACCEPT, &GameHandler::handleQuestConfirmAccept); + registerHandler(Opcode::SMSG_ITEM_TEXT_QUERY_RESPONSE, &GameHandler::handleItemTextQueryResponse); + registerHandler(Opcode::SMSG_SUMMON_REQUEST, &GameHandler::handleSummonRequest); + dispatchTable_[Opcode::SMSG_SUMMON_CANCEL] = [this](network::Packet& /*packet*/) { + pendingSummonRequest_ = false; + addSystemChatMessage("Summon cancelled."); + }; + registerHandler(Opcode::SMSG_TRADE_STATUS, &GameHandler::handleTradeStatus); + registerHandler(Opcode::SMSG_TRADE_STATUS_EXTENDED, &GameHandler::handleTradeStatusExtended); + registerHandler(Opcode::SMSG_LOOT_ROLL, &GameHandler::handleLootRoll); + registerHandler(Opcode::SMSG_LOOT_ROLL_WON, &GameHandler::handleLootRollWon); + dispatchTable_[Opcode::SMSG_LOOT_MASTER_LIST] = [this](network::Packet& packet) { + masterLootCandidates_.clear(); + if (!packet.hasRemaining(1)) return; + uint8_t mlCount = packet.readUInt8(); + masterLootCandidates_.reserve(mlCount); + for (uint8_t i = 0; i < mlCount; ++i) { + if (!packet.hasRemaining(8)) break; + masterLootCandidates_.push_back(packet.readUInt64()); + } + }; + registerHandler(Opcode::SMSG_GOSSIP_MESSAGE, &GameHandler::handleGossipMessage); + registerHandler(Opcode::SMSG_QUESTGIVER_QUEST_LIST, &GameHandler::handleQuestgiverQuestList); + registerHandler(Opcode::SMSG_GOSSIP_COMPLETE, &GameHandler::handleGossipComplete); + + // Bind point + dispatchTable_[Opcode::SMSG_BINDPOINTUPDATE] = [this](network::Packet& packet) { + BindPointUpdateData data; + if (BindPointUpdateParser::parse(packet, data)) { + glm::vec3 canonical = core::coords::serverToCanonical( + glm::vec3(data.x, data.y, data.z)); + bool wasSet = hasHomeBind_; + hasHomeBind_ = true; + homeBindMapId_ = data.mapId; + homeBindZoneId_ = data.zoneId; + homeBindPos_ = canonical; + if (bindPointCallback_) + bindPointCallback_(data.mapId, canonical.x, canonical.y, canonical.z); + if (wasSet) { + std::string bindMsg = "Your home has been set"; + std::string zoneName = getAreaName(data.zoneId); + if (!zoneName.empty()) bindMsg += " to " + zoneName; + bindMsg += '.'; + addSystemChatMessage(bindMsg); + } + } + }; + + // Spirit healer / resurrect + dispatchTable_[Opcode::SMSG_SPIRIT_HEALER_CONFIRM] = [this](network::Packet& packet) { + if (!packet.hasRemaining(8)) return; + uint64_t npcGuid = packet.readUInt64(); + if (npcGuid) { + resurrectCasterGuid_ = npcGuid; + resurrectCasterName_ = ""; + resurrectIsSpiritHealer_ = true; + resurrectRequestPending_ = true; + } + }; + dispatchTable_[Opcode::SMSG_RESURRECT_REQUEST] = [this](network::Packet& packet) { + if (!packet.hasRemaining(8)) return; + uint64_t casterGuid = packet.readUInt64(); + std::string casterName; + if (packet.hasData()) + casterName = packet.readString(); + if (casterGuid) { + resurrectCasterGuid_ = casterGuid; + resurrectIsSpiritHealer_ = false; + if (!casterName.empty()) { + resurrectCasterName_ = casterName; + } else { + resurrectCasterName_ = lookupName(casterGuid); + } + resurrectRequestPending_ = true; + fireAddonEvent("RESURRECT_REQUEST", {resurrectCasterName_}); + } + }; + + // Time sync + dispatchTable_[Opcode::SMSG_TIME_SYNC_REQ] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t counter = packet.readUInt32(); + if (socket) { + network::Packet resp(wireOpcode(Opcode::CMSG_TIME_SYNC_RESP)); + resp.writeUInt32(counter); + resp.writeUInt32(nextMovementTimestampMs()); + socket->send(resp); + } + }; + + // Vendor/trainer + registerHandler(Opcode::SMSG_LIST_INVENTORY, &GameHandler::handleListInventory); + registerHandler(Opcode::SMSG_TRAINER_LIST, &GameHandler::handleTrainerList); + dispatchTable_[Opcode::SMSG_TRAINER_BUY_SUCCEEDED] = [this](network::Packet& packet) { + /*uint64_t guid =*/ packet.readUInt64(); + uint32_t spellId = packet.readUInt32(); + if (!knownSpells.count(spellId)) { + knownSpells.insert(spellId); + } + const std::string& name = getSpellName(spellId); + if (!name.empty()) + addSystemChatMessage("You have learned " + name + "."); + else + addSystemChatMessage("Spell learned."); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestActivate(); }); + fireAddonEvent("TRAINER_UPDATE", {}); + fireAddonEvent("SPELLS_CHANGED", {}); + }; + dispatchTable_[Opcode::SMSG_TRAINER_BUY_FAILED] = [this](network::Packet& packet) { + /*uint64_t trainerGuid =*/ packet.readUInt64(); + uint32_t spellId = packet.readUInt32(); + uint32_t errorCode = 0; + if (packet.hasRemaining(4)) + errorCode = packet.readUInt32(); + const std::string& spellName = getSpellName(spellId); + std::string msg = "Cannot learn "; + if (!spellName.empty()) msg += spellName; + else msg += "spell #" + std::to_string(spellId); + if (errorCode == 0) msg += " (not enough money)"; + else if (errorCode == 1) msg += " (not enough skill)"; + else if (errorCode == 2) msg += " (already known)"; + else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")"; + addUIError(msg); + addSystemChatMessage(msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playError(); }); + }; + + // Minimap ping + dispatchTable_[Opcode::MSG_MINIMAP_PING] = [this](network::Packet& packet) { + const bool mmTbcLike = isPreWotlk(); + if (!packet.hasRemaining(mmTbcLike ? 8u : 1u) ) return; + uint64_t senderGuid = mmTbcLike + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) return; + float pingX = packet.readFloat(); + float pingY = packet.readFloat(); + MinimapPing ping; + ping.senderGuid = senderGuid; + ping.wowX = pingY; + ping.wowY = pingX; + ping.age = 0.0f; + minimapPings_.push_back(ping); + if (senderGuid != playerGuid) { + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); }); + } + }; + dispatchTable_[Opcode::SMSG_ZONE_UNDER_ATTACK] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t areaId = packet.readUInt32(); + std::string areaName = getAreaName(areaId); + std::string msg = areaName.empty() + ? std::string("A zone is under attack!") + : (areaName + " is under attack!"); + addUIError(msg); + addSystemChatMessage(msg); + } + }; + + // Spirit healer time / durability + dispatchTable_[Opcode::SMSG_AREA_SPIRIT_HEALER_TIME] = [this](network::Packet& packet) { + if (packet.hasRemaining(12)) { + /*uint64_t guid =*/ packet.readUInt64(); + uint32_t timeMs = packet.readUInt32(); + uint32_t secs = timeMs / 1000; + char buf[128]; + std::snprintf(buf, sizeof(buf), "You will be able to resurrect in %u seconds.", secs); + addSystemChatMessage(buf); + } + }; + dispatchTable_[Opcode::SMSG_DURABILITY_DAMAGE_DEATH] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t pct = packet.readUInt32(); + char buf[80]; + std::snprintf(buf, sizeof(buf), + "You have lost %u%% of your gear's durability due to death.", pct); + addUIError(buf); + addSystemChatMessage(buf); + } + }; + + // Factions + dispatchTable_[Opcode::SMSG_INITIALIZE_FACTIONS] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t count = packet.readUInt32(); + size_t needed = static_cast(count) * 5; + if (!packet.hasRemaining(needed)) { packet.skipAll(); return; } + initialFactions_.clear(); + initialFactions_.reserve(count); + for (uint32_t i = 0; i < count; ++i) { + FactionStandingInit fs{}; + fs.flags = packet.readUInt8(); + fs.standing = static_cast(packet.readUInt32()); + initialFactions_.push_back(fs); + } + }; + dispatchTable_[Opcode::SMSG_SET_FACTION_STANDING] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + /*uint8_t showVisual =*/ packet.readUInt8(); + uint32_t count = packet.readUInt32(); + count = std::min(count, 128u); + loadFactionNameCache(); + for (uint32_t i = 0; i < count && packet.hasRemaining(8); ++i) { + uint32_t factionId = packet.readUInt32(); + int32_t standing = static_cast(packet.readUInt32()); + int32_t oldStanding = 0; + auto it = factionStandings_.find(factionId); + if (it != factionStandings_.end()) oldStanding = it->second; + factionStandings_[factionId] = standing; + int32_t delta = standing - oldStanding; + if (delta != 0) { + std::string name = getFactionName(factionId); + char buf[256]; + std::snprintf(buf, sizeof(buf), "Reputation with %s %s by %d.", + name.c_str(), delta > 0 ? "increased" : "decreased", std::abs(delta)); + addSystemChatMessage(buf); + watchedFactionId_ = factionId; + if (repChangeCallback_) repChangeCallback_(name, delta, standing); + fireAddonEvent("UPDATE_FACTION", {}); + fireAddonEvent("CHAT_MSG_COMBAT_FACTION_CHANGE", {std::string(buf)}); + } + } + }; + dispatchTable_[Opcode::SMSG_SET_FACTION_ATWAR] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) { packet.skipAll(); return; } + uint32_t repListId = packet.readUInt32(); + uint8_t setAtWar = packet.readUInt8(); + if (repListId < initialFactions_.size()) { + if (setAtWar) + initialFactions_[repListId].flags |= FACTION_FLAG_AT_WAR; + else + initialFactions_[repListId].flags &= ~FACTION_FLAG_AT_WAR; + } + }; + dispatchTable_[Opcode::SMSG_SET_FACTION_VISIBLE] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) { packet.skipAll(); return; } + uint32_t repListId = packet.readUInt32(); + uint8_t visible = packet.readUInt8(); + if (repListId < initialFactions_.size()) { + if (visible) + initialFactions_[repListId].flags |= FACTION_FLAG_VISIBLE; + else + initialFactions_[repListId].flags &= ~FACTION_FLAG_VISIBLE; + } + }; + dispatchTable_[Opcode::SMSG_FEATURE_SYSTEM_STATUS] = [this](network::Packet& packet) { + packet.skipAll(); + }; + + // Spell modifiers (separate lambdas: *logicalOp was used to determine isFlat) + { + auto makeSpellModHandler = [this](bool isFlat) { + return [this, isFlat](network::Packet& packet) { + auto& modMap = isFlat ? spellFlatMods_ : spellPctMods_; + while (packet.hasRemaining(6)) { + uint8_t groupIndex = packet.readUInt8(); + uint8_t modOpRaw = packet.readUInt8(); + int32_t value = static_cast(packet.readUInt32()); + if (groupIndex > 5 || modOpRaw >= SPELL_MOD_OP_COUNT) continue; + SpellModKey key{ static_cast(modOpRaw), groupIndex }; + modMap[key] = value; } - } - } - if (taxiWindowOpen_ && taxiNpcGuid_ != 0) { - auto npc = entityManager.getEntity(taxiNpcGuid_); - if (npc) { - float dx = movementInfo.x - npc->getX(); - float dy = movementInfo.y - npc->getY(); - float dist = std::sqrt(dx * dx + dy * dy); - if (dist > 15.0f) { - closeTaxi(); - LOG_INFO("Taxi window closed: walked too far from NPC"); - } - } - } - if (trainerWindowOpen_ && currentTrainerList_.trainerGuid != 0) { - auto npc = entityManager.getEntity(currentTrainerList_.trainerGuid); - if (npc) { - float dx = movementInfo.x - npc->getX(); - float dy = movementInfo.y - npc->getY(); - float dist = std::sqrt(dx * dx + dy * dy); - if (dist > 15.0f) { - closeTrainer(); - LOG_INFO("Trainer closed: walked too far from NPC"); - } - } - } - - // Update entity movement interpolation (keeps targeting in sync with visuals) - // Only update entities within reasonable distance for performance - const float updateRadiusSq = 150.0f * 150.0f; // 150 unit radius - auto playerEntity = entityManager.getEntity(playerGuid); - glm::vec3 playerPos = playerEntity ? glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()) : glm::vec3(0.0f); - - for (auto& [guid, entity] : entityManager.getEntities()) { - // Always update player - if (guid == playerGuid) { - entity->updateMovement(deltaTime); - continue; - } - // Keep selected/engaged target interpolation exact for UI targeting circle. - if (guid == targetGuid || guid == autoAttackTarget) { - entity->updateMovement(deltaTime); - continue; - } - - // Distance cull other entities (use latest position to avoid culling by stale origin) - glm::vec3 entityPos(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); - float distSq = glm::dot(entityPos - playerPos, entityPos - playerPos); - if (distSq < updateRadiusSq) { - entity->updateMovement(deltaTime); - } - } - + packet.skipAll(); + }; + }; + dispatchTable_[Opcode::SMSG_SET_FLAT_SPELL_MODIFIER] = makeSpellModHandler(true); + dispatchTable_[Opcode::SMSG_SET_PCT_SPELL_MODIFIER] = makeSpellModHandler(false); } + + // Spell delayed + dispatchTable_[Opcode::SMSG_SPELL_DELAYED] = [this](network::Packet& packet) { + const bool spellDelayTbcLike = isPreWotlk(); + if (!packet.hasRemaining(spellDelayTbcLike ? 8u : 1u) ) return; + uint64_t caster = spellDelayTbcLike + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + uint32_t delayMs = packet.readUInt32(); + if (delayMs == 0) return; + float delaySec = delayMs / 1000.0f; + if (caster == playerGuid) { + if (casting) { + castTimeRemaining += delaySec; + castTimeTotal += delaySec; + } + } else { + auto it = unitCastStates_.find(caster); + if (it != unitCastStates_.end() && it->second.casting) { + it->second.timeRemaining += delaySec; + it->second.timeTotal += delaySec; + } + } + }; + + // Proficiency + dispatchTable_[Opcode::SMSG_SET_PROFICIENCY] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint8_t itemClass = packet.readUInt8(); + uint32_t mask = packet.readUInt32(); + if (itemClass == 2) weaponProficiency_ = mask; + else if (itemClass == 4) armorProficiency_ = mask; + }; + + // Loot money / misc consume + dispatchTable_[Opcode::SMSG_LOOT_MONEY_NOTIFY] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t amount = packet.readUInt32(); + if (packet.hasRemaining(1)) + /*uint8_t soleLooter =*/ packet.readUInt8(); + playerMoneyCopper_ += amount; + pendingMoneyDelta_ = amount; + pendingMoneyDeltaTimer_ = 2.0f; + uint64_t notifyGuid = pendingLootMoneyGuid_ != 0 ? pendingLootMoneyGuid_ : currentLoot.lootGuid; + pendingLootMoneyGuid_ = 0; + pendingLootMoneyAmount_ = 0; + pendingLootMoneyNotifyTimer_ = 0.0f; + bool alreadyAnnounced = false; + auto it = localLootState_.find(notifyGuid); + if (it != localLootState_.end()) { + alreadyAnnounced = it->second.moneyTaken; + it->second.moneyTaken = true; + } + if (!alreadyAnnounced) { + addSystemChatMessage("Looted: " + formatCopperAmount(amount)); + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + if (auto* sfx = renderer->getUiSoundManager()) { + if (amount >= 10000) sfx->playLootCoinLarge(); + else sfx->playLootCoinSmall(); + } + } + if (notifyGuid != 0) + recentLootMoneyAnnounceCooldowns_[notifyGuid] = 1.5f; + } + fireAddonEvent("PLAYER_MONEY", {}); + }; + for (auto op : { Opcode::SMSG_LOOT_CLEAR_MONEY, Opcode::SMSG_NPC_TEXT_UPDATE }) { + dispatchTable_[op] = [](network::Packet& /*packet*/) {}; + } + + // Play sound + dispatchTable_[Opcode::SMSG_PLAY_SOUND] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t soundId = packet.readUInt32(); + if (playSoundCallback_) playSoundCallback_(soundId); + } + }; + + // Server messages + dispatchTable_[Opcode::SMSG_SERVER_MESSAGE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t msgType = packet.readUInt32(); + std::string msg = packet.readString(); + if (!msg.empty()) { + std::string prefix; + switch (msgType) { + case 1: prefix = "[Shutdown] "; addUIError("Server shutdown: " + msg); break; + case 2: prefix = "[Restart] "; addUIError("Server restart: " + msg); break; + case 4: prefix = "[Shutdown cancelled] "; break; + case 5: prefix = "[Restart cancelled] "; break; + default: prefix = "[Server] "; break; + } + addSystemChatMessage(prefix + msg); + } + } + }; + dispatchTable_[Opcode::SMSG_CHAT_SERVER_MESSAGE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + /*uint32_t msgType =*/ packet.readUInt32(); + std::string msg = packet.readString(); + if (!msg.empty()) addSystemChatMessage("[Announcement] " + msg); + } + }; + dispatchTable_[Opcode::SMSG_AREA_TRIGGER_MESSAGE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + /*uint32_t len =*/ packet.readUInt32(); + std::string msg = packet.readString(); + if (!msg.empty()) { + addUIError(msg); + addSystemChatMessage(msg); + areaTriggerMsgs_.push_back(msg); + } + } + }; + dispatchTable_[Opcode::SMSG_TRIGGER_CINEMATIC] = [this](network::Packet& packet) { + packet.skipAll(); + network::Packet ack(wireOpcode(Opcode::CMSG_NEXT_CINEMATIC_CAMERA)); + socket->send(ack); + }; + + // ---- Batch 5: Teleport, taxi, BG, LFG, arena, movement relay, mail, bank, auction, quests ---- + + // Teleport + for (auto op : { Opcode::MSG_MOVE_TELEPORT, Opcode::MSG_MOVE_TELEPORT_ACK }) { + dispatchTable_[op] = [this](network::Packet& packet) { handleTeleportAck(packet); }; + } + dispatchTable_[Opcode::SMSG_TRANSFER_PENDING] = [this](network::Packet& packet) { + uint32_t pendingMapId = packet.readUInt32(); + if (packet.hasRemaining(8)) { + packet.readUInt32(); // transportEntry + packet.readUInt32(); // transportMapId + } + (void)pendingMapId; + }; + registerHandler(Opcode::SMSG_NEW_WORLD, &GameHandler::handleNewWorld); + dispatchTable_[Opcode::SMSG_TRANSFER_ABORTED] = [this](network::Packet& packet) { + uint32_t mapId = packet.readUInt32(); + uint8_t reason = (packet.hasData()) ? packet.readUInt8() : 0; + (void)mapId; + const char* abortMsg = nullptr; + switch (reason) { + case 0x01: abortMsg = "Transfer aborted: difficulty unavailable."; break; + case 0x02: abortMsg = "Transfer aborted: expansion required."; break; + case 0x03: abortMsg = "Transfer aborted: instance not found."; break; + case 0x04: abortMsg = "Transfer aborted: too many instances. Please wait before entering a new instance."; break; + case 0x06: abortMsg = "Transfer aborted: instance is full."; break; + case 0x07: abortMsg = "Transfer aborted: zone is in combat."; break; + case 0x08: abortMsg = "Transfer aborted: you are already in this instance."; break; + case 0x09: abortMsg = "Transfer aborted: not enough players."; break; + default: abortMsg = "Transfer aborted."; break; + } + addUIError(abortMsg); + addSystemChatMessage(abortMsg); + }; + + // Taxi + registerHandler(Opcode::SMSG_SHOWTAXINODES, &GameHandler::handleShowTaxiNodes); + registerHandler(Opcode::SMSG_ACTIVATETAXIREPLY, &GameHandler::handleActivateTaxiReply); + dispatchTable_[Opcode::SMSG_STANDSTATE_UPDATE] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + standState_ = packet.readUInt8(); + if (standStateCallback_) standStateCallback_(standState_); + } + }; + dispatchTable_[Opcode::SMSG_NEW_TAXI_PATH] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("New flight path discovered!"); + }; + + // Battlefield / BG + registerHandler(Opcode::SMSG_BATTLEFIELD_STATUS, &GameHandler::handleBattlefieldStatus); + registerHandler(Opcode::SMSG_BATTLEFIELD_LIST, &GameHandler::handleBattlefieldList); + dispatchTable_[Opcode::SMSG_BATTLEFIELD_PORT_DENIED] = [this](network::Packet& /*packet*/) { + addUIError("Battlefield port denied."); + addSystemChatMessage("Battlefield port denied."); + }; + dispatchTable_[Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS] = [this](network::Packet& packet) { + bgPlayerPositions_.clear(); + for (int grp = 0; grp < 2; ++grp) { + if (!packet.hasRemaining(4)) break; + uint32_t count = packet.readUInt32(); + for (uint32_t i = 0; i < count && packet.hasRemaining(16); ++i) { + BgPlayerPosition pos; + pos.guid = packet.readUInt64(); + pos.wowX = packet.readFloat(); + pos.wowY = packet.readFloat(); + pos.group = grp; + bgPlayerPositions_.push_back(pos); + } + } + }; + dispatchTable_[Opcode::SMSG_REMOVED_FROM_PVP_QUEUE] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("You have been removed from the PvP queue."); + }; + dispatchTable_[Opcode::SMSG_GROUP_JOINED_BATTLEGROUND] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("Your group has joined the battleground."); + }; + dispatchTable_[Opcode::SMSG_JOINED_BATTLEGROUND_QUEUE] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("You have joined the battleground queue."); + }; + dispatchTable_[Opcode::SMSG_BATTLEGROUND_PLAYER_JOINED] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint64_t guid = packet.readUInt64(); + const auto& name = lookupName(guid); + if (!name.empty()) + addSystemChatMessage(name + " has entered the battleground."); + } + }; + dispatchTable_[Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint64_t guid = packet.readUInt64(); + const auto& name = lookupName(guid); + if (!name.empty()) + addSystemChatMessage(name + " has left the battleground."); + } + }; + + // Instance + for (auto op : { Opcode::SMSG_INSTANCE_DIFFICULTY, Opcode::MSG_SET_DUNGEON_DIFFICULTY }) { + dispatchTable_[op] = [this](network::Packet& packet) { handleInstanceDifficulty(packet); }; + } + dispatchTable_[Opcode::SMSG_INSTANCE_SAVE_CREATED] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("You are now saved to this instance."); + }; + dispatchTable_[Opcode::SMSG_RAID_INSTANCE_MESSAGE] = [this](network::Packet& packet) { + if (!packet.hasRemaining(12)) return; + uint32_t msgType = packet.readUInt32(); + uint32_t mapId = packet.readUInt32(); + packet.readUInt32(); // diff + std::string mapLabel = getMapName(mapId); + if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId); + if (msgType == 1 && packet.hasRemaining(4)) { + uint32_t timeLeft = packet.readUInt32(); + addSystemChatMessage(mapLabel + " will reset in " + std::to_string(timeLeft / 60) + " minute(s)."); + } else if (msgType == 2) { + addSystemChatMessage("You have been saved to " + mapLabel + "."); + } else if (msgType == 3) { + addSystemChatMessage("Welcome to " + mapLabel + "."); + } + }; + dispatchTable_[Opcode::SMSG_INSTANCE_RESET] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t mapId = packet.readUInt32(); + auto it = std::remove_if(instanceLockouts_.begin(), instanceLockouts_.end(), + [mapId](const InstanceLockout& lo){ return lo.mapId == mapId; }); + instanceLockouts_.erase(it, instanceLockouts_.end()); + std::string mapLabel = getMapName(mapId); + if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId); + addSystemChatMessage(mapLabel + " has been reset."); + }; + dispatchTable_[Opcode::SMSG_INSTANCE_RESET_FAILED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(8)) return; + uint32_t mapId = packet.readUInt32(); + uint32_t reason = packet.readUInt32(); + static const char* resetFailReasons[] = { + "Not max level.", "Offline party members.", "Party members inside.", + "Party members changing zone.", "Heroic difficulty only." + }; + const char* reasonMsg = (reason < 5) ? resetFailReasons[reason] : "Unknown reason."; + std::string mapLabel = getMapName(mapId); + if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId); + addUIError("Cannot reset " + mapLabel + ": " + reasonMsg); + addSystemChatMessage("Cannot reset " + mapLabel + ": " + reasonMsg); + }; + dispatchTable_[Opcode::SMSG_INSTANCE_LOCK_WARNING_QUERY] = [this](network::Packet& packet) { + if (!socket || !packet.hasRemaining(17)) return; + uint32_t ilMapId = packet.readUInt32(); + uint32_t ilDiff = packet.readUInt32(); + uint32_t ilTimeLeft = packet.readUInt32(); + packet.readUInt32(); // unk + uint8_t ilLocked = packet.readUInt8(); + std::string ilName = getMapName(ilMapId); + if (ilName.empty()) ilName = "instance #" + std::to_string(ilMapId); + static const char* kDiff[] = {"Normal","Heroic","25-Man","25-Man Heroic"}; + std::string ilMsg = "Entering " + ilName; + if (ilDiff < 4) ilMsg += std::string(" (") + kDiff[ilDiff] + ")"; + if (ilLocked && ilTimeLeft > 0) + ilMsg += " — " + std::to_string(ilTimeLeft / 60) + " min remaining."; + else + ilMsg += "."; + addSystemChatMessage(ilMsg); + network::Packet resp(wireOpcode(Opcode::CMSG_INSTANCE_LOCK_RESPONSE)); + resp.writeUInt8(1); + socket->send(resp); + }; + + // LFG + registerHandler(Opcode::SMSG_LFG_JOIN_RESULT, &GameHandler::handleLfgJoinResult); + registerHandler(Opcode::SMSG_LFG_QUEUE_STATUS, &GameHandler::handleLfgQueueStatus); + registerHandler(Opcode::SMSG_LFG_PROPOSAL_UPDATE, &GameHandler::handleLfgProposalUpdate); + registerHandler(Opcode::SMSG_LFG_ROLE_CHECK_UPDATE, &GameHandler::handleLfgRoleCheckUpdate); + for (auto op : { Opcode::SMSG_LFG_UPDATE_PLAYER, Opcode::SMSG_LFG_UPDATE_PARTY }) { + dispatchTable_[op] = [this](network::Packet& packet) { handleLfgUpdatePlayer(packet); }; + } + registerHandler(Opcode::SMSG_LFG_PLAYER_REWARD, &GameHandler::handleLfgPlayerReward); + registerHandler(Opcode::SMSG_LFG_BOOT_PROPOSAL_UPDATE, &GameHandler::handleLfgBootProposalUpdate); + registerHandler(Opcode::SMSG_LFG_TELEPORT_DENIED, &GameHandler::handleLfgTeleportDenied); + dispatchTable_[Opcode::SMSG_LFG_DISABLED] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("The Dungeon Finder is currently disabled."); + }; + dispatchTable_[Opcode::SMSG_LFG_OFFER_CONTINUE] = [this](network::Packet& /*packet*/) { + addSystemChatMessage("Dungeon Finder: You may continue your dungeon."); + }; + dispatchTable_[Opcode::SMSG_LFG_ROLE_CHOSEN] = [this](network::Packet& packet) { + if (!packet.hasRemaining(13)) { packet.skipAll(); return; } + uint64_t roleGuid = packet.readUInt64(); + uint8_t ready = packet.readUInt8(); + uint32_t roles = packet.readUInt32(); + std::string roleName; + if (roles & 0x02) roleName += "Tank "; + if (roles & 0x04) roleName += "Healer "; + if (roles & 0x08) roleName += "DPS "; + if (roleName.empty()) roleName = "None"; + std::string pName = "A player"; + if (auto e = entityManager.getEntity(roleGuid)) + if (auto u = std::dynamic_pointer_cast(e)) + pName = u->getName(); + if (ready) addSystemChatMessage(pName + " has chosen: " + roleName); + packet.skipAll(); + }; + for (auto op : { Opcode::SMSG_LFG_UPDATE_SEARCH, Opcode::SMSG_UPDATE_LFG_LIST, + Opcode::SMSG_LFG_PLAYER_INFO, Opcode::SMSG_LFG_PARTY_INFO }) + registerSkipHandler(op); + dispatchTable_[Opcode::SMSG_OPEN_LFG_DUNGEON_FINDER] = [this](network::Packet& packet) { + packet.skipAll(); + if (openLfgCallback_) openLfgCallback_(); + }; + + // Arena + registerHandler(Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT, &GameHandler::handleArenaTeamCommandResult); + registerHandler(Opcode::SMSG_ARENA_TEAM_QUERY_RESPONSE, &GameHandler::handleArenaTeamQueryResponse); + registerHandler(Opcode::SMSG_ARENA_TEAM_ROSTER, &GameHandler::handleArenaTeamRoster); + registerHandler(Opcode::SMSG_ARENA_TEAM_INVITE, &GameHandler::handleArenaTeamInvite); + registerHandler(Opcode::SMSG_ARENA_TEAM_EVENT, &GameHandler::handleArenaTeamEvent); + registerHandler(Opcode::SMSG_ARENA_TEAM_STATS, &GameHandler::handleArenaTeamStats); + registerHandler(Opcode::SMSG_ARENA_ERROR, &GameHandler::handleArenaError); + registerHandler(Opcode::MSG_PVP_LOG_DATA, &GameHandler::handlePvpLogData); + dispatchTable_[Opcode::MSG_TALENT_WIPE_CONFIRM] = [this](network::Packet& packet) { + if (!packet.hasRemaining(12)) { packet.skipAll(); return; } + talentWipeNpcGuid_ = packet.readUInt64(); + talentWipeCost_ = packet.readUInt32(); + talentWipePending_ = true; + fireAddonEvent("CONFIRM_TALENT_WIPE", {std::to_string(talentWipeCost_)}); + }; + + // MSG_MOVE_* relay (26 opcodes → handleOtherPlayerMovement) + for (auto op : { Opcode::MSG_MOVE_START_FORWARD, Opcode::MSG_MOVE_START_BACKWARD, + Opcode::MSG_MOVE_STOP, Opcode::MSG_MOVE_START_STRAFE_LEFT, + Opcode::MSG_MOVE_START_STRAFE_RIGHT, Opcode::MSG_MOVE_STOP_STRAFE, + Opcode::MSG_MOVE_JUMP, Opcode::MSG_MOVE_START_TURN_LEFT, + Opcode::MSG_MOVE_START_TURN_RIGHT, Opcode::MSG_MOVE_STOP_TURN, + Opcode::MSG_MOVE_SET_FACING, Opcode::MSG_MOVE_FALL_LAND, + Opcode::MSG_MOVE_HEARTBEAT, Opcode::MSG_MOVE_START_SWIM, + Opcode::MSG_MOVE_STOP_SWIM, Opcode::MSG_MOVE_SET_WALK_MODE, + Opcode::MSG_MOVE_SET_RUN_MODE, Opcode::MSG_MOVE_START_PITCH_UP, + Opcode::MSG_MOVE_START_PITCH_DOWN, Opcode::MSG_MOVE_STOP_PITCH, + Opcode::MSG_MOVE_START_ASCEND, Opcode::MSG_MOVE_STOP_ASCEND, + Opcode::MSG_MOVE_START_DESCEND, Opcode::MSG_MOVE_SET_PITCH, + Opcode::MSG_MOVE_GRAVITY_CHNG, Opcode::MSG_MOVE_UPDATE_CAN_FLY, + Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY, + Opcode::MSG_MOVE_ROOT, Opcode::MSG_MOVE_UNROOT }) { + dispatchTable_[op] = [this](network::Packet& packet) { + if (state == WorldState::IN_WORLD) handleOtherPlayerMovement(packet); + }; + } + + // MSG_MOVE_SET_*_SPEED relay (7 opcodes → handleMoveSetSpeed) + for (auto op : { Opcode::MSG_MOVE_SET_RUN_SPEED, Opcode::MSG_MOVE_SET_RUN_BACK_SPEED, + Opcode::MSG_MOVE_SET_WALK_SPEED, Opcode::MSG_MOVE_SET_SWIM_SPEED, + Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED, Opcode::MSG_MOVE_SET_FLIGHT_SPEED, + Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED }) { + dispatchTable_[op] = [this](network::Packet& packet) { + if (state == WorldState::IN_WORLD) handleMoveSetSpeed(packet); + }; + } + + // Mail + registerHandler(Opcode::SMSG_SHOW_MAILBOX, &GameHandler::handleShowMailbox); + registerHandler(Opcode::SMSG_MAIL_LIST_RESULT, &GameHandler::handleMailListResult); + registerHandler(Opcode::SMSG_SEND_MAIL_RESULT, &GameHandler::handleSendMailResult); + registerHandler(Opcode::SMSG_RECEIVED_MAIL, &GameHandler::handleReceivedMail); + registerHandler(Opcode::MSG_QUERY_NEXT_MAIL_TIME, &GameHandler::handleQueryNextMailTime); + + // Inspect / channel list + registerHandler(Opcode::SMSG_INSPECT_RESULTS_UPDATE, &GameHandler::handleInspectResults); + dispatchTable_[Opcode::SMSG_CHANNEL_LIST] = [this](network::Packet& packet) { + std::string chanName = packet.readString(); + if (!packet.hasRemaining(5)) return; + /*uint8_t chanFlags =*/ packet.readUInt8(); + uint32_t memberCount = packet.readUInt32(); + memberCount = std::min(memberCount, 200u); + addSystemChatMessage(chanName + " has " + std::to_string(memberCount) + " member(s):"); + for (uint32_t i = 0; i < memberCount; ++i) { + if (!packet.hasRemaining(9)) break; + uint64_t memberGuid = packet.readUInt64(); + uint8_t memberFlags = packet.readUInt8(); + std::string name; + auto entity = entityManager.getEntity(memberGuid); + if (entity) { + auto player = std::dynamic_pointer_cast(entity); + if (player && !player->getName().empty()) name = player->getName(); + } + if (name.empty()) name = lookupName(memberGuid); + if (name.empty()) name = "(unknown)"; + std::string entry = " " + name; + if (memberFlags & 0x01) entry += " [Moderator]"; + if (memberFlags & 0x02) entry += " [Muted]"; + addSystemChatMessage(entry); + } + }; + + // Bank + registerHandler(Opcode::SMSG_SHOW_BANK, &GameHandler::handleShowBank); + registerHandler(Opcode::SMSG_BUY_BANK_SLOT_RESULT, &GameHandler::handleBuyBankSlotResult); + + // Guild bank + registerHandler(Opcode::SMSG_GUILD_BANK_LIST, &GameHandler::handleGuildBankList); + + // Auction house + registerHandler(Opcode::MSG_AUCTION_HELLO, &GameHandler::handleAuctionHello); + registerHandler(Opcode::SMSG_AUCTION_LIST_RESULT, &GameHandler::handleAuctionListResult); + registerHandler(Opcode::SMSG_AUCTION_OWNER_LIST_RESULT, &GameHandler::handleAuctionOwnerListResult); + registerHandler(Opcode::SMSG_AUCTION_BIDDER_LIST_RESULT, &GameHandler::handleAuctionBidderListResult); + registerHandler(Opcode::SMSG_AUCTION_COMMAND_RESULT, &GameHandler::handleAuctionCommandResult); + + // Questgiver status + dispatchTable_[Opcode::SMSG_QUESTGIVER_STATUS] = [this](network::Packet& packet) { + if (packet.hasRemaining(9)) { + uint64_t npcGuid = packet.readUInt64(); + uint8_t status = packetParsers_->readQuestGiverStatus(packet); + npcQuestStatus_[npcGuid] = static_cast(status); + } + }; + dispatchTable_[Opcode::SMSG_QUESTGIVER_STATUS_MULTIPLE] = [this](network::Packet& packet) { + if (!packet.hasRemaining(4)) return; + uint32_t count = packet.readUInt32(); + for (uint32_t i = 0; i < count; ++i) { + if (!packet.hasRemaining(9)) break; + uint64_t npcGuid = packet.readUInt64(); + uint8_t status = packetParsers_->readQuestGiverStatus(packet); + npcQuestStatus_[npcGuid] = static_cast(status); + } + }; + registerHandler(Opcode::SMSG_QUESTGIVER_QUEST_DETAILS, &GameHandler::handleQuestDetails); + dispatchTable_[Opcode::SMSG_QUESTLOG_FULL] = [this](network::Packet& /*packet*/) { + addUIError("Your quest log is full."); + addSystemChatMessage("Your quest log is full."); + }; + registerHandler(Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS, &GameHandler::handleQuestRequestItems); + registerHandler(Opcode::SMSG_QUESTGIVER_OFFER_REWARD, &GameHandler::handleQuestOfferReward); + + // Group set leader + dispatchTable_[Opcode::SMSG_GROUP_SET_LEADER] = [this](network::Packet& packet) { + if (!packet.hasData()) return; + std::string leaderName = packet.readString(); + for (const auto& m : partyData.members) { + if (m.name == leaderName) { partyData.leaderGuid = m.guid; break; } + } + if (!leaderName.empty()) + addSystemChatMessage(leaderName + " is now the group leader."); + fireAddonEvent("PARTY_LEADER_CHANGED", {}); + fireAddonEvent("GROUP_ROSTER_UPDATE", {}); + }; + + // Gameobject / page text + registerHandler(Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE, &GameHandler::handleGameObjectQueryResponse); + registerHandler(Opcode::SMSG_GAMEOBJECT_PAGETEXT, &GameHandler::handleGameObjectPageText); + registerHandler(Opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE, &GameHandler::handlePageTextQueryResponse); + dispatchTable_[Opcode::SMSG_GAMEOBJECT_CUSTOM_ANIM] = [this](network::Packet& packet) { + if (packet.getSize() < 12) return; + uint64_t guid = packet.readUInt64(); + uint32_t animId = packet.readUInt32(); + if (gameObjectCustomAnimCallback_) + gameObjectCustomAnimCallback_(guid, animId); + if (animId == 0) { + auto goEnt = entityManager.getEntity(guid); + if (goEnt && goEnt->getType() == ObjectType::GAMEOBJECT) { + auto go = std::static_pointer_cast(goEnt); + auto* info = getCachedGameObjectInfo(go->getEntry()); + if (info && info->type == 17) { + addUIError("A fish is on your line!"); + addSystemChatMessage("A fish is on your line!"); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); }); + } + } + } + }; + + // Resurrect failed / item refund / socket gems / item time + dispatchTable_[Opcode::SMSG_RESURRECT_FAILED] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t reason = packet.readUInt32(); + const char* msg = (reason == 1) ? "The target cannot be resurrected right now." + : (reason == 2) ? "Cannot resurrect in this area." + : "Resurrection failed."; + addUIError(msg); + addSystemChatMessage(msg); + } + }; + dispatchTable_[Opcode::SMSG_ITEM_REFUND_RESULT] = [this](network::Packet& packet) { + if (packet.hasRemaining(12)) { + packet.readUInt64(); // itemGuid + uint32_t result = packet.readUInt32(); + addSystemChatMessage(result == 0 ? "Item returned. Refund processed." + : "Could not return item for refund."); + } + }; + dispatchTable_[Opcode::SMSG_SOCKET_GEMS_RESULT] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t result = packet.readUInt32(); + if (result == 0) addSystemChatMessage("Gems socketed successfully."); + else addSystemChatMessage("Failed to socket gems."); + } + }; + dispatchTable_[Opcode::SMSG_ITEM_TIME_UPDATE] = [this](network::Packet& packet) { + if (packet.hasRemaining(12)) { + packet.readUInt64(); // itemGuid + packet.readUInt32(); // durationMs + } + }; + + // ---- Batch 6: Spell miss / env damage / control / spell failure ---- + + // ---- SMSG_SPELLLOGMISS ---- + dispatchTable_[Opcode::SMSG_SPELLLOGMISS] = [this](network::Packet& packet) { + // All expansions: uint32 spellId first. + // WotLK/Classic: spellId(4) + packed_guid caster + uint8 unk + uint32 count + // + count × (packed_guid victim + uint8 missInfo) + // TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count + // + count × (uint64 victim + uint8 missInfo) + // All expansions append uint32 reflectSpellId + uint8 reflectResult when + // missInfo==11 (REFLECT). + const bool spellMissUsesFullGuid = isActiveExpansion("tbc"); + auto readSpellMissGuid = [&]() -> uint64_t { + if (spellMissUsesFullGuid) + return (packet.hasRemaining(8)) ? packet.readUInt64() : 0; + return packet.readPackedGuid(); + }; + // spellId prefix present in all expansions + if (!packet.hasRemaining(4)) return; + uint32_t spellId = packet.readUInt32(); + if (!packet.hasRemaining(spellMissUsesFullGuid ? 8u : 1u) || (!spellMissUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t casterGuid = readSpellMissGuid(); + if (!packet.hasRemaining(5)) return; + /*uint8_t unk =*/ packet.readUInt8(); + const uint32_t rawCount = packet.readUInt32(); + if (rawCount > 128) { + LOG_WARNING("SMSG_SPELLLOGMISS: miss count capped (requested=", rawCount, ")"); + } + const uint32_t storedLimit = std::min(rawCount, 128u); + + struct SpellMissLogEntry { + uint64_t victimGuid = 0; + uint8_t missInfo = 0; + uint32_t reflectSpellId = 0; // Only valid when missInfo==11 (REFLECT) + }; + std::vector parsedMisses; + parsedMisses.reserve(storedLimit); + + bool truncated = false; + for (uint32_t i = 0; i < rawCount; ++i) { + if (!packet.hasRemaining(spellMissUsesFullGuid ? 9u : 2u) || (!spellMissUsesFullGuid && !packet.hasFullPackedGuid())) { + truncated = true; + return; + } + const uint64_t victimGuid = readSpellMissGuid(); + if (!packet.hasRemaining(1)) { + truncated = true; + return; + } + const uint8_t missInfo = packet.readUInt8(); + // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult + uint32_t reflectSpellId = 0; + if (missInfo == 11) { + if (packet.hasRemaining(5)) { + reflectSpellId = packet.readUInt32(); + /*uint8_t reflectResult =*/ packet.readUInt8(); + } else { + truncated = true; + return; + } + } + if (i < storedLimit) { + parsedMisses.push_back({victimGuid, missInfo, reflectSpellId}); + } + } + + if (truncated) { + packet.skipAll(); + return; + } + + for (const auto& miss : parsedMisses) { + const uint64_t victimGuid = miss.victimGuid; + const uint8_t missInfo = miss.missInfo; + CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo); + // For REFLECT, use the reflected spell ID so combat text shows the spell name + uint32_t combatSpellId = (ct == CombatTextEntry::REFLECT && miss.reflectSpellId != 0) + ? miss.reflectSpellId : spellId; + if (casterGuid == playerGuid) { + // We cast a spell and it missed the target + addCombatText(ct, 0, combatSpellId, true, 0, casterGuid, victimGuid); + } else if (victimGuid == playerGuid) { + // Enemy spell missed us (we dodged/parried/blocked/resisted/etc.) + addCombatText(ct, 0, combatSpellId, false, 0, casterGuid, victimGuid); + } + } + }; + + // ---- Environmental damage log ---- + dispatchTable_[Opcode::SMSG_ENVIRONMENTALDAMAGELOG] = [this](network::Packet& packet) { + // uint64 victimGuid + uint8 envDamageType + uint32 damage + uint32 absorb + uint32 resist + if (!packet.hasRemaining(21)) return; + uint64_t victimGuid = packet.readUInt64(); + /*uint8_t envType =*/ packet.readUInt8(); + uint32_t damage = packet.readUInt32(); + uint32_t absorb = packet.readUInt32(); + uint32_t resist = packet.readUInt32(); + if (victimGuid == playerGuid) { + // Environmental damage: no caster GUID, victim = player + if (damage > 0) + addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast(damage), 0, false, 0, 0, victimGuid); + if (absorb > 0) + addCombatText(CombatTextEntry::ABSORB, static_cast(absorb), 0, false, 0, 0, victimGuid); + if (resist > 0) + addCombatText(CombatTextEntry::RESIST, static_cast(resist), 0, false, 0, 0, victimGuid); + } + }; + + // ---- Client control update ---- + dispatchTable_[Opcode::SMSG_CLIENT_CONTROL_UPDATE] = [this](network::Packet& packet) { + // Minimal parse: PackedGuid + uint8 allowMovement. + if (!packet.hasRemaining(2)) { + LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE too short: ", packet.getSize(), " bytes"); + return; + } + uint8_t guidMask = packet.readUInt8(); + size_t guidBytes = 0; + uint64_t controlGuid = 0; + for (int i = 0; i < 8; ++i) { + if (guidMask & (1u << i)) ++guidBytes; + } + if (!packet.hasRemaining(guidBytes) + 1) { + LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE malformed (truncated packed guid)"); + packet.skipAll(); + return; + } + for (int i = 0; i < 8; ++i) { + if (guidMask & (1u << i)) { + uint8_t b = packet.readUInt8(); + controlGuid |= (static_cast(b) << (i * 8)); + } + } + bool allowMovement = (packet.readUInt8() != 0); + if (controlGuid == 0 || controlGuid == playerGuid) { + bool changed = (serverMovementAllowed_ != allowMovement); + serverMovementAllowed_ = allowMovement; + if (changed && !allowMovement) { + // Force-stop local movement immediately when server revokes control. + movementInfo.flags &= ~(static_cast(MovementFlags::FORWARD) | + static_cast(MovementFlags::BACKWARD) | + static_cast(MovementFlags::STRAFE_LEFT) | + static_cast(MovementFlags::STRAFE_RIGHT) | + static_cast(MovementFlags::TURN_LEFT) | + static_cast(MovementFlags::TURN_RIGHT)); + sendMovement(Opcode::MSG_MOVE_STOP); + sendMovement(Opcode::MSG_MOVE_STOP_STRAFE); + sendMovement(Opcode::MSG_MOVE_STOP_TURN); + sendMovement(Opcode::MSG_MOVE_STOP_SWIM); + addSystemChatMessage("Movement disabled by server."); + fireAddonEvent("PLAYER_CONTROL_LOST", {}); + } else if (changed && allowMovement) { + addSystemChatMessage("Movement re-enabled."); + fireAddonEvent("PLAYER_CONTROL_GAINED", {}); + } + } + }; + + // ---- Spell failure ---- + dispatchTable_[Opcode::SMSG_SPELL_FAILURE] = [this](network::Packet& packet) { + // WotLK: packed_guid + uint8 castCount + uint32 spellId + uint8 failReason + // TBC: full uint64 + uint8 castCount + uint32 spellId + uint8 failReason + // Classic: full uint64 + uint32 spellId + uint8 failReason (NO castCount) + const bool isClassic = isClassicLikeExpansion(); + const bool isTbc = isActiveExpansion("tbc"); + uint64_t failGuid = (isClassic || isTbc) + ? (packet.hasRemaining(8) ? packet.readUInt64() : 0) + : packet.readPackedGuid(); + // Classic omits the castCount byte; TBC and WotLK include it + const size_t remainingFields = isClassic ? 5u : 6u; // spellId(4)+reason(1) [+castCount(1)] + if (packet.hasRemaining(remainingFields)) { + if (!isClassic) /*uint8_t castCount =*/ packet.readUInt8(); + uint32_t failSpellId = packet.readUInt32(); + uint8_t rawFailReason = packet.readUInt8(); + // Classic result enum starts at 0=AFFECTING_COMBAT; shift +1 for WotLK table + uint8_t failReason = isClassic ? static_cast(rawFailReason + 1) : rawFailReason; + if (failGuid == playerGuid && failReason != 0) { + // Show interruption/failure reason in chat and error overlay for player + int pt = -1; + if (auto pe = entityManager.getEntity(playerGuid)) + if (auto pu = std::dynamic_pointer_cast(pe)) + pt = static_cast(pu->getPowerType()); + const char* reason = getSpellCastResultString(failReason, pt); + if (reason) { + // Prefix with spell name for context, e.g. "Fireball: Not in range" + const std::string& sName = getSpellName(failSpellId); + std::string fullMsg = sName.empty() ? reason + : sName + ": " + reason; + addUIError(fullMsg); + MessageChatData emsg; + emsg.type = ChatType::SYSTEM; + emsg.language = ChatLanguage::UNIVERSAL; + emsg.message = std::move(fullMsg); + addLocalChatMessage(emsg); + } + } + } + // Fire UNIT_SPELLCAST_INTERRUPTED for Lua addons + if (addonEventCallback_) { + auto unitId = (failGuid == 0) ? std::string("player") : guidToUnitId(failGuid); + if (!unitId.empty()) { + fireAddonEvent("UNIT_SPELLCAST_INTERRUPTED", {unitId}); + fireAddonEvent("UNIT_SPELLCAST_STOP", {unitId}); + } + } + if (failGuid == playerGuid || failGuid == 0) { + // Player's own cast failed — clear gather-node loot target so the + // next timed cast doesn't try to loot a stale interrupted gather node. + casting = false; + castIsChannel = false; + currentCastSpellId = 0; + lastInteractedGoGuid_ = 0; + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; + withSoundManager(&rendering::Renderer::getSpellSoundManager, [](auto* ssm) { ssm->stopPrecast(); }); + if (spellCastAnimCallback_) { + spellCastAnimCallback_(playerGuid, false, false); + } + } else { + // Another unit's cast failed — clear their tracked cast bar + unitCastStates_.erase(failGuid); + if (spellCastAnimCallback_) { + spellCastAnimCallback_(failGuid, false, false); + } + } + }; + + // ---- Achievement / fishing delegates ---- + dispatchTable_[Opcode::SMSG_ACHIEVEMENT_EARNED] = [this](network::Packet& packet) { + handleAchievementEarned(packet); + }; + dispatchTable_[Opcode::SMSG_ALL_ACHIEVEMENT_DATA] = [this](network::Packet& packet) { + handleAllAchievementData(packet); + }; + dispatchTable_[Opcode::SMSG_ITEM_COOLDOWN] = [this](network::Packet& packet) { + // uint64 itemGuid + uint32 spellId + uint32 cooldownMs + size_t rem = packet.getRemainingSize(); + if (rem >= 16) { + uint64_t itemGuid = packet.readUInt64(); + uint32_t spellId = packet.readUInt32(); + uint32_t cdMs = packet.readUInt32(); + float cdSec = cdMs / 1000.0f; + if (cdSec > 0.0f) { + if (spellId != 0) { + auto it = spellCooldowns.find(spellId); + if (it == spellCooldowns.end()) { + spellCooldowns[spellId] = cdSec; + } else { + it->second = mergeCooldownSeconds(it->second, cdSec); + } + } + // Resolve itemId from the GUID so item-type slots are also updated + uint32_t itemId = 0; + auto iit = onlineItems_.find(itemGuid); + if (iit != onlineItems_.end()) itemId = iit->second.entry; + for (auto& slot : actionBar) { + bool match = (spellId != 0 && slot.type == ActionBarSlot::SPELL && slot.id == spellId) + || (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId); + if (match) { + float prevRemaining = slot.cooldownRemaining; + float merged = mergeCooldownSeconds(slot.cooldownRemaining, cdSec); + slot.cooldownRemaining = merged; + if (slot.cooldownTotal <= 0.0f || prevRemaining <= 0.0f) { + slot.cooldownTotal = cdSec; + } else { + slot.cooldownTotal = std::max(slot.cooldownTotal, merged); + } + } + } + LOG_DEBUG("SMSG_ITEM_COOLDOWN: itemGuid=0x", std::hex, itemGuid, std::dec, + " spellId=", spellId, " itemId=", itemId, " cd=", cdSec, "s"); + } + } + }; + dispatchTable_[Opcode::SMSG_FISH_NOT_HOOKED] = [this](network::Packet& packet) { + addSystemChatMessage("Your fish got away."); + }; + dispatchTable_[Opcode::SMSG_FISH_ESCAPED] = [this](network::Packet& packet) { + addSystemChatMessage("Your fish escaped!"); + }; + + // ---- Auto-repeat / auras / dispel / totem ---- + dispatchTable_[Opcode::SMSG_CANCEL_AUTO_REPEAT] = [this](network::Packet& packet) { + // Server signals to stop a repeating spell (wand/shoot); no client action needed + }; + dispatchTable_[Opcode::SMSG_AURA_UPDATE] = [this](network::Packet& packet) { + handleAuraUpdate(packet, false); + }; + dispatchTable_[Opcode::SMSG_AURA_UPDATE_ALL] = [this](network::Packet& packet) { + handleAuraUpdate(packet, true); + }; + dispatchTable_[Opcode::SMSG_DISPEL_FAILED] = [this](network::Packet& packet) { + // WotLK: uint32 dispelSpellId + packed_guid caster + packed_guid victim + // [+ count × uint32 failedSpellId] + // Classic: uint32 dispelSpellId + packed_guid caster + packed_guid victim + // [+ count × uint32 failedSpellId] + // TBC: uint64 caster + uint64 victim + uint32 spellId + // [+ count × uint32 failedSpellId] + const bool dispelUsesFullGuid = isActiveExpansion("tbc"); + uint32_t dispelSpellId = 0; + uint64_t dispelCasterGuid = 0; + if (dispelUsesFullGuid) { + if (!packet.hasRemaining(20)) return; + dispelCasterGuid = packet.readUInt64(); + /*uint64_t victim =*/ packet.readUInt64(); + dispelSpellId = packet.readUInt32(); + } else { + if (!packet.hasRemaining(4)) return; + dispelSpellId = packet.readUInt32(); + if (!packet.hasFullPackedGuid()) { + packet.skipAll(); return; + } + dispelCasterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.skipAll(); return; + } + /*uint64_t victim =*/ packet.readPackedGuid(); + } + // Only show failure to the player who attempted the dispel + if (dispelCasterGuid == playerGuid) { + const auto& name = getSpellName(dispelSpellId); + char buf[128]; + if (!name.empty()) + std::snprintf(buf, sizeof(buf), "%s failed to dispel.", name.c_str()); + else + std::snprintf(buf, sizeof(buf), "Dispel failed! (spell %u)", dispelSpellId); + addSystemChatMessage(buf); + } + }; + dispatchTable_[Opcode::SMSG_TOTEM_CREATED] = [this](network::Packet& packet) { + // WotLK: uint8 slot + packed_guid + uint32 duration + uint32 spellId + // TBC/Classic: uint8 slot + uint64 guid + uint32 duration + uint32 spellId + const bool totemTbcLike = isPreWotlk(); + if (!packet.hasRemaining(totemTbcLike ? 17u : 9u) ) return; + uint8_t slot = packet.readUInt8(); + if (totemTbcLike) + /*uint64_t guid =*/ packet.readUInt64(); + else + /*uint64_t guid =*/ packet.readPackedGuid(); + if (!packet.hasRemaining(8)) return; + uint32_t duration = packet.readUInt32(); + uint32_t spellId = packet.readUInt32(); + LOG_DEBUG("SMSG_TOTEM_CREATED: slot=", static_cast(slot), + " spellId=", spellId, " duration=", duration, "ms"); + if (slot < NUM_TOTEM_SLOTS) { + activeTotemSlots_[slot].spellId = spellId; + activeTotemSlots_[slot].durationMs = duration; + activeTotemSlots_[slot].placedAt = std::chrono::steady_clock::now(); + } + }; + + // ---- SMSG_ENVIRONMENTAL_DAMAGE_LOG (distinct from SMSG_ENVIRONMENTALDAMAGELOG) ---- + dispatchTable_[Opcode::SMSG_ENVIRONMENTAL_DAMAGE_LOG] = [this](network::Packet& packet) { + // uint64 victimGuid + uint8 envDmgType + uint32 damage + uint32 absorbed + uint32 resisted + // envDmgType: 0=Exhausted(fatigue), 1=Drowning, 2=Fall, 3=Lava, 4=Slime, 5=Fire + if (!packet.hasRemaining(21)) { packet.skipAll(); return; } + uint64_t victimGuid = packet.readUInt64(); + uint8_t envType = packet.readUInt8(); + uint32_t dmg = packet.readUInt32(); + uint32_t envAbs = packet.readUInt32(); + uint32_t envRes = packet.readUInt32(); + if (victimGuid == playerGuid) { + // Environmental damage: pass envType via powerType field for display differentiation + if (dmg > 0) + addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast(dmg), 0, false, envType, 0, victimGuid); + if (envAbs > 0) + addCombatText(CombatTextEntry::ABSORB, static_cast(envAbs), 0, false, 0, 0, victimGuid); + if (envRes > 0) + addCombatText(CombatTextEntry::RESIST, static_cast(envRes), 0, false, 0, 0, victimGuid); + } + packet.skipAll(); + }; + + // ---- Spline move flag changes for other units (unroot/unset_hover/water_walk) ---- + for (auto op : {Opcode::SMSG_SPLINE_MOVE_UNROOT, + Opcode::SMSG_SPLINE_MOVE_UNSET_HOVER, + Opcode::SMSG_SPLINE_MOVE_WATER_WALK}) { + dispatchTable_[op] = [this](network::Packet& packet) { + // Minimal parse: PackedGuid only — no animation-relevant state change. + if (packet.hasRemaining(1)) { + (void)packet.readPackedGuid(); + } + }; + } + + dispatchTable_[Opcode::SMSG_SPLINE_MOVE_UNSET_FLYING] = [this](network::Packet& packet) { + // PackedGuid + synthesised move-flags=0 → clears flying animation. + if (!packet.hasRemaining(1)) return; + uint64_t guid = packet.readPackedGuid(); + if (guid == 0 || guid == playerGuid || !unitMoveFlagsCallback_) return; + unitMoveFlagsCallback_(guid, 0u); // clear flying/CAN_FLY + }; + + // ---- Spline speed changes for other units ---- + // These use *logicalOp to distinguish which speed to set, so each gets a separate lambda. + dispatchTable_[Opcode::SMSG_SPLINE_SET_FLIGHT_SPEED] = [this](network::Packet& packet) { + // Minimal parse: PackedGuid + float speed + if (!packet.hasRemaining(5)) return; + uint64_t sGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float sSpeed = packet.readFloat(); + if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) { + serverFlightSpeed_ = sSpeed; + } + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_FLIGHT_BACK_SPEED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t sGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float sSpeed = packet.readFloat(); + if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) { + serverFlightBackSpeed_ = sSpeed; + } + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t sGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float sSpeed = packet.readFloat(); + if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) { + serverSwimBackSpeed_ = sSpeed; + } + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_WALK_SPEED] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t sGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float sSpeed = packet.readFloat(); + if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) { + serverWalkSpeed_ = sSpeed; + } + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_TURN_RATE] = [this](network::Packet& packet) { + if (!packet.hasRemaining(5)) return; + uint64_t sGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + float sSpeed = packet.readFloat(); + if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) { + serverTurnRate_ = sSpeed; // rad/s + } + }; + dispatchTable_[Opcode::SMSG_SPLINE_SET_PITCH_RATE] = [this](network::Packet& packet) { + // Minimal parse: PackedGuid + float speed — pitch rate not stored locally + if (!packet.hasRemaining(5)) return; + (void)packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + (void)packet.readFloat(); + }; + + // ---- Threat updates ---- + for (auto op : {Opcode::SMSG_HIGHEST_THREAT_UPDATE, + Opcode::SMSG_THREAT_UPDATE}) { + dispatchTable_[op] = [this](network::Packet& packet) { + // Both packets share the same format: + // packed_guid (unit) + packed_guid (highest-threat target or target, unused here) + // + uint32 count + count × (packed_guid victim + uint32 threat) + if (!packet.hasRemaining(1)) return; + uint64_t unitGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(1)) return; + (void)packet.readPackedGuid(); // highest-threat / current target + if (!packet.hasRemaining(4)) return; + uint32_t cnt = packet.readUInt32(); + if (cnt > 100) { packet.skipAll(); return; } // sanity + std::vector list; + list.reserve(cnt); + for (uint32_t i = 0; i < cnt; ++i) { + if (!packet.hasRemaining(1)) return; + ThreatEntry entry; + entry.victimGuid = packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + entry.threat = packet.readUInt32(); + list.push_back(entry); + } + // Sort descending by threat so highest is first + std::sort(list.begin(), list.end(), + [](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; }); + threatLists_[unitGuid] = std::move(list); + fireAddonEvent("UNIT_THREAT_LIST_UPDATE", {}); + }; + } + + // ---- Player movement flag changes (server-pushed) ---- + dispatchTable_[Opcode::SMSG_MOVE_GRAVITY_DISABLE] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "GRAVITY_DISABLE", Opcode::CMSG_MOVE_GRAVITY_DISABLE_ACK, + static_cast(MovementFlags::LEVITATING), true); + }; + dispatchTable_[Opcode::SMSG_MOVE_GRAVITY_ENABLE] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "GRAVITY_ENABLE", Opcode::CMSG_MOVE_GRAVITY_ENABLE_ACK, + static_cast(MovementFlags::LEVITATING), false); + }; + dispatchTable_[Opcode::SMSG_MOVE_LAND_WALK] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "LAND_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK, + static_cast(MovementFlags::WATER_WALK), false); + }; + dispatchTable_[Opcode::SMSG_MOVE_NORMAL_FALL] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "NORMAL_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK, + static_cast(MovementFlags::FEATHER_FALL), false); + }; + dispatchTable_[Opcode::SMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "SET_CAN_TRANSITION_SWIM_FLY", + Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, true); + }; + dispatchTable_[Opcode::SMSG_MOVE_UNSET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "UNSET_CAN_TRANSITION_SWIM_FLY", + Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, false); + }; + dispatchTable_[Opcode::SMSG_MOVE_SET_COLLISION_HGT] = [this](network::Packet& packet) { + handleMoveSetCollisionHeight(packet); + }; + dispatchTable_[Opcode::SMSG_MOVE_SET_FLIGHT] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "SET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK, + static_cast(MovementFlags::FLYING), true); + }; + dispatchTable_[Opcode::SMSG_MOVE_UNSET_FLIGHT] = [this](network::Packet& packet) { + handleForceMoveFlagChange(packet, "UNSET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK, + static_cast(MovementFlags::FLYING), false); + }; + + // ---- Batch 7: World states, action buttons, level-up, vendor, inventory ---- + + // ---- SMSG_INIT_WORLD_STATES ---- + dispatchTable_[Opcode::SMSG_INIT_WORLD_STATES] = [this](network::Packet& packet) { + // WotLK format: uint32 mapId, uint32 zoneId, uint32 areaId, uint16 count, N*(uint32 key, uint32 val) + // Classic/TBC format: uint32 mapId, uint32 zoneId, uint16 count, N*(uint32 key, uint32 val) + if (!packet.hasRemaining(10)) { + LOG_WARNING("SMSG_INIT_WORLD_STATES too short: ", packet.getSize(), " bytes"); + return; + } + worldStateMapId_ = packet.readUInt32(); + { + uint32_t newZoneId = packet.readUInt32(); + if (newZoneId != worldStateZoneId_ && newZoneId != 0) { + worldStateZoneId_ = newZoneId; + fireAddonEvent("ZONE_CHANGED_NEW_AREA", {}); + fireAddonEvent("ZONE_CHANGED", {}); + } else { + worldStateZoneId_ = newZoneId; + } + } + // WotLK adds areaId (uint32) before count; Classic/TBC/Turtle use the shorter format + size_t remaining = packet.getRemainingSize(); + bool isWotLKFormat = isActiveExpansion("wotlk"); + if (isWotLKFormat && remaining >= 6) { + packet.readUInt32(); // areaId (WotLK only) + } + uint16_t count = packet.readUInt16(); + size_t needed = static_cast(count) * 8; + size_t available = packet.getRemainingSize(); + if (available < needed) { + // Be tolerant across expansion/private-core variants: if packet shape + // still looks like N*(key,val) dwords, parse what is present. + if ((available % 8) == 0) { + uint16_t adjustedCount = static_cast(available / 8); + LOG_WARNING("SMSG_INIT_WORLD_STATES count mismatch: header=", count, + " adjusted=", adjustedCount, " (available=", available, ")"); + count = adjustedCount; + needed = available; + } else { + LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed, + " bytes of state pairs, got ", available); + packet.skipAll(); + return; + } + } + worldStates_.clear(); + worldStates_.reserve(count); + for (uint16_t i = 0; i < count; ++i) { + uint32_t key = packet.readUInt32(); + uint32_t val = packet.readUInt32(); + worldStates_[key] = val; + } + }; + + // ---- SMSG_ACTION_BUTTONS ---- + dispatchTable_[Opcode::SMSG_ACTION_BUTTONS] = [this](network::Packet& packet) { + // Slot encoding differs by expansion: + // Classic/Turtle: uint16 actionId + uint8 type + uint8 misc + // type: 0=spell, 1=item, 64=macro + // TBC/WotLK: uint32 packed = actionId | (type << 24) + // type: 0x00=spell, 0x80=item, 0x40=macro + // Format differences: + // Classic 1.12: no mode byte, 120 slots (480 bytes) + // TBC 2.4.3: no mode byte, 132 slots (528 bytes) + // WotLK 3.3.5a: uint8 mode + 144 slots (577 bytes) + size_t rem = packet.getRemainingSize(); + const bool hasModeByteExp = isActiveExpansion("wotlk"); + int serverBarSlots; + if (isClassicLikeExpansion()) { + serverBarSlots = 120; + } else if (isActiveExpansion("tbc")) { + serverBarSlots = 132; + } else { + serverBarSlots = 144; + } + if (hasModeByteExp) { + if (rem < 1) return; + /*uint8_t mode =*/ packet.readUInt8(); + rem--; + } + for (int i = 0; i < serverBarSlots; ++i) { + if (rem < 4) return; + uint32_t packed = packet.readUInt32(); + rem -= 4; + if (i >= ACTION_BAR_SLOTS) continue; // only load bars 1 and 2 + if (packed == 0) { + // Empty slot — only clear if not already set to Attack/Hearthstone defaults + // so we don't wipe hardcoded fallbacks when the server sends zeros. + continue; + } + uint8_t type = 0; + uint32_t id = 0; + if (isClassicLikeExpansion()) { + id = packed & 0x0000FFFFu; + type = static_cast((packed >> 16) & 0xFF); + } else { + type = static_cast((packed >> 24) & 0xFF); + id = packed & 0x00FFFFFFu; + } + if (id == 0) continue; + ActionBarSlot slot; + switch (type) { + case 0x00: slot.type = ActionBarSlot::SPELL; slot.id = id; break; + case 0x01: slot.type = ActionBarSlot::ITEM; slot.id = id; break; // Classic item + case 0x80: slot.type = ActionBarSlot::ITEM; slot.id = id; break; // TBC/WotLK item + case 0x40: slot.type = ActionBarSlot::MACRO; slot.id = id; break; // macro (all expansions) + default: continue; // unknown — leave as-is + } + actionBar[i] = slot; + } + // Apply any pending cooldowns from spellCooldowns to newly populated slots. + // SMSG_SPELL_COOLDOWN often arrives before SMSG_ACTION_BUTTONS during login, + // so the per-slot cooldownRemaining would be 0 without this sync. + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id != 0) { + auto cdIt = spellCooldowns.find(slot.id); + if (cdIt != spellCooldowns.end() && cdIt->second > 0.0f) { + slot.cooldownRemaining = cdIt->second; + slot.cooldownTotal = cdIt->second; + } + } else if (slot.type == ActionBarSlot::ITEM && slot.id != 0) { + // Items (potions, trinkets): look up the item's on-use spell + // and check if that spell has a pending cooldown. + const auto* qi = getItemInfo(slot.id); + if (qi && qi->valid) { + for (const auto& sp : qi->spells) { + if (sp.spellId == 0) continue; + auto cdIt = spellCooldowns.find(sp.spellId); + if (cdIt != spellCooldowns.end() && cdIt->second > 0.0f) { + slot.cooldownRemaining = cdIt->second; + slot.cooldownTotal = cdIt->second; + break; + } + } + } + } + } + LOG_INFO("SMSG_ACTION_BUTTONS: populated action bar from server"); + fireAddonEvent("ACTIONBAR_SLOT_CHANGED", {}); + packet.skipAll(); + }; + + // ---- SMSG_LEVELUP_INFO / SMSG_LEVELUP_INFO_ALT (shared body) ---- + for (auto op : {Opcode::SMSG_LEVELUP_INFO, Opcode::SMSG_LEVELUP_INFO_ALT}) { + dispatchTable_[op] = [this](network::Packet& packet) { + // Server-authoritative level-up event. + // WotLK layout: uint32 newLevel + uint32 hpDelta + uint32 manaDelta + 5x uint32 statDeltas + if (packet.hasRemaining(4)) { + uint32_t newLevel = packet.readUInt32(); + if (newLevel > 0) { + // Parse stat deltas (WotLK layout has 7 more uint32s) + lastLevelUpDeltas_ = {}; + if (packet.hasRemaining(28)) { + lastLevelUpDeltas_.hp = packet.readUInt32(); + lastLevelUpDeltas_.mana = packet.readUInt32(); + lastLevelUpDeltas_.str = packet.readUInt32(); + lastLevelUpDeltas_.agi = packet.readUInt32(); + lastLevelUpDeltas_.sta = packet.readUInt32(); + lastLevelUpDeltas_.intel = packet.readUInt32(); + lastLevelUpDeltas_.spi = packet.readUInt32(); + } + uint32_t oldLevel = serverPlayerLevel_; + serverPlayerLevel_ = std::max(serverPlayerLevel_, newLevel); + for (auto& ch : characters) { + if (ch.guid == playerGuid) { + ch.level = serverPlayerLevel_; + return; + } + } + if (newLevel > oldLevel) { + addSystemChatMessage("You have reached level " + std::to_string(newLevel) + "!"); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); }); + if (levelUpCallback_) levelUpCallback_(newLevel); + fireAddonEvent("PLAYER_LEVEL_UP", {std::to_string(newLevel)}); + } + } + } + packet.skipAll(); + }; + } + + // ---- SMSG_SELL_ITEM ---- + dispatchTable_[Opcode::SMSG_SELL_ITEM] = [this](network::Packet& packet) { + // uint64 vendorGuid, uint64 itemGuid, uint8 result + if (packet.hasRemaining(17)) { + uint64_t vendorGuid = packet.readUInt64(); + uint64_t itemGuid = packet.readUInt64(); + uint8_t result = packet.readUInt8(); + LOG_INFO("SMSG_SELL_ITEM: vendorGuid=0x", std::hex, vendorGuid, + " itemGuid=0x", itemGuid, std::dec, + " result=", static_cast(result)); + if (result == 0) { + pendingSellToBuyback_.erase(itemGuid); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playDropOnGround(); }); + fireAddonEvent("BAG_UPDATE", {}); + fireAddonEvent("PLAYER_MONEY", {}); + } else { + bool removedPending = false; + auto it = pendingSellToBuyback_.find(itemGuid); + if (it != pendingSellToBuyback_.end()) { + for (auto bit = buybackItems_.begin(); bit != buybackItems_.end(); ++bit) { + if (bit->itemGuid == itemGuid) { + buybackItems_.erase(bit); + return; + } + } + pendingSellToBuyback_.erase(it); + removedPending = true; + } + if (!removedPending) { + // Some cores return a non-item GUID on sell failure; drop the newest + // optimistic entry if it is still pending so stale rows don't block buyback. + if (!buybackItems_.empty()) { + uint64_t frontGuid = buybackItems_.front().itemGuid; + if (pendingSellToBuyback_.erase(frontGuid) > 0) { + buybackItems_.pop_front(); + removedPending = true; + } + } + } + if (!removedPending && !pendingSellToBuyback_.empty()) { + // Last-resort desync recovery. + pendingSellToBuyback_.clear(); + buybackItems_.clear(); + } + static const char* sellErrors[] = { + "OK", "Can't find item", "Can't sell item", + "Can't find vendor", "You don't own that item", + "Unknown error", "Only empty bag" + }; + const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error"; + addUIError(std::string("Sell failed: ") + msg); + addSystemChatMessage(std::string("Sell failed: ") + msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playError(); }); + LOG_WARNING("SMSG_SELL_ITEM error: ", static_cast(result), " (", msg, ")"); + } + } + }; + + // ---- SMSG_INVENTORY_CHANGE_FAILURE ---- + dispatchTable_[Opcode::SMSG_INVENTORY_CHANGE_FAILURE] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t error = packet.readUInt8(); + if (error != 0) { + LOG_WARNING("SMSG_INVENTORY_CHANGE_FAILURE: error=", static_cast(error)); + // After error byte: item_guid1(8) + item_guid2(8) + bag_slot(1) = 17 bytes + uint32_t requiredLevel = 0; + if (packet.hasRemaining(17)) { + packet.readUInt64(); // item_guid1 + packet.readUInt64(); // item_guid2 + packet.readUInt8(); // bag_slot + // Error 1 = EQUIP_ERR_LEVEL_REQ: server appends required level as uint32 + if (error == 1 && packet.hasRemaining(4)) + requiredLevel = packet.readUInt32(); + } + // InventoryResult enum (AzerothCore 3.3.5a) + const char* errMsg = nullptr; + char levelBuf[64]; + switch (error) { + case 1: + if (requiredLevel > 0) { + std::snprintf(levelBuf, sizeof(levelBuf), + "You must reach level %u to use that item.", requiredLevel); + addUIError(levelBuf); + addSystemChatMessage(levelBuf); + } else { + addUIError("You must reach a higher level to use that item."); + addSystemChatMessage("You must reach a higher level to use that item."); + } + return; + case 2: errMsg = "You don't have the required skill."; break; + case 3: errMsg = "That item doesn't go in that slot."; break; + case 4: errMsg = "That bag is full."; break; + case 5: errMsg = "Can't put bags in bags."; break; + case 6: errMsg = "Can't trade equipped bags."; break; + case 7: errMsg = "That slot only holds ammo."; break; + case 8: errMsg = "You can't use that item."; break; + case 9: errMsg = "No equipment slot available."; break; + case 10: errMsg = "You can never use that item."; break; + case 11: errMsg = "You can never use that item."; break; + case 12: errMsg = "No equipment slot available."; break; + case 13: errMsg = "Can't equip with a two-handed weapon."; break; + case 14: errMsg = "Can't dual-wield."; break; + case 15: errMsg = "That item doesn't go in that bag."; break; + case 16: errMsg = "That item doesn't go in that bag."; break; + case 17: errMsg = "You can't carry any more of those."; break; + case 18: errMsg = "No equipment slot available."; break; + case 19: errMsg = "Can't stack those items."; break; + case 20: errMsg = "That item can't be equipped."; break; + case 21: errMsg = "Can't swap items."; break; + case 22: errMsg = "That slot is empty."; break; + case 23: errMsg = "Item not found."; break; + case 24: errMsg = "Can't drop soulbound items."; break; + case 25: errMsg = "Out of range."; break; + case 26: errMsg = "Need to split more than 1."; break; + case 27: errMsg = "Split failed."; break; + case 28: errMsg = "Not enough reagents."; break; + case 29: errMsg = "Not enough money."; break; + case 30: errMsg = "Not a bag."; break; + case 31: errMsg = "Can't destroy non-empty bag."; break; + case 32: errMsg = "You don't own that item."; break; + case 33: errMsg = "You can only have one quiver."; break; + case 34: errMsg = "No free bank slots."; break; + case 35: errMsg = "No bank here."; break; + case 36: errMsg = "Item is locked."; break; + case 37: errMsg = "You are stunned."; break; + case 38: errMsg = "You are dead."; break; + case 39: errMsg = "Can't do that right now."; break; + case 40: errMsg = "Internal bag error."; break; + case 49: errMsg = "Loot is gone."; break; + case 50: errMsg = "Inventory is full."; break; + case 51: errMsg = "Bank is full."; break; + case 52: errMsg = "That item is sold out."; break; + case 58: errMsg = "That object is busy."; break; + case 60: errMsg = "Can't do that in combat."; break; + case 61: errMsg = "Can't do that while disarmed."; break; + case 63: errMsg = "Requires a higher rank."; break; + case 64: errMsg = "Requires higher reputation."; break; + case 67: errMsg = "That item is unique-equipped."; break; + case 69: errMsg = "Not enough honor points."; break; + case 70: errMsg = "Not enough arena points."; break; + case 77: errMsg = "Too much gold."; break; + case 78: errMsg = "Can't do that during arena match."; break; + case 80: errMsg = "Requires a personal arena rating."; break; + case 87: errMsg = "Requires a higher level."; break; + case 88: errMsg = "Requires the right talent."; break; + default: break; + } + std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; + addUIError(msg); + addSystemChatMessage(msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playError(); }); + } + } + }; + + // ---- SMSG_BUY_FAILED ---- + dispatchTable_[Opcode::SMSG_BUY_FAILED] = [this](network::Packet& packet) { + // vendorGuid(8) + itemId(4) + errorCode(1) + if (packet.hasRemaining(13)) { + uint64_t vendorGuid = packet.readUInt64(); + uint32_t itemIdOrSlot = packet.readUInt32(); + uint8_t errCode = packet.readUInt8(); + LOG_INFO("SMSG_BUY_FAILED: vendorGuid=0x", std::hex, vendorGuid, std::dec, + " item/slot=", itemIdOrSlot, + " err=", static_cast(errCode), + " pendingBuybackSlot=", pendingBuybackSlot_, + " pendingBuybackWireSlot=", pendingBuybackWireSlot_, + " pendingBuyItemId=", pendingBuyItemId_, + " pendingBuyItemSlot=", pendingBuyItemSlot_); + if (pendingBuybackSlot_ >= 0) { + // Some cores require probing absolute buyback slots until a live entry is found. + if (errCode == 0) { + constexpr uint16_t kWotlkCmsgBuybackItemOpcode = 0x290; + constexpr uint32_t kBuybackSlotEnd = 85; + if (pendingBuybackWireSlot_ >= 74 && pendingBuybackWireSlot_ < kBuybackSlotEnd && + socket && state == WorldState::IN_WORLD && currentVendorItems.vendorGuid != 0) { + ++pendingBuybackWireSlot_; + LOG_INFO("Buyback retry: vendorGuid=0x", std::hex, currentVendorItems.vendorGuid, + std::dec, " uiSlot=", pendingBuybackSlot_, + " wireSlot=", pendingBuybackWireSlot_); + network::Packet retry(kWotlkCmsgBuybackItemOpcode); + retry.writeUInt64(currentVendorItems.vendorGuid); + retry.writeUInt32(pendingBuybackWireSlot_); + socket->send(retry); + return; + } + // Exhausted slot probe: drop stale local row and advance. + if (pendingBuybackSlot_ < static_cast(buybackItems_.size())) { + buybackItems_.erase(buybackItems_.begin() + pendingBuybackSlot_); + } + pendingBuybackSlot_ = -1; + pendingBuybackWireSlot_ = 0; + if (currentVendorItems.vendorGuid != 0 && socket && state == WorldState::IN_WORLD) { + auto pkt = ListInventoryPacket::build(currentVendorItems.vendorGuid); + socket->send(pkt); + } + return; + } + pendingBuybackSlot_ = -1; + pendingBuybackWireSlot_ = 0; + } + + const char* msg = "Purchase failed."; + switch (errCode) { + case 0: msg = "Purchase failed: item not found."; break; + case 2: msg = "You don't have enough money."; break; + case 4: msg = "Seller is too far away."; break; + case 5: msg = "That item is sold out."; break; + case 6: msg = "You can't carry any more items."; break; + default: break; + } + addUIError(msg); + addSystemChatMessage(msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playError(); }); + } + }; + + // ---- SMSG_BUY_ITEM ---- + dispatchTable_[Opcode::SMSG_BUY_ITEM] = [this](network::Packet& packet) { + // uint64 vendorGuid + uint32 vendorSlot + int32 newCount + uint32 itemCount + // Confirms a successful CMSG_BUY_ITEM. The inventory update arrives via SMSG_UPDATE_OBJECT. + if (packet.hasRemaining(20)) { + /*uint64_t vendorGuid =*/ packet.readUInt64(); + /*uint32_t vendorSlot =*/ packet.readUInt32(); + /*int32_t newCount =*/ static_cast(packet.readUInt32()); + uint32_t itemCount = packet.readUInt32(); + // Show purchase confirmation with item name if available + if (pendingBuyItemId_ != 0) { + std::string itemLabel; + uint32_t buyQuality = 1; + if (const ItemQueryResponseData* info = getItemInfo(pendingBuyItemId_)) { + if (!info->name.empty()) itemLabel = info->name; + buyQuality = info->quality; + } + if (itemLabel.empty()) itemLabel = "item #" + std::to_string(pendingBuyItemId_); + std::string msg = "Purchased: " + buildItemLink(pendingBuyItemId_, buyQuality, itemLabel); + if (itemCount > 1) msg += " x" + std::to_string(itemCount); + addSystemChatMessage(msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playPickupBag(); }); + } + pendingBuyItemId_ = 0; + pendingBuyItemSlot_ = 0; + fireAddonEvent("MERCHANT_UPDATE", {}); + fireAddonEvent("BAG_UPDATE", {}); + } + }; + + // ---- MSG_RAID_TARGET_UPDATE ---- + dispatchTable_[Opcode::MSG_RAID_TARGET_UPDATE] = [this](network::Packet& packet) { + // uint8 type: 0 = full update (8 × (uint8 icon + uint64 guid)), + // 1 = single update (uint8 icon + uint64 guid) + size_t remRTU = packet.getRemainingSize(); + if (remRTU < 1) return; + uint8_t rtuType = packet.readUInt8(); + if (rtuType == 0) { + // Full update: always 8 entries + for (uint32_t i = 0; i < kRaidMarkCount; ++i) { + if (!packet.hasRemaining(9)) return; + uint8_t icon = packet.readUInt8(); + uint64_t guid = packet.readUInt64(); + if (icon < kRaidMarkCount) + raidTargetGuids_[icon] = guid; + } + } else { + // Single update + if (packet.hasRemaining(9)) { + uint8_t icon = packet.readUInt8(); + uint64_t guid = packet.readUInt64(); + if (icon < kRaidMarkCount) + raidTargetGuids_[icon] = guid; + } + } + LOG_DEBUG("MSG_RAID_TARGET_UPDATE: type=", static_cast(rtuType)); + fireAddonEvent("RAID_TARGET_UPDATE", {}); + }; + + // ---- SMSG_CRITERIA_UPDATE ---- + dispatchTable_[Opcode::SMSG_CRITERIA_UPDATE] = [this](network::Packet& packet) { + // uint32 criteriaId + uint64 progress + uint32 elapsedTime + uint32 creationTime + if (packet.hasRemaining(20)) { + uint32_t criteriaId = packet.readUInt32(); + uint64_t progress = packet.readUInt64(); + packet.readUInt32(); // elapsedTime + packet.readUInt32(); // creationTime + uint64_t oldProgress = 0; + auto cpit = criteriaProgress_.find(criteriaId); + if (cpit != criteriaProgress_.end()) oldProgress = cpit->second; + criteriaProgress_[criteriaId] = progress; + LOG_DEBUG("SMSG_CRITERIA_UPDATE: id=", criteriaId, " progress=", progress); + // Fire addon event for achievement tracking addons + if (progress != oldProgress) + fireAddonEvent("CRITERIA_UPDATE", {std::to_string(criteriaId), std::to_string(progress)}); + } + }; + + // ---- SMSG_BARBER_SHOP_RESULT ---- + dispatchTable_[Opcode::SMSG_BARBER_SHOP_RESULT] = [this](network::Packet& packet) { + // uint32 result (0 = success, 1 = no money, 2 = not barber, 3 = sitting) + if (packet.hasRemaining(4)) { + uint32_t result = packet.readUInt32(); + if (result == 0) { + addSystemChatMessage("Hairstyle changed."); + barberShopOpen_ = false; + fireAddonEvent("BARBER_SHOP_CLOSE", {}); + } else { + const char* msg = (result == 1) ? "Not enough money for new hairstyle." + : (result == 2) ? "You are not at a barber shop." + : (result == 3) ? "You must stand up to use the barber shop." + : "Barber shop unavailable."; + addUIError(msg); + addSystemChatMessage(msg); + } + LOG_DEBUG("SMSG_BARBER_SHOP_RESULT: result=", result); + } + }; + + // ---- SMSG_QUESTGIVER_QUEST_FAILED ---- + dispatchTable_[Opcode::SMSG_QUESTGIVER_QUEST_FAILED] = [this](network::Packet& packet) { + // uint32 questId + uint32 reason + if (packet.hasRemaining(8)) { + uint32_t questId = packet.readUInt32(); + uint32_t reason = packet.readUInt32(); + auto questTitle = getQuestTitle(questId); + const char* reasonStr = nullptr; + switch (reason) { + case 1: reasonStr = "failed conditions"; break; + case 2: reasonStr = "inventory full"; break; + case 3: reasonStr = "too far away"; break; + case 4: reasonStr = "another quest is blocking"; break; + case 5: reasonStr = "wrong time of day"; break; + case 6: reasonStr = "wrong race"; break; + case 7: reasonStr = "wrong class"; break; + } + std::string msg = questTitle.empty() ? "Quest" : ('"' + questTitle + '"'); + msg += " failed"; + if (reasonStr) msg += std::string(": ") + reasonStr; + msg += '.'; + addSystemChatMessage(msg); + } + }; + + + // ----------------------------------------------------------------------- + // Batch 8-12: Remaining opcodes (inspects, quests, auctions, spells, + // calendars, battlefields, voice, misc consume-only) + // ----------------------------------------------------------------------- + // uint32 setIndex + uint64 guid — equipment set was successfully saved + dispatchTable_[Opcode::SMSG_EQUIPMENT_SET_SAVED] = [this](network::Packet& packet) { + // uint32 setIndex + uint64 guid — equipment set was successfully saved + std::string setName; + if (packet.hasRemaining(12)) { + uint32_t setIndex = packet.readUInt32(); + uint64_t setGuid = packet.readUInt64(); + // Update the local set's GUID so subsequent "Update" calls + // use the server-assigned GUID instead of 0 (which would + // create a duplicate instead of updating). + bool found = false; + for (auto& es : equipmentSets_) { + if (es.setGuid == setGuid || es.setId == setIndex) { + es.setGuid = setGuid; + setName = es.name; + found = true; + break; + } + } + // Also update public-facing info + for (auto& info : equipmentSetInfo_) { + if (info.setGuid == setGuid || info.setId == setIndex) { + info.setGuid = setGuid; + break; + } + } + // If the set doesn't exist locally yet (new save), add a + // placeholder entry so it shows up in the UI immediately. + if (!found && setGuid != 0) { + EquipmentSet newEs; + newEs.setGuid = setGuid; + newEs.setId = setIndex; + newEs.name = pendingSaveSetName_; + newEs.iconName = pendingSaveSetIcon_; + for (int s = 0; s < 19; ++s) + newEs.itemGuids[s] = getEquipSlotGuid(s); + equipmentSets_.push_back(std::move(newEs)); + EquipmentSetInfo newInfo; + newInfo.setGuid = setGuid; + newInfo.setId = setIndex; + newInfo.name = pendingSaveSetName_; + newInfo.iconName = pendingSaveSetIcon_; + equipmentSetInfo_.push_back(std::move(newInfo)); + setName = pendingSaveSetName_; + } + pendingSaveSetName_.clear(); + pendingSaveSetIcon_.clear(); + LOG_INFO("SMSG_EQUIPMENT_SET_SAVED: index=", setIndex, + " guid=", setGuid, " name=", setName); + } + addSystemChatMessage(setName.empty() + ? std::string("Equipment set saved.") + : "Equipment set \"" + setName + "\" saved."); + }; + // WotLK: packed_guid victim + packed_guid caster + uint32 spellId + uint32 count + effects + // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint32 count + effects + // Classic/Vanilla: packed_guid (same as WotLK) + dispatchTable_[Opcode::SMSG_PERIODICAURALOG] = [this](network::Packet& packet) { + // WotLK: packed_guid victim + packed_guid caster + uint32 spellId + uint32 count + effects + // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint32 count + effects + // Classic/Vanilla: packed_guid (same as WotLK) + const bool periodicTbc = isActiveExpansion("tbc"); + const size_t guidMinSz = periodicTbc ? 8u : 2u; + if (!packet.hasRemaining(guidMinSz)) return; + uint64_t victimGuid = periodicTbc + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(guidMinSz)) return; + uint64_t casterGuid = periodicTbc + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) return; + uint32_t spellId = packet.readUInt32(); + uint32_t count = packet.readUInt32(); + bool isPlayerVictim = (victimGuid == playerGuid); + bool isPlayerCaster = (casterGuid == playerGuid); + if (!isPlayerVictim && !isPlayerCaster) { + packet.skipAll(); + return; + } + for (uint32_t i = 0; i < count && packet.hasRemaining(1); ++i) { + uint8_t auraType = packet.readUInt8(); + if (auraType == 3 || auraType == 89) { + // Classic/TBC: damage(4)+school(4)+absorbed(4)+resisted(4) = 16 bytes + // WotLK 3.3.5a: damage(4)+overkill(4)+school(4)+absorbed(4)+resisted(4)+isCrit(1) = 21 bytes + const bool periodicWotlk = isActiveExpansion("wotlk"); + const size_t dotSz = periodicWotlk ? 21u : 16u; + if (!packet.hasRemaining(dotSz)) break; + uint32_t dmg = packet.readUInt32(); + if (periodicWotlk) /*uint32_t overkill=*/ packet.readUInt32(); + /*uint32_t school=*/ packet.readUInt32(); + uint32_t abs = packet.readUInt32(); + uint32_t res = packet.readUInt32(); + bool dotCrit = false; + if (periodicWotlk) dotCrit = (packet.readUInt8() != 0); + if (dmg > 0) + addCombatText(dotCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::PERIODIC_DAMAGE, + static_cast(dmg), + spellId, isPlayerCaster, 0, casterGuid, victimGuid); + if (abs > 0) + addCombatText(CombatTextEntry::ABSORB, static_cast(abs), + spellId, isPlayerCaster, 0, casterGuid, victimGuid); + if (res > 0) + addCombatText(CombatTextEntry::RESIST, static_cast(res), + spellId, isPlayerCaster, 0, casterGuid, victimGuid); + } else if (auraType == 8 || auraType == 124 || auraType == 45) { + // Classic/TBC: heal(4)+maxHeal(4)+overHeal(4) = 12 bytes + // WotLK 3.3.5a: heal(4)+maxHeal(4)+overHeal(4)+absorbed(4)+isCrit(1) = 17 bytes + const bool healWotlk = isActiveExpansion("wotlk"); + const size_t hotSz = healWotlk ? 17u : 12u; + if (!packet.hasRemaining(hotSz)) break; + uint32_t heal = packet.readUInt32(); + /*uint32_t max=*/ packet.readUInt32(); + /*uint32_t over=*/ packet.readUInt32(); + uint32_t hotAbs = 0; + bool hotCrit = false; + if (healWotlk) { + hotAbs = packet.readUInt32(); + hotCrit = (packet.readUInt8() != 0); + } + addCombatText(hotCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::PERIODIC_HEAL, + static_cast(heal), + spellId, isPlayerCaster, 0, casterGuid, victimGuid); + if (hotAbs > 0) + addCombatText(CombatTextEntry::ABSORB, static_cast(hotAbs), + spellId, isPlayerCaster, 0, casterGuid, victimGuid); + } else if (auraType == 46 || auraType == 91) { + // OBS_MOD_POWER / PERIODIC_ENERGIZE: miscValue(powerType) + amount + // Common in WotLK: Replenishment, Mana Spring Totem, Divine Plea, etc. + if (!packet.hasRemaining(8)) break; + uint8_t periodicPowerType = static_cast(packet.readUInt32()); + uint32_t amount = packet.readUInt32(); + if ((isPlayerVictim || isPlayerCaster) && amount > 0) + addCombatText(CombatTextEntry::ENERGIZE, static_cast(amount), + spellId, isPlayerCaster, periodicPowerType, casterGuid, victimGuid); + } else if (auraType == 98) { + // PERIODIC_MANA_LEECH: miscValue(powerType) + amount + float multiplier + if (!packet.hasRemaining(12)) break; + uint8_t powerType = static_cast(packet.readUInt32()); + uint32_t amount = packet.readUInt32(); + float multiplier = packet.readFloat(); + if (isPlayerVictim && amount > 0) + addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(amount), + spellId, false, powerType, casterGuid, victimGuid); + if (isPlayerCaster && amount > 0 && multiplier > 0.0f && std::isfinite(multiplier)) { + const uint32_t gainedAmount = static_cast( + std::lround(static_cast(amount) * static_cast(multiplier))); + if (gainedAmount > 0) { + addCombatText(CombatTextEntry::ENERGIZE, static_cast(gainedAmount), + spellId, true, powerType, casterGuid, casterGuid); + } + } + } else { + // Unknown/untracked aura type — stop parsing this event safely + packet.skipAll(); + break; + } + } + packet.skipAll(); + }; + // WotLK: packed_guid victim + packed_guid caster + uint32 spellId + uint8 powerType + int32 amount + // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint8 powerType + int32 amount + // Classic/Vanilla: packed_guid (same as WotLK) + dispatchTable_[Opcode::SMSG_SPELLENERGIZELOG] = [this](network::Packet& packet) { + // WotLK: packed_guid victim + packed_guid caster + uint32 spellId + uint8 powerType + int32 amount + // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint8 powerType + int32 amount + // Classic/Vanilla: packed_guid (same as WotLK) + const bool energizeTbc = isActiveExpansion("tbc"); + auto readEnergizeGuid = [&]() -> uint64_t { + if (energizeTbc) + return (packet.hasRemaining(8)) ? packet.readUInt64() : 0; + return packet.readPackedGuid(); + }; + if (!packet.hasRemaining(energizeTbc ? 8u : 1u) || (!energizeTbc && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t victimGuid = readEnergizeGuid(); + if (!packet.hasRemaining(energizeTbc ? 8u : 1u) || (!energizeTbc && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t casterGuid = readEnergizeGuid(); + if (!packet.hasRemaining(9)) { + packet.skipAll(); return; + } + uint32_t spellId = packet.readUInt32(); + uint8_t energizePowerType = packet.readUInt8(); + int32_t amount = static_cast(packet.readUInt32()); + bool isPlayerVictim = (victimGuid == playerGuid); + bool isPlayerCaster = (casterGuid == playerGuid); + if ((isPlayerVictim || isPlayerCaster) && amount > 0) + addCombatText(CombatTextEntry::ENERGIZE, amount, spellId, isPlayerCaster, energizePowerType, casterGuid, victimGuid); + packet.skipAll(); + }; + // uint32 currentZoneLightId + uint32 overrideLightId + uint32 transitionMs + dispatchTable_[Opcode::SMSG_OVERRIDE_LIGHT] = [this](network::Packet& packet) { + // uint32 currentZoneLightId + uint32 overrideLightId + uint32 transitionMs + if (packet.hasRemaining(12)) { + uint32_t zoneLightId = packet.readUInt32(); + uint32_t overrideLightId = packet.readUInt32(); + uint32_t transitionMs = packet.readUInt32(); + overrideLightId_ = overrideLightId; + overrideLightTransMs_ = transitionMs; + LOG_DEBUG("SMSG_OVERRIDE_LIGHT: zone=", zoneLightId, + " override=", overrideLightId, " transition=", transitionMs, "ms"); + } + }; + // Classic 1.12: uint32 weatherType + float intensity (8 bytes, no isAbrupt) + // TBC 2.4.3 / WotLK 3.3.5a: uint32 weatherType + float intensity + uint8 isAbrupt (9 bytes) + dispatchTable_[Opcode::SMSG_WEATHER] = [this](network::Packet& packet) { + // Classic 1.12: uint32 weatherType + float intensity (8 bytes, no isAbrupt) + // TBC 2.4.3 / WotLK 3.3.5a: uint32 weatherType + float intensity + uint8 isAbrupt (9 bytes) + if (packet.hasRemaining(8)) { + uint32_t wType = packet.readUInt32(); + float wIntensity = packet.readFloat(); + if (packet.hasRemaining(1)) + /*uint8_t isAbrupt =*/ packet.readUInt8(); + uint32_t prevWeatherType = weatherType_; + weatherType_ = wType; + weatherIntensity_ = wIntensity; + const char* typeName = (wType == 1) ? "Rain" : (wType == 2) ? "Snow" : (wType == 3) ? "Storm" : "Clear"; + LOG_INFO("Weather changed: type=", wType, " (", typeName, "), intensity=", wIntensity); + // Announce weather changes (including initial zone weather) + if (wType != prevWeatherType) { + const char* weatherMsg = nullptr; + if (wIntensity < 0.05f || wType == 0) { + if (prevWeatherType != 0) + weatherMsg = "The weather clears."; + } else if (wType == 1) { + weatherMsg = "It begins to rain."; + } else if (wType == 2) { + weatherMsg = "It begins to snow."; + } else if (wType == 3) { + weatherMsg = "A storm rolls in."; + } + if (weatherMsg) addSystemChatMessage(weatherMsg); + } + // Notify addons of weather change + fireAddonEvent("WEATHER_CHANGED", {std::to_string(wType), std::to_string(wIntensity)}); + // Storm transition: trigger a low-frequency thunder rumble shake + if (wType == 3 && wIntensity > 0.3f && cameraShakeCallback_) { + float mag = 0.03f + wIntensity * 0.04f; // 0.03–0.07 units + cameraShakeCallback_(mag, 6.0f, 0.6f); + } + } + }; + // Server-script text message — display in system chat + dispatchTable_[Opcode::SMSG_SCRIPT_MESSAGE] = [this](network::Packet& packet) { + // Server-script text message — display in system chat + std::string msg = packet.readString(); + if (!msg.empty()) { + addSystemChatMessage(msg); + LOG_INFO("SMSG_SCRIPT_MESSAGE: ", msg); + } + }; + // uint64 targetGuid + uint64 casterGuid + uint32 spellId + uint32 displayId + uint32 animType + dispatchTable_[Opcode::SMSG_ENCHANTMENTLOG] = [this](network::Packet& packet) { + // uint64 targetGuid + uint64 casterGuid + uint32 spellId + uint32 displayId + uint32 animType + if (packet.hasRemaining(28)) { + uint64_t enchTargetGuid = packet.readUInt64(); + uint64_t enchCasterGuid = packet.readUInt64(); + uint32_t enchSpellId = packet.readUInt32(); + /*uint32_t displayId =*/ packet.readUInt32(); + /*uint32_t animType =*/ packet.readUInt32(); + LOG_DEBUG("SMSG_ENCHANTMENTLOG: spellId=", enchSpellId); + // Show enchant message if the player is involved + if (enchTargetGuid == playerGuid || enchCasterGuid == playerGuid) { + const std::string& enchName = getSpellName(enchSpellId); + std::string casterName = lookupName(enchCasterGuid); + if (!enchName.empty()) { + std::string msg; + if (enchCasterGuid == playerGuid) + msg = "You enchant with " + enchName + "."; + else if (!casterName.empty()) + msg = casterName + " enchants your item with " + enchName + "."; + else + msg = "Your item has been enchanted with " + enchName + "."; + addSystemChatMessage(msg); + } + } + } + }; + // Quest query failed - parse failure reason + dispatchTable_[Opcode::SMSG_QUESTGIVER_QUEST_INVALID] = [this](network::Packet& packet) { + // Quest query failed - parse failure reason + if (packet.hasRemaining(4)) { + uint32_t failReason = packet.readUInt32(); + pendingTurnInRewardRequest_ = false; + const char* reasonStr = "Unknown"; + switch (failReason) { + case 0: reasonStr = "Don't have quest"; break; + case 1: reasonStr = "Quest level too low"; break; + case 4: reasonStr = "Insufficient money"; break; + case 5: reasonStr = "Inventory full"; break; + case 13: reasonStr = "Already on that quest"; break; + case 18: reasonStr = "Already completed quest"; break; + case 19: reasonStr = "Can't take any more quests"; break; + } + LOG_WARNING("Quest invalid: reason=", failReason, " (", reasonStr, ")"); + if (!pendingQuestAcceptTimeouts_.empty()) { + std::vector pendingQuestIds; + pendingQuestIds.reserve(pendingQuestAcceptTimeouts_.size()); + for (const auto& pending : pendingQuestAcceptTimeouts_) { + pendingQuestIds.push_back(pending.first); + } + for (uint32_t questId : pendingQuestIds) { + const uint64_t npcGuid = pendingQuestAcceptNpcGuids_.count(questId) != 0 + ? pendingQuestAcceptNpcGuids_[questId] : 0; + if (failReason == 13) { + std::string fallbackTitle = "Quest #" + std::to_string(questId); + std::string fallbackObjectives; + if (currentQuestDetails.questId == questId) { + if (!currentQuestDetails.title.empty()) fallbackTitle = currentQuestDetails.title; + fallbackObjectives = currentQuestDetails.objectives; + } + addQuestToLocalLogIfMissing(questId, fallbackTitle, fallbackObjectives); + triggerQuestAcceptResync(questId, npcGuid, "already-on-quest"); + } else if (failReason == 18) { + triggerQuestAcceptResync(questId, npcGuid, "already-completed"); + } + clearPendingQuestAccept(questId); + } + } + // Only show error to user for real errors (not informational messages) + if (failReason != 13 && failReason != 18) { // Don't spam "already on/completed" + addSystemChatMessage(std::string("Quest unavailable: ") + reasonStr); + } + } + }; + // Mark quest as complete in local log + dispatchTable_[Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE] = [this](network::Packet& packet) { + // Mark quest as complete in local log + if (packet.hasRemaining(4)) { + uint32_t questId = packet.readUInt32(); + LOG_INFO("Quest completed: questId=", questId); + if (pendingTurnInQuestId_ == questId) { + pendingTurnInQuestId_ = 0; + pendingTurnInNpcGuid_ = 0; + pendingTurnInRewardRequest_ = false; + } + for (auto it = questLog_.begin(); it != questLog_.end(); ++it) { + if (it->questId == questId) { + // Fire toast callback before erasing + if (questCompleteCallback_) { + questCompleteCallback_(questId, it->title); + } + // Play quest-complete sound + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestComplete(); }); + questLog_.erase(it); + LOG_INFO(" Removed quest ", questId, " from quest log"); + fireAddonEvent("QUEST_TURNED_IN", {std::to_string(questId)}); + break; + } + } + } + fireAddonEvent("QUEST_LOG_UPDATE", {}); + fireAddonEvent("UNIT_QUEST_LOG_CHANGED", {"player"}); + // Re-query all nearby quest giver NPCs so markers refresh + if (socket) { + for (const auto& [guid, entity] : entityManager.getEntities()) { + if (entity->getType() != ObjectType::UNIT) continue; + auto unit = std::static_pointer_cast(entity); + if (unit->getNpcFlags() & 0x02) { + network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + qsPkt.writeUInt64(guid); + socket->send(qsPkt); + } + } + } + }; + // Quest kill count update + // Compatibility: some classic-family opcode tables swap ADD_KILL and COMPLETE. + dispatchTable_[Opcode::SMSG_QUESTUPDATE_ADD_KILL] = [this](network::Packet& packet) { + // Quest kill count update + // Compatibility: some classic-family opcode tables swap ADD_KILL and COMPLETE. + size_t rem = packet.getRemainingSize(); + if (rem >= 12) { + uint32_t questId = packet.readUInt32(); + clearPendingQuestAccept(questId); + uint32_t entry = packet.readUInt32(); // Creature entry + uint32_t count = packet.readUInt32(); // Current kills + uint32_t reqCount = 0; + if (packet.hasRemaining(4)) { + reqCount = packet.readUInt32(); // Required kills (if present) + } + + LOG_INFO("Quest kill update: questId=", questId, " entry=", entry, + " count=", count, "/", reqCount); + + // Update quest log with kill count + for (auto& quest : questLog_) { + if (quest.questId == questId) { + // Preserve prior required count if this packet variant omits it. + if (reqCount == 0) { + auto it = quest.killCounts.find(entry); + if (it != quest.killCounts.end()) reqCount = it->second.second; + } + // Fall back to killObjectives (parsed from SMSG_QUEST_QUERY_RESPONSE). + // Note: npcOrGoId < 0 means game object; server always sends entry as uint32 + // in QUESTUPDATE_ADD_KILL regardless of type, so match by absolute value. + if (reqCount == 0) { + for (const auto& obj : quest.killObjectives) { + if (obj.npcOrGoId == 0 || obj.required == 0) continue; + uint32_t objEntry = static_cast( + obj.npcOrGoId > 0 ? obj.npcOrGoId : -obj.npcOrGoId); + if (objEntry == entry) { + reqCount = obj.required; + break; + } + } + } + if (reqCount == 0) reqCount = count; // last-resort: avoid 0/0 display + quest.killCounts[entry] = {count, reqCount}; + + std::string creatureName = getCachedCreatureName(entry); + std::string progressMsg = quest.title + ": "; + if (!creatureName.empty()) { + progressMsg += creatureName + " "; + } + progressMsg += std::to_string(count) + "/" + std::to_string(reqCount); + addSystemChatMessage(progressMsg); + + if (questProgressCallback_) { + questProgressCallback_(quest.title, creatureName, count, reqCount); + } + fireAddonEvent("QUEST_WATCH_UPDATE", {std::to_string(questId)}); + fireAddonEvent("QUEST_LOG_UPDATE", {}); + fireAddonEvent("UNIT_QUEST_LOG_CHANGED", {"player"}); + + LOG_INFO("Updated kill count for quest ", questId, ": ", + count, "/", reqCount); + break; + } + } + } else if (rem >= 4) { + // Swapped mapping fallback: treat as QUESTUPDATE_COMPLETE packet. + uint32_t questId = packet.readUInt32(); + clearPendingQuestAccept(questId); + LOG_INFO("Quest objectives completed (compat via ADD_KILL): questId=", questId); + for (auto& quest : questLog_) { + if (quest.questId == questId) { + quest.complete = true; + addSystemChatMessage("Quest Complete: " + quest.title); + break; + } + } + } + }; + // Quest item count update: itemId + count + dispatchTable_[Opcode::SMSG_QUESTUPDATE_ADD_ITEM] = [this](network::Packet& packet) { + // Quest item count update: itemId + count + if (packet.hasRemaining(8)) { + uint32_t itemId = packet.readUInt32(); + uint32_t count = packet.readUInt32(); + queryItemInfo(itemId, 0); + + std::string itemLabel = "item #" + std::to_string(itemId); + uint32_t questItemQuality = 1; + if (const ItemQueryResponseData* info = getItemInfo(itemId)) { + if (!info->name.empty()) itemLabel = info->name; + questItemQuality = info->quality; + } + + bool updatedAny = false; + for (auto& quest : questLog_) { + if (quest.complete) continue; + bool tracksItem = + quest.requiredItemCounts.count(itemId) > 0 || + quest.itemCounts.count(itemId) > 0; + // Also check itemObjectives parsed from SMSG_QUEST_QUERY_RESPONSE in case + // requiredItemCounts hasn't been populated yet (race during quest accept). + if (!tracksItem) { + for (const auto& obj : quest.itemObjectives) { + if (obj.itemId == itemId && obj.required > 0) { + quest.requiredItemCounts.emplace(itemId, obj.required); + tracksItem = true; + break; + } + } + } + if (!tracksItem) continue; + quest.itemCounts[itemId] = count; + updatedAny = true; + } + addSystemChatMessage("Quest item: " + buildItemLink(itemId, questItemQuality, itemLabel) + " (" + std::to_string(count) + ")"); + + if (questProgressCallback_ && updatedAny) { + // Find the quest that tracks this item to get title and required count + for (const auto& quest : questLog_) { + if (quest.complete) continue; + if (quest.itemCounts.count(itemId) == 0) continue; + uint32_t required = 0; + auto rIt = quest.requiredItemCounts.find(itemId); + if (rIt != quest.requiredItemCounts.end()) required = rIt->second; + if (required == 0) { + for (const auto& obj : quest.itemObjectives) { + if (obj.itemId == itemId) { required = obj.required; break; } + } + } + if (required == 0) required = count; + questProgressCallback_(quest.title, itemLabel, count, required); + break; + } + } + + if (updatedAny) { + fireAddonEvent("QUEST_WATCH_UPDATE", {}); + fireAddonEvent("QUEST_LOG_UPDATE", {}); + fireAddonEvent("UNIT_QUEST_LOG_CHANGED", {"player"}); + } + LOG_INFO("Quest item update: itemId=", itemId, " count=", count, + " trackedQuestsUpdated=", updatedAny); + } + }; + // Quest objectives completed - mark as ready to turn in. + // Compatibility: some classic-family opcode tables swap COMPLETE and ADD_KILL. + dispatchTable_[Opcode::SMSG_QUESTUPDATE_COMPLETE] = [this](network::Packet& packet) { + // Quest objectives completed - mark as ready to turn in. + // Compatibility: some classic-family opcode tables swap COMPLETE and ADD_KILL. + size_t rem = packet.getRemainingSize(); + if (rem >= 12) { + uint32_t questId = packet.readUInt32(); + clearPendingQuestAccept(questId); + uint32_t entry = packet.readUInt32(); + uint32_t count = packet.readUInt32(); + uint32_t reqCount = 0; + if (packet.hasRemaining(4)) reqCount = packet.readUInt32(); + if (reqCount == 0) reqCount = count; + LOG_INFO("Quest kill update (compat via COMPLETE): questId=", questId, + " entry=", entry, " count=", count, "/", reqCount); + for (auto& quest : questLog_) { + if (quest.questId == questId) { + quest.killCounts[entry] = {count, reqCount}; + addSystemChatMessage(quest.title + ": " + std::to_string(count) + + "/" + std::to_string(reqCount)); + break; + } + } + } else if (rem >= 4) { + uint32_t questId = packet.readUInt32(); + clearPendingQuestAccept(questId); + LOG_INFO("Quest objectives completed: questId=", questId); + + for (auto& quest : questLog_) { + if (quest.questId == questId) { + quest.complete = true; + addSystemChatMessage("Quest Complete: " + quest.title); + LOG_INFO("Marked quest ", questId, " as complete"); + break; + } + } + } + }; + // This opcode is aliased to SMSG_SET_REST_START in the opcode table + // because both share opcode 0x21E in WotLK 3.3.5a. + // In WotLK: payload = uint32 areaId (entering rest) or 0 (leaving rest). + // In Classic/TBC: payload = uint32 questId (force-remove a quest). + dispatchTable_[Opcode::SMSG_QUEST_FORCE_REMOVE] = [this](network::Packet& packet) { + // This opcode is aliased to SMSG_SET_REST_START in the opcode table + // because both share opcode 0x21E in WotLK 3.3.5a. + // In WotLK: payload = uint32 areaId (entering rest) or 0 (leaving rest). + // In Classic/TBC: payload = uint32 questId (force-remove a quest). + if (!packet.hasRemaining(4)) { + LOG_WARNING("SMSG_QUEST_FORCE_REMOVE/SET_REST_START too short"); + return; + } + uint32_t value = packet.readUInt32(); + + // WotLK uses this opcode as SMSG_SET_REST_START: non-zero = entering + // a rest area (inn/city), zero = leaving. Classic/TBC use it for quest removal. + if (!isClassicLikeExpansion() && !isActiveExpansion("tbc")) { + // WotLK: treat as SET_REST_START + bool nowResting = (value != 0); + if (nowResting != isResting_) { + isResting_ = nowResting; + addSystemChatMessage(isResting_ ? "You are now resting." + : "You are no longer resting."); + fireAddonEvent("PLAYER_UPDATE_RESTING", {}); + } + return; + } + + // Classic/TBC: treat as QUEST_FORCE_REMOVE (uint32 questId) + uint32_t questId = value; + clearPendingQuestAccept(questId); + pendingQuestQueryIds_.erase(questId); + if (questId == 0) { + // Some servers emit a zero-id variant during world bootstrap. + // Treat as no-op to avoid false "Quest removed" spam. + return; + } + + bool removed = false; + std::string removedTitle; + for (auto it = questLog_.begin(); it != questLog_.end(); ++it) { + if (it->questId == questId) { + removedTitle = it->title; + questLog_.erase(it); + removed = true; + break; + } + } + if (currentQuestDetails.questId == questId) { + questDetailsOpen = false; + questDetailsOpenTime = std::chrono::steady_clock::time_point{}; + currentQuestDetails = QuestDetailsData{}; + removed = true; + } + if (currentQuestRequestItems_.questId == questId) { + questRequestItemsOpen_ = false; + currentQuestRequestItems_ = QuestRequestItemsData{}; + removed = true; + } + if (currentQuestOfferReward_.questId == questId) { + questOfferRewardOpen_ = false; + currentQuestOfferReward_ = QuestOfferRewardData{}; + removed = true; + } + if (removed) { + if (!removedTitle.empty()) { + addSystemChatMessage("Quest removed: " + removedTitle); + } else { + addSystemChatMessage("Quest removed (ID " + std::to_string(questId) + ")."); + } + fireAddonEvent("QUEST_LOG_UPDATE", {}); + fireAddonEvent("UNIT_QUEST_LOG_CHANGED", {"player"}); + fireAddonEvent("QUEST_REMOVED", {std::to_string(questId)}); + } + }; + dispatchTable_[Opcode::SMSG_QUEST_QUERY_RESPONSE] = [this](network::Packet& packet) { + if (packet.getSize() < 8) { + LOG_WARNING("SMSG_QUEST_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)"); + return; + } + + uint32_t questId = packet.readUInt32(); + packet.readUInt32(); // questMethod + + // Classic/Turtle = stride 3, TBC = stride 4 — all use 40 fixed fields + 4 strings. + // WotLK = stride 5, uses 55 fixed fields + 5 strings. + const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() <= 4; + const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout); + const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout); + const QuestQueryRewards rwds = tryParseQuestRewards(packet.getData(), isClassicLayout); + + for (auto& q : questLog_) { + if (q.questId != questId) continue; + + const int existingScore = scoreQuestTitle(q.title); + const bool parsedStrong = isStrongQuestTitle(parsed.title); + const bool parsedLongEnough = parsed.title.size() >= 6; + const bool notShorterThanExisting = + isPlaceholderQuestTitle(q.title) || q.title.empty() || parsed.title.size() + 2 >= q.title.size(); + const bool shouldReplaceTitle = + parsed.score > -1000 && + parsedStrong && + parsedLongEnough && + notShorterThanExisting && + (isPlaceholderQuestTitle(q.title) || q.title.empty() || parsed.score >= existingScore + 12); + + if (shouldReplaceTitle && !parsed.title.empty()) { + q.title = parsed.title; + } + if (!parsed.objectives.empty() && + (q.objectives.empty() || q.objectives.size() < 16)) { + q.objectives = parsed.objectives; + } + + // Store structured kill/item objectives for later kill-count restoration. + if (objs.valid) { + for (int i = 0; i < 4; ++i) { + q.killObjectives[i].npcOrGoId = objs.kills[i].npcOrGoId; + q.killObjectives[i].required = objs.kills[i].required; + } + for (int i = 0; i < 6; ++i) { + q.itemObjectives[i].itemId = objs.items[i].itemId; + q.itemObjectives[i].required = objs.items[i].required; + } + // Now that we have the objective creature IDs, apply any packed kill + // counts from the player update fields that arrived at login. + applyPackedKillCountsFromFields(q); + // Pre-fetch creature/GO names and item info so objective display is + // populated by the time the player opens the quest log. + for (int i = 0; i < 4; ++i) { + int32_t id = objs.kills[i].npcOrGoId; + if (id == 0 || objs.kills[i].required == 0) continue; + if (id > 0) queryCreatureInfo(static_cast(id), 0); + else queryGameObjectInfo(static_cast(-id), 0); + } + for (int i = 0; i < 6; ++i) { + if (objs.items[i].itemId != 0 && objs.items[i].required != 0) + queryItemInfo(objs.items[i].itemId, 0); + } + LOG_DEBUG("Quest ", questId, " objectives parsed: kills=[", + objs.kills[0].npcOrGoId, "/", objs.kills[0].required, ", ", + objs.kills[1].npcOrGoId, "/", objs.kills[1].required, ", ", + objs.kills[2].npcOrGoId, "/", objs.kills[2].required, ", ", + objs.kills[3].npcOrGoId, "/", objs.kills[3].required, "]"); + } + + // Store reward data and pre-fetch item info for icons. + if (rwds.valid) { + q.rewardMoney = rwds.rewardMoney; + for (int i = 0; i < 4; ++i) { + q.rewardItems[i].itemId = rwds.itemId[i]; + q.rewardItems[i].count = (rwds.itemId[i] != 0) ? rwds.itemCount[i] : 0; + if (rwds.itemId[i] != 0) queryItemInfo(rwds.itemId[i], 0); + } + for (int i = 0; i < 6; ++i) { + q.rewardChoiceItems[i].itemId = rwds.choiceItemId[i]; + q.rewardChoiceItems[i].count = (rwds.choiceItemId[i] != 0) ? rwds.choiceItemCount[i] : 0; + if (rwds.choiceItemId[i] != 0) queryItemInfo(rwds.choiceItemId[i], 0); + } + } + break; + } + + pendingQuestQueryIds_.erase(questId); + }; + // WotLK: uint64 playerGuid + uint8 teamCount + per-team fields + dispatchTable_[Opcode::MSG_INSPECT_ARENA_TEAMS] = [this](network::Packet& packet) { + // WotLK: uint64 playerGuid + uint8 teamCount + per-team fields + if (!packet.hasRemaining(9)) { + packet.skipAll(); + return; + } + uint64_t inspGuid = packet.readUInt64(); + uint8_t teamCount = packet.readUInt8(); + if (teamCount > 3) teamCount = 3; // 2v2, 3v3, 5v5 + if (inspGuid == inspectResult_.guid || inspectResult_.guid == 0) { + inspectResult_.guid = inspGuid; + inspectResult_.arenaTeams.clear(); + for (uint8_t t = 0; t < teamCount; ++t) { + if (!packet.hasRemaining(21)) break; + InspectArenaTeam team; + team.teamId = packet.readUInt32(); + team.type = packet.readUInt8(); + team.weekGames = packet.readUInt32(); + team.weekWins = packet.readUInt32(); + team.seasonGames = packet.readUInt32(); + team.seasonWins = packet.readUInt32(); + team.name = packet.readString(); + if (!packet.hasRemaining(4)) break; + team.personalRating = packet.readUInt32(); + inspectResult_.arenaTeams.push_back(std::move(team)); + } + } + LOG_DEBUG("MSG_INSPECT_ARENA_TEAMS: guid=0x", std::hex, inspGuid, std::dec, + " teams=", static_cast(teamCount)); + }; + // auctionId(u32) + action(u32) + error(u32) + itemEntry(u32) + randomPropertyId(u32) + ... + // action: 0=sold/won, 1=expired, 2=bid placed on your auction + dispatchTable_[Opcode::SMSG_AUCTION_OWNER_NOTIFICATION] = [this](network::Packet& packet) { + // auctionId(u32) + action(u32) + error(u32) + itemEntry(u32) + randomPropertyId(u32) + ... + // action: 0=sold/won, 1=expired, 2=bid placed on your auction + if (packet.hasRemaining(16)) { + /*uint32_t auctionId =*/ packet.readUInt32(); + uint32_t action = packet.readUInt32(); + /*uint32_t error =*/ packet.readUInt32(); + uint32_t itemEntry = packet.readUInt32(); + int32_t ownerRandProp = 0; + if (packet.hasRemaining(4)) + ownerRandProp = static_cast(packet.readUInt32()); + ensureItemInfo(itemEntry); + auto* info = getItemInfo(itemEntry); + std::string rawName = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); + if (ownerRandProp != 0) { + std::string suffix = getRandomPropertyName(ownerRandProp); + if (!suffix.empty()) rawName += " " + suffix; + } + uint32_t aucQuality = info ? info->quality : 1u; + std::string itemLink = buildItemLink(itemEntry, aucQuality, rawName); + if (action == 1) + addSystemChatMessage("Your auction of " + itemLink + " has expired."); + else if (action == 2) + addSystemChatMessage("A bid has been placed on your auction of " + itemLink + "."); + else + addSystemChatMessage("Your auction of " + itemLink + " has sold!"); + } + packet.skipAll(); + }; + // auctionHouseId(u32) + auctionId(u32) + bidderGuid(u64) + bidAmount(u32) + outbidAmount(u32) + itemEntry(u32) + randomPropertyId(u32) + dispatchTable_[Opcode::SMSG_AUCTION_BIDDER_NOTIFICATION] = [this](network::Packet& packet) { + // auctionHouseId(u32) + auctionId(u32) + bidderGuid(u64) + bidAmount(u32) + outbidAmount(u32) + itemEntry(u32) + randomPropertyId(u32) + if (packet.hasRemaining(8)) { + /*uint32_t auctionId =*/ packet.readUInt32(); + uint32_t itemEntry = packet.readUInt32(); + int32_t bidRandProp = 0; + // Try to read randomPropertyId if enough data remains + if (packet.hasRemaining(4)) + bidRandProp = static_cast(packet.readUInt32()); + ensureItemInfo(itemEntry); + auto* info = getItemInfo(itemEntry); + std::string rawName2 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); + if (bidRandProp != 0) { + std::string suffix = getRandomPropertyName(bidRandProp); + if (!suffix.empty()) rawName2 += " " + suffix; + } + uint32_t bidQuality = info ? info->quality : 1u; + std::string bidLink = buildItemLink(itemEntry, bidQuality, rawName2); + addSystemChatMessage("You have been outbid on " + bidLink + "."); + } + packet.skipAll(); + }; + // uint32 auctionId + uint32 itemEntry + uint32 itemRandom — auction expired/cancelled + dispatchTable_[Opcode::SMSG_AUCTION_REMOVED_NOTIFICATION] = [this](network::Packet& packet) { + // uint32 auctionId + uint32 itemEntry + uint32 itemRandom — auction expired/cancelled + if (packet.hasRemaining(12)) { + /*uint32_t auctionId =*/ packet.readUInt32(); + uint32_t itemEntry = packet.readUInt32(); + int32_t itemRandom = static_cast(packet.readUInt32()); + ensureItemInfo(itemEntry); + auto* info = getItemInfo(itemEntry); + std::string rawName3 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); + if (itemRandom != 0) { + std::string suffix = getRandomPropertyName(itemRandom); + if (!suffix.empty()) rawName3 += " " + suffix; + } + uint32_t remQuality = info ? info->quality : 1u; + std::string remLink = buildItemLink(itemEntry, remQuality, rawName3); + addSystemChatMessage("Your auction of " + remLink + " has expired."); + } + packet.skipAll(); + }; + // uint64 containerGuid — tells client to open this container + // The actual items come via update packets; we just log this. + dispatchTable_[Opcode::SMSG_OPEN_CONTAINER] = [this](network::Packet& packet) { + // uint64 containerGuid — tells client to open this container + // The actual items come via update packets; we just log this. + if (packet.hasRemaining(8)) { + uint64_t containerGuid = packet.readUInt64(); + LOG_DEBUG("SMSG_OPEN_CONTAINER: guid=0x", std::hex, containerGuid, std::dec); + } + }; + // PackedGuid (player guid) + uint32 vehicleId + // vehicleId == 0 means the player left the vehicle + dispatchTable_[Opcode::SMSG_PLAYER_VEHICLE_DATA] = [this](network::Packet& packet) { + // PackedGuid (player guid) + uint32 vehicleId + // vehicleId == 0 means the player left the vehicle + if (packet.hasRemaining(1)) { + (void)packet.readPackedGuid(); // player guid (unused) + } + if (packet.hasRemaining(4)) { + vehicleId_ = packet.readUInt32(); + } else { + vehicleId_ = 0; + } + }; + // guid(8) + status(1): status 1 = NPC has available/new routes for this player + dispatchTable_[Opcode::SMSG_TAXINODE_STATUS] = [this](network::Packet& packet) { + // guid(8) + status(1): status 1 = NPC has available/new routes for this player + if (packet.hasRemaining(9)) { + uint64_t npcGuid = packet.readUInt64(); + uint8_t status = packet.readUInt8(); + taxiNpcHasRoutes_[npcGuid] = (status != 0); + } + }; + // TBC 2.4.3 aura tracking: replaces SMSG_AURA_UPDATE which doesn't exist in TBC. + // Format: uint64 targetGuid + uint8 count + N×{uint8 slot, uint32 spellId, + // uint8 effectIndex, uint8 flags, uint32 durationMs, uint32 maxDurationMs} + dispatchTable_[Opcode::SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE] = [this](network::Packet& packet) { + // TBC 2.4.3 aura tracking: replaces SMSG_AURA_UPDATE which doesn't exist in TBC. + // Format: uint64 targetGuid + uint8 count + N×{uint8 slot, uint32 spellId, + // uint8 effectIndex, uint8 flags, uint32 durationMs, uint32 maxDurationMs} + const bool isInit = true; + auto remaining = [&]() { return packet.getRemainingSize(); }; + if (remaining() < 9) { packet.skipAll(); return; } + uint64_t auraTargetGuid = packet.readUInt64(); + uint8_t count = packet.readUInt8(); + + std::vector* auraList = nullptr; + if (auraTargetGuid == playerGuid) auraList = &playerAuras; + else if (auraTargetGuid == targetGuid) auraList = &targetAuras; + else if (auraTargetGuid != 0) auraList = &unitAurasCache_[auraTargetGuid]; + + if (auraList && isInit) auraList->clear(); + + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + for (uint8_t i = 0; i < count && remaining() >= 15; i++) { + uint8_t slot = packet.readUInt8(); // 1 byte + uint32_t spellId = packet.readUInt32(); // 4 bytes + (void) packet.readUInt8(); // effectIndex: 1 byte (unused for slot display) + uint8_t flags = packet.readUInt8(); // 1 byte + uint32_t durationMs = packet.readUInt32(); // 4 bytes + uint32_t maxDurMs = packet.readUInt32(); // 4 bytes — total 15 bytes per entry + + if (auraList) { + while (auraList->size() <= slot) auraList->push_back(AuraSlot{}); + AuraSlot& a = (*auraList)[slot]; + a.spellId = spellId; + // TBC uses same flag convention as Classic: 0x02=harmful, 0x04=beneficial. + // Normalize to WotLK SMSG_AURA_UPDATE convention: 0x80=debuff, 0=buff. + a.flags = (flags & 0x02) ? 0x80u : 0u; + a.durationMs = (durationMs == 0xFFFFFFFF) ? -1 : static_cast(durationMs); + a.maxDurationMs= (maxDurMs == 0xFFFFFFFF) ? -1 : static_cast(maxDurMs); + a.receivedAtMs = nowMs; + } + } + packet.skipAll(); + }; + // TBC 2.4.3 aura tracking: replaces SMSG_AURA_UPDATE which doesn't exist in TBC. + // Format: uint64 targetGuid + uint8 count + N×{uint8 slot, uint32 spellId, + // uint8 effectIndex, uint8 flags, uint32 durationMs, uint32 maxDurationMs} + dispatchTable_[Opcode::SMSG_SET_EXTRA_AURA_INFO_OBSOLETE] = [this](network::Packet& packet) { + // TBC 2.4.3 aura tracking: replaces SMSG_AURA_UPDATE which doesn't exist in TBC. + // Format: uint64 targetGuid + uint8 count + N×{uint8 slot, uint32 spellId, + // uint8 effectIndex, uint8 flags, uint32 durationMs, uint32 maxDurationMs} + const bool isInit = false; + auto remaining = [&]() { return packet.getRemainingSize(); }; + if (remaining() < 9) { packet.skipAll(); return; } + uint64_t auraTargetGuid = packet.readUInt64(); + uint8_t count = packet.readUInt8(); + + std::vector* auraList = nullptr; + if (auraTargetGuid == playerGuid) auraList = &playerAuras; + else if (auraTargetGuid == targetGuid) auraList = &targetAuras; + else if (auraTargetGuid != 0) auraList = &unitAurasCache_[auraTargetGuid]; + + if (auraList && isInit) auraList->clear(); + + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + for (uint8_t i = 0; i < count && remaining() >= 15; i++) { + uint8_t slot = packet.readUInt8(); // 1 byte + uint32_t spellId = packet.readUInt32(); // 4 bytes + (void) packet.readUInt8(); // effectIndex: 1 byte (unused for slot display) + uint8_t flags = packet.readUInt8(); // 1 byte + uint32_t durationMs = packet.readUInt32(); // 4 bytes + uint32_t maxDurMs = packet.readUInt32(); // 4 bytes — total 15 bytes per entry + + if (auraList) { + while (auraList->size() <= slot) auraList->push_back(AuraSlot{}); + AuraSlot& a = (*auraList)[slot]; + a.spellId = spellId; + // TBC uses same flag convention as Classic: 0x02=harmful, 0x04=beneficial. + // Normalize to WotLK SMSG_AURA_UPDATE convention: 0x80=debuff, 0=buff. + a.flags = (flags & 0x02) ? 0x80u : 0u; + a.durationMs = (durationMs == 0xFFFFFFFF) ? -1 : static_cast(durationMs); + a.maxDurationMs= (maxDurMs == 0xFFFFFFFF) ? -1 : static_cast(maxDurMs); + a.receivedAtMs = nowMs; + } + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_GUILD_DECLINE] = [this](network::Packet& packet) { + if (packet.hasData()) { + std::string name = packet.readString(); + addSystemChatMessage(name + " declined your guild invitation."); + } + }; + // Clear cached talent data so the talent screen reflects the reset. + dispatchTable_[Opcode::SMSG_TALENTS_INVOLUNTARILY_RESET] = [this](network::Packet& packet) { + // Clear cached talent data so the talent screen reflects the reset. + learnedTalents_[0].clear(); + learnedTalents_[1].clear(); + addUIError("Your talents have been reset by the server."); + addSystemChatMessage("Your talents have been reset by the server."); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_SET_REST_START] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t restTrigger = packet.readUInt32(); + isResting_ = (restTrigger > 0); + addSystemChatMessage(isResting_ ? "You are now resting." + : "You are no longer resting."); + fireAddonEvent("PLAYER_UPDATE_RESTING", {}); + } + }; + dispatchTable_[Opcode::SMSG_UPDATE_AURA_DURATION] = [this](network::Packet& packet) { + if (packet.hasRemaining(5)) { + uint8_t slot = packet.readUInt8(); + uint32_t durationMs = packet.readUInt32(); + handleUpdateAuraDuration(slot, durationMs); + } + }; + dispatchTable_[Opcode::SMSG_ITEM_NAME_QUERY_RESPONSE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t itemId = packet.readUInt32(); + std::string name = packet.readString(); + if (!itemInfoCache_.count(itemId) && !name.empty()) { + ItemQueryResponseData stub; + stub.entry = itemId; + stub.name = std::move(name); + stub.valid = true; + itemInfoCache_[itemId] = std::move(stub); + } + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_MOUNTSPECIAL_ANIM] = [this](network::Packet& packet) { (void)packet.readPackedGuid(); }; + dispatchTable_[Opcode::SMSG_CHAR_CUSTOMIZE] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t result = packet.readUInt8(); + addSystemChatMessage(result == 0 ? "Character customization complete." + : "Character customization failed."); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_CHAR_FACTION_CHANGE] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t result = packet.readUInt8(); + addSystemChatMessage(result == 0 ? "Faction change complete." + : "Faction change failed."); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_INVALIDATE_PLAYER] = [this](network::Packet& packet) { + if (packet.hasRemaining(8)) { + uint64_t guid = packet.readUInt64(); + playerNameCache.erase(guid); + } + }; + // uint32 movieId — we don't play movies; acknowledge immediately. + dispatchTable_[Opcode::SMSG_TRIGGER_MOVIE] = [this](network::Packet& packet) { + // uint32 movieId — we don't play movies; acknowledge immediately. + packet.skipAll(); + // WotLK servers expect CMSG_COMPLETE_MOVIE after the movie finishes; + // without it, the server may hang or disconnect the client. + uint16_t wire = wireOpcode(Opcode::CMSG_COMPLETE_MOVIE); + if (wire != 0xFFFF) { + network::Packet ack(wire); + socket->send(ack); + LOG_DEBUG("SMSG_TRIGGER_MOVIE: skipped, sent CMSG_COMPLETE_MOVIE"); + } + }; + registerHandler(Opcode::SMSG_EQUIPMENT_SET_LIST, &GameHandler::handleEquipmentSetList); + dispatchTable_[Opcode::SMSG_EQUIPMENT_SET_USE_RESULT] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t result = packet.readUInt8(); + if (result != 0) { addUIError("Failed to equip item set."); addSystemChatMessage("Failed to equip item set."); } + } + }; + // Server-side LFG invite timed out (no response within time limit) + dispatchTable_[Opcode::SMSG_LFG_TIMEDOUT] = [this](network::Packet& packet) { + // Server-side LFG invite timed out (no response within time limit) + addSystemChatMessage("Dungeon Finder: Invite timed out."); + if (openLfgCallback_) openLfgCallback_(); + packet.skipAll(); + }; + // Another party member failed to respond to a LFG role-check in time + dispatchTable_[Opcode::SMSG_LFG_OTHER_TIMEDOUT] = [this](network::Packet& packet) { + // Another party member failed to respond to a LFG role-check in time + addSystemChatMessage("Dungeon Finder: Another player's invite timed out."); + if (openLfgCallback_) openLfgCallback_(); + packet.skipAll(); + }; + // uint32 result — LFG auto-join attempt failed (player selected auto-join at queue time) + dispatchTable_[Opcode::SMSG_LFG_AUTOJOIN_FAILED] = [this](network::Packet& packet) { + // uint32 result — LFG auto-join attempt failed (player selected auto-join at queue time) + if (packet.hasRemaining(4)) { + uint32_t result = packet.readUInt32(); + (void)result; + } + addUIError("Dungeon Finder: Auto-join failed."); + addSystemChatMessage("Dungeon Finder: Auto-join failed."); + packet.skipAll(); + }; + // No eligible players found for auto-join + dispatchTable_[Opcode::SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER] = [this](network::Packet& packet) { + // No eligible players found for auto-join + addUIError("Dungeon Finder: No players available for auto-join."); + addSystemChatMessage("Dungeon Finder: No players available for auto-join."); + packet.skipAll(); + }; + // Party leader is currently set to Looking for More (LFM) mode + dispatchTable_[Opcode::SMSG_LFG_LEADER_IS_LFM] = [this](network::Packet& packet) { + // Party leader is currently set to Looking for More (LFM) mode + addSystemChatMessage("Your party leader is currently Looking for More."); + packet.skipAll(); + }; + // uint32 zoneId + uint8 level_min + uint8 level_max — player queued for meeting stone + dispatchTable_[Opcode::SMSG_MEETINGSTONE_SETQUEUE] = [this](network::Packet& packet) { + // uint32 zoneId + uint8 level_min + uint8 level_max — player queued for meeting stone + if (packet.hasRemaining(6)) { + uint32_t zoneId = packet.readUInt32(); + uint8_t levelMin = packet.readUInt8(); + uint8_t levelMax = packet.readUInt8(); + char buf[128]; + std::string zoneName = getAreaName(zoneId); + if (!zoneName.empty()) + std::snprintf(buf, sizeof(buf), + "You are now in the Meeting Stone queue for %s (levels %u-%u).", + zoneName.c_str(), levelMin, levelMax); + else + std::snprintf(buf, sizeof(buf), + "You are now in the Meeting Stone queue for zone %u (levels %u-%u).", + zoneId, levelMin, levelMax); + addSystemChatMessage(buf); + LOG_INFO("SMSG_MEETINGSTONE_SETQUEUE: zone=", zoneId, + " levels=", static_cast(levelMin), "-", static_cast(levelMax)); + } + packet.skipAll(); + }; + // Server confirms group found and teleport summon is ready + dispatchTable_[Opcode::SMSG_MEETINGSTONE_COMPLETE] = [this](network::Packet& packet) { + // Server confirms group found and teleport summon is ready + addSystemChatMessage("Meeting Stone: Your group is ready! Use the Meeting Stone to summon."); + LOG_INFO("SMSG_MEETINGSTONE_COMPLETE"); + packet.skipAll(); + }; + // Meeting stone search is still ongoing + dispatchTable_[Opcode::SMSG_MEETINGSTONE_IN_PROGRESS] = [this](network::Packet& packet) { + // Meeting stone search is still ongoing + addSystemChatMessage("Meeting Stone: Searching for group members..."); + LOG_DEBUG("SMSG_MEETINGSTONE_IN_PROGRESS"); + packet.skipAll(); + }; + // uint64 memberGuid — a player was added to your group via meeting stone + dispatchTable_[Opcode::SMSG_MEETINGSTONE_MEMBER_ADDED] = [this](network::Packet& packet) { + // uint64 memberGuid — a player was added to your group via meeting stone + if (packet.hasRemaining(8)) { + uint64_t memberGuid = packet.readUInt64(); + const auto& memberName = lookupName(memberGuid); + if (!memberName.empty()) { + addSystemChatMessage("Meeting Stone: " + memberName + + " has been added to your group."); + } else { + addSystemChatMessage("Meeting Stone: A new player has been added to your group."); + } + LOG_INFO("SMSG_MEETINGSTONE_MEMBER_ADDED: guid=0x", std::hex, memberGuid, std::dec); + } + }; + // uint8 reason — failed to join group via meeting stone + // 0=target_not_in_lfg, 1=target_in_party, 2=target_invalid_map, 3=target_not_available + dispatchTable_[Opcode::SMSG_MEETINGSTONE_JOINFAILED] = [this](network::Packet& packet) { + // uint8 reason — failed to join group via meeting stone + // 0=target_not_in_lfg, 1=target_in_party, 2=target_invalid_map, 3=target_not_available + static const char* kMeetingstoneErrors[] = { + "Target player is not using the Meeting Stone.", + "Target player is already in a group.", + "You are not in a valid zone for that Meeting Stone.", + "Target player is not available.", + }; + if (packet.hasRemaining(1)) { + uint8_t reason = packet.readUInt8(); + const char* msg = (reason < 4) ? kMeetingstoneErrors[reason] + : "Meeting Stone: Could not join group."; + addSystemChatMessage(msg); + LOG_INFO("SMSG_MEETINGSTONE_JOINFAILED: reason=", static_cast(reason)); + } + }; + // Player was removed from the meeting stone queue (left, or group disbanded) + dispatchTable_[Opcode::SMSG_MEETINGSTONE_LEAVE] = [this](network::Packet& packet) { + // Player was removed from the meeting stone queue (left, or group disbanded) + addSystemChatMessage("You have left the Meeting Stone queue."); + LOG_DEBUG("SMSG_MEETINGSTONE_LEAVE"); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_GMTICKET_CREATE] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t res = packet.readUInt8(); + addSystemChatMessage(res == 1 ? "GM ticket submitted." + : "Failed to submit GM ticket."); + } + }; + dispatchTable_[Opcode::SMSG_GMTICKET_UPDATETEXT] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t res = packet.readUInt8(); + addSystemChatMessage(res == 1 ? "GM ticket updated." + : "Failed to update GM ticket."); + } + }; + dispatchTable_[Opcode::SMSG_GMTICKET_DELETETICKET] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t res = packet.readUInt8(); + addSystemChatMessage(res == 9 ? "GM ticket deleted." + : "No ticket to delete."); + } + }; + // WotLK 3.3.5a format: + // uint8 status — 1=no ticket, 6=has open ticket, 3=closed, 10=suspended + // If status == 6 (GMTICKET_STATUS_HASTEXT): + // cstring ticketText + // uint32 ticketAge (seconds old) + // uint32 daysUntilOld (days remaining before escalation) + // float waitTimeHours (estimated GM wait time) + dispatchTable_[Opcode::SMSG_GMTICKET_GETTICKET] = [this](network::Packet& packet) { + // WotLK 3.3.5a format: + // uint8 status — 1=no ticket, 6=has open ticket, 3=closed, 10=suspended + // If status == 6 (GMTICKET_STATUS_HASTEXT): + // cstring ticketText + // uint32 ticketAge (seconds old) + // uint32 daysUntilOld (days remaining before escalation) + // float waitTimeHours (estimated GM wait time) + if (!packet.hasRemaining(1)) { packet.skipAll(); return; } + uint8_t gmStatus = packet.readUInt8(); + // Status 6 = GMTICKET_STATUS_HASTEXT — open ticket with text + if (gmStatus == 6 && packet.hasRemaining(1)) { + gmTicketText_ = packet.readString(); + uint32_t ageSec = (packet.hasRemaining(4)) ? packet.readUInt32() : 0; + /*uint32_t daysLeft =*/ (packet.hasRemaining(4)) ? packet.readUInt32() : 0; + gmTicketWaitHours_ = (packet.hasRemaining(4)) + ? packet.readFloat() : 0.0f; + gmTicketActive_ = true; + char buf[256]; + if (ageSec < 60) { + std::snprintf(buf, sizeof(buf), + "You have an open GM ticket (submitted %us ago). Estimated wait: %.1f hours.", + ageSec, gmTicketWaitHours_); + } else { + uint32_t ageMin = ageSec / 60; + std::snprintf(buf, sizeof(buf), + "You have an open GM ticket (submitted %um ago). Estimated wait: %.1f hours.", + ageMin, gmTicketWaitHours_); + } + addSystemChatMessage(buf); + LOG_INFO("SMSG_GMTICKET_GETTICKET: open ticket age=", ageSec, + "s wait=", gmTicketWaitHours_, "h"); + } else if (gmStatus == 3) { + gmTicketActive_ = false; + gmTicketText_.clear(); + addSystemChatMessage("Your GM ticket has been closed."); + LOG_INFO("SMSG_GMTICKET_GETTICKET: ticket closed"); + } else if (gmStatus == 10) { + gmTicketActive_ = false; + gmTicketText_.clear(); + addSystemChatMessage("Your GM ticket has been suspended."); + LOG_INFO("SMSG_GMTICKET_GETTICKET: ticket suspended"); + } else { + // Status 1 = no open ticket (default/no ticket) + gmTicketActive_ = false; + gmTicketText_.clear(); + LOG_DEBUG("SMSG_GMTICKET_GETTICKET: no open ticket (status=", static_cast(gmStatus), ")"); + } + packet.skipAll(); + }; + // uint32 status: 1 = GM support available, 0 = offline/unavailable + dispatchTable_[Opcode::SMSG_GMTICKET_SYSTEMSTATUS] = [this](network::Packet& packet) { + // uint32 status: 1 = GM support available, 0 = offline/unavailable + if (packet.hasRemaining(4)) { + uint32_t sysStatus = packet.readUInt32(); + gmSupportAvailable_ = (sysStatus != 0); + addSystemChatMessage(gmSupportAvailable_ + ? "GM support is currently available." + : "GM support is currently unavailable."); + LOG_INFO("SMSG_GMTICKET_SYSTEMSTATUS: available=", gmSupportAvailable_); + } + packet.skipAll(); + }; + // uint8 runeIndex + uint8 newRuneType (0=Blood,1=Unholy,2=Frost,3=Death) + dispatchTable_[Opcode::SMSG_CONVERT_RUNE] = [this](network::Packet& packet) { + // uint8 runeIndex + uint8 newRuneType (0=Blood,1=Unholy,2=Frost,3=Death) + if (!packet.hasRemaining(2)) { + packet.skipAll(); + return; + } + uint8_t idx = packet.readUInt8(); + uint8_t type = packet.readUInt8(); + if (idx < 6) playerRunes_[idx].type = static_cast(type & 0x3); + }; + // uint8 runeReadyMask (bit i=1 → rune i is ready) + // uint8[6] cooldowns (0=ready, 255=just used → readyFraction = 1 - val/255) + dispatchTable_[Opcode::SMSG_RESYNC_RUNES] = [this](network::Packet& packet) { + // uint8 runeReadyMask (bit i=1 → rune i is ready) + // uint8[6] cooldowns (0=ready, 255=just used → readyFraction = 1 - val/255) + if (!packet.hasRemaining(7)) { + packet.skipAll(); + return; + } + uint8_t readyMask = packet.readUInt8(); + for (int i = 0; i < 6; i++) { + uint8_t cd = packet.readUInt8(); + playerRunes_[i].ready = (readyMask & (1u << i)) != 0; + playerRunes_[i].readyFraction = 1.0f - cd / 255.0f; + if (playerRunes_[i].ready) playerRunes_[i].readyFraction = 1.0f; + } + }; + // uint32 runeMask (bit i=1 → rune i just became ready) + dispatchTable_[Opcode::SMSG_ADD_RUNE_POWER] = [this](network::Packet& packet) { + // uint32 runeMask (bit i=1 → rune i just became ready) + if (!packet.hasRemaining(4)) { + packet.skipAll(); + return; + } + uint32_t runeMask = packet.readUInt32(); + for (int i = 0; i < 6; i++) { + if (runeMask & (1u << i)) { + playerRunes_[i].ready = true; + playerRunes_[i].readyFraction = 1.0f; + } + } + }; + // Classic: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + schoolMask(4) + // TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4) + // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) + dispatchTable_[Opcode::SMSG_SPELLDAMAGESHIELD] = [this](network::Packet& packet) { + // Classic: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + schoolMask(4) + // TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4) + // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) + const bool shieldTbc = isActiveExpansion("tbc"); + const bool shieldWotlkLike = !isClassicLikeExpansion() && !shieldTbc; + const auto shieldRem = [&]() { return packet.getRemainingSize(); }; + const size_t shieldMinSz = shieldTbc ? 24u : 2u; + if (!packet.hasRemaining(shieldMinSz)) { + packet.skipAll(); return; + } + if (!shieldTbc && (!packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t victimGuid = shieldTbc + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(shieldTbc ? 8u : 1u) || (!shieldTbc && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t casterGuid = shieldTbc + ? packet.readUInt64() : packet.readPackedGuid(); + const size_t shieldTailSize = shieldWotlkLike ? 16u : 12u; + if (shieldRem() < shieldTailSize) { + packet.skipAll(); return; + } + uint32_t shieldSpellId = packet.readUInt32(); + uint32_t damage = packet.readUInt32(); + if (shieldWotlkLike) + /*uint32_t absorbed =*/ packet.readUInt32(); + /*uint32_t school =*/ packet.readUInt32(); + // Show combat text: damage shield reflect + if (casterGuid == playerGuid) { + // We have a damage shield that reflected damage + addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(damage), shieldSpellId, true, 0, casterGuid, victimGuid); + } else if (victimGuid == playerGuid) { + // A damage shield hit us (e.g. target's Thorns) + addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(damage), shieldSpellId, false, 0, casterGuid, victimGuid); + } + }; + // WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType + // TBC: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8 + dispatchTable_[Opcode::SMSG_SPELLORDAMAGE_IMMUNE] = [this](network::Packet& packet) { + // WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType + // TBC: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8 + const bool immuneUsesFullGuid = isActiveExpansion("tbc"); + const size_t minSz = immuneUsesFullGuid ? 21u : 2u; + if (!packet.hasRemaining(minSz)) { + packet.skipAll(); return; + } + if (!immuneUsesFullGuid && !packet.hasFullPackedGuid()) { + packet.skipAll(); return; + } + uint64_t casterGuid = immuneUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(immuneUsesFullGuid ? 8u : 2u) || (!immuneUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t victimGuid = immuneUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(5)) return; + uint32_t immuneSpellId = packet.readUInt32(); + /*uint8_t saveType =*/ packet.readUInt8(); + // Show IMMUNE text when the player is the caster (we hit an immune target) + // or the victim (we are immune) + if (casterGuid == playerGuid || victimGuid == playerGuid) { + addCombatText(CombatTextEntry::IMMUNE, 0, immuneSpellId, + casterGuid == playerGuid, 0, casterGuid, victimGuid); + } + }; + // WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen + // TBC: full uint64 casterGuid + full uint64 victimGuid + ... + // + uint32 count + count × (uint32 dispelled_spellId + uint32 unk) + dispatchTable_[Opcode::SMSG_SPELLDISPELLOG] = [this](network::Packet& packet) { + // WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen + // TBC: full uint64 casterGuid + full uint64 victimGuid + ... + // + uint32 count + count × (uint32 dispelled_spellId + uint32 unk) + const bool dispelUsesFullGuid = isActiveExpansion("tbc"); + if (!packet.hasRemaining(dispelUsesFullGuid ? 8u : 1u) || (!dispelUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t casterGuid = dispelUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(dispelUsesFullGuid ? 8u : 1u) || (!dispelUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t victimGuid = dispelUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(9)) return; + /*uint32_t dispelSpell =*/ packet.readUInt32(); + uint8_t isStolen = packet.readUInt8(); + uint32_t count = packet.readUInt32(); + // Preserve every dispelled aura in the combat log instead of collapsing + // multi-aura packets down to the first entry only. + const size_t dispelEntrySize = dispelUsesFullGuid ? 8u : 5u; + std::vector dispelledIds; + dispelledIds.reserve(count); + for (uint32_t i = 0; i < count && packet.hasRemaining(dispelEntrySize); ++i) { + uint32_t dispelledId = packet.readUInt32(); + if (dispelUsesFullGuid) { + /*uint32_t unk =*/ packet.readUInt32(); + } else { + /*uint8_t isPositive =*/ packet.readUInt8(); + } + if (dispelledId != 0) { + dispelledIds.push_back(dispelledId); + } + } + // Show system message if player was victim or caster + if (victimGuid == playerGuid || casterGuid == playerGuid) { + std::vector loggedIds; + if (isStolen) { + loggedIds.reserve(dispelledIds.size()); + for (uint32_t dispelledId : dispelledIds) { + if (shouldLogSpellstealAura(casterGuid, victimGuid, dispelledId)) + loggedIds.push_back(dispelledId); + } + } else { + loggedIds = dispelledIds; + } + + const std::string displaySpellNames = formatSpellNameList(*this, loggedIds); + if (!displaySpellNames.empty()) { + char buf[256]; + const char* passiveVerb = loggedIds.size() == 1 ? "was" : "were"; + if (isStolen) { + if (victimGuid == playerGuid && casterGuid != playerGuid) + std::snprintf(buf, sizeof(buf), "%s %s stolen.", + displaySpellNames.c_str(), passiveVerb); + else if (casterGuid == playerGuid) + std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellNames.c_str()); + else + std::snprintf(buf, sizeof(buf), "%s %s stolen.", + displaySpellNames.c_str(), passiveVerb); + } else { + if (victimGuid == playerGuid && casterGuid != playerGuid) + std::snprintf(buf, sizeof(buf), "%s %s dispelled.", + displaySpellNames.c_str(), passiveVerb); + else if (casterGuid == playerGuid) + std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellNames.c_str()); + else + std::snprintf(buf, sizeof(buf), "%s %s dispelled.", + displaySpellNames.c_str(), passiveVerb); + } + addSystemChatMessage(buf); + } + // Preserve stolen auras as spellsteal events so the log wording stays accurate. + if (!loggedIds.empty()) { + bool isPlayerCaster = (casterGuid == playerGuid); + for (uint32_t dispelledId : loggedIds) { + addCombatText(isStolen ? CombatTextEntry::STEAL : CombatTextEntry::DISPEL, + 0, dispelledId, isPlayerCaster, 0, + casterGuid, victimGuid); + } + } + } + packet.skipAll(); + }; + // Sent to the CASTER (Mage) when Spellsteal succeeds. + // Wire format mirrors SPELLDISPELLOG: + // WotLK/Classic/Turtle: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count + // + count × (uint32 stolenSpellId + uint8 isPositive) + // TBC: full uint64 victim + full uint64 caster + same tail + dispatchTable_[Opcode::SMSG_SPELLSTEALLOG] = [this](network::Packet& packet) { + // Sent to the CASTER (Mage) when Spellsteal succeeds. + // Wire format mirrors SPELLDISPELLOG: + // WotLK/Classic/Turtle: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count + // + count × (uint32 stolenSpellId + uint8 isPositive) + // TBC: full uint64 victim + full uint64 caster + same tail + const bool stealUsesFullGuid = isActiveExpansion("tbc"); + if (!packet.hasRemaining(stealUsesFullGuid ? 8u : 1u) || (!stealUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t stealVictim = stealUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(stealUsesFullGuid ? 8u : 1u) || (!stealUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t stealCaster = stealUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(9)) { + packet.skipAll(); return; + } + /*uint32_t stealSpellId =*/ packet.readUInt32(); + /*uint8_t isStolen =*/ packet.readUInt8(); + uint32_t stealCount = packet.readUInt32(); + // Preserve every stolen aura in the combat log instead of only the first. + const size_t stealEntrySize = stealUsesFullGuid ? 8u : 5u; + std::vector stolenIds; + stolenIds.reserve(stealCount); + for (uint32_t i = 0; i < stealCount && packet.hasRemaining(stealEntrySize); ++i) { + uint32_t stolenId = packet.readUInt32(); + if (stealUsesFullGuid) { + /*uint32_t unk =*/ packet.readUInt32(); + } else { + /*uint8_t isPos =*/ packet.readUInt8(); + } + if (stolenId != 0) { + stolenIds.push_back(stolenId); + } + } + if (stealCaster == playerGuid || stealVictim == playerGuid) { + std::vector loggedIds; + loggedIds.reserve(stolenIds.size()); + for (uint32_t stolenId : stolenIds) { + if (shouldLogSpellstealAura(stealCaster, stealVictim, stolenId)) + loggedIds.push_back(stolenId); + } + + const std::string displaySpellNames = formatSpellNameList(*this, loggedIds); + if (!displaySpellNames.empty()) { + char buf[256]; + if (stealCaster == playerGuid) + std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellNames.c_str()); + else + std::snprintf(buf, sizeof(buf), "%s %s stolen.", displaySpellNames.c_str(), + loggedIds.size() == 1 ? "was" : "were"); + addSystemChatMessage(buf); + } + // Some servers emit both SPELLDISPELLOG(isStolen=1) and SPELLSTEALLOG + // for the same aura. Keep the first event and suppress the duplicate. + if (!loggedIds.empty()) { + bool isPlayerCaster = (stealCaster == playerGuid); + for (uint32_t stolenId : loggedIds) { + addCombatText(CombatTextEntry::STEAL, 0, stolenId, isPlayerCaster, 0, + stealCaster, stealVictim); + } + } + } + packet.skipAll(); + }; + // WotLK/Classic/Turtle: packed_guid target + packed_guid caster + uint32 spellId + ... + // TBC: uint64 target + uint64 caster + uint32 spellId + ... + dispatchTable_[Opcode::SMSG_SPELL_CHANCE_PROC_LOG] = [this](network::Packet& packet) { + // WotLK/Classic/Turtle: packed_guid target + packed_guid caster + uint32 spellId + ... + // TBC: uint64 target + uint64 caster + uint32 spellId + ... + const bool procChanceUsesFullGuid = isActiveExpansion("tbc"); + auto readProcChanceGuid = [&]() -> uint64_t { + if (procChanceUsesFullGuid) + return (packet.hasRemaining(8)) ? packet.readUInt64() : 0; + return packet.readPackedGuid(); + }; + if (!packet.hasRemaining(procChanceUsesFullGuid ? 8u : 1u) || (!procChanceUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t procTargetGuid = readProcChanceGuid(); + if (!packet.hasRemaining(procChanceUsesFullGuid ? 8u : 1u) || (!procChanceUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t procCasterGuid = readProcChanceGuid(); + if (!packet.hasRemaining(4)) { + packet.skipAll(); return; + } + uint32_t procSpellId = packet.readUInt32(); + // Show a "PROC!" floating text when the player triggers the proc + if (procCasterGuid == playerGuid && procSpellId > 0) + addCombatText(CombatTextEntry::PROC_TRIGGER, 0, procSpellId, true, 0, + procCasterGuid, procTargetGuid); + packet.skipAll(); + }; + // Sent when a unit is killed by a spell with SPELL_ATTR_EX2_INSTAKILL (e.g. Execute, Obliterate, etc.) + // WotLK/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId + // TBC: full uint64 caster + full uint64 victim + uint32 spellId + dispatchTable_[Opcode::SMSG_SPELLINSTAKILLLOG] = [this](network::Packet& packet) { + // Sent when a unit is killed by a spell with SPELL_ATTR_EX2_INSTAKILL (e.g. Execute, Obliterate, etc.) + // WotLK/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId + // TBC: full uint64 caster + full uint64 victim + uint32 spellId + const bool ikUsesFullGuid = isActiveExpansion("tbc"); + auto ik_rem = [&]() { return packet.getRemainingSize(); }; + if (ik_rem() < (ikUsesFullGuid ? 8u : 1u) + || (!ikUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t ikCaster = ikUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (ik_rem() < (ikUsesFullGuid ? 8u : 1u) + || (!ikUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t ikVictim = ikUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (ik_rem() < 4) { + packet.skipAll(); return; + } + uint32_t ikSpell = packet.readUInt32(); + // Show kill/death feedback for the local player + if (ikCaster == playerGuid) { + addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim); + } else if (ikVictim == playerGuid) { + addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, false, 0, ikCaster, ikVictim); + addUIError("You were killed by an instant-kill effect."); + addSystemChatMessage("You were killed by an instant-kill effect."); + } + LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster, + " victim=0x", ikVictim, std::dec, " spell=", ikSpell); + packet.skipAll(); + }; + // WotLK/Classic/Turtle: packed_guid caster + uint32 spellId + uint32 effectCount + // TBC: uint64 caster + uint32 spellId + uint32 effectCount + // Per-effect: uint8 effectType + uint32 effectLogCount + effect-specific data + // Effect 10 = POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier + // Effect 11 = HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier + // Effect 24 = CREATE_ITEM: uint32 itemEntry + // Effect 26 = INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id + // Effect 49 = FEED_PET: uint32 itemEntry + // Effect 114= CREATE_ITEM2: uint32 itemEntry (same layout as CREATE_ITEM) + dispatchTable_[Opcode::SMSG_SPELLLOGEXECUTE] = [this](network::Packet& packet) { + // WotLK/Classic/Turtle: packed_guid caster + uint32 spellId + uint32 effectCount + // TBC: uint64 caster + uint32 spellId + uint32 effectCount + // Per-effect: uint8 effectType + uint32 effectLogCount + effect-specific data + // Effect 10 = POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier + // Effect 11 = HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier + // Effect 24 = CREATE_ITEM: uint32 itemEntry + // Effect 26 = INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id + // Effect 49 = FEED_PET: uint32 itemEntry + // Effect 114= CREATE_ITEM2: uint32 itemEntry (same layout as CREATE_ITEM) + const bool exeUsesFullGuid = isActiveExpansion("tbc"); + if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) ) { + packet.skipAll(); return; + } + if (!exeUsesFullGuid && !packet.hasFullPackedGuid()) { + packet.skipAll(); return; + } + uint64_t exeCaster = exeUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) { + packet.skipAll(); return; + } + uint32_t exeSpellId = packet.readUInt32(); + uint32_t exeEffectCount = packet.readUInt32(); + exeEffectCount = std::min(exeEffectCount, 32u); // sanity + + const bool isPlayerCaster = (exeCaster == playerGuid); + for (uint32_t ei = 0; ei < exeEffectCount; ++ei) { + if (!packet.hasRemaining(5)) break; + uint8_t effectType = packet.readUInt8(); + uint32_t effectLogCount = packet.readUInt32(); + effectLogCount = std::min(effectLogCount, 64u); // sanity + if (effectType == 10) { + // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) || (!exeUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); break; + } + uint64_t drainTarget = exeUsesFullGuid + ? packet.readUInt64() + : packet.readPackedGuid(); + if (!packet.hasRemaining(12)) { packet.skipAll(); break; } + uint32_t drainAmount = packet.readUInt32(); + uint32_t drainPower = packet.readUInt32(); // 0=mana,1=rage,3=energy,6=runic + float drainMult = packet.readFloat(); + if (drainAmount > 0) { + if (drainTarget == playerGuid) + addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, false, + static_cast(drainPower), + exeCaster, drainTarget); + if (isPlayerCaster) { + if (drainTarget != playerGuid) { + addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, true, + static_cast(drainPower), exeCaster, drainTarget); + } + if (drainMult > 0.0f && std::isfinite(drainMult)) { + const uint32_t gainedAmount = static_cast( + std::lround(static_cast(drainAmount) * static_cast(drainMult))); + if (gainedAmount > 0) { + addCombatText(CombatTextEntry::ENERGIZE, static_cast(gainedAmount), exeSpellId, true, + static_cast(drainPower), exeCaster, exeCaster); + } + } + } + } + LOG_DEBUG("SMSG_SPELLLOGEXECUTE POWER_DRAIN: spell=", exeSpellId, + " power=", drainPower, " amount=", drainAmount, + " multiplier=", drainMult); + } + } else if (effectType == 11) { + // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) || (!exeUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); break; + } + uint64_t leechTarget = exeUsesFullGuid + ? packet.readUInt64() + : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) { packet.skipAll(); break; } + uint32_t leechAmount = packet.readUInt32(); + float leechMult = packet.readFloat(); + if (leechAmount > 0) { + if (leechTarget == playerGuid) { + addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(leechAmount), exeSpellId, false, 0, + exeCaster, leechTarget); + } else if (isPlayerCaster) { + addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(leechAmount), exeSpellId, true, 0, + exeCaster, leechTarget); + } + if (isPlayerCaster && leechMult > 0.0f && std::isfinite(leechMult)) { + const uint32_t gainedAmount = static_cast( + std::lround(static_cast(leechAmount) * static_cast(leechMult))); + if (gainedAmount > 0) { + addCombatText(CombatTextEntry::HEAL, static_cast(gainedAmount), exeSpellId, true, 0, + exeCaster, exeCaster); + } + } + } + LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId, + " amount=", leechAmount, " multiplier=", leechMult); + } + } else if (effectType == 24 || effectType == 114) { + // SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(4)) break; + uint32_t itemEntry = packet.readUInt32(); + if (isPlayerCaster && itemEntry != 0) { + ensureItemInfo(itemEntry); + const ItemQueryResponseData* info = getItemInfo(itemEntry); + std::string itemName = info && !info->name.empty() + ? info->name : ("item #" + std::to_string(itemEntry)); + const auto& spellName = getSpellName(exeSpellId); + std::string msg = spellName.empty() + ? ("You create: " + itemName + ".") + : ("You create " + itemName + " using " + spellName + "."); + addSystemChatMessage(msg); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE CREATE_ITEM: spell=", exeSpellId, + " item=", itemEntry, " name=", itemName); + + // Repeat-craft queue: re-cast if more crafts remaining + if (craftQueueRemaining_ > 0 && craftQueueSpellId_ == exeSpellId) { + --craftQueueRemaining_; + if (craftQueueRemaining_ > 0) { + castSpell(craftQueueSpellId_, 0); + } else { + craftQueueSpellId_ = 0; + } + } + } + } + } else if (effectType == 26) { + // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) || (!exeUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); break; + } + uint64_t icTarget = exeUsesFullGuid + ? packet.readUInt64() + : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) { packet.skipAll(); break; } + uint32_t icSpellId = packet.readUInt32(); + // Clear the interrupted unit's cast bar immediately + unitCastStates_.erase(icTarget); + // Record interrupt in combat log when player is involved + if (isPlayerCaster || icTarget == playerGuid) + addCombatText(CombatTextEntry::INTERRUPT, 0, icSpellId, isPlayerCaster, 0, + exeCaster, icTarget); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", exeSpellId, + " interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec); + } + } else if (effectType == 49) { + // SPELL_EFFECT_FEED_PET: uint32 itemEntry per log entry + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(4)) break; + uint32_t feedItem = packet.readUInt32(); + if (isPlayerCaster && feedItem != 0) { + ensureItemInfo(feedItem); + const ItemQueryResponseData* info = getItemInfo(feedItem); + std::string itemName = info && !info->name.empty() + ? info->name : ("item #" + std::to_string(feedItem)); + uint32_t feedQuality = info ? info->quality : 1u; + addSystemChatMessage("You feed your pet " + buildItemLink(feedItem, feedQuality, itemName) + "."); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE FEED_PET: item=", feedItem, " name=", itemName); + } + } + } else { + // Unknown effect type — stop parsing to avoid misalignment + packet.skipAll(); + break; + } + } + packet.skipAll(); + }; + // TBC 2.4.3: clear a single aura slot for a unit + // Format: uint64 targetGuid + uint8 slot + dispatchTable_[Opcode::SMSG_CLEAR_EXTRA_AURA_INFO] = [this](network::Packet& packet) { + // TBC 2.4.3: clear a single aura slot for a unit + // Format: uint64 targetGuid + uint8 slot + if (packet.hasRemaining(9)) { + uint64_t clearGuid = packet.readUInt64(); + uint8_t slot = packet.readUInt8(); + std::vector* auraList = nullptr; + if (clearGuid == playerGuid) auraList = &playerAuras; + else if (clearGuid == targetGuid) auraList = &targetAuras; + if (auraList && slot < auraList->size()) { + (*auraList)[slot] = AuraSlot{}; + } + } + packet.skipAll(); + }; + // Format: uint64 itemGuid + uint32 slot + uint32 durationSec + uint64 playerGuid + // slot: 0=main-hand, 1=off-hand, 2=ranged + dispatchTable_[Opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE] = [this](network::Packet& packet) { + // Format: uint64 itemGuid + uint32 slot + uint32 durationSec + uint64 playerGuid + // slot: 0=main-hand, 1=off-hand, 2=ranged + if (!packet.hasRemaining(24)) { + packet.skipAll(); return; + } + /*uint64_t itemGuid =*/ packet.readUInt64(); + uint32_t enchSlot = packet.readUInt32(); + uint32_t durationSec = packet.readUInt32(); + /*uint64_t playerGuid =*/ packet.readUInt64(); + + // Clamp to known slots (0-2) + if (enchSlot > 2) { return; } + + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + if (durationSec == 0) { + // Enchant expired / removed — erase the slot entry + tempEnchantTimers_.erase( + std::remove_if(tempEnchantTimers_.begin(), tempEnchantTimers_.end(), + [enchSlot](const TempEnchantTimer& t) { return t.slot == enchSlot; }), + tempEnchantTimers_.end()); + } else { + uint64_t expireMs = nowMs + static_cast(durationSec) * 1000u; + bool found = false; + for (auto& t : tempEnchantTimers_) { + if (t.slot == enchSlot) { t.expireMs = expireMs; found = true; break; } + } + if (!found) tempEnchantTimers_.push_back({enchSlot, expireMs}); + + // Warn at important thresholds + if (durationSec <= 60 && durationSec > 55) { + const char* slotName = (enchSlot < 3) ? kTempEnchantSlotNames[enchSlot] : "weapon"; + char buf[80]; + std::snprintf(buf, sizeof(buf), "Weapon enchant (%s) expires in 1 minute!", slotName); + addSystemChatMessage(buf); + } else if (durationSec <= 300 && durationSec > 295) { + const char* slotName = (enchSlot < 3) ? kTempEnchantSlotNames[enchSlot] : "weapon"; + char buf[80]; + std::snprintf(buf, sizeof(buf), "Weapon enchant (%s) expires in 5 minutes.", slotName); + addSystemChatMessage(buf); + } + } + LOG_DEBUG("SMSG_ITEM_ENCHANT_TIME_UPDATE: slot=", enchSlot, " dur=", durationSec, "s"); + }; + // uint8 result: 0=success, 1=failed, 2=disabled + dispatchTable_[Opcode::SMSG_COMPLAIN_RESULT] = [this](network::Packet& packet) { + // uint8 result: 0=success, 1=failed, 2=disabled + if (packet.hasRemaining(1)) { + uint8_t result = packet.readUInt8(); + if (result == 0) + addSystemChatMessage("Your complaint has been submitted."); + else if (result == 2) + addUIError("Report a Player is currently disabled."); + } + packet.skipAll(); + }; + // WotLK: packed_guid caster + packed_guid target + uint32 spellId + uint32 remainingMs + uint32 totalMs + uint8 schoolMask + // TBC/Classic: uint64 caster + uint64 target + ... + dispatchTable_[Opcode::SMSG_RESUME_CAST_BAR] = [this](network::Packet& packet) { + // WotLK: packed_guid caster + packed_guid target + uint32 spellId + uint32 remainingMs + uint32 totalMs + uint8 schoolMask + // TBC/Classic: uint64 caster + uint64 target + ... + const bool rcbTbc = isPreWotlk(); + auto remaining = [&]() { return packet.getRemainingSize(); }; + if (remaining() < (rcbTbc ? 8u : 1u)) return; + uint64_t caster = rcbTbc + ? packet.readUInt64() : packet.readPackedGuid(); + if (remaining() < (rcbTbc ? 8u : 1u)) return; + if (rcbTbc) packet.readUInt64(); // target (discard) + else (void)packet.readPackedGuid(); // target + if (remaining() < 12) return; + uint32_t spellId = packet.readUInt32(); + uint32_t remainMs = packet.readUInt32(); + uint32_t totalMs = packet.readUInt32(); + if (totalMs > 0) { + if (caster == playerGuid) { + casting = true; + castIsChannel = false; + currentCastSpellId = spellId; + castTimeTotal = totalMs / 1000.0f; + castTimeRemaining = remainMs / 1000.0f; + } else { + auto& s = unitCastStates_[caster]; + s.casting = true; + s.spellId = spellId; + s.timeTotal = totalMs / 1000.0f; + s.timeRemaining = remainMs / 1000.0f; + } + LOG_DEBUG("SMSG_RESUME_CAST_BAR: caster=0x", std::hex, caster, std::dec, + " spell=", spellId, " remaining=", remainMs, "ms total=", totalMs, "ms"); + } + }; + // casterGuid + uint32 spellId + uint32 totalDurationMs + dispatchTable_[Opcode::MSG_CHANNEL_START] = [this](network::Packet& packet) { + // casterGuid + uint32 spellId + uint32 totalDurationMs + const bool tbcOrClassic = isPreWotlk(); + uint64_t chanCaster = tbcOrClassic + ? (packet.hasRemaining(8) ? packet.readUInt64() : 0) + : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) return; + uint32_t chanSpellId = packet.readUInt32(); + uint32_t chanTotalMs = packet.readUInt32(); + if (chanTotalMs > 0 && chanCaster != 0) { + if (chanCaster == playerGuid) { + casting = true; + castIsChannel = true; + currentCastSpellId = chanSpellId; + castTimeTotal = chanTotalMs / 1000.0f; + castTimeRemaining = castTimeTotal; + } else { + auto& s = unitCastStates_[chanCaster]; + s.casting = true; + s.isChannel = true; + s.spellId = chanSpellId; + s.timeTotal = chanTotalMs / 1000.0f; + s.timeRemaining = s.timeTotal; + s.interruptible = isSpellInterruptible(chanSpellId); + } + LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec, + " spell=", chanSpellId, " total=", chanTotalMs, "ms"); + // Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons + if (addonEventCallback_) { + auto unitId = guidToUnitId(chanCaster); + if (!unitId.empty()) + fireAddonEvent("UNIT_SPELLCAST_CHANNEL_START", {unitId, std::to_string(chanSpellId)}); + } + } + }; + // casterGuid + uint32 remainingMs + dispatchTable_[Opcode::MSG_CHANNEL_UPDATE] = [this](network::Packet& packet) { + // casterGuid + uint32 remainingMs + const bool tbcOrClassic2 = isPreWotlk(); + uint64_t chanCaster2 = tbcOrClassic2 + ? (packet.hasRemaining(8) ? packet.readUInt64() : 0) + : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; + uint32_t chanRemainMs = packet.readUInt32(); + if (chanCaster2 == playerGuid) { + castTimeRemaining = chanRemainMs / 1000.0f; + if (chanRemainMs == 0) { + casting = false; + castIsChannel = false; + currentCastSpellId = 0; + } + } else if (chanCaster2 != 0) { + auto it = unitCastStates_.find(chanCaster2); + if (it != unitCastStates_.end()) { + it->second.timeRemaining = chanRemainMs / 1000.0f; + if (chanRemainMs == 0) unitCastStates_.erase(it); + } + } + LOG_DEBUG("MSG_CHANNEL_UPDATE: caster=0x", std::hex, chanCaster2, std::dec, + " remaining=", chanRemainMs, "ms"); + // Fire UNIT_SPELLCAST_CHANNEL_STOP when channel ends + if (chanRemainMs == 0) { + auto unitId = guidToUnitId(chanCaster2); + if (!unitId.empty()) + fireAddonEvent("UNIT_SPELLCAST_CHANNEL_STOP", {unitId}); + } + }; + // uint32 slot + packed_guid unit (0 packed = clear slot) + dispatchTable_[Opcode::SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT] = [this](network::Packet& packet) { + // uint32 slot + packed_guid unit (0 packed = clear slot) + if (!packet.hasRemaining(5)) { + packet.skipAll(); + return; + } + uint32_t slot = packet.readUInt32(); + uint64_t unit = packet.readPackedGuid(); + if (slot < kMaxEncounterSlots) { + encounterUnitGuids_[slot] = unit; + LOG_DEBUG("SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT: slot=", slot, + " guid=0x", std::hex, unit, std::dec); + } + }; + // charName (cstring) + guid (uint64) + achievementId (uint32) + ... + dispatchTable_[Opcode::SMSG_SERVER_FIRST_ACHIEVEMENT] = [this](network::Packet& packet) { + // charName (cstring) + guid (uint64) + achievementId (uint32) + ... + if (packet.hasData()) { + std::string charName = packet.readString(); + if (packet.hasRemaining(12)) { + /*uint64_t guid =*/ packet.readUInt64(); + uint32_t achievementId = packet.readUInt32(); + loadAchievementNameCache(); + auto nit = achievementNameCache_.find(achievementId); + char buf[256]; + if (nit != achievementNameCache_.end() && !nit->second.empty()) { + std::snprintf(buf, sizeof(buf), + "%s is the first on the realm to earn: %s!", + charName.c_str(), nit->second.c_str()); + } else { + std::snprintf(buf, sizeof(buf), + "%s is the first on the realm to earn achievement #%u!", + charName.c_str(), achievementId); + } + addSystemChatMessage(buf); + } + } + packet.skipAll(); + }; + registerHandler(Opcode::SMSG_SET_FORCED_REACTIONS, &GameHandler::handleSetForcedReactions); + dispatchTable_[Opcode::SMSG_SUSPEND_COMMS] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t seqIdx = packet.readUInt32(); + if (socket) { + network::Packet ack(wireOpcode(Opcode::CMSG_SUSPEND_COMMS_ACK)); + ack.writeUInt32(seqIdx); + socket->send(ack); + } + } + }; + // SMSG_PRE_RESURRECT: packed GUID of the player who can self-resurrect. + // Sent when the dead player has Reincarnation (Shaman), Twisting Nether (Warlock), + // or Deathpact (Death Knight passive). The client must send CMSG_SELF_RES to accept. + dispatchTable_[Opcode::SMSG_PRE_RESURRECT] = [this](network::Packet& packet) { + // SMSG_PRE_RESURRECT: packed GUID of the player who can self-resurrect. + // Sent when the dead player has Reincarnation (Shaman), Twisting Nether (Warlock), + // or Deathpact (Death Knight passive). The client must send CMSG_SELF_RES to accept. + uint64_t targetGuid = packet.readPackedGuid(); + if (targetGuid == playerGuid || targetGuid == 0) { + selfResAvailable_ = true; + LOG_INFO("SMSG_PRE_RESURRECT: self-resurrection available (guid=0x", + std::hex, targetGuid, std::dec, ")"); + } + }; + dispatchTable_[Opcode::SMSG_PLAYERBINDERROR] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t error = packet.readUInt32(); + if (error == 0) { + addUIError("Your hearthstone is not bound."); + addSystemChatMessage("Your hearthstone is not bound."); + } else { + addUIError("Hearthstone bind failed."); + addSystemChatMessage("Hearthstone bind failed."); + } + } + }; + dispatchTable_[Opcode::SMSG_RAID_GROUP_ONLY] = [this](network::Packet& packet) { + addUIError("You must be in a raid group to enter this instance."); + addSystemChatMessage("You must be in a raid group to enter this instance."); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_RAID_READY_CHECK_ERROR] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t err = packet.readUInt8(); + if (err == 0) { addUIError("Ready check failed: not in a group."); addSystemChatMessage("Ready check failed: not in a group."); } + else if (err == 1) { addUIError("Ready check failed: in instance."); addSystemChatMessage("Ready check failed: in instance."); } + else { addUIError("Ready check failed."); addSystemChatMessage("Ready check failed."); } + } + }; + dispatchTable_[Opcode::SMSG_RESET_FAILED_NOTIFY] = [this](network::Packet& packet) { + addUIError("Cannot reset instance: another player is still inside."); + addSystemChatMessage("Cannot reset instance: another player is still inside."); + packet.skipAll(); + }; + // uint32 splitType + uint32 deferTime + string realmName + // Client must respond with CMSG_REALM_SPLIT to avoid session timeout on some servers. + dispatchTable_[Opcode::SMSG_REALM_SPLIT] = [this](network::Packet& packet) { + // uint32 splitType + uint32 deferTime + string realmName + // Client must respond with CMSG_REALM_SPLIT to avoid session timeout on some servers. + uint32_t splitType = 0; + if (packet.hasRemaining(4)) + splitType = packet.readUInt32(); + packet.skipAll(); + if (socket) { + network::Packet resp(wireOpcode(Opcode::CMSG_REALM_SPLIT)); + resp.writeUInt32(splitType); + resp.writeString("3.3.5"); + socket->send(resp); + LOG_DEBUG("SMSG_REALM_SPLIT splitType=", splitType, " — sent CMSG_REALM_SPLIT ack"); + } + }; + dispatchTable_[Opcode::SMSG_REAL_GROUP_UPDATE] = [this](network::Packet& packet) { + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() < 1) return; + uint8_t newGroupType = packet.readUInt8(); + if (rem() < 4) return; + uint32_t newMemberFlags = packet.readUInt32(); + if (rem() < 8) return; + uint64_t newLeaderGuid = packet.readUInt64(); + + partyData.groupType = newGroupType; + partyData.leaderGuid = newLeaderGuid; + + // Update local player's flags in the member list + uint64_t localGuid = playerGuid; + for (auto& m : partyData.members) { + if (m.guid == localGuid) { + m.flags = static_cast(newMemberFlags & 0xFF); + break; + } + } + LOG_DEBUG("SMSG_REAL_GROUP_UPDATE groupType=", static_cast(newGroupType), + " memberFlags=0x", std::hex, newMemberFlags, std::dec, + " leaderGuid=", newLeaderGuid); + fireAddonEvent("PARTY_LEADER_CHANGED", {}); + fireAddonEvent("GROUP_ROSTER_UPDATE", {}); + }; + dispatchTable_[Opcode::SMSG_PLAY_MUSIC] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t soundId = packet.readUInt32(); + if (playMusicCallback_) playMusicCallback_(soundId); + } + }; + dispatchTable_[Opcode::SMSG_PLAY_OBJECT_SOUND] = [this](network::Packet& packet) { + if (packet.hasRemaining(12)) { + // uint32 soundId + uint64 sourceGuid + uint32_t soundId = packet.readUInt32(); + uint64_t srcGuid = packet.readUInt64(); + LOG_DEBUG("SMSG_PLAY_OBJECT_SOUND: id=", soundId, " src=0x", std::hex, srcGuid, std::dec); + if (playPositionalSoundCallback_) playPositionalSoundCallback_(soundId, srcGuid); + else if (playSoundCallback_) playSoundCallback_(soundId); + } else if (packet.hasRemaining(4)) { + uint32_t soundId = packet.readUInt32(); + if (playSoundCallback_) playSoundCallback_(soundId); + } + }; + // uint64 targetGuid + uint32 visualId (same structure as SMSG_PLAY_SPELL_VISUAL) + dispatchTable_[Opcode::SMSG_PLAY_SPELL_IMPACT] = [this](network::Packet& packet) { + // uint64 targetGuid + uint32 visualId (same structure as SMSG_PLAY_SPELL_VISUAL) + if (!packet.hasRemaining(12)) { + packet.skipAll(); return; + } + uint64_t impTargetGuid = packet.readUInt64(); + uint32_t impVisualId = packet.readUInt32(); + if (impVisualId == 0) return; + auto* renderer = core::Application::getInstance().getRenderer(); + if (!renderer) return; + glm::vec3 spawnPos; + if (impTargetGuid == playerGuid) { + spawnPos = renderer->getCharacterPosition(); + } else { + auto entity = entityManager.getEntity(impTargetGuid); + if (!entity) return; + glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); + spawnPos = core::coords::canonicalToRender(canonical); + } + renderer->playSpellVisual(impVisualId, spawnPos, /*useImpactKit=*/true); + }; + // WotLK/Classic/Turtle: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId + // + float resistFactor + uint32 targetRes + uint32 resistedValue + ... + // TBC: same layout but full uint64 GUIDs + // Show RESIST combat text when player resists an incoming spell. + dispatchTable_[Opcode::SMSG_RESISTLOG] = [this](network::Packet& packet) { + // WotLK/Classic/Turtle: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId + // + float resistFactor + uint32 targetRes + uint32 resistedValue + ... + // TBC: same layout but full uint64 GUIDs + // Show RESIST combat text when player resists an incoming spell. + const bool rlUsesFullGuid = isActiveExpansion("tbc"); + auto rl_rem = [&]() { return packet.getRemainingSize(); }; + if (rl_rem() < 4) { packet.skipAll(); return; } + /*uint32_t hitInfo =*/ packet.readUInt32(); + if (rl_rem() < (rlUsesFullGuid ? 8u : 1u) + || (!rlUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t attackerGuid = rlUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (rl_rem() < (rlUsesFullGuid ? 8u : 1u) + || (!rlUsesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); return; + } + uint64_t victimGuid = rlUsesFullGuid + ? packet.readUInt64() : packet.readPackedGuid(); + if (rl_rem() < 4) { packet.skipAll(); return; } + uint32_t spellId = packet.readUInt32(); + // Resist payload includes: + // float resistFactor + uint32 targetResistance + uint32 resistedValue. + // Require the full payload so truncated packets cannot synthesize + // zero-value resist events. + if (rl_rem() < 12) { packet.skipAll(); return; } + /*float resistFactor =*/ packet.readFloat(); + /*uint32_t targetRes =*/ packet.readUInt32(); + int32_t resistedAmount = static_cast(packet.readUInt32()); + // Show RESIST when the player is involved on either side. + if (resistedAmount > 0 && victimGuid == playerGuid) { + addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid); + } else if (resistedAmount > 0 && attackerGuid == playerGuid) { + addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_READ_ITEM_OK] = [this](network::Packet& packet) { + bookPages_.clear(); // fresh book for this item read + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_READ_ITEM_FAILED] = [this](network::Packet& packet) { + addUIError("You cannot read this item."); + addSystemChatMessage("You cannot read this item."); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_QUERY_QUESTS_COMPLETED_RESPONSE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t count = packet.readUInt32(); + if (count <= 4096) { + for (uint32_t i = 0; i < count; ++i) { + if (!packet.hasRemaining(4)) break; + uint32_t questId = packet.readUInt32(); + completedQuests_.insert(questId); + } + LOG_DEBUG("SMSG_QUERY_QUESTS_COMPLETED_RESPONSE: ", count, " completed quests"); + } + } + packet.skipAll(); + }; + // WotLK 3.3.5a format: uint64 guid + uint32 questId + uint32 count + uint32 reqCount + // Classic format: uint64 guid + uint32 questId + uint32 count (no reqCount) + dispatchTable_[Opcode::SMSG_QUESTUPDATE_ADD_PVP_KILL] = [this](network::Packet& packet) { + // WotLK 3.3.5a format: uint64 guid + uint32 questId + uint32 count + uint32 reqCount + // Classic format: uint64 guid + uint32 questId + uint32 count (no reqCount) + if (packet.hasRemaining(16)) { + /*uint64_t guid =*/ packet.readUInt64(); + uint32_t questId = packet.readUInt32(); + uint32_t count = packet.readUInt32(); + uint32_t reqCount = 0; + if (packet.hasRemaining(4)) { + reqCount = packet.readUInt32(); + } + + // Update quest log kill counts (PvP kills use entry=0 as the key + // since there's no specific creature entry — one slot per quest). + constexpr uint32_t PVP_KILL_ENTRY = 0u; + for (auto& quest : questLog_) { + if (quest.questId != questId) continue; + + if (reqCount == 0) { + auto it = quest.killCounts.find(PVP_KILL_ENTRY); + if (it != quest.killCounts.end()) reqCount = it->second.second; + } + if (reqCount == 0) { + // Pull required count from kill objectives (npcOrGoId == 0 slot, if any) + for (const auto& obj : quest.killObjectives) { + if (obj.npcOrGoId == 0 && obj.required > 0) { + reqCount = obj.required; + break; + } + } + } + if (reqCount == 0) reqCount = count; + quest.killCounts[PVP_KILL_ENTRY] = {count, reqCount}; + + std::string progressMsg = quest.title + ": PvP kills " + + std::to_string(count) + "/" + std::to_string(reqCount); + addSystemChatMessage(progressMsg); + break; + } + } + }; + dispatchTable_[Opcode::SMSG_NPC_WONT_TALK] = [this](network::Packet& packet) { + addUIError("That creature can't talk to you right now."); + addSystemChatMessage("That creature can't talk to you right now."); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_OFFER_PETITION_ERROR] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t err = packet.readUInt32(); + if (err == 1) addSystemChatMessage("Player is already in a guild."); + else if (err == 2) addSystemChatMessage("Player already has a petition."); + else addSystemChatMessage("Cannot offer petition to that player."); + } + }; + registerHandler(Opcode::SMSG_PETITION_QUERY_RESPONSE, &GameHandler::handlePetitionQueryResponse); + registerHandler(Opcode::SMSG_PETITION_SHOW_SIGNATURES, &GameHandler::handlePetitionShowSignatures); + registerHandler(Opcode::SMSG_PETITION_SIGN_RESULTS, &GameHandler::handlePetitionSignResults); + // uint64 petGuid, uint32 mode + // mode bits: low byte = command state, next byte = react state + dispatchTable_[Opcode::SMSG_PET_MODE] = [this](network::Packet& packet) { + // uint64 petGuid, uint32 mode + // mode bits: low byte = command state, next byte = react state + if (packet.hasRemaining(12)) { + uint64_t modeGuid = packet.readUInt64(); + uint32_t mode = packet.readUInt32(); + if (modeGuid == petGuid_) { + petCommand_ = static_cast(mode & 0xFF); + petReact_ = static_cast((mode >> 8) & 0xFF); + LOG_DEBUG("SMSG_PET_MODE: command=", static_cast(petCommand_), + " react=", static_cast(petReact_)); + } + } + packet.skipAll(); + }; + // Pet bond broken (died or forcibly dismissed) — clear pet state + dispatchTable_[Opcode::SMSG_PET_BROKEN] = [this](network::Packet& packet) { + // Pet bond broken (died or forcibly dismissed) — clear pet state + petGuid_ = 0; + petSpellList_.clear(); + petAutocastSpells_.clear(); + memset(petActionSlots_, 0, sizeof(petActionSlots_)); + addSystemChatMessage("Your pet has died."); + LOG_INFO("SMSG_PET_BROKEN: pet bond broken"); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_PET_LEARNED_SPELL] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t spellId = packet.readUInt32(); + petSpellList_.push_back(spellId); + const std::string& sname = getSpellName(spellId); + addSystemChatMessage("Your pet has learned " + (sname.empty() ? "a new ability." : sname + ".")); + LOG_DEBUG("SMSG_PET_LEARNED_SPELL: spellId=", spellId); + fireAddonEvent("PET_BAR_UPDATE", {}); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_PET_UNLEARNED_SPELL] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t spellId = packet.readUInt32(); + petSpellList_.erase( + std::remove(petSpellList_.begin(), petSpellList_.end(), spellId), + petSpellList_.end()); + petAutocastSpells_.erase(spellId); + LOG_DEBUG("SMSG_PET_UNLEARNED_SPELL: spellId=", spellId); + } + packet.skipAll(); + }; + // WotLK: castCount(1) + spellId(4) + reason(1) + // Classic/TBC: spellId(4) + reason(1) (no castCount) + dispatchTable_[Opcode::SMSG_PET_CAST_FAILED] = [this](network::Packet& packet) { + // WotLK: castCount(1) + spellId(4) + reason(1) + // Classic/TBC: spellId(4) + reason(1) (no castCount) + const bool hasCount = isActiveExpansion("wotlk"); + const size_t minSize = hasCount ? 6u : 5u; + if (packet.hasRemaining(minSize)) { + if (hasCount) /*uint8_t castCount =*/ packet.readUInt8(); + uint32_t spellId = packet.readUInt32(); + uint8_t reason = (packet.hasRemaining(1)) + ? packet.readUInt8() : 0; + LOG_DEBUG("SMSG_PET_CAST_FAILED: spell=", spellId, + " reason=", static_cast(reason)); + if (reason != 0) { + const char* reasonStr = getSpellCastResultString(reason); + const std::string& sName = getSpellName(spellId); + std::string errMsg; + if (reasonStr && *reasonStr) + errMsg = sName.empty() ? reasonStr : (sName + ": " + reasonStr); + else + errMsg = sName.empty() ? "Pet spell failed." : (sName + ": Pet spell failed."); + addSystemChatMessage(errMsg); + } + } + packet.skipAll(); + }; + // uint64 petGuid + uint32 cost (copper) + for (auto op : { Opcode::SMSG_PET_GUIDS, Opcode::SMSG_PET_DISMISS_SOUND, Opcode::SMSG_PET_ACTION_SOUND, Opcode::SMSG_PET_UNLEARN_CONFIRM }) { + dispatchTable_[op] = [this](network::Packet& packet) { + // uint64 petGuid + uint32 cost (copper) + if (packet.hasRemaining(12)) { + petUnlearnGuid_ = packet.readUInt64(); + petUnlearnCost_ = packet.readUInt32(); + petUnlearnPending_ = true; + } + packet.skipAll(); + }; + } + // Server signals that the pet can now be named (first tame) + dispatchTable_[Opcode::SMSG_PET_RENAMEABLE] = [this](network::Packet& packet) { + // Server signals that the pet can now be named (first tame) + petRenameablePending_ = true; + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_PET_NAME_INVALID] = [this](network::Packet& packet) { + addUIError("That pet name is invalid. Please choose a different name."); + addSystemChatMessage("That pet name is invalid. Please choose a different name."); + packet.skipAll(); + }; + // Classic 1.12: PackedGUID + 19×uint32 itemEntries (EQUIPMENT_SLOT_END=19) + // This opcode is only reachable on Classic servers; TBC/WotLK wire 0x115 maps to + // SMSG_INSPECT_RESULTS_UPDATE which is handled separately. + dispatchTable_[Opcode::SMSG_INSPECT] = [this](network::Packet& packet) { + // Classic 1.12: PackedGUID + 19×uint32 itemEntries (EQUIPMENT_SLOT_END=19) + // This opcode is only reachable on Classic servers; TBC/WotLK wire 0x115 maps to + // SMSG_INSPECT_RESULTS_UPDATE which is handled separately. + if (!packet.hasRemaining(2)) { + packet.skipAll(); return; + } + uint64_t guid = packet.readPackedGuid(); + if (guid == 0) { packet.skipAll(); return; } + + constexpr int kGearSlots = 19; + size_t needed = kGearSlots * sizeof(uint32_t); + if (!packet.hasRemaining(needed)) { + packet.skipAll(); return; + } + + std::array items{}; + for (int s = 0; s < kGearSlots; ++s) + items[s] = packet.readUInt32(); + + // Resolve player name + auto ent = entityManager.getEntity(guid); + std::string playerName = "Target"; + if (ent) { + auto pl = std::dynamic_pointer_cast(ent); + if (pl && !pl->getName().empty()) playerName = pl->getName(); + } + + // Populate inspect result immediately (no talent data in Classic SMSG_INSPECT) + inspectResult_.guid = guid; + inspectResult_.playerName = playerName; + inspectResult_.totalTalents = 0; + inspectResult_.unspentTalents = 0; + inspectResult_.talentGroups = 0; + inspectResult_.activeTalentGroup = 0; + inspectResult_.itemEntries = items; + inspectResult_.enchantIds = {}; + + // Also cache for future talent-inspect cross-reference + inspectedPlayerItemEntries_[guid] = items; + + // Trigger item queries for non-empty slots + for (int s = 0; s < kGearSlots; ++s) { + if (items[s] != 0) queryItemInfo(items[s], 0); + } + + LOG_INFO("SMSG_INSPECT (Classic): ", playerName, " has gear in ", + std::count_if(items.begin(), items.end(), + [](uint32_t e) { return e != 0; }), "/19 slots"); + if (addonEventCallback_) { + char guidBuf[32]; + snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)guid); + fireAddonEvent("INSPECT_READY", {guidBuf}); + } + }; + // Same wire format as SMSG_COMPRESSED_MOVES: uint8 size + uint16 opcode + payload[] + dispatchTable_[Opcode::SMSG_MULTIPLE_MOVES] = [this](network::Packet& packet) { + // Same wire format as SMSG_COMPRESSED_MOVES: uint8 size + uint16 opcode + payload[] + handleCompressedMoves(packet); + }; + // Each sub-packet uses the standard WotLK server wire format: + // uint16_be subSize (includes the 2-byte opcode; payload = subSize - 2) + // uint16_le subOpcode + // payload (subSize - 2 bytes) + dispatchTable_[Opcode::SMSG_MULTIPLE_PACKETS] = [this](network::Packet& packet) { + // Each sub-packet uses the standard WotLK server wire format: + // uint16_be subSize (includes the 2-byte opcode; payload = subSize - 2) + // uint16_le subOpcode + // payload (subSize - 2 bytes) + const auto& pdata = packet.getData(); + size_t dataLen = pdata.size(); + size_t pos = packet.getReadPos(); + static uint32_t multiPktWarnCount = 0; + std::vector subPackets; + while (pos + 4 <= dataLen) { + uint16_t subSize = static_cast( + (static_cast(pdata[pos]) << 8) | pdata[pos + 1]); + if (subSize < 2) break; + size_t payloadLen = subSize - 2; + if (pos + 4 + payloadLen > dataLen) { + if (++multiPktWarnCount <= 10) { + LOG_WARNING("SMSG_MULTIPLE_PACKETS: sub-packet overruns buffer at pos=", + pos, " subSize=", subSize, " dataLen=", dataLen); + } + break; + } + uint16_t subOpcode = static_cast(pdata[pos + 2]) | + (static_cast(pdata[pos + 3]) << 8); + std::vector subPayload(pdata.begin() + pos + 4, + pdata.begin() + pos + 4 + payloadLen); + subPackets.emplace_back(subOpcode, std::move(subPayload)); + pos += 4 + payloadLen; + } + for (auto it = subPackets.rbegin(); it != subPackets.rend(); ++it) { + enqueueIncomingPacketFront(std::move(*it)); + } + packet.skipAll(); + }; + // Recruit-A-Friend: a mentor is offering to grant you a level + dispatchTable_[Opcode::SMSG_PROPOSE_LEVEL_GRANT] = [this](network::Packet& packet) { + // Recruit-A-Friend: a mentor is offering to grant you a level + if (packet.hasRemaining(8)) { + uint64_t mentorGuid = packet.readUInt64(); + std::string mentorName; + auto ent = entityManager.getEntity(mentorGuid); + if (auto* unit = dynamic_cast(ent.get())) mentorName = unit->getName(); + if (mentorName.empty()) mentorName = lookupName(mentorGuid); + addSystemChatMessage(mentorName.empty() + ? "A player is offering to grant you a level." + : (mentorName + " is offering to grant you a level.")); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_REFER_A_FRIEND_EXPIRED] = [this](network::Packet& packet) { + addSystemChatMessage("Your Recruit-A-Friend link has expired."); + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_REFER_A_FRIEND_FAILURE] = [this](network::Packet& packet) { + if (packet.hasRemaining(4)) { + uint32_t reason = packet.readUInt32(); + static const char* kRafErrors[] = { + "Not eligible", // 0 + "Target not eligible", // 1 + "Too many referrals", // 2 + "Wrong faction", // 3 + "Not a recruit", // 4 + "Recruit requirements not met", // 5 + "Level above requirement", // 6 + "Friend needs account upgrade", // 7 + }; + const char* msg = (reason < 8) ? kRafErrors[reason] + : "Recruit-A-Friend failed."; + addSystemChatMessage(std::string("Recruit-A-Friend: ") + msg); + } + packet.skipAll(); + }; + dispatchTable_[Opcode::SMSG_REPORT_PVP_AFK_RESULT] = [this](network::Packet& packet) { + if (packet.hasRemaining(1)) { + uint8_t result = packet.readUInt8(); + if (result == 0) + addSystemChatMessage("AFK report submitted."); + else + addSystemChatMessage("Cannot report that player as AFK right now."); + } + packet.skipAll(); + }; + registerHandler(Opcode::SMSG_RESPOND_INSPECT_ACHIEVEMENTS, &GameHandler::handleRespondInspectAchievements); + registerHandler(Opcode::SMSG_QUEST_POI_QUERY_RESPONSE, &GameHandler::handleQuestPoiQueryResponse); + dispatchTable_[Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA] = [this](network::Packet& packet) { + vehicleId_ = 0; // Vehicle ride cancelled; clear UI + packet.skipAll(); + }; + // uint32 type (0=normal, 1=heavy, 2=tired/restricted) + uint32 minutes played + dispatchTable_[Opcode::SMSG_PLAY_TIME_WARNING] = [this](network::Packet& packet) { + // uint32 type (0=normal, 1=heavy, 2=tired/restricted) + uint32 minutes played + if (packet.hasRemaining(4)) { + uint32_t warnType = packet.readUInt32(); + uint32_t minutesPlayed = (packet.hasRemaining(4)) + ? packet.readUInt32() : 0; + const char* severity = (warnType >= 2) ? "[Tired] " : "[Play Time] "; + char buf[128]; + if (minutesPlayed > 0) { + uint32_t h = minutesPlayed / 60; + uint32_t m = minutesPlayed % 60; + if (h > 0) + std::snprintf(buf, sizeof(buf), "%sYou have been playing for %uh %um.", severity, h, m); + else + std::snprintf(buf, sizeof(buf), "%sYou have been playing for %um.", severity, m); + } else { + std::snprintf(buf, sizeof(buf), "%sYou have been playing for a long time.", severity); + } + addSystemChatMessage(buf); + addUIError(buf); + } + }; + registerHandler(Opcode::SMSG_ITEM_QUERY_MULTIPLE_RESPONSE, &GameHandler::handleItemQueryResponse); + // WotLK 3.3.5a format: + // uint64 mirrorGuid — GUID of the mirror image unit + // uint32 displayId — display ID to render the image with + // uint8 raceId — race of caster + // uint8 genderFlag — gender of caster + // uint8 classId — class of caster + // uint64 casterGuid — GUID of the player who cast the spell + // Followed by equipped item display IDs (11 × uint32) if casterGuid != 0 + // Purpose: tells client how to render the image (same appearance as caster). + // We parse the GUIDs so units render correctly via their existing display IDs. + dispatchTable_[Opcode::SMSG_MIRRORIMAGE_DATA] = [this](network::Packet& packet) { + // WotLK 3.3.5a format: + // uint64 mirrorGuid — GUID of the mirror image unit + // uint32 displayId — display ID to render the image with + // uint8 raceId — race of caster + // uint8 genderFlag — gender of caster + // uint8 classId — class of caster + // uint64 casterGuid — GUID of the player who cast the spell + // Followed by equipped item display IDs (11 × uint32) if casterGuid != 0 + // Purpose: tells client how to render the image (same appearance as caster). + // We parse the GUIDs so units render correctly via their existing display IDs. + if (!packet.hasRemaining(8)) return; + uint64_t mirrorGuid = packet.readUInt64(); + if (!packet.hasRemaining(4)) return; + uint32_t displayId = packet.readUInt32(); + if (!packet.hasRemaining(3)) return; + /*uint8_t raceId =*/ packet.readUInt8(); + /*uint8_t gender =*/ packet.readUInt8(); + /*uint8_t classId =*/ packet.readUInt8(); + // Apply display ID to the mirror image unit so it renders correctly + if (mirrorGuid != 0 && displayId != 0) { + auto entity = entityManager.getEntity(mirrorGuid); + if (entity) { + auto unit = std::dynamic_pointer_cast(entity); + if (unit && unit->getDisplayId() == 0) + unit->setDisplayId(displayId); + } + } + LOG_DEBUG("SMSG_MIRRORIMAGE_DATA: mirrorGuid=0x", std::hex, mirrorGuid, + " displayId=", std::dec, displayId); + packet.skipAll(); + }; + // uint64 battlefieldGuid + uint32 zoneId + uint64 expireUnixTime (seconds) + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_ENTRY_INVITE] = [this](network::Packet& packet) { + // uint64 battlefieldGuid + uint32 zoneId + uint64 expireUnixTime (seconds) + if (!packet.hasRemaining(20)) { + packet.skipAll(); return; + } + uint64_t bfGuid = packet.readUInt64(); + uint32_t bfZoneId = packet.readUInt32(); + uint64_t expireTime = packet.readUInt64(); + (void)bfGuid; (void)expireTime; + // Store the invitation so the UI can show a prompt + bfMgrInvitePending_ = true; + bfMgrZoneId_ = bfZoneId; + char buf[128]; + std::string bfZoneName = getAreaName(bfZoneId); + if (!bfZoneName.empty()) + std::snprintf(buf, sizeof(buf), + "You are invited to the outdoor battlefield in %s. Click to enter.", + bfZoneName.c_str()); + else + std::snprintf(buf, sizeof(buf), + "You are invited to the outdoor battlefield in zone %u. Click to enter.", + bfZoneId); + addSystemChatMessage(buf); + LOG_INFO("SMSG_BATTLEFIELD_MGR_ENTRY_INVITE: zoneId=", bfZoneId); + }; + // uint64 battlefieldGuid + uint8 isSafe (1=pvp zones enabled) + uint8 onQueue + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_ENTERED] = [this](network::Packet& packet) { + // uint64 battlefieldGuid + uint8 isSafe (1=pvp zones enabled) + uint8 onQueue + if (packet.hasRemaining(8)) { + uint64_t bfGuid2 = packet.readUInt64(); + (void)bfGuid2; + uint8_t isSafe = (packet.hasRemaining(1)) ? packet.readUInt8() : 0; + uint8_t onQueue = (packet.hasRemaining(1)) ? packet.readUInt8() : 0; + bfMgrInvitePending_ = false; + bfMgrActive_ = true; + addSystemChatMessage(isSafe ? "You are in the battlefield zone (safe area)." + : "You have entered the battlefield!"); + if (onQueue) addSystemChatMessage("You are in the battlefield queue."); + LOG_INFO("SMSG_BATTLEFIELD_MGR_ENTERED: isSafe=", static_cast(isSafe), " onQueue=", static_cast(onQueue)); + } + packet.skipAll(); + }; + // uint64 battlefieldGuid + uint32 battlefieldId + uint64 expireTime + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_QUEUE_INVITE] = [this](network::Packet& packet) { + // uint64 battlefieldGuid + uint32 battlefieldId + uint64 expireTime + if (!packet.hasRemaining(20)) { + packet.skipAll(); return; + } + uint64_t bfGuid3 = packet.readUInt64(); + uint32_t bfId = packet.readUInt32(); + uint64_t expTime = packet.readUInt64(); + (void)bfGuid3; (void)expTime; + bfMgrInvitePending_ = true; + bfMgrZoneId_ = bfId; + char buf[128]; + std::snprintf(buf, sizeof(buf), + "A spot has opened in the battlefield queue (battlefield %u).", bfId); + addSystemChatMessage(buf); + LOG_INFO("SMSG_BATTLEFIELD_MGR_QUEUE_INVITE: bfId=", bfId); + }; + // uint32 battlefieldId + uint32 teamId + uint8 accepted + uint8 loggingEnabled + uint8 result + // result: 0=queued, 1=not_in_group, 2=too_high_level, 3=too_low_level, + // 4=in_cooldown, 5=queued_other_bf, 6=bf_full + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_QUEUE_REQUEST_RESPONSE] = [this](network::Packet& packet) { + // uint32 battlefieldId + uint32 teamId + uint8 accepted + uint8 loggingEnabled + uint8 result + // result: 0=queued, 1=not_in_group, 2=too_high_level, 3=too_low_level, + // 4=in_cooldown, 5=queued_other_bf, 6=bf_full + if (!packet.hasRemaining(11)) { + packet.skipAll(); return; + } + uint32_t bfId2 = packet.readUInt32(); + /*uint32_t teamId =*/ packet.readUInt32(); + uint8_t accepted = packet.readUInt8(); + /*uint8_t logging =*/ packet.readUInt8(); + uint8_t result = packet.readUInt8(); + (void)bfId2; + if (accepted) { + addSystemChatMessage("You have joined the battlefield queue."); + } else { + static const char* kBfQueueErrors[] = { + "Queued for battlefield.", "Not in a group.", "Level too high.", + "Level too low.", "Battlefield in cooldown.", "Already queued for another battlefield.", + "Battlefield is full." + }; + const char* msg = (result < 7) ? kBfQueueErrors[result] + : "Battlefield queue request failed."; + addSystemChatMessage(std::string("Battlefield: ") + msg); + } + LOG_INFO("SMSG_BATTLEFIELD_MGR_QUEUE_REQUEST_RESPONSE: accepted=", static_cast(accepted), + " result=", static_cast(result)); + packet.skipAll(); + }; + // uint64 battlefieldGuid + uint8 remove + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_EJECT_PENDING] = [this](network::Packet& packet) { + // uint64 battlefieldGuid + uint8 remove + if (packet.hasRemaining(9)) { + uint64_t bfGuid4 = packet.readUInt64(); + uint8_t remove = packet.readUInt8(); + (void)bfGuid4; + if (remove) { + addSystemChatMessage("You will be removed from the battlefield shortly."); + } + LOG_INFO("SMSG_BATTLEFIELD_MGR_EJECT_PENDING: remove=", static_cast(remove)); + } + packet.skipAll(); + }; + // uint64 battlefieldGuid + uint32 reason + uint32 battleStatus + uint8 relocated + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_EJECTED] = [this](network::Packet& packet) { + // uint64 battlefieldGuid + uint32 reason + uint32 battleStatus + uint8 relocated + if (packet.hasRemaining(17)) { + uint64_t bfGuid5 = packet.readUInt64(); + uint32_t reason = packet.readUInt32(); + /*uint32_t status =*/ packet.readUInt32(); + uint8_t relocated = packet.readUInt8(); + (void)bfGuid5; + static const char* kEjectReasons[] = { + "Removed from battlefield.", "Transported from battlefield.", + "Left battlefield voluntarily.", "Offline.", + }; + const char* msg = (reason < 4) ? kEjectReasons[reason] + : "You have been ejected from the battlefield."; + addSystemChatMessage(msg); + if (relocated) addSystemChatMessage("You have been relocated outside the battlefield."); + LOG_INFO("SMSG_BATTLEFIELD_MGR_EJECTED: reason=", reason, " relocated=", static_cast(relocated)); + } + bfMgrActive_ = false; + bfMgrInvitePending_ = false; + packet.skipAll(); + }; + // uint32 oldState + uint32 newState + // States: 0=Waiting, 1=Starting, 2=InProgress, 3=Ending, 4=Cooldown + dispatchTable_[Opcode::SMSG_BATTLEFIELD_MGR_STATE_CHANGE] = [this](network::Packet& packet) { + // uint32 oldState + uint32 newState + // States: 0=Waiting, 1=Starting, 2=InProgress, 3=Ending, 4=Cooldown + if (packet.hasRemaining(8)) { + /*uint32_t oldState =*/ packet.readUInt32(); + uint32_t newState = packet.readUInt32(); + static const char* kBfStates[] = { + "waiting", "starting", "in progress", "ending", "in cooldown" + }; + const char* stateStr = (newState < 5) ? kBfStates[newState] : "unknown state"; + char buf[128]; + std::snprintf(buf, sizeof(buf), "Battlefield is now %s.", stateStr); + addSystemChatMessage(buf); + LOG_INFO("SMSG_BATTLEFIELD_MGR_STATE_CHANGE: newState=", newState); + } + packet.skipAll(); + }; + // uint32 numPending — number of unacknowledged calendar invites + dispatchTable_[Opcode::SMSG_CALENDAR_SEND_NUM_PENDING] = [this](network::Packet& packet) { + // uint32 numPending — number of unacknowledged calendar invites + if (packet.hasRemaining(4)) { + uint32_t numPending = packet.readUInt32(); + calendarPendingInvites_ = numPending; + if (numPending > 0) { + char buf[64]; + std::snprintf(buf, sizeof(buf), + "You have %u pending calendar invite%s.", + numPending, numPending == 1 ? "" : "s"); + addSystemChatMessage(buf); + } + LOG_DEBUG("SMSG_CALENDAR_SEND_NUM_PENDING: ", numPending, " pending invites"); + } + }; + // uint32 command + uint8 result + cstring info + // result 0 = success; non-zero = error code + // command values: 0=add,1=get,2=guild_filter,3=arena_team,4=update,5=remove, + // 6=copy,7=invite,8=rsvp,9=remove_invite,10=status,11=moderator_status + dispatchTable_[Opcode::SMSG_CALENDAR_COMMAND_RESULT] = [this](network::Packet& packet) { + // uint32 command + uint8 result + cstring info + // result 0 = success; non-zero = error code + // command values: 0=add,1=get,2=guild_filter,3=arena_team,4=update,5=remove, + // 6=copy,7=invite,8=rsvp,9=remove_invite,10=status,11=moderator_status + if (!packet.hasRemaining(5)) { + packet.skipAll(); return; + } + /*uint32_t command =*/ packet.readUInt32(); + uint8_t result = packet.readUInt8(); + std::string info = (packet.hasData()) ? packet.readString() : ""; + if (result != 0) { + // Map common calendar error codes to friendly strings + static const char* kCalendarErrors[] = { + "", + "Calendar: Internal error.", // 1 = CALENDAR_ERROR_INTERNAL + "Calendar: Guild event limit reached.",// 2 + "Calendar: Event limit reached.", // 3 + "Calendar: You cannot invite that player.", // 4 + "Calendar: No invites remaining.", // 5 + "Calendar: Invalid date.", // 6 + "Calendar: Cannot invite yourself.", // 7 + "Calendar: Cannot modify this event.", // 8 + "Calendar: Not invited.", // 9 + "Calendar: Already invited.", // 10 + "Calendar: Player not found.", // 11 + "Calendar: Not enough focus.", // 12 + "Calendar: Event locked.", // 13 + "Calendar: Event deleted.", // 14 + "Calendar: Not a moderator.", // 15 + }; + const char* errMsg = (result < 16) ? kCalendarErrors[result] + : "Calendar: Command failed."; + if (errMsg && errMsg[0] != '\0') addSystemChatMessage(errMsg); + else if (!info.empty()) addSystemChatMessage("Calendar: " + info); + } + packet.skipAll(); + }; + // Rich notification: eventId(8) + title(cstring) + eventTime(8) + flags(4) + + // eventType(1) + dungeonId(4) + inviteId(8) + status(1) + rank(1) + + // isGuildEvent(1) + inviterGuid(8) + dispatchTable_[Opcode::SMSG_CALENDAR_EVENT_INVITE_ALERT] = [this](network::Packet& packet) { + // Rich notification: eventId(8) + title(cstring) + eventTime(8) + flags(4) + + // eventType(1) + dungeonId(4) + inviteId(8) + status(1) + rank(1) + + // isGuildEvent(1) + inviterGuid(8) + if (!packet.hasRemaining(9)) { + packet.skipAll(); return; + } + /*uint64_t eventId =*/ packet.readUInt64(); + std::string title = (packet.hasData()) ? packet.readString() : ""; + packet.skipAll(); // consume remaining fields + if (!title.empty()) { + addSystemChatMessage("Calendar invite: " + title); + } else { + addSystemChatMessage("You have a new calendar invite."); + } + if (calendarPendingInvites_ < 255) ++calendarPendingInvites_; + LOG_INFO("SMSG_CALENDAR_EVENT_INVITE_ALERT: title='", title, "'"); + }; + // Sent when an event invite's RSVP status changes for the local player + // Format: inviteId(8) + eventId(8) + eventType(1) + flags(4) + + // inviteTime(8) + status(1) + rank(1) + isGuildEvent(1) + title(cstring) + dispatchTable_[Opcode::SMSG_CALENDAR_EVENT_STATUS] = [this](network::Packet& packet) { + // Sent when an event invite's RSVP status changes for the local player + // Format: inviteId(8) + eventId(8) + eventType(1) + flags(4) + + // inviteTime(8) + status(1) + rank(1) + isGuildEvent(1) + title(cstring) + if (!packet.hasRemaining(31)) { + packet.skipAll(); return; + } + /*uint64_t inviteId =*/ packet.readUInt64(); + /*uint64_t eventId =*/ packet.readUInt64(); + /*uint8_t evType =*/ packet.readUInt8(); + /*uint32_t flags =*/ packet.readUInt32(); + /*uint64_t invTime =*/ packet.readUInt64(); + uint8_t status = packet.readUInt8(); + /*uint8_t rank =*/ packet.readUInt8(); + /*uint8_t isGuild =*/ packet.readUInt8(); + std::string evTitle = (packet.hasData()) ? packet.readString() : ""; + // status: 0=Invited,1=Accepted,2=Declined,3=Confirmed,4=Out,5=Standby,6=SignedUp,7=Not Signed Up,8=Tentative + static const char* kRsvpStatus[] = { + "invited", "accepted", "declined", "confirmed", + "out", "on standby", "signed up", "not signed up", "tentative" + }; + const char* statusStr = (status < 9) ? kRsvpStatus[status] : "unknown"; + if (!evTitle.empty()) { + char buf[256]; + std::snprintf(buf, sizeof(buf), "Calendar event '%s': your RSVP is %s.", + evTitle.c_str(), statusStr); + addSystemChatMessage(buf); + } + packet.skipAll(); + }; + // uint64 inviteId + uint64 eventId + uint32 mapId + uint32 difficulty + uint64 resetTime + dispatchTable_[Opcode::SMSG_CALENDAR_RAID_LOCKOUT_ADDED] = [this](network::Packet& packet) { + // uint64 inviteId + uint64 eventId + uint32 mapId + uint32 difficulty + uint64 resetTime + if (packet.hasRemaining(28)) { + /*uint64_t inviteId =*/ packet.readUInt64(); + /*uint64_t eventId =*/ packet.readUInt64(); + uint32_t mapId = packet.readUInt32(); + uint32_t difficulty = packet.readUInt32(); + /*uint64_t resetTime =*/ packet.readUInt64(); + std::string mapLabel = getMapName(mapId); + if (mapLabel.empty()) mapLabel = "map #" + std::to_string(mapId); + static const char* kDiff[] = {"Normal","Heroic","25-Man","25-Man Heroic"}; + const char* diffStr = (difficulty < 4) ? kDiff[difficulty] : nullptr; + std::string msg = "Calendar: Raid lockout added for " + mapLabel; + if (diffStr) msg += std::string(" (") + diffStr + ")"; + msg += '.'; + addSystemChatMessage(msg); + LOG_DEBUG("SMSG_CALENDAR_RAID_LOCKOUT_ADDED: mapId=", mapId, " difficulty=", difficulty); + } + packet.skipAll(); + }; + // uint64 inviteId + uint64 eventId + uint32 mapId + uint32 difficulty + dispatchTable_[Opcode::SMSG_CALENDAR_RAID_LOCKOUT_REMOVED] = [this](network::Packet& packet) { + // uint64 inviteId + uint64 eventId + uint32 mapId + uint32 difficulty + if (packet.hasRemaining(20)) { + /*uint64_t inviteId =*/ packet.readUInt64(); + /*uint64_t eventId =*/ packet.readUInt64(); + uint32_t mapId = packet.readUInt32(); + uint32_t difficulty = packet.readUInt32(); + std::string mapLabel = getMapName(mapId); + if (mapLabel.empty()) mapLabel = "map #" + std::to_string(mapId); + static const char* kDiff[] = {"Normal","Heroic","25-Man","25-Man Heroic"}; + const char* diffStr = (difficulty < 4) ? kDiff[difficulty] : nullptr; + std::string msg = "Calendar: Raid lockout removed for " + mapLabel; + if (diffStr) msg += std::string(" (") + diffStr + ")"; + msg += '.'; + addSystemChatMessage(msg); + LOG_DEBUG("SMSG_CALENDAR_RAID_LOCKOUT_REMOVED: mapId=", mapId, + " difficulty=", difficulty); + } + packet.skipAll(); + }; + // uint32 unixTime — server's current unix timestamp; use to sync gameTime_ + dispatchTable_[Opcode::SMSG_SERVERTIME] = [this](network::Packet& packet) { + // uint32 unixTime — server's current unix timestamp; use to sync gameTime_ + if (packet.hasRemaining(4)) { + uint32_t srvTime = packet.readUInt32(); + if (srvTime > 0) { + gameTime_ = static_cast(srvTime); + LOG_DEBUG("SMSG_SERVERTIME: serverTime=", srvTime); + } + } + }; + // uint64 kickerGuid + uint32 kickReasonType + null-terminated reason string + // kickReasonType: 0=other, 1=afk, 2=vote kick + dispatchTable_[Opcode::SMSG_KICK_REASON] = [this](network::Packet& packet) { + // uint64 kickerGuid + uint32 kickReasonType + null-terminated reason string + // kickReasonType: 0=other, 1=afk, 2=vote kick + if (!packet.hasRemaining(12)) { + packet.skipAll(); + return; + } + uint64_t kickerGuid = packet.readUInt64(); + uint32_t reasonType = packet.readUInt32(); + std::string reason; + if (packet.hasData()) + reason = packet.readString(); + (void)kickerGuid; + (void)reasonType; + std::string msg = "You have been removed from the group."; + if (!reason.empty()) + msg = "You have been removed from the group: " + reason; + else if (reasonType == 1) + msg = "You have been removed from the group for being AFK."; + else if (reasonType == 2) + msg = "You have been removed from the group by vote."; + addSystemChatMessage(msg); + addUIError(msg); + LOG_INFO("SMSG_KICK_REASON: reasonType=", reasonType, + " reason='", reason, "'"); + }; + // uint32 throttleMs — rate-limited group action; notify the player + dispatchTable_[Opcode::SMSG_GROUPACTION_THROTTLED] = [this](network::Packet& packet) { + // uint32 throttleMs — rate-limited group action; notify the player + if (packet.hasRemaining(4)) { + uint32_t throttleMs = packet.readUInt32(); + char buf[128]; + if (throttleMs > 0) { + std::snprintf(buf, sizeof(buf), + "Group action throttled. Please wait %.1f seconds.", + throttleMs / 1000.0f); + } else { + std::snprintf(buf, sizeof(buf), "Group action throttled."); + } + addSystemChatMessage(buf); + LOG_DEBUG("SMSG_GROUPACTION_THROTTLED: throttleMs=", throttleMs); + } + }; + // WotLK 3.3.5a: uint32 ticketId + string subject + string body + uint32 count + // per count: string responseText + dispatchTable_[Opcode::SMSG_GMRESPONSE_RECEIVED] = [this](network::Packet& packet) { + // WotLK 3.3.5a: uint32 ticketId + string subject + string body + uint32 count + // per count: string responseText + if (!packet.hasRemaining(4)) { + packet.skipAll(); + return; + } + uint32_t ticketId = packet.readUInt32(); + std::string subject; + std::string body; + if (packet.hasData()) subject = packet.readString(); + if (packet.hasData()) body = packet.readString(); + uint32_t responseCount = 0; + if (packet.hasRemaining(4)) + responseCount = packet.readUInt32(); + std::string responseText; + for (uint32_t i = 0; i < responseCount && i < 10; ++i) { + if (packet.hasData()) { + std::string t = packet.readString(); + if (i == 0) responseText = t; + } + } + (void)ticketId; + std::string msg; + if (!responseText.empty()) + msg = "[GM Response] " + responseText; + else if (!body.empty()) + msg = "[GM Response] " + body; + else if (!subject.empty()) + msg = "[GM Response] " + subject; + else + msg = "[GM Response] Your ticket has been answered."; + addSystemChatMessage(msg); + addUIError(msg); + LOG_INFO("SMSG_GMRESPONSE_RECEIVED: ticketId=", ticketId, + " subject='", subject, "'"); + }; + // uint32 ticketId + uint8 status (1=open, 2=surveyed, 3=need_more_help) + dispatchTable_[Opcode::SMSG_GMRESPONSE_STATUS_UPDATE] = [this](network::Packet& packet) { + // uint32 ticketId + uint8 status (1=open, 2=surveyed, 3=need_more_help) + if (packet.hasRemaining(5)) { + uint32_t ticketId = packet.readUInt32(); + uint8_t status = packet.readUInt8(); + const char* statusStr = (status == 1) ? "open" + : (status == 2) ? "answered" + : (status == 3) ? "needs more info" + : "updated"; + char buf[128]; + std::snprintf(buf, sizeof(buf), + "[GM Ticket #%u] Status: %s.", ticketId, statusStr); + addSystemChatMessage(buf); + LOG_DEBUG("SMSG_GMRESPONSE_STATUS_UPDATE: ticketId=", ticketId, + " status=", static_cast(status)); + } + }; + // GM ticket status (new/updated); no ticket UI yet + registerSkipHandler(Opcode::SMSG_GM_TICKET_STATUS_UPDATE); + // Client uses this outbound; treat inbound variant as no-op for robustness. + registerSkipHandler(Opcode::MSG_MOVE_WORLDPORT_ACK); + // Observed custom server packet (8 bytes). Safe-consume for now. + registerSkipHandler(Opcode::MSG_MOVE_TIME_SKIPPED); + // loggingOut_ already cleared by cancelLogout(); this is server's confirmation + registerSkipHandler(Opcode::SMSG_LOGOUT_CANCEL_ACK); + // These packets are not damage-shield events. Consume them without + // synthesizing reflected damage entries or misattributing GUIDs. + registerSkipHandler(Opcode::SMSG_AURACASTLOG); + // These packets are not damage-shield events. Consume them without + // synthesizing reflected damage entries or misattributing GUIDs. + registerSkipHandler(Opcode::SMSG_SPELLBREAKLOG); + // Consume silently — informational, no UI action needed + registerSkipHandler(Opcode::SMSG_ITEM_REFUND_INFO_RESPONSE); + // Consume silently — informational, no UI action needed + registerSkipHandler(Opcode::SMSG_LOOT_LIST); + // Same format as LOCKOUT_ADDED; consume + registerSkipHandler(Opcode::SMSG_CALENDAR_RAID_LOCKOUT_UPDATED); + // Consume — remaining server notifications not yet parsed + for (auto op : { + Opcode::SMSG_AFK_MONITOR_INFO_RESPONSE, + Opcode::SMSG_AUCTION_LIST_PENDING_SALES, + Opcode::SMSG_AVAILABLE_VOICE_CHANNEL, + Opcode::SMSG_CALENDAR_ARENA_TEAM, + Opcode::SMSG_CALENDAR_CLEAR_PENDING_ACTION, + Opcode::SMSG_CALENDAR_EVENT_INVITE, + Opcode::SMSG_CALENDAR_EVENT_INVITE_NOTES, + Opcode::SMSG_CALENDAR_EVENT_INVITE_NOTES_ALERT, + Opcode::SMSG_CALENDAR_EVENT_INVITE_REMOVED, + Opcode::SMSG_CALENDAR_EVENT_INVITE_REMOVED_ALERT, + Opcode::SMSG_CALENDAR_EVENT_INVITE_STATUS_ALERT, + Opcode::SMSG_CALENDAR_EVENT_MODERATOR_STATUS_ALERT, + Opcode::SMSG_CALENDAR_EVENT_REMOVED_ALERT, + Opcode::SMSG_CALENDAR_EVENT_UPDATED_ALERT, + Opcode::SMSG_CALENDAR_FILTER_GUILD, + Opcode::SMSG_CALENDAR_SEND_CALENDAR, + Opcode::SMSG_CALENDAR_SEND_EVENT, + Opcode::SMSG_CHEAT_DUMP_ITEMS_DEBUG_ONLY_RESPONSE, + Opcode::SMSG_CHEAT_DUMP_ITEMS_DEBUG_ONLY_RESPONSE_WRITE_FILE, + Opcode::SMSG_CHEAT_PLAYER_LOOKUP, + Opcode::SMSG_CHECK_FOR_BOTS, + Opcode::SMSG_COMMENTATOR_GET_PLAYER_INFO, + Opcode::SMSG_COMMENTATOR_MAP_INFO, + Opcode::SMSG_COMMENTATOR_PLAYER_INFO, + Opcode::SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT1, + Opcode::SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT2, + Opcode::SMSG_COMMENTATOR_STATE_CHANGED, + Opcode::SMSG_COOLDOWN_CHEAT, + Opcode::SMSG_DANCE_QUERY_RESPONSE, + Opcode::SMSG_DBLOOKUP, + Opcode::SMSG_DEBUGAURAPROC, + Opcode::SMSG_DEBUG_AISTATE, + Opcode::SMSG_DEBUG_LIST_TARGETS, + Opcode::SMSG_DEBUG_SERVER_GEO, + Opcode::SMSG_DUMP_OBJECTS_DATA, + Opcode::SMSG_FORCEACTIONSHOW, + Opcode::SMSG_GM_PLAYER_INFO, + Opcode::SMSG_GODMODE, + Opcode::SMSG_IGNORE_DIMINISHING_RETURNS_CHEAT, + Opcode::SMSG_IGNORE_REQUIREMENTS_CHEAT, + Opcode::SMSG_INVALIDATE_DANCE, + Opcode::SMSG_LFG_PENDING_INVITE, + Opcode::SMSG_LFG_PENDING_MATCH, + Opcode::SMSG_LFG_PENDING_MATCH_DONE, + Opcode::SMSG_LFG_UPDATE, + Opcode::SMSG_LFG_UPDATE_LFG, + Opcode::SMSG_LFG_UPDATE_LFM, + Opcode::SMSG_LFG_UPDATE_QUEUED, + Opcode::SMSG_MOVE_CHARACTER_CHEAT, + Opcode::SMSG_NOTIFY_DANCE, + Opcode::SMSG_NOTIFY_DEST_LOC_SPELL_CAST, + Opcode::SMSG_PETGODMODE, + Opcode::SMSG_PET_UPDATE_COMBO_POINTS, + Opcode::SMSG_PLAYER_SKINNED, + Opcode::SMSG_PLAY_DANCE, + Opcode::SMSG_PROFILEDATA_RESPONSE, + Opcode::SMSG_PVP_QUEUE_STATS, + Opcode::SMSG_QUERY_OBJECT_POSITION, + Opcode::SMSG_QUERY_OBJECT_ROTATION, + Opcode::SMSG_REDIRECT_CLIENT, + Opcode::SMSG_RESET_RANGED_COMBAT_TIMER, + Opcode::SMSG_SEND_ALL_COMBAT_LOG, + Opcode::SMSG_SET_EXTRA_AURA_INFO_NEED_UPDATE, + Opcode::SMSG_SET_PLAYER_DECLINED_NAMES_RESULT, + Opcode::SMSG_SET_PROJECTILE_POSITION, + Opcode::SMSG_SPELL_CHANCE_RESIST_PUSHBACK, + Opcode::SMSG_SPELL_UPDATE_CHAIN_TARGETS, + Opcode::SMSG_STOP_DANCE, + Opcode::SMSG_TEST_DROP_RATE_RESULT, + Opcode::SMSG_UPDATE_ACCOUNT_DATA, + Opcode::SMSG_UPDATE_ACCOUNT_DATA_COMPLETE, + Opcode::SMSG_UPDATE_INSTANCE_OWNERSHIP, + Opcode::SMSG_UPDATE_LAST_INSTANCE, + Opcode::SMSG_VOICESESSION_FULL, + Opcode::SMSG_VOICE_CHAT_STATUS, + Opcode::SMSG_VOICE_PARENTAL_CONTROLS, + Opcode::SMSG_VOICE_SESSION_ADJUST_PRIORITY, + Opcode::SMSG_VOICE_SESSION_ENABLE, + Opcode::SMSG_VOICE_SESSION_LEAVE, + Opcode::SMSG_VOICE_SESSION_ROSTER_UPDATE, + Opcode::SMSG_VOICE_SET_TALKER_MUTED + }) { registerSkipHandler(op); } } void GameHandler::handlePacket(network::Packet& packet) { @@ -1292,9 +7718,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint16_t opcode = packet.getOpcode(); + try { - const bool allowVanillaAliases = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool allowVanillaAliases = isPreWotlk(); // Vanilla compatibility aliases: // - 0x006B: can be SMSG_COMPRESSED_MOVES on some vanilla-family servers @@ -1326,7 +7753,7 @@ void GameHandler::handlePacket(network::Packet& packet) { } // Expected weather payload: uint32 weatherType, float intensity, uint8 abrupt - if (packet.getSize() - packet.getReadPos() >= 9) { + if (packet.hasRemaining(9)) { uint32_t wType = packet.readUInt32(); float wIntensity = packet.readFloat(); uint8_t abrupt = packet.readUInt8(); @@ -1352,7 +7779,7 @@ void GameHandler::handlePacket(network::Packet& packet) { } } else if (allowVanillaAliases && opcode == 0x0103) { // Expected play-music payload: uint32 sound/music id - if (packet.getSize() - packet.getReadPos() == 4) { + if (packet.getRemainingSize() == 4) { uint32_t soundId = packet.readUInt32(); LOG_INFO("SMSG_PLAY_MUSIC (0x0103 alias): soundId=", soundId); if (playMusicCallback_) playMusicCallback_(soundId); @@ -1361,7 +7788,7 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (opcode == 0x0480) { // Observed on this WotLK profile immediately after CMSG_BUYBACK_ITEM. // Treat as vendor/buyback transaction result (7-byte payload on this core). - if (packet.getSize() - packet.getReadPos() >= 7) { + if (packet.hasRemaining(7)) { uint8_t opType = packet.readUInt8(); uint8_t resultCode = packet.readUInt8(); uint8_t slotOrCount = packet.readUInt8(); @@ -1422,7 +7849,7 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (opcode == 0x046A) { // Server-specific vendor/buyback state packet (observed 25-byte records). // Consume to keep stream aligned; currently not used for gameplay logic. - if (packet.getSize() - packet.getReadPos() >= 25) { + if (packet.hasRemaining(25)) { packet.setReadPos(packet.getReadPos() + 25); return; } @@ -1454,4817 +7881,38 @@ void GameHandler::handlePacket(network::Packet& packet) { return; } - switch (*logicalOp) { - case Opcode::SMSG_AUTH_CHALLENGE: - if (state == WorldState::CONNECTED) { - handleAuthChallenge(packet); - } else { - LOG_WARNING("Unexpected SMSG_AUTH_CHALLENGE in state: ", worldStateName(state)); + // Dispatch via the opcode handler table + auto it = dispatchTable_.find(*logicalOp); + if (it != dispatchTable_.end()) { + it->second(packet); + } else { + // In pre-world states we need full visibility (char create/login handshakes). + // In-world we keep de-duplication to avoid heavy log I/O in busy areas. + if (state != WorldState::IN_WORLD) { + static std::unordered_set loggedUnhandledByState; + const uint32_t key = (static_cast(static_cast(state)) << 16) | + static_cast(opcode); + if (loggedUnhandledByState.insert(key).second) { + LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec, + " state=", static_cast(state), + " size=", packet.getSize()); + const auto& data = packet.getData(); + std::string hex; + size_t limit = std::min(data.size(), 48); + hex.reserve(limit * 3); + for (size_t i = 0; i < limit; ++i) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", data[i]); + hex += b; + } + LOG_INFO("Unhandled opcode payload hex (first ", limit, " bytes): ", hex); + } + } else { + static std::unordered_set loggedUnhandledOpcodes; + if (loggedUnhandledOpcodes.insert(static_cast(opcode)).second) { + LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec); } - break; - - case Opcode::SMSG_AUTH_RESPONSE: - if (state == WorldState::AUTH_SENT) { - handleAuthResponse(packet); - } else { - LOG_WARNING("Unexpected SMSG_AUTH_RESPONSE in state: ", worldStateName(state)); - } - break; - - case Opcode::SMSG_CHAR_CREATE: - handleCharCreateResponse(packet); - break; - - case Opcode::SMSG_CHAR_DELETE: { - uint8_t result = packet.readUInt8(); - lastCharDeleteResult_ = result; - bool success = (result == 0x00 || result == 0x47); // Common success codes - LOG_INFO("SMSG_CHAR_DELETE result: ", (int)result, success ? " (success)" : " (failed)"); - requestCharacterList(); - if (charDeleteCallback_) charDeleteCallback_(success); - break; - } - - case Opcode::SMSG_CHAR_ENUM: - if (state == WorldState::CHAR_LIST_REQUESTED) { - handleCharEnum(packet); - } else { - LOG_WARNING("Unexpected SMSG_CHAR_ENUM in state: ", worldStateName(state)); - } - break; - - case Opcode::SMSG_CHARACTER_LOGIN_FAILED: - handleCharLoginFailed(packet); - break; - - case Opcode::SMSG_LOGIN_VERIFY_WORLD: - if (state == WorldState::ENTERING_WORLD || state == WorldState::IN_WORLD) { - handleLoginVerifyWorld(packet); - } else { - LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", worldStateName(state)); - } - break; - - case Opcode::SMSG_LOGIN_SETTIMESPEED: - // Can be received during login or at any time after - handleLoginSetTimeSpeed(packet); - break; - - case Opcode::SMSG_CLIENTCACHE_VERSION: - // Early pre-world packet in some realms (e.g. Warmane profile) - handleClientCacheVersion(packet); - break; - - case Opcode::SMSG_TUTORIAL_FLAGS: - // Often sent during char-list stage (8x uint32 tutorial flags) - handleTutorialFlags(packet); - break; - - case Opcode::SMSG_WARDEN_DATA: - handleWardenData(packet); - break; - - case Opcode::SMSG_ACCOUNT_DATA_TIMES: - // Can be received at any time after authentication - handleAccountDataTimes(packet); - break; - - case Opcode::SMSG_MOTD: - // Can be received at any time after entering world - handleMotd(packet); - break; - - case Opcode::SMSG_NOTIFICATION: - // Vanilla/Classic server notification (single string) - handleNotification(packet); - break; - - case Opcode::SMSG_PONG: - // Can be received at any time after entering world - handlePong(packet); - break; - - case Opcode::SMSG_UPDATE_OBJECT: - LOG_DEBUG("Received SMSG_UPDATE_OBJECT, state=", static_cast(state), " size=", packet.getSize()); - // Can be received after entering world - if (state == WorldState::IN_WORLD) { - handleUpdateObject(packet); - } - break; - - case Opcode::SMSG_COMPRESSED_UPDATE_OBJECT: - LOG_DEBUG("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast(state), " size=", packet.getSize()); - // Compressed version of UPDATE_OBJECT - if (state == WorldState::IN_WORLD) { - handleCompressedUpdateObject(packet); - } - break; - case Opcode::SMSG_DESTROY_OBJECT: - // Can be received after entering world - if (state == WorldState::IN_WORLD) { - handleDestroyObject(packet); - } - break; - - case Opcode::SMSG_MESSAGECHAT: - // Can be received after entering world - if (state == WorldState::IN_WORLD) { - handleMessageChat(packet); - } - break; - case Opcode::SMSG_GM_MESSAGECHAT: - // GM → player message: same wire format as SMSG_MESSAGECHAT - if (state == WorldState::IN_WORLD) { - handleMessageChat(packet); - } - break; - - case Opcode::SMSG_TEXT_EMOTE: - if (state == WorldState::IN_WORLD) { - handleTextEmote(packet); - } - break; - case Opcode::SMSG_EMOTE: { - if (state != WorldState::IN_WORLD) break; - // SMSG_EMOTE: uint32 emoteAnim, uint64 sourceGuid - if (packet.getSize() - packet.getReadPos() < 12) break; - uint32_t emoteAnim = packet.readUInt32(); - uint64_t sourceGuid = packet.readUInt64(); - if (emoteAnimCallback_ && sourceGuid != 0) { - emoteAnimCallback_(sourceGuid, emoteAnim); - } - break; - } - - case Opcode::SMSG_CHANNEL_NOTIFY: - // Accept during ENTERING_WORLD too — server auto-joins channels before VERIFY_WORLD - if (state == WorldState::IN_WORLD || state == WorldState::ENTERING_WORLD) { - handleChannelNotify(packet); - } - break; - case Opcode::SMSG_CHAT_PLAYER_NOT_FOUND: { - // string: name of the player not found (for failed whispers) - std::string name = packet.readString(); - if (!name.empty()) { - addSystemChatMessage("No player named '" + name + "' is currently playing."); - } - break; - } - case Opcode::SMSG_CHAT_PLAYER_AMBIGUOUS: { - // string: ambiguous player name (multiple matches) - std::string name = packet.readString(); - if (!name.empty()) { - addSystemChatMessage("Player name '" + name + "' is ambiguous."); - } - break; - } - case Opcode::SMSG_CHAT_WRONG_FACTION: - addSystemChatMessage("You cannot send messages to members of that faction."); - break; - case Opcode::SMSG_CHAT_NOT_IN_PARTY: - addSystemChatMessage("You are not in a party."); - break; - case Opcode::SMSG_CHAT_RESTRICTED: - addSystemChatMessage("You cannot send chat messages in this area."); - break; - - case Opcode::SMSG_QUERY_TIME_RESPONSE: - if (state == WorldState::IN_WORLD) { - handleQueryTimeResponse(packet); - } - break; - - case Opcode::SMSG_PLAYED_TIME: - if (state == WorldState::IN_WORLD) { - handlePlayedTime(packet); - } - break; - - case Opcode::SMSG_WHO: - if (state == WorldState::IN_WORLD) { - handleWho(packet); - } - break; - - case Opcode::SMSG_FRIEND_STATUS: - if (state == WorldState::IN_WORLD) { - handleFriendStatus(packet); - } - break; - case Opcode::SMSG_CONTACT_LIST: - handleContactList(packet); - break; - case Opcode::SMSG_FRIEND_LIST: - // Classic 1.12 and TBC friend list (WotLK uses SMSG_CONTACT_LIST instead) - handleFriendList(packet); - break; - case Opcode::SMSG_IGNORE_LIST: { - // uint8 count + count × (uint64 guid + string name) - // Populate ignoreCache so /unignore works for pre-existing ignores. - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t ignCount = packet.readUInt8(); - for (uint8_t i = 0; i < ignCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 8) break; - uint64_t ignGuid = packet.readUInt64(); - std::string ignName = packet.readString(); - if (!ignName.empty() && ignGuid != 0) { - ignoreCache[ignName] = ignGuid; - } - } - LOG_DEBUG("SMSG_IGNORE_LIST: loaded ", (int)ignCount, " ignored players"); - break; - } - - case Opcode::MSG_RANDOM_ROLL: - if (state == WorldState::IN_WORLD) { - handleRandomRoll(packet); - } - break; - case Opcode::SMSG_ITEM_PUSH_RESULT: { - // Item received notification (loot, quest reward, trade, etc.) - // guid(8) + received(1) + created(1) + showInChat(1) + bagSlot(1) + itemSlot(4) - // + itemId(4) + itemSuffixFactor(4) + randomPropertyId(4) + count(4) + totalCount(4) - constexpr size_t kMinSize = 8 + 1 + 1 + 1 + 1 + 4 + 4 + 4 + 4 + 4 + 4; - if (packet.getSize() - packet.getReadPos() >= kMinSize) { - /*uint64_t recipientGuid =*/ packet.readUInt64(); - /*uint8_t received =*/ packet.readUInt8(); // 0=looted/generated, 1=received from trade - /*uint8_t created =*/ packet.readUInt8(); // 0=stack added, 1=new item slot - uint8_t showInChat = packet.readUInt8(); - /*uint8_t bagSlot =*/ packet.readUInt8(); - /*uint32_t itemSlot =*/ packet.readUInt32(); - uint32_t itemId = packet.readUInt32(); - /*uint32_t suffixFactor =*/ packet.readUInt32(); - /*int32_t randomProp =*/ static_cast(packet.readUInt32()); - uint32_t count = packet.readUInt32(); - /*uint32_t totalCount =*/ packet.readUInt32(); - - queryItemInfo(itemId, 0); - if (showInChat) { - std::string itemName = "item #" + std::to_string(itemId); - if (const ItemQueryResponseData* info = getItemInfo(itemId)) { - if (!info->name.empty()) itemName = info->name; - } - std::string msg = "Received: " + itemName; - if (count > 1) msg += " x" + std::to_string(count); - addSystemChatMessage(msg); - } - LOG_INFO("Item push: itemId=", itemId, " count=", count, - " showInChat=", static_cast(showInChat)); - } - break; - } - - case Opcode::SMSG_LOGOUT_RESPONSE: - handleLogoutResponse(packet); - break; - - case Opcode::SMSG_LOGOUT_COMPLETE: - handleLogoutComplete(packet); - break; - - // ---- Phase 1: Foundation ---- - case Opcode::SMSG_NAME_QUERY_RESPONSE: - handleNameQueryResponse(packet); - break; - - case Opcode::SMSG_CREATURE_QUERY_RESPONSE: - handleCreatureQueryResponse(packet); - break; - - case Opcode::SMSG_ITEM_QUERY_SINGLE_RESPONSE: - handleItemQueryResponse(packet); - break; - - case Opcode::SMSG_INSPECT_TALENT: - handleInspectResults(packet); - break; - case Opcode::SMSG_ADDON_INFO: - case Opcode::SMSG_EXPECTED_SPAM_RECORDS: - // Optional system payloads that are safe to consume. - packet.setReadPos(packet.getSize()); - break; - - // ---- XP ---- - case Opcode::SMSG_LOG_XPGAIN: - handleXpGain(packet); - break; - case Opcode::SMSG_EXPLORATION_EXPERIENCE: { - // uint32 areaId + uint32 xpGained - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t areaId = packet.readUInt32(); - uint32_t xpGained = packet.readUInt32(); - if (xpGained > 0) { - std::string areaName = getAreaName(areaId); - std::string msg; - if (!areaName.empty()) { - msg = "Discovered " + areaName + "! Gained " + - std::to_string(xpGained) + " experience."; - } else { - char buf[128]; - std::snprintf(buf, sizeof(buf), - "Discovered new area! Gained %u experience.", xpGained); - msg = buf; - } - addSystemChatMessage(msg); - // XP is updated via PLAYER_XP update fields from the server. - } - } - break; - } - case Opcode::SMSG_PET_TAME_FAILURE: { - // uint8 reason: 0=invalid_creature, 1=too_many_pets, 2=already_tamed, etc. - const char* reasons[] = { - "Invalid creature", "Too many pets", "Already tamed", - "Wrong faction", "Level too low", "Creature not tameable", - "Can't control", "Can't command" - }; - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t reason = packet.readUInt8(); - const char* msg = (reason < 8) ? reasons[reason] : "Unknown reason"; - std::string s = std::string("Failed to tame: ") + msg; - addSystemChatMessage(s); - } - break; - } - case Opcode::SMSG_PET_ACTION_FEEDBACK: { - // uint8 action + uint8 flags - packet.setReadPos(packet.getSize()); // Consume; no UI for pet feedback yet. - break; - } - case Opcode::SMSG_PET_NAME_QUERY_RESPONSE: { - // uint32 petNumber + string name + uint32 timestamp + bool declined - packet.setReadPos(packet.getSize()); // Consume; pet names shown via unit objects. - break; - } - case Opcode::SMSG_QUESTUPDATE_FAILED: { - // uint32 questId - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t questId = packet.readUInt32(); - char buf[128]; - std::snprintf(buf, sizeof(buf), "Quest %u failed!", questId); - addSystemChatMessage(buf); - } - break; - } - case Opcode::SMSG_QUESTUPDATE_FAILEDTIMER: { - // uint32 questId - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t questId = packet.readUInt32(); - char buf[128]; - std::snprintf(buf, sizeof(buf), "Quest %u timed out!", questId); - addSystemChatMessage(buf); - } - break; - } - - // ---- Entity health/power delta updates ---- - case Opcode::SMSG_HEALTH_UPDATE: { - // WotLK: packed_guid + uint32 health - // TBC: full uint64 + uint32 health - // Classic/Vanilla: packed_guid + uint32 health (same as WotLK) - const bool huTbc = isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (huTbc ? 8u : 2u)) break; - uint64_t guid = huTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t hp = packet.readUInt32(); - auto entity = entityManager.getEntity(guid); - if (auto* unit = dynamic_cast(entity.get())) { - unit->setHealth(hp); - } - break; - } - case Opcode::SMSG_POWER_UPDATE: { - // WotLK: packed_guid + uint8 powerType + uint32 value - // TBC: full uint64 + uint8 powerType + uint32 value - // Classic/Vanilla: packed_guid + uint8 powerType + uint32 value (same as WotLK) - const bool puTbc = isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (puTbc ? 8u : 2u)) break; - uint64_t guid = puTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 5) break; - uint8_t powerType = packet.readUInt8(); - uint32_t value = packet.readUInt32(); - auto entity = entityManager.getEntity(guid); - if (auto* unit = dynamic_cast(entity.get())) { - unit->setPowerByType(powerType, value); - } - break; - } - - // ---- World state single update ---- - case Opcode::SMSG_UPDATE_WORLD_STATE: { - // uint32 field + uint32 value - if (packet.getSize() - packet.getReadPos() < 8) break; - uint32_t field = packet.readUInt32(); - uint32_t value = packet.readUInt32(); - worldStates_[field] = value; - LOG_DEBUG("SMSG_UPDATE_WORLD_STATE: field=", field, " value=", value); - break; - } - case Opcode::SMSG_WORLD_STATE_UI_TIMER_UPDATE: { - // uint32 time (server unix timestamp) — used to sync UI timers (arena, BG) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t serverTime = packet.readUInt32(); - LOG_DEBUG("SMSG_WORLD_STATE_UI_TIMER_UPDATE: serverTime=", serverTime); - } - break; - } - case Opcode::SMSG_PVP_CREDIT: { - // uint32 honorPoints + uint64 victimGuid + uint32 victimRank - if (packet.getSize() - packet.getReadPos() >= 16) { - uint32_t honor = packet.readUInt32(); - uint64_t victimGuid = packet.readUInt64(); - uint32_t rank = packet.readUInt32(); - LOG_INFO("SMSG_PVP_CREDIT: honor=", honor, " victim=0x", std::hex, victimGuid, - std::dec, " rank=", rank); - std::string msg = "You gain " + std::to_string(honor) + " honor points."; - addSystemChatMessage(msg); - } - break; - } - - // ---- Combo points ---- - case Opcode::SMSG_UPDATE_COMBO_POINTS: { - // WotLK: packed_guid (target) + uint8 points - // TBC: full uint64 (target) + uint8 points - // Classic/Vanilla: packed_guid (target) + uint8 points (same as WotLK) - const bool cpTbc = isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (cpTbc ? 8u : 2u)) break; - uint64_t target = cpTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 1) break; - comboPoints_ = packet.readUInt8(); - comboTarget_ = target; - LOG_DEBUG("SMSG_UPDATE_COMBO_POINTS: target=0x", std::hex, target, - std::dec, " points=", static_cast(comboPoints_)); - break; - } - - // ---- Mirror timers (breath/fatigue/feign death) ---- - case Opcode::SMSG_START_MIRROR_TIMER: { - // uint32 type + int32 value + int32 maxValue + int32 scale + uint32 tracker + uint8 paused - if (packet.getSize() - packet.getReadPos() < 21) break; - uint32_t type = packet.readUInt32(); - int32_t value = static_cast(packet.readUInt32()); - int32_t maxV = static_cast(packet.readUInt32()); - int32_t scale = static_cast(packet.readUInt32()); - /*uint32_t tracker =*/ packet.readUInt32(); - uint8_t paused = packet.readUInt8(); - if (type < 3) { - mirrorTimers_[type].value = value; - mirrorTimers_[type].maxValue = maxV; - mirrorTimers_[type].scale = scale; - mirrorTimers_[type].paused = (paused != 0); - mirrorTimers_[type].active = true; - } - break; - } - case Opcode::SMSG_STOP_MIRROR_TIMER: { - // uint32 type - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t type = packet.readUInt32(); - if (type < 3) { - mirrorTimers_[type].active = false; - mirrorTimers_[type].value = 0; - } - break; - } - case Opcode::SMSG_PAUSE_MIRROR_TIMER: { - // uint32 type + uint8 paused - if (packet.getSize() - packet.getReadPos() < 5) break; - uint32_t type = packet.readUInt32(); - uint8_t paused = packet.readUInt8(); - if (type < 3) { - mirrorTimers_[type].paused = (paused != 0); - } - break; - } - - // ---- Cast result (WotLK extended cast failed) ---- - case Opcode::SMSG_CAST_RESULT: { - // WotLK: castCount(u8) + spellId(u32) + result(u8) - // TBC/Classic: spellId(u32) + result(u8) (no castCount prefix) - // If result == 0, the spell successfully began; otherwise treat like SMSG_CAST_FAILED. - uint32_t castResultSpellId = 0; - uint8_t castResult = 0; - if (packetParsers_->parseCastResult(packet, castResultSpellId, castResult)) { - if (castResult != 0) { - casting = false; - castIsChannel = false; - currentCastSpellId = 0; - castTimeRemaining = 0.0f; - // Pass player's power type so result 85 says "Not enough rage/energy/etc." - int playerPowerType = -1; - if (auto pe = entityManager.getEntity(playerGuid)) { - if (auto pu = std::dynamic_pointer_cast(pe)) - playerPowerType = static_cast(pu->getPowerType()); - } - const char* reason = getSpellCastResultString(castResult, playerPowerType); - std::string errMsg = reason ? reason - : ("Spell cast failed (error " + std::to_string(castResult) + ")"); - addUIError(errMsg); - MessageChatData msg; - msg.type = ChatType::SYSTEM; - msg.language = ChatLanguage::UNIVERSAL; - msg.message = errMsg; - addLocalChatMessage(msg); - } - } - break; - } - - // ---- Spell failed on another unit ---- - case Opcode::SMSG_SPELL_FAILED_OTHER: { - // WotLK: packed_guid + uint8 castCount + uint32 spellId + uint8 reason - // TBC/Classic: full uint64 + uint8 castCount + uint32 spellId + uint8 reason - const bool tbcLike2 = isClassicLikeExpansion() || isActiveExpansion("tbc"); - uint64_t failOtherGuid = tbcLike2 - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) - : UpdateObjectParser::readPackedGuid(packet); - if (failOtherGuid != 0 && failOtherGuid != playerGuid) { - unitCastStates_.erase(failOtherGuid); - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Spell proc resist log ---- - case Opcode::SMSG_PROCRESIST: { - // WotLK: packed_guid caster + packed_guid victim + uint32 spellId + ... - // TBC/Classic: uint64 caster + uint64 victim + uint32 spellId + ... - const bool prTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - auto readPrGuid = [&]() -> uint64_t { - if (prTbcLike) - return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; - return UpdateObjectParser::readPackedGuid(packet); - }; - if (packet.getSize() - packet.getReadPos() < (prTbcLike ? 8u : 1u)) break; - /*uint64_t caster =*/ readPrGuid(); - if (packet.getSize() - packet.getReadPos() < (prTbcLike ? 8u : 1u)) break; - uint64_t victim = readPrGuid(); - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t spellId = packet.readUInt32(); - if (victim == playerGuid) - addCombatText(CombatTextEntry::RESIST, 0, spellId, false); - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Loot start roll (Need/Greed popup trigger) ---- - case Opcode::SMSG_LOOT_START_ROLL: { - // WotLK 3.3.5a: uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId - // + uint32 randomSuffix + uint32 randomPropId + uint32 countdown + uint8 voteMask (33 bytes) - // Classic/TBC: uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId - // + uint32 countdown + uint8 voteMask (25 bytes) - const bool isWotLK = isActiveExpansion("wotlk"); - const size_t minSize = isWotLK ? 33u : 25u; - if (packet.getSize() - packet.getReadPos() < minSize) break; - uint64_t objectGuid = packet.readUInt64(); - /*uint32_t mapId =*/ packet.readUInt32(); - uint32_t slot = packet.readUInt32(); - uint32_t itemId = packet.readUInt32(); - if (isWotLK) { - /*uint32_t randSuffix =*/ packet.readUInt32(); - /*uint32_t randProp =*/ packet.readUInt32(); - } - /*uint32_t countdown =*/ packet.readUInt32(); - /*uint8_t voteMask =*/ packet.readUInt8(); - // Trigger the roll popup for local player - pendingLootRollActive_ = true; - pendingLootRoll_.objectGuid = objectGuid; - pendingLootRoll_.slot = slot; - pendingLootRoll_.itemId = itemId; - auto* info = getItemInfo(itemId); - pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); - pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; - LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName, - ") slot=", slot); - break; - } - - // ---- Pet stable result ---- - case Opcode::SMSG_STABLE_RESULT: { - // uint8 result - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t result = packet.readUInt8(); - const char* msg = nullptr; - switch (result) { - case 0x01: msg = "Pet stored in stable."; break; - case 0x06: msg = "Pet retrieved from stable."; break; - case 0x07: msg = "Stable slot purchased."; break; - case 0x08: msg = "Stable list updated."; break; - case 0x09: msg = "Stable failed: not enough money or other error."; break; - default: break; - } - if (msg) addSystemChatMessage(msg); - LOG_INFO("SMSG_STABLE_RESULT: result=", static_cast(result)); - break; - } - - // ---- Title earned ---- - case Opcode::SMSG_TITLE_EARNED: { - // uint32 titleBitIndex + uint32 isLost - if (packet.getSize() - packet.getReadPos() < 8) break; - uint32_t titleBit = packet.readUInt32(); - uint32_t isLost = packet.readUInt32(); - char buf[128]; - std::snprintf(buf, sizeof(buf), - isLost ? "Title removed (ID %u)." : "Title earned (ID %u)!", - titleBit); - addSystemChatMessage(buf); - LOG_INFO("SMSG_TITLE_EARNED: id=", titleBit, " lost=", isLost); - break; - } - - case Opcode::SMSG_LEARNED_DANCE_MOVES: - // Contains bitmask of learned dance moves — cosmetic only, no gameplay effect. - LOG_DEBUG("SMSG_LEARNED_DANCE_MOVES: ignored (size=", packet.getSize(), ")"); - break; - - // ---- Hearthstone binding ---- - case Opcode::SMSG_PLAYERBOUND: { - // uint64 binderGuid + uint32 mapId + uint32 zoneId - if (packet.getSize() - packet.getReadPos() < 16) break; - /*uint64_t binderGuid =*/ packet.readUInt64(); - uint32_t mapId = packet.readUInt32(); - uint32_t zoneId = packet.readUInt32(); - char buf[128]; - std::snprintf(buf, sizeof(buf), - "Your home location has been set (map %u, zone %u).", mapId, zoneId); - addSystemChatMessage(buf); - break; - } - case Opcode::SMSG_BINDER_CONFIRM: { - // uint64 npcGuid — server confirming bind point has been set - addSystemChatMessage("This innkeeper is now your home location."); - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Phase shift (WotLK phasing) ---- - case Opcode::SMSG_SET_PHASE_SHIFT: { - // uint32 phaseFlags [+ packed guid + uint16 count + repeated uint16 phaseIds] - // Just consume; phasing doesn't require action from client in WotLK - packet.setReadPos(packet.getSize()); - break; - } - - // ---- XP gain toggle ---- - case Opcode::SMSG_TOGGLE_XP_GAIN: { - // uint8 enabled - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t enabled = packet.readUInt8(); - addSystemChatMessage(enabled ? "XP gain enabled." : "XP gain disabled."); - break; - } - - // ---- Gossip POI (quest map markers) ---- - case Opcode::SMSG_GOSSIP_POI: { - // uint32 flags + float x + float y + uint32 icon + uint32 data + string name - if (packet.getSize() - packet.getReadPos() < 20) break; - /*uint32_t flags =*/ packet.readUInt32(); - float poiX = packet.readFloat(); // WoW canonical coords - float poiY = packet.readFloat(); - uint32_t icon = packet.readUInt32(); - uint32_t data = packet.readUInt32(); - std::string name = packet.readString(); - GossipPoi poi; - poi.x = poiX; - poi.y = poiY; - poi.icon = icon; - poi.data = data; - poi.name = std::move(name); - gossipPois_.push_back(std::move(poi)); - LOG_DEBUG("SMSG_GOSSIP_POI: x=", poiX, " y=", poiY, " icon=", icon); - break; - } - - // ---- Character service results ---- - case Opcode::SMSG_CHAR_RENAME: { - // uint32 result (0=success) + uint64 guid + string newName - if (packet.getSize() - packet.getReadPos() >= 13) { - uint32_t result = packet.readUInt32(); - /*uint64_t guid =*/ packet.readUInt64(); - std::string newName = packet.readString(); - if (result == 0) { - addSystemChatMessage("Character name changed to: " + newName); - } else { - addSystemChatMessage("Character rename failed (error " + std::to_string(result) + ")."); - } - LOG_INFO("SMSG_CHAR_RENAME: result=", result, " newName=", newName); - } - break; - } - case Opcode::SMSG_BINDZONEREPLY: { - // uint32 result (0=success, 1=too far) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t result = packet.readUInt32(); - if (result == 0) { - addSystemChatMessage("Your home is now set to this location."); - } else { - addSystemChatMessage("You are too far from the innkeeper."); - } - } - break; - } - case Opcode::SMSG_CHANGEPLAYER_DIFFICULTY_RESULT: { - // uint32 result - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t result = packet.readUInt32(); - if (result == 0) { - addSystemChatMessage("Difficulty changed."); - } else { - static const char* reasons[] = { - "", "Error", "Too many members", "Already in dungeon", - "You are in a battleground", "Raid not allowed in heroic", - "You must be in a raid group", "Player not in group" - }; - const char* msg = (result < 8) ? reasons[result] : "Difficulty change failed."; - addSystemChatMessage(std::string("Cannot change difficulty: ") + msg); - } - } - break; - } - case Opcode::SMSG_CORPSE_NOT_IN_INSTANCE: - addSystemChatMessage("Your corpse is outside this instance. Release spirit to retrieve it."); - break; - case Opcode::SMSG_CROSSED_INEBRIATION_THRESHOLD: { - // uint64 playerGuid + uint32 threshold - if (packet.getSize() - packet.getReadPos() >= 12) { - uint64_t guid = packet.readUInt64(); - uint32_t threshold = packet.readUInt32(); - if (guid == playerGuid && threshold > 0) { - addSystemChatMessage("You feel rather drunk."); - } - LOG_DEBUG("SMSG_CROSSED_INEBRIATION_THRESHOLD: guid=0x", std::hex, guid, - std::dec, " threshold=", threshold); - } - break; - } - case Opcode::SMSG_CLEAR_FAR_SIGHT_IMMEDIATE: - // Far sight cancelled; viewport returns to player camera - LOG_DEBUG("SMSG_CLEAR_FAR_SIGHT_IMMEDIATE"); - break; - case Opcode::SMSG_COMBAT_EVENT_FAILED: - // Combat event could not be executed (e.g. invalid target for special ability) - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_FORCE_ANIM: { - // packed_guid + uint32 animId — force entity to play animation - if (packet.getSize() - packet.getReadPos() >= 1) { - uint64_t animGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t animId = packet.readUInt32(); - if (emoteAnimCallback_) - emoteAnimCallback_(animGuid, animId); - } - } - break; - } - case Opcode::SMSG_GAMEOBJECT_DESPAWN_ANIM: - case Opcode::SMSG_GAMEOBJECT_RESET_STATE: - case Opcode::SMSG_FLIGHT_SPLINE_SYNC: - case Opcode::SMSG_FORCE_DISPLAY_UPDATE: - case Opcode::SMSG_FORCE_SEND_QUEUED_PACKETS: - case Opcode::SMSG_FORCE_SET_VEHICLE_REC_ID: - case Opcode::SMSG_CORPSE_MAP_POSITION_QUERY_RESPONSE: - case Opcode::SMSG_DAMAGE_CALC_LOG: - case Opcode::SMSG_DYNAMIC_DROP_ROLL_RESULT: - case Opcode::SMSG_DESTRUCTIBLE_BUILDING_DAMAGE: - case Opcode::SMSG_FORCED_DEATH_UPDATE: - // Consume — handled by broader object update or not yet implemented - packet.setReadPos(packet.getSize()); - break; - - // ---- Zone defense messages ---- - case Opcode::SMSG_DEFENSE_MESSAGE: { - // uint32 zoneId + string message — used for PvP zone attack alerts - if (packet.getSize() - packet.getReadPos() >= 5) { - /*uint32_t zoneId =*/ packet.readUInt32(); - std::string defMsg = packet.readString(); - if (!defMsg.empty()) { - addSystemChatMessage("[Defense] " + defMsg); - } - } - break; - } - case Opcode::SMSG_CORPSE_RECLAIM_DELAY: { - // uint32 delayMs before player can reclaim corpse - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t delayMs = packet.readUInt32(); - uint32_t delaySec = (delayMs + 999) / 1000; - addSystemChatMessage("You can reclaim your corpse in " + - std::to_string(delaySec) + " seconds."); - LOG_DEBUG("SMSG_CORPSE_RECLAIM_DELAY: ", delayMs, "ms"); - } - break; - } - case Opcode::SMSG_DEATH_RELEASE_LOC: { - // uint32 mapId + float x + float y + float z — corpse/spirit healer position - if (packet.getSize() - packet.getReadPos() >= 16) { - corpseMapId_ = packet.readUInt32(); - corpseX_ = packet.readFloat(); - corpseY_ = packet.readFloat(); - corpseZ_ = packet.readFloat(); - LOG_INFO("SMSG_DEATH_RELEASE_LOC: map=", corpseMapId_, - " x=", corpseX_, " y=", corpseY_, " z=", corpseZ_); - } - break; - } - case Opcode::SMSG_ENABLE_BARBER_SHOP: - // Sent by server when player sits in barber chair — triggers barber shop UI - // No payload; we don't have barber shop UI yet, so just log - LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); - break; - case Opcode::SMSG_FEIGN_DEATH_RESISTED: - addUIError("Your Feign Death was resisted."); - addSystemChatMessage("Your Feign Death attempt was resisted."); - LOG_DEBUG("SMSG_FEIGN_DEATH_RESISTED"); - break; - case Opcode::SMSG_CHANNEL_MEMBER_COUNT: { - // string channelName + uint8 flags + uint32 memberCount - std::string chanName = packet.readString(); - if (packet.getSize() - packet.getReadPos() >= 5) { - /*uint8_t flags =*/ packet.readUInt8(); - uint32_t count = packet.readUInt32(); - LOG_DEBUG("SMSG_CHANNEL_MEMBER_COUNT: channel=", chanName, " members=", count); - } - break; - } - case Opcode::SMSG_GAMETIME_SET: - case Opcode::SMSG_GAMETIME_UPDATE: - // Server time correction: uint32 gameTimePacked (seconds since epoch) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t gameTimePacked = packet.readUInt32(); - gameTime_ = static_cast(gameTimePacked); - LOG_DEBUG("Server game time update: ", gameTime_, "s"); - } - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_GAMESPEED_SET: - // Server speed correction: uint32 gameTimePacked + float timeSpeed - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t gameTimePacked = packet.readUInt32(); - float timeSpeed = packet.readFloat(); - gameTime_ = static_cast(gameTimePacked); - timeSpeed_ = timeSpeed; - LOG_DEBUG("Server game speed update: time=", gameTime_, " speed=", timeSpeed_); - } - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_GAMETIMEBIAS_SET: - // Time bias — consume without processing - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_ACHIEVEMENT_DELETED: - case Opcode::SMSG_CRITERIA_DELETED: - // Consume achievement/criteria removal notifications - packet.setReadPos(packet.getSize()); - break; - - // ---- Combat clearing ---- - case Opcode::SMSG_ATTACKSWING_DEADTARGET: - // Target died mid-swing: clear auto-attack - autoAttacking = false; - autoAttackTarget = 0; - break; - case Opcode::SMSG_THREAT_CLEAR: - // All threat dropped on the local player (e.g. Vanish, Feign Death) - threatLists_.clear(); - LOG_DEBUG("SMSG_THREAT_CLEAR: threat wiped"); - break; - case Opcode::SMSG_THREAT_REMOVE: { - // packed_guid (unit) + packed_guid (victim whose threat was removed) - if (packet.getSize() - packet.getReadPos() < 1) break; - uint64_t unitGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 1) break; - uint64_t victimGuid = UpdateObjectParser::readPackedGuid(packet); - auto it = threatLists_.find(unitGuid); - if (it != threatLists_.end()) { - auto& list = it->second; - list.erase(std::remove_if(list.begin(), list.end(), - [victimGuid](const ThreatEntry& e){ return e.victimGuid == victimGuid; }), - list.end()); - if (list.empty()) threatLists_.erase(it); - } - break; - } - case Opcode::SMSG_HIGHEST_THREAT_UPDATE: - case Opcode::SMSG_THREAT_UPDATE: { - // Both packets share the same format: - // packed_guid (unit) + packed_guid (highest-threat target or target, unused here) - // + uint32 count + count × (packed_guid victim + uint32 threat) - if (packet.getSize() - packet.getReadPos() < 1) break; - uint64_t unitGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 1) break; - (void)UpdateObjectParser::readPackedGuid(packet); // highest-threat / current target - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t cnt = packet.readUInt32(); - if (cnt > 100) { packet.setReadPos(packet.getSize()); break; } // sanity - std::vector list; - list.reserve(cnt); - for (uint32_t i = 0; i < cnt; ++i) { - if (packet.getSize() - packet.getReadPos() < 1) break; - ThreatEntry entry; - entry.victimGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - entry.threat = packet.readUInt32(); - list.push_back(entry); - } - // Sort descending by threat so highest is first - std::sort(list.begin(), list.end(), - [](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; }); - threatLists_[unitGuid] = std::move(list); - break; - } - - case Opcode::SMSG_CANCEL_COMBAT: - // Server-side combat state reset - autoAttacking = false; - autoAttackTarget = 0; - autoAttackRequested_ = false; - break; - - case Opcode::SMSG_BREAK_TARGET: - // Server breaking our targeting (PvP flag, etc.) - // uint64 guid — consume; target cleared if it matches - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t bGuid = packet.readUInt64(); - if (bGuid == targetGuid) targetGuid = 0; - } - break; - - case Opcode::SMSG_CLEAR_TARGET: - // uint64 guid — server cleared targeting on a unit (or 0 = clear all) - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t cGuid = packet.readUInt64(); - if (cGuid == 0 || cGuid == targetGuid) targetGuid = 0; - } - break; - - // ---- Server-forced dismount ---- - case Opcode::SMSG_DISMOUNT: - // No payload — server forcing dismount - currentMountDisplayId_ = 0; - if (mountCallback_) mountCallback_(0); - break; - - case Opcode::SMSG_MOUNTRESULT: { - // uint32 result: 0=error, 1=invalid, 2=not in range, 3=already mounted, 4=ok - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t result = packet.readUInt32(); - if (result != 4) { - const char* msgs[] = { "Cannot mount here.", "Invalid mount spell.", "Too far away to mount.", "Already mounted." }; - addSystemChatMessage(result < 4 ? msgs[result] : "Cannot mount."); - } - break; - } - case Opcode::SMSG_DISMOUNTRESULT: { - // uint32 result: 0=ok, others=error - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t result = packet.readUInt32(); - if (result != 0) addSystemChatMessage("Cannot dismount here."); - break; - } - - // ---- Loot notifications ---- - case Opcode::SMSG_LOOT_ALL_PASSED: { - // WotLK 3.3.5a: uint64 objectGuid + uint32 slot + uint32 itemId + uint32 randSuffix + uint32 randPropId (24 bytes) - // Classic/TBC: uint64 objectGuid + uint32 slot + uint32 itemId (16 bytes) - const bool isWotLK = isActiveExpansion("wotlk"); - const size_t minSize = isWotLK ? 24u : 16u; - if (packet.getSize() - packet.getReadPos() < minSize) break; - /*uint64_t objGuid =*/ packet.readUInt64(); - /*uint32_t slot =*/ packet.readUInt32(); - uint32_t itemId = packet.readUInt32(); - if (isWotLK) { - /*uint32_t randSuffix =*/ packet.readUInt32(); - /*uint32_t randProp =*/ packet.readUInt32(); - } - auto* info = getItemInfo(itemId); - char buf[256]; - std::snprintf(buf, sizeof(buf), "Everyone passed on [%s].", - info ? info->name.c_str() : std::to_string(itemId).c_str()); - addSystemChatMessage(buf); - pendingLootRollActive_ = false; - break; - } - case Opcode::SMSG_LOOT_ITEM_NOTIFY: { - // uint64 looterGuid + uint64 lootGuid + uint32 itemId + uint32 count - if (packet.getSize() - packet.getReadPos() < 24) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t looterGuid = packet.readUInt64(); - /*uint64_t lootGuid =*/ packet.readUInt64(); - uint32_t itemId = packet.readUInt32(); - uint32_t count = packet.readUInt32(); - // Show loot message for party members (not the player — SMSG_ITEM_PUSH_RESULT covers that) - if (isInGroup() && looterGuid != playerGuid) { - auto nit = playerNameCache.find(looterGuid); - std::string looterName = (nit != playerNameCache.end()) ? nit->second : ""; - if (!looterName.empty()) { - queryItemInfo(itemId, 0); - std::string itemName = "item #" + std::to_string(itemId); - if (const ItemQueryResponseData* info = getItemInfo(itemId)) { - if (!info->name.empty()) itemName = info->name; - } - char buf[256]; - if (count > 1) - std::snprintf(buf, sizeof(buf), "%s loots %s x%u.", looterName.c_str(), itemName.c_str(), count); - else - std::snprintf(buf, sizeof(buf), "%s loots %s.", looterName.c_str(), itemName.c_str()); - addSystemChatMessage(buf); - } - } - break; - } - case Opcode::SMSG_LOOT_SLOT_CHANGED: - // uint64 objectGuid + uint32 slot + ... — consume - packet.setReadPos(packet.getSize()); - break; - - // ---- Spell log miss ---- - case Opcode::SMSG_SPELLLOGMISS: { - // All expansions: uint32 spellId first. - // WotLK: spellId(4) + packed_guid caster + uint8 unk + uint32 count - // + count × (packed_guid victim + uint8 missInfo) - // [missInfo==11(REFLECT): + uint32 reflectSpellId + uint8 reflectResult] - // TBC/Classic: spellId(4) + uint64 caster + uint8 unk + uint32 count - // + count × (uint64 victim + uint8 missInfo) - const bool spellMissTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - auto readSpellMissGuid = [&]() -> uint64_t { - if (spellMissTbcLike) - return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; - return UpdateObjectParser::readPackedGuid(packet); - }; - // spellId prefix present in all expansions - if (packet.getSize() - packet.getReadPos() < 4) break; - /*uint32_t spellId =*/ packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < (spellMissTbcLike ? 8 : 1)) break; - uint64_t casterGuid = readSpellMissGuid(); - if (packet.getSize() - packet.getReadPos() < 5) break; - /*uint8_t unk =*/ packet.readUInt8(); - uint32_t count = packet.readUInt32(); - count = std::min(count, 32u); - for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < (spellMissTbcLike ? 9u : 2u)) break; - /*uint64_t victimGuid =*/ readSpellMissGuid(); - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t missInfo = packet.readUInt8(); - // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult - if (missInfo == 11 && !spellMissTbcLike) { - if (packet.getSize() - packet.getReadPos() >= 5) { - /*uint32_t reflectSpellId =*/ packet.readUInt32(); - /*uint8_t reflectResult =*/ packet.readUInt8(); - } else { - packet.setReadPos(packet.getSize()); - break; - } - } - // Show combat text only for local player's spell misses - if (casterGuid == playerGuid) { - static const CombatTextEntry::Type missTypes[] = { - CombatTextEntry::MISS, // 0=MISS - CombatTextEntry::DODGE, // 1=DODGE - CombatTextEntry::PARRY, // 2=PARRY - CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::MISS, // 4=EVADE - CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::MISS, // 6=DEFLECT - CombatTextEntry::ABSORB, // 7=ABSORB - CombatTextEntry::RESIST, // 8=RESIST - }; - CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] : CombatTextEntry::MISS; - addCombatText(ct, 0, 0, true); - } - } - break; - } - - // ---- Environmental damage log ---- - case Opcode::SMSG_ENVIRONMENTALDAMAGELOG: { - // uint64 victimGuid + uint8 envDamageType + uint32 damage + uint32 absorb + uint32 resist - if (packet.getSize() - packet.getReadPos() < 21) break; - uint64_t victimGuid = packet.readUInt64(); - /*uint8_t envType =*/ packet.readUInt8(); - uint32_t damage = packet.readUInt32(); - uint32_t absorb = packet.readUInt32(); - uint32_t resist = packet.readUInt32(); - if (victimGuid == playerGuid) { - if (damage > 0) - addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast(damage), 0, false); - if (absorb > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(absorb), 0, false); - if (resist > 0) - addCombatText(CombatTextEntry::RESIST, static_cast(resist), 0, false); - } - break; - } - - // ---- Creature Movement ---- - case Opcode::SMSG_MONSTER_MOVE: - handleMonsterMove(packet); - break; - - case Opcode::SMSG_COMPRESSED_MOVES: - handleCompressedMoves(packet); - break; - - case Opcode::SMSG_MONSTER_MOVE_TRANSPORT: - handleMonsterMoveTransport(packet); - break; - case Opcode::SMSG_SPLINE_MOVE_FEATHER_FALL: - case Opcode::SMSG_SPLINE_MOVE_GRAVITY_DISABLE: - case Opcode::SMSG_SPLINE_MOVE_GRAVITY_ENABLE: - case Opcode::SMSG_SPLINE_MOVE_LAND_WALK: - case Opcode::SMSG_SPLINE_MOVE_NORMAL_FALL: - case Opcode::SMSG_SPLINE_MOVE_ROOT: - case Opcode::SMSG_SPLINE_MOVE_SET_HOVER: { - // Minimal parse: PackedGuid only — no animation-relevant state change. - if (packet.getSize() - packet.getReadPos() >= 1) { - (void)UpdateObjectParser::readPackedGuid(packet); - } - break; - } - case Opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE: - case Opcode::SMSG_SPLINE_MOVE_SET_RUN_MODE: - case Opcode::SMSG_SPLINE_MOVE_SET_FLYING: - case Opcode::SMSG_SPLINE_MOVE_START_SWIM: - case Opcode::SMSG_SPLINE_MOVE_STOP_SWIM: { - // PackedGuid + synthesised move-flags → drives animation state in application layer. - // SWIMMING=0x00200000, WALKING=0x00000100, CAN_FLY=0x00800000, FLYING=0x01000000 - if (packet.getSize() - packet.getReadPos() < 1) break; - uint64_t guid = UpdateObjectParser::readPackedGuid(packet); - if (guid == 0 || guid == playerGuid || !unitMoveFlagsCallback_) break; - uint32_t synthFlags = 0; - if (*logicalOp == Opcode::SMSG_SPLINE_MOVE_START_SWIM) - synthFlags = 0x00200000u; // SWIMMING - else if (*logicalOp == Opcode::SMSG_SPLINE_MOVE_SET_WALK_MODE) - synthFlags = 0x00000100u; // WALKING - else if (*logicalOp == Opcode::SMSG_SPLINE_MOVE_SET_FLYING) - synthFlags = 0x01000000u | 0x00800000u; // FLYING | CAN_FLY - // STOP_SWIM and SET_RUN_MODE: synthFlags stays 0 → clears swim/walk - unitMoveFlagsCallback_(guid, synthFlags); - break; - } - case Opcode::SMSG_SPLINE_SET_RUN_SPEED: - case Opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED: - case Opcode::SMSG_SPLINE_SET_SWIM_SPEED: { - // Minimal parse: PackedGuid + float speed - if (packet.getSize() - packet.getReadPos() < 5) break; - uint64_t guid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - float speed = packet.readFloat(); - if (guid == playerGuid && std::isfinite(speed) && speed > 0.01f && speed < 200.0f) { - if (*logicalOp == Opcode::SMSG_SPLINE_SET_RUN_SPEED) - serverRunSpeed_ = speed; - else if (*logicalOp == Opcode::SMSG_SPLINE_SET_RUN_BACK_SPEED) - serverRunBackSpeed_ = speed; - else if (*logicalOp == Opcode::SMSG_SPLINE_SET_SWIM_SPEED) - serverSwimSpeed_ = speed; - } - break; - } - - // ---- Speed Changes ---- - case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE: - handleForceRunSpeedChange(packet); - break; - case Opcode::SMSG_FORCE_MOVE_ROOT: - handleForceMoveRootState(packet, true); - break; - case Opcode::SMSG_FORCE_MOVE_UNROOT: - handleForceMoveRootState(packet, false); - break; - - // ---- Other force speed changes ---- - case Opcode::SMSG_FORCE_WALK_SPEED_CHANGE: - handleForceSpeedChange(packet, "WALK_SPEED", Opcode::CMSG_FORCE_WALK_SPEED_CHANGE_ACK, &serverWalkSpeed_); - break; - case Opcode::SMSG_FORCE_RUN_BACK_SPEED_CHANGE: - handleForceSpeedChange(packet, "RUN_BACK_SPEED", Opcode::CMSG_FORCE_RUN_BACK_SPEED_CHANGE_ACK, &serverRunBackSpeed_); - break; - case Opcode::SMSG_FORCE_SWIM_SPEED_CHANGE: - handleForceSpeedChange(packet, "SWIM_SPEED", Opcode::CMSG_FORCE_SWIM_SPEED_CHANGE_ACK, &serverSwimSpeed_); - break; - case Opcode::SMSG_FORCE_SWIM_BACK_SPEED_CHANGE: - handleForceSpeedChange(packet, "SWIM_BACK_SPEED", Opcode::CMSG_FORCE_SWIM_BACK_SPEED_CHANGE_ACK, &serverSwimBackSpeed_); - break; - case Opcode::SMSG_FORCE_FLIGHT_SPEED_CHANGE: - handleForceSpeedChange(packet, "FLIGHT_SPEED", Opcode::CMSG_FORCE_FLIGHT_SPEED_CHANGE_ACK, &serverFlightSpeed_); - break; - case Opcode::SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE: - handleForceSpeedChange(packet, "FLIGHT_BACK_SPEED", Opcode::CMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE_ACK, &serverFlightBackSpeed_); - break; - case Opcode::SMSG_FORCE_TURN_RATE_CHANGE: - handleForceSpeedChange(packet, "TURN_RATE", Opcode::CMSG_FORCE_TURN_RATE_CHANGE_ACK, &serverTurnRate_); - break; - case Opcode::SMSG_FORCE_PITCH_RATE_CHANGE: - handleForceSpeedChange(packet, "PITCH_RATE", Opcode::CMSG_FORCE_PITCH_RATE_CHANGE_ACK, &serverPitchRate_); - break; - - // ---- Movement flag toggle ACKs ---- - case Opcode::SMSG_MOVE_SET_CAN_FLY: - handleForceMoveFlagChange(packet, "SET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK, - static_cast(MovementFlags::CAN_FLY), true); - break; - case Opcode::SMSG_MOVE_UNSET_CAN_FLY: - handleForceMoveFlagChange(packet, "UNSET_CAN_FLY", Opcode::CMSG_MOVE_SET_CAN_FLY_ACK, - static_cast(MovementFlags::CAN_FLY), false); - break; - case Opcode::SMSG_MOVE_FEATHER_FALL: - handleForceMoveFlagChange(packet, "FEATHER_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK, - static_cast(MovementFlags::FEATHER_FALL), true); - break; - case Opcode::SMSG_MOVE_WATER_WALK: - handleForceMoveFlagChange(packet, "WATER_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK, - static_cast(MovementFlags::WATER_WALK), true); - break; - case Opcode::SMSG_MOVE_SET_HOVER: - handleForceMoveFlagChange(packet, "SET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK, - static_cast(MovementFlags::HOVER), true); - break; - case Opcode::SMSG_MOVE_UNSET_HOVER: - handleForceMoveFlagChange(packet, "UNSET_HOVER", Opcode::CMSG_MOVE_HOVER_ACK, - static_cast(MovementFlags::HOVER), false); - break; - - // ---- Knockback ---- - case Opcode::SMSG_MOVE_KNOCK_BACK: - handleMoveKnockBack(packet); - break; - - case Opcode::SMSG_CLIENT_CONTROL_UPDATE: { - // Minimal parse: PackedGuid + uint8 allowMovement. - if (packet.getSize() - packet.getReadPos() < 2) { - LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE too short: ", packet.getSize(), " bytes"); - break; - } - uint8_t guidMask = packet.readUInt8(); - size_t guidBytes = 0; - uint64_t controlGuid = 0; - for (int i = 0; i < 8; ++i) { - if (guidMask & (1u << i)) ++guidBytes; - } - if (packet.getSize() - packet.getReadPos() < guidBytes + 1) { - LOG_WARNING("SMSG_CLIENT_CONTROL_UPDATE malformed (truncated packed guid)"); - packet.setReadPos(packet.getSize()); - break; - } - for (int i = 0; i < 8; ++i) { - if (guidMask & (1u << i)) { - uint8_t b = packet.readUInt8(); - controlGuid |= (static_cast(b) << (i * 8)); - } - } - bool allowMovement = (packet.readUInt8() != 0); - if (controlGuid == 0 || controlGuid == playerGuid) { - bool changed = (serverMovementAllowed_ != allowMovement); - serverMovementAllowed_ = allowMovement; - if (changed && !allowMovement) { - // Force-stop local movement immediately when server revokes control. - movementInfo.flags &= ~(static_cast(MovementFlags::FORWARD) | - static_cast(MovementFlags::BACKWARD) | - static_cast(MovementFlags::STRAFE_LEFT) | - static_cast(MovementFlags::STRAFE_RIGHT) | - static_cast(MovementFlags::TURN_LEFT) | - static_cast(MovementFlags::TURN_RIGHT)); - sendMovement(Opcode::MSG_MOVE_STOP); - sendMovement(Opcode::MSG_MOVE_STOP_STRAFE); - sendMovement(Opcode::MSG_MOVE_STOP_TURN); - sendMovement(Opcode::MSG_MOVE_STOP_SWIM); - addSystemChatMessage("Movement disabled by server."); - } else if (changed && allowMovement) { - addSystemChatMessage("Movement re-enabled."); - } - } - break; - } - - // ---- Phase 2: Combat ---- - case Opcode::SMSG_ATTACKSTART: - handleAttackStart(packet); - break; - case Opcode::SMSG_ATTACKSTOP: - handleAttackStop(packet); - break; - case Opcode::SMSG_ATTACKSWING_NOTINRANGE: - autoAttackOutOfRange_ = true; - if (autoAttackRangeWarnCooldown_ <= 0.0f) { - addSystemChatMessage("Target is too far away."); - autoAttackRangeWarnCooldown_ = 1.25f; - } - if (autoAttackRequested_ && autoAttackTarget != 0 && socket) { - // Avoid blind immediate resend loops when target is clearly out of melee range. - bool likelyInRange = true; - if (auto target = entityManager.getEntity(autoAttackTarget)) { - float dx = movementInfo.x - target->getLatestX(); - float dy = movementInfo.y - target->getLatestY(); - float dz = movementInfo.z - target->getLatestZ(); - float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz); - likelyInRange = (dist3d <= 7.5f); - } - if (likelyInRange) { - auto pkt = AttackSwingPacket::build(autoAttackTarget); - socket->send(pkt); - } - } - break; - case Opcode::SMSG_ATTACKSWING_BADFACING: - if (autoAttackRequested_ && autoAttackTarget != 0) { - auto targetEntity = entityManager.getEntity(autoAttackTarget); - if (targetEntity) { - float toTargetX = targetEntity->getX() - movementInfo.x; - float toTargetY = targetEntity->getY() - movementInfo.y; - if (std::abs(toTargetX) > 0.01f || std::abs(toTargetY) > 0.01f) { - movementInfo.orientation = std::atan2(-toTargetY, toTargetX); - sendMovement(Opcode::MSG_MOVE_SET_FACING); - sendMovement(Opcode::MSG_MOVE_HEARTBEAT); - } - } - if (socket) { - auto pkt = AttackSwingPacket::build(autoAttackTarget); - socket->send(pkt); - } - } - break; - case Opcode::SMSG_ATTACKSWING_NOTSTANDING: - case Opcode::SMSG_ATTACKSWING_CANT_ATTACK: - autoAttackOutOfRange_ = false; - autoAttackOutOfRangeTime_ = 0.0f; - break; - case Opcode::SMSG_ATTACKERSTATEUPDATE: - handleAttackerStateUpdate(packet); - break; - case Opcode::SMSG_AI_REACTION: { - // SMSG_AI_REACTION: uint64 guid, uint32 reaction - if (packet.getSize() - packet.getReadPos() < 12) break; - uint64_t guid = packet.readUInt64(); - uint32_t reaction = packet.readUInt32(); - // Reaction 2 commonly indicates aggro. - if (reaction == 2 && npcAggroCallback_) { - auto entity = entityManager.getEntity(guid); - if (entity) { - npcAggroCallback_(guid, glm::vec3(entity->getX(), entity->getY(), entity->getZ())); - } - } - break; - } - case Opcode::SMSG_SPELLNONMELEEDAMAGELOG: - handleSpellDamageLog(packet); - break; - case Opcode::SMSG_PLAY_SPELL_VISUAL: { - // Minimal parse: uint64 casterGuid, uint32 visualId - if (packet.getSize() - packet.getReadPos() < 12) break; - packet.readUInt64(); - packet.readUInt32(); - break; - } - case Opcode::SMSG_SPELLHEALLOG: - handleSpellHealLog(packet); - break; - - // ---- Phase 3: Spells ---- - case Opcode::SMSG_INITIAL_SPELLS: - handleInitialSpells(packet); - break; - case Opcode::SMSG_CAST_FAILED: - handleCastFailed(packet); - break; - case Opcode::SMSG_SPELL_START: - handleSpellStart(packet); - break; - case Opcode::SMSG_SPELL_GO: - handleSpellGo(packet); - break; - case Opcode::SMSG_SPELL_FAILURE: { - // WotLK: packed_guid + uint8 castCount + uint32 spellId + uint8 failReason - // TBC: full uint64 + uint8 castCount + uint32 spellId + uint8 failReason - // Classic: full uint64 + uint32 spellId + uint8 failReason (NO castCount) - const bool isClassic = isClassicLikeExpansion(); - const bool isTbc = isActiveExpansion("tbc"); - uint64_t failGuid = (isClassic || isTbc) - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) - : UpdateObjectParser::readPackedGuid(packet); - // Classic omits the castCount byte; TBC and WotLK include it - const size_t remainingFields = isClassic ? 5u : 6u; // spellId(4)+reason(1) [+castCount(1)] - if (packet.getSize() - packet.getReadPos() >= remainingFields) { - if (!isClassic) /*uint8_t castCount =*/ packet.readUInt8(); - /*uint32_t spellId =*/ packet.readUInt32(); - uint8_t rawFailReason = packet.readUInt8(); - // Classic result enum starts at 0=AFFECTING_COMBAT; shift +1 for WotLK table - uint8_t failReason = isClassic ? static_cast(rawFailReason + 1) : rawFailReason; - if (failGuid == playerGuid && failReason != 0) { - // Show interruption/failure reason in chat and error overlay for player - int pt = -1; - if (auto pe = entityManager.getEntity(playerGuid)) - if (auto pu = std::dynamic_pointer_cast(pe)) - pt = static_cast(pu->getPowerType()); - const char* reason = getSpellCastResultString(failReason, pt); - if (reason) { - addUIError(reason); - MessageChatData emsg; - emsg.type = ChatType::SYSTEM; - emsg.language = ChatLanguage::UNIVERSAL; - emsg.message = reason; - addLocalChatMessage(emsg); - } - } - } - if (failGuid == playerGuid || failGuid == 0) { - // Player's own cast failed - casting = false; - castIsChannel = false; - currentCastSpellId = 0; - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ssm = renderer->getSpellSoundManager()) { - ssm->stopPrecast(); - } - } - if (spellCastAnimCallback_) { - spellCastAnimCallback_(playerGuid, false, false); - } - } else { - // Another unit's cast failed — clear their tracked cast bar - unitCastStates_.erase(failGuid); - if (spellCastAnimCallback_) { - spellCastAnimCallback_(failGuid, false, false); - } - } - break; - } - case Opcode::SMSG_SPELL_COOLDOWN: - handleSpellCooldown(packet); - break; - case Opcode::SMSG_COOLDOWN_EVENT: - handleCooldownEvent(packet); - break; - case Opcode::SMSG_CLEAR_COOLDOWN: { - // spellId(u32) + guid(u64): clear cooldown for the given spell/guid - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t spellId = packet.readUInt32(); - // guid is present but we only track per-spell for the local player - spellCooldowns.erase(spellId); - for (auto& slot : actionBar) { - if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) { - slot.cooldownRemaining = 0.0f; - } - } - LOG_DEBUG("SMSG_CLEAR_COOLDOWN: spellId=", spellId); - } - break; - } - case Opcode::SMSG_MODIFY_COOLDOWN: { - // spellId(u32) + diffMs(i32): adjust cooldown remaining by diffMs - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t spellId = packet.readUInt32(); - int32_t diffMs = static_cast(packet.readUInt32()); - float diffSec = diffMs / 1000.0f; - auto it = spellCooldowns.find(spellId); - if (it != spellCooldowns.end()) { - it->second = std::max(0.0f, it->second + diffSec); - for (auto& slot : actionBar) { - if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) { - slot.cooldownRemaining = std::max(0.0f, slot.cooldownRemaining + diffSec); - } - } - } - LOG_DEBUG("SMSG_MODIFY_COOLDOWN: spellId=", spellId, " diff=", diffMs, "ms"); - } - break; - } - case Opcode::SMSG_ACHIEVEMENT_EARNED: - handleAchievementEarned(packet); - break; - case Opcode::SMSG_ALL_ACHIEVEMENT_DATA: - handleAllAchievementData(packet); - break; - case Opcode::SMSG_ITEM_COOLDOWN: { - // uint64 itemGuid + uint32 spellId + uint32 cooldownMs - size_t rem = packet.getSize() - packet.getReadPos(); - if (rem >= 16) { - uint64_t itemGuid = packet.readUInt64(); - uint32_t spellId = packet.readUInt32(); - uint32_t cdMs = packet.readUInt32(); - float cdSec = cdMs / 1000.0f; - if (cdSec > 0.0f) { - if (spellId != 0) spellCooldowns[spellId] = cdSec; - // Resolve itemId from the GUID so item-type slots are also updated - uint32_t itemId = 0; - auto iit = onlineItems_.find(itemGuid); - if (iit != onlineItems_.end()) itemId = iit->second.entry; - for (auto& slot : actionBar) { - bool match = (spellId != 0 && slot.type == ActionBarSlot::SPELL && slot.id == spellId) - || (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId); - if (match) { - slot.cooldownTotal = cdSec; - slot.cooldownRemaining = cdSec; - } - } - LOG_DEBUG("SMSG_ITEM_COOLDOWN: itemGuid=0x", std::hex, itemGuid, std::dec, - " spellId=", spellId, " itemId=", itemId, " cd=", cdSec, "s"); - } - } - break; - } - case Opcode::SMSG_FISH_NOT_HOOKED: - addSystemChatMessage("Your fish got away."); - break; - case Opcode::SMSG_FISH_ESCAPED: - addSystemChatMessage("Your fish escaped!"); - break; - case Opcode::MSG_MINIMAP_PING: { - // WotLK: packed_guid + float posX + float posY - // TBC/Classic: uint64 + float posX + float posY - const bool mmTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (mmTbcLike ? 8u : 1u)) break; - uint64_t senderGuid = mmTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) break; - float pingX = packet.readFloat(); // server sends map-coord X (east-west) - float pingY = packet.readFloat(); // server sends map-coord Y (north-south) - MinimapPing ping; - ping.senderGuid = senderGuid; - ping.wowX = pingY; // canonical WoW X = north = server's posY - ping.wowY = pingX; // canonical WoW Y = west = server's posX - ping.age = 0.0f; - minimapPings_.push_back(ping); - break; - } - case Opcode::SMSG_ZONE_UNDER_ATTACK: { - // uint32 areaId - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t areaId = packet.readUInt32(); - std::string areaName = getAreaName(areaId); - std::string msg = areaName.empty() - ? std::string("A zone is under attack!") - : (areaName + " is under attack!"); - addSystemChatMessage(msg); - } - break; - } - case Opcode::SMSG_CANCEL_AUTO_REPEAT: - break; // Server signals to stop a repeating spell (wand/shoot); no client action needed - case Opcode::SMSG_AURA_UPDATE: - handleAuraUpdate(packet, false); - break; - case Opcode::SMSG_AURA_UPDATE_ALL: - handleAuraUpdate(packet, true); - break; - case Opcode::SMSG_DISPEL_FAILED: { - // WotLK: uint32 dispelSpellId + packed_guid caster + packed_guid victim - // [+ count × uint32 failedSpellId] - // TBC/Classic: uint64 caster + uint64 victim + uint32 spellId - // [+ count × uint32 failedSpellId] - const bool dispelTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - uint32_t dispelSpellId = 0; - if (dispelTbcLike) { - if (packet.getSize() - packet.getReadPos() < 20) break; - /*uint64_t caster =*/ packet.readUInt64(); - /*uint64_t victim =*/ packet.readUInt64(); - dispelSpellId = packet.readUInt32(); - } else { - if (packet.getSize() - packet.getReadPos() < 4) break; - dispelSpellId = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < 1) break; - /*uint64_t caster =*/ UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 1) break; - /*uint64_t victim =*/ UpdateObjectParser::readPackedGuid(packet); - } - { - loadSpellNameCache(); - auto it = spellNameCache_.find(dispelSpellId); - char buf[128]; - if (it != spellNameCache_.end() && !it->second.name.empty()) - std::snprintf(buf, sizeof(buf), "%s failed to dispel.", it->second.name.c_str()); - else - std::snprintf(buf, sizeof(buf), "Dispel failed! (spell %u)", dispelSpellId); - addSystemChatMessage(buf); - } - break; - } - case Opcode::SMSG_TOTEM_CREATED: { - // WotLK: uint8 slot + packed_guid + uint32 duration + uint32 spellId - // TBC/Classic: uint8 slot + uint64 guid + uint32 duration + uint32 spellId - const bool totemTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (totemTbcLike ? 17u : 9u)) break; - uint8_t slot = packet.readUInt8(); - if (totemTbcLike) - /*uint64_t guid =*/ packet.readUInt64(); - else - /*uint64_t guid =*/ UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) break; - uint32_t duration = packet.readUInt32(); - uint32_t spellId = packet.readUInt32(); - LOG_DEBUG("SMSG_TOTEM_CREATED: slot=", (int)slot, - " spellId=", spellId, " duration=", duration, "ms"); - break; - } - case Opcode::SMSG_AREA_SPIRIT_HEALER_TIME: { - // uint64 guid + uint32 timeLeftMs - if (packet.getSize() - packet.getReadPos() >= 12) { - /*uint64_t guid =*/ packet.readUInt64(); - uint32_t timeMs = packet.readUInt32(); - uint32_t secs = timeMs / 1000; - char buf[128]; - std::snprintf(buf, sizeof(buf), - "You will be able to resurrect in %u seconds.", secs); - addSystemChatMessage(buf); - } - break; - } - case Opcode::SMSG_DURABILITY_DAMAGE_DEATH: { - // uint32 percent (how much durability was lost due to death) - if (packet.getSize() - packet.getReadPos() >= 4) { - /*uint32_t pct =*/ packet.readUInt32(); - addSystemChatMessage("You have lost 10% of your gear's durability due to death."); - } - break; - } - case Opcode::SMSG_LEARNED_SPELL: - handleLearnedSpell(packet); - break; - case Opcode::SMSG_SUPERCEDED_SPELL: - handleSupercededSpell(packet); - break; - case Opcode::SMSG_REMOVED_SPELL: - handleRemovedSpell(packet); - break; - case Opcode::SMSG_SEND_UNLEARN_SPELLS: - handleUnlearnSpells(packet); - break; - - // ---- Talents ---- - case Opcode::SMSG_TALENTS_INFO: - handleTalentsInfo(packet); - break; - - // ---- Phase 4: Group ---- - case Opcode::SMSG_GROUP_INVITE: - handleGroupInvite(packet); - break; - case Opcode::SMSG_GROUP_DECLINE: - handleGroupDecline(packet); - break; - case Opcode::SMSG_GROUP_LIST: - handleGroupList(packet); - break; - case Opcode::SMSG_GROUP_DESTROYED: - // The group was disbanded; clear all party state. - partyData.members.clear(); - partyData.memberCount = 0; - partyData.leaderGuid = 0; - addSystemChatMessage("Your party has been disbanded."); - LOG_INFO("SMSG_GROUP_DESTROYED: party cleared"); - break; - case Opcode::SMSG_GROUP_CANCEL: - // Group invite was cancelled before being accepted. - addSystemChatMessage("Group invite cancelled."); - LOG_DEBUG("SMSG_GROUP_CANCEL"); - break; - case Opcode::SMSG_GROUP_UNINVITE: - handleGroupUninvite(packet); - break; - case Opcode::SMSG_PARTY_COMMAND_RESULT: - handlePartyCommandResult(packet); - break; - case Opcode::SMSG_PARTY_MEMBER_STATS: - handlePartyMemberStats(packet, false); - break; - case Opcode::SMSG_PARTY_MEMBER_STATS_FULL: - handlePartyMemberStats(packet, true); - break; - case Opcode::MSG_RAID_READY_CHECK: { - // Server is broadcasting a ready check (someone in the raid initiated it). - // Payload: empty body, or optional uint64 initiator GUID in some builds. - pendingReadyCheck_ = true; - readyCheckReadyCount_ = 0; - readyCheckNotReadyCount_ = 0; - readyCheckInitiator_.clear(); - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t initiatorGuid = packet.readUInt64(); - auto entity = entityManager.getEntity(initiatorGuid); - if (auto* unit = dynamic_cast(entity.get())) { - readyCheckInitiator_ = unit->getName(); - } - } - if (readyCheckInitiator_.empty() && partyData.leaderGuid != 0) { - // Identify initiator from party leader - for (const auto& member : partyData.members) { - if (member.guid == partyData.leaderGuid) { readyCheckInitiator_ = member.name; break; } - } - } - addSystemChatMessage(readyCheckInitiator_.empty() - ? "Ready check initiated!" - : readyCheckInitiator_ + " initiated a ready check!"); - LOG_INFO("MSG_RAID_READY_CHECK: initiator=", readyCheckInitiator_); - break; - } - case Opcode::MSG_RAID_READY_CHECK_CONFIRM: { - // guid (8) + uint8 isReady (0=not ready, 1=ready) - if (packet.getSize() - packet.getReadPos() < 9) { packet.setReadPos(packet.getSize()); break; } - uint64_t respGuid = packet.readUInt64(); - uint8_t isReady = packet.readUInt8(); - if (isReady) ++readyCheckReadyCount_; - else ++readyCheckNotReadyCount_; - auto nit = playerNameCache.find(respGuid); - std::string rname; - if (nit != playerNameCache.end()) rname = nit->second; - else { - auto ent = entityManager.getEntity(respGuid); - if (ent) rname = std::static_pointer_cast(ent)->getName(); - } - if (!rname.empty()) { - char rbuf[128]; - std::snprintf(rbuf, sizeof(rbuf), "%s is %s.", rname.c_str(), isReady ? "Ready" : "Not Ready"); - addSystemChatMessage(rbuf); - } - break; - } - case Opcode::MSG_RAID_READY_CHECK_FINISHED: { - // Ready check complete — summarize results - char fbuf[128]; - std::snprintf(fbuf, sizeof(fbuf), "Ready check complete: %u ready, %u not ready.", - readyCheckReadyCount_, readyCheckNotReadyCount_); - addSystemChatMessage(fbuf); - pendingReadyCheck_ = false; - readyCheckReadyCount_ = 0; - readyCheckNotReadyCount_ = 0; - break; - } - case Opcode::SMSG_RAID_INSTANCE_INFO: - handleRaidInstanceInfo(packet); - break; - case Opcode::SMSG_DUEL_REQUESTED: - handleDuelRequested(packet); - break; - case Opcode::SMSG_DUEL_COMPLETE: - handleDuelComplete(packet); - break; - case Opcode::SMSG_DUEL_WINNER: - handleDuelWinner(packet); - break; - case Opcode::SMSG_DUEL_OUTOFBOUNDS: - addUIError("You are out of the duel area!"); - addSystemChatMessage("You are out of the duel area!"); - break; - case Opcode::SMSG_DUEL_INBOUNDS: - // Re-entered the duel area; no special action needed. - break; - case Opcode::SMSG_DUEL_COUNTDOWN: - // Countdown timer — no action needed; server also sends UNIT_FIELD_FLAGS update. - break; - case Opcode::SMSG_PARTYKILLLOG: { - // uint64 killerGuid + uint64 victimGuid - if (packet.getSize() - packet.getReadPos() < 16) break; - uint64_t killerGuid = packet.readUInt64(); - uint64_t victimGuid = packet.readUInt64(); - // Show kill message in party chat style - auto nameForGuid = [&](uint64_t g) -> std::string { - // Check player name cache first - auto nit = playerNameCache.find(g); - if (nit != playerNameCache.end()) return nit->second; - // Fall back to entity name (NPCs) - auto ent = entityManager.getEntity(g); - if (ent && (ent->getType() == game::ObjectType::UNIT || - ent->getType() == game::ObjectType::PLAYER)) { - auto unit = std::static_pointer_cast(ent); - return unit->getName(); - } - return {}; - }; - std::string killerName = nameForGuid(killerGuid); - std::string victimName = nameForGuid(victimGuid); - if (!killerName.empty() && !victimName.empty()) { - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s killed %s.", - killerName.c_str(), victimName.c_str()); - addSystemChatMessage(buf); - } - break; - } - - // ---- Guild ---- - case Opcode::SMSG_GUILD_INFO: - handleGuildInfo(packet); - break; - case Opcode::SMSG_GUILD_ROSTER: - handleGuildRoster(packet); - break; - case Opcode::SMSG_GUILD_QUERY_RESPONSE: - handleGuildQueryResponse(packet); - break; - case Opcode::SMSG_GUILD_EVENT: - handleGuildEvent(packet); - break; - case Opcode::SMSG_GUILD_INVITE: - handleGuildInvite(packet); - break; - case Opcode::SMSG_GUILD_COMMAND_RESULT: - handleGuildCommandResult(packet); - break; - case Opcode::SMSG_PET_SPELLS: - handlePetSpells(packet); - break; - case Opcode::SMSG_PETITION_SHOWLIST: - handlePetitionShowlist(packet); - break; - case Opcode::SMSG_TURN_IN_PETITION_RESULTS: - handleTurnInPetitionResults(packet); - break; - - // ---- Phase 5: Loot/Gossip/Vendor ---- - case Opcode::SMSG_LOOT_RESPONSE: - handleLootResponse(packet); - break; - case Opcode::SMSG_LOOT_RELEASE_RESPONSE: - handleLootReleaseResponse(packet); - break; - case Opcode::SMSG_LOOT_REMOVED: - handleLootRemoved(packet); - break; - case Opcode::SMSG_QUEST_CONFIRM_ACCEPT: - handleQuestConfirmAccept(packet); - break; - case Opcode::SMSG_ITEM_TEXT_QUERY_RESPONSE: - handleItemTextQueryResponse(packet); - break; - case Opcode::SMSG_SUMMON_REQUEST: - handleSummonRequest(packet); - break; - case Opcode::SMSG_SUMMON_CANCEL: - pendingSummonRequest_ = false; - addSystemChatMessage("Summon cancelled."); - break; - case Opcode::SMSG_TRADE_STATUS: - handleTradeStatus(packet); - break; - case Opcode::SMSG_TRADE_STATUS_EXTENDED: - handleTradeStatusExtended(packet); - break; - case Opcode::SMSG_LOOT_ROLL: - handleLootRoll(packet); - break; - case Opcode::SMSG_LOOT_ROLL_WON: - handleLootRollWon(packet); - break; - case Opcode::SMSG_LOOT_MASTER_LIST: - // Master looter list — no UI yet; consume to avoid unhandled warning. - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_GOSSIP_MESSAGE: - handleGossipMessage(packet); - break; - case Opcode::SMSG_QUESTGIVER_QUEST_LIST: - handleQuestgiverQuestList(packet); - break; - case Opcode::SMSG_BINDPOINTUPDATE: { - BindPointUpdateData data; - if (BindPointUpdateParser::parse(packet, data)) { - LOG_INFO("Bindpoint updated: mapId=", data.mapId, - " pos=(", data.x, ", ", data.y, ", ", data.z, ")"); - glm::vec3 canonical = core::coords::serverToCanonical( - glm::vec3(data.x, data.y, data.z)); - // Only show message if bind point was already set (not initial login sync) - bool wasSet = hasHomeBind_; - hasHomeBind_ = true; - homeBindMapId_ = data.mapId; - homeBindPos_ = canonical; - if (bindPointCallback_) { - bindPointCallback_(data.mapId, canonical.x, canonical.y, canonical.z); - } - if (wasSet) { - addSystemChatMessage("Your home has been set."); - } - } else { - LOG_WARNING("Failed to parse SMSG_BINDPOINTUPDATE"); - } - break; - } - case Opcode::SMSG_GOSSIP_COMPLETE: - handleGossipComplete(packet); - break; - case Opcode::SMSG_SPIRIT_HEALER_CONFIRM: { - if (packet.getSize() - packet.getReadPos() < 8) { - LOG_WARNING("SMSG_SPIRIT_HEALER_CONFIRM too short"); - break; - } - uint64_t npcGuid = packet.readUInt64(); - LOG_INFO("Spirit healer confirm from 0x", std::hex, npcGuid, std::dec); - if (npcGuid) { - resurrectCasterGuid_ = npcGuid; - resurrectCasterName_ = ""; - resurrectIsSpiritHealer_ = true; - resurrectRequestPending_ = true; - } - break; - } - case Opcode::SMSG_RESURRECT_REQUEST: { - if (packet.getSize() - packet.getReadPos() < 8) { - LOG_WARNING("SMSG_RESURRECT_REQUEST too short"); - break; - } - uint64_t casterGuid = packet.readUInt64(); - // Optional caster name (CString, may be absent on some server builds) - std::string casterName; - if (packet.getReadPos() < packet.getSize()) { - casterName = packet.readString(); - } - LOG_INFO("Resurrect request from 0x", std::hex, casterGuid, std::dec, - " name='", casterName, "'"); - if (casterGuid) { - resurrectCasterGuid_ = casterGuid; - resurrectIsSpiritHealer_ = false; - if (!casterName.empty()) { - resurrectCasterName_ = casterName; - } else { - auto nit = playerNameCache.find(casterGuid); - resurrectCasterName_ = (nit != playerNameCache.end()) ? nit->second : ""; - } - resurrectRequestPending_ = true; - } - break; - } - case Opcode::SMSG_TIME_SYNC_REQ: { - if (packet.getSize() - packet.getReadPos() < 4) { - LOG_WARNING("SMSG_TIME_SYNC_REQ too short"); - break; - } - uint32_t counter = packet.readUInt32(); - LOG_DEBUG("Time sync request counter: ", counter); - if (socket) { - network::Packet resp(wireOpcode(Opcode::CMSG_TIME_SYNC_RESP)); - resp.writeUInt32(counter); - resp.writeUInt32(nextMovementTimestampMs()); - socket->send(resp); - } - break; - } - case Opcode::SMSG_LIST_INVENTORY: - handleListInventory(packet); - break; - case Opcode::SMSG_TRAINER_LIST: - handleTrainerList(packet); - break; - case Opcode::SMSG_TRAINER_BUY_SUCCEEDED: { - uint64_t guid = packet.readUInt64(); - uint32_t spellId = packet.readUInt32(); - (void)guid; - - // Add to known spells immediately for prerequisite re-evaluation - // (SMSG_LEARNED_SPELL may come separately, but we need immediate update) - if (!knownSpells.count(spellId)) { - knownSpells.insert(spellId); - LOG_INFO("Added spell ", spellId, " to known spells (trainer purchase)"); - } - - const std::string& name = getSpellName(spellId); - if (!name.empty()) - addSystemChatMessage("You have learned " + name + "."); - else - addSystemChatMessage("Spell learned."); - break; - } - case Opcode::SMSG_TRAINER_BUY_FAILED: { - // Server rejected the spell purchase - // Packet format: uint64 trainerGuid, uint32 spellId, uint32 errorCode - uint64_t trainerGuid = packet.readUInt64(); - uint32_t spellId = packet.readUInt32(); - uint32_t errorCode = 0; - if (packet.getSize() - packet.getReadPos() >= 4) { - errorCode = packet.readUInt32(); - } - LOG_WARNING("Trainer buy spell failed: guid=", trainerGuid, - " spellId=", spellId, " error=", errorCode); - - const std::string& spellName = getSpellName(spellId); - std::string msg = "Cannot learn "; - if (!spellName.empty()) msg += spellName; - else msg += "spell #" + std::to_string(spellId); - - // Common error reasons - if (errorCode == 0) msg += " (not enough money)"; - else if (errorCode == 1) msg += " (not enough skill)"; - else if (errorCode == 2) msg += " (already known)"; - else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")"; - - addSystemChatMessage(msg); - break; - } - - // Silently ignore common packets we don't handle yet - case Opcode::SMSG_INIT_WORLD_STATES: { - // WotLK format: uint32 mapId, uint32 zoneId, uint32 areaId, uint16 count, N*(uint32 key, uint32 val) - // Classic/TBC format: uint32 mapId, uint32 zoneId, uint16 count, N*(uint32 key, uint32 val) - if (packet.getSize() - packet.getReadPos() < 10) { - LOG_WARNING("SMSG_INIT_WORLD_STATES too short: ", packet.getSize(), " bytes"); - break; - } - worldStateMapId_ = packet.readUInt32(); - worldStateZoneId_ = packet.readUInt32(); - // WotLK adds areaId (uint32) before count; detect by checking if payload would be consistent - size_t remaining = packet.getSize() - packet.getReadPos(); - bool isWotLKFormat = isActiveExpansion("wotlk") || isActiveExpansion("turtle"); - if (isWotLKFormat && remaining >= 6) { - packet.readUInt32(); // areaId (WotLK only) - } - uint16_t count = packet.readUInt16(); - size_t needed = static_cast(count) * 8; - size_t available = packet.getSize() - packet.getReadPos(); - if (available < needed) { - // Be tolerant across expansion/private-core variants: if packet shape - // still looks like N*(key,val) dwords, parse what is present. - if ((available % 8) == 0) { - uint16_t adjustedCount = static_cast(available / 8); - LOG_WARNING("SMSG_INIT_WORLD_STATES count mismatch: header=", count, - " adjusted=", adjustedCount, " (available=", available, ")"); - count = adjustedCount; - needed = available; - } else { - LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed, - " bytes of state pairs, got ", available); - packet.setReadPos(packet.getSize()); - break; - } - } - worldStates_.clear(); - worldStates_.reserve(count); - for (uint16_t i = 0; i < count; ++i) { - uint32_t key = packet.readUInt32(); - uint32_t val = packet.readUInt32(); - worldStates_[key] = val; - } - break; - } - case Opcode::SMSG_INITIALIZE_FACTIONS: { - // Minimal parse: uint32 count, repeated (uint8 flags, int32 standing) - if (packet.getSize() - packet.getReadPos() < 4) { - LOG_WARNING("SMSG_INITIALIZE_FACTIONS too short: ", packet.getSize(), " bytes"); - break; - } - uint32_t count = packet.readUInt32(); - size_t needed = static_cast(count) * 5; - if (packet.getSize() - packet.getReadPos() < needed) { - LOG_WARNING("SMSG_INITIALIZE_FACTIONS truncated: expected ", needed, - " bytes of faction data, got ", packet.getSize() - packet.getReadPos()); - packet.setReadPos(packet.getSize()); - break; - } - initialFactions_.clear(); - initialFactions_.reserve(count); - for (uint32_t i = 0; i < count; ++i) { - FactionStandingInit fs{}; - fs.flags = packet.readUInt8(); - fs.standing = static_cast(packet.readUInt32()); - initialFactions_.push_back(fs); - } - break; - } - case Opcode::SMSG_SET_FACTION_STANDING: { - // uint8 showVisualEffect + uint32 count + count × (uint32 factionId + int32 standing) - if (packet.getSize() - packet.getReadPos() < 5) break; - /*uint8_t showVisual =*/ packet.readUInt8(); - uint32_t count = packet.readUInt32(); - count = std::min(count, 128u); - loadFactionNameCache(); - for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 8; ++i) { - uint32_t factionId = packet.readUInt32(); - int32_t standing = static_cast(packet.readUInt32()); - int32_t oldStanding = 0; - auto it = factionStandings_.find(factionId); - if (it != factionStandings_.end()) oldStanding = it->second; - factionStandings_[factionId] = standing; - int32_t delta = standing - oldStanding; - if (delta != 0) { - std::string name = getFactionName(factionId); - char buf[256]; - std::snprintf(buf, sizeof(buf), "Reputation with %s %s by %d.", - name.c_str(), - delta > 0 ? "increased" : "decreased", - std::abs(delta)); - addSystemChatMessage(buf); - if (repChangeCallback_) repChangeCallback_(name, delta, standing); - } - LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing); - } - break; - } - case Opcode::SMSG_SET_FACTION_ATWAR: - case Opcode::SMSG_SET_FACTION_VISIBLE: - // uint32 factionId [+ uint8 flags for ATWAR] — consume; hostility is tracked via update fields - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_FEATURE_SYSTEM_STATUS: - case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: - case Opcode::SMSG_SET_PCT_SPELL_MODIFIER: - // Different formats than SMSG_SPELL_DELAYED — consume and ignore - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_SPELL_DELAYED: { - // WotLK: packed_guid (caster) + uint32 delayMs - // TBC/Classic: uint64 (caster) + uint32 delayMs - const bool spellDelayTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (spellDelayTbcLike ? 8u : 1u)) break; - uint64_t caster = spellDelayTbcLike - ? packet.readUInt64() - : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t delayMs = packet.readUInt32(); - if (delayMs == 0) break; - float delaySec = delayMs / 1000.0f; - if (caster == playerGuid) { - if (casting) castTimeRemaining += delaySec; - } else { - auto it = unitCastStates_.find(caster); - if (it != unitCastStates_.end() && it->second.casting) { - it->second.timeRemaining += delaySec; - it->second.timeTotal += delaySec; - } - } - break; - } - case Opcode::SMSG_EQUIPMENT_SET_SAVED: - // uint32 setIndex + uint64 guid — equipment set was successfully saved - LOG_DEBUG("Equipment set saved"); - break; - case Opcode::SMSG_PERIODICAURALOG: { - // WotLK: packed_guid victim + packed_guid caster + uint32 spellId + uint32 count + effects - // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint32 count + effects - // Classic/Vanilla: packed_guid (same as WotLK) - const bool periodicTbc = isActiveExpansion("tbc"); - const size_t guidMinSz = periodicTbc ? 8u : 2u; - if (packet.getSize() - packet.getReadPos() < guidMinSz) break; - uint64_t victimGuid = periodicTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < guidMinSz) break; - uint64_t casterGuid = periodicTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) break; - uint32_t spellId = packet.readUInt32(); - uint32_t count = packet.readUInt32(); - bool isPlayerVictim = (victimGuid == playerGuid); - bool isPlayerCaster = (casterGuid == playerGuid); - if (!isPlayerVictim && !isPlayerCaster) { - packet.setReadPos(packet.getSize()); - break; - } - for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 1; ++i) { - uint8_t auraType = packet.readUInt8(); - if (auraType == 3 || auraType == 89) { - // Classic/TBC: damage(4)+school(4)+absorbed(4)+resisted(4) = 16 bytes - // WotLK 3.3.5a: damage(4)+overkill(4)+school(4)+absorbed(4)+resisted(4) = 20 bytes - const bool periodicWotlk = isActiveExpansion("wotlk"); - const size_t dotSz = periodicWotlk ? 20u : 16u; - if (packet.getSize() - packet.getReadPos() < dotSz) break; - uint32_t dmg = packet.readUInt32(); - if (periodicWotlk) /*uint32_t overkill=*/ packet.readUInt32(); - /*uint32_t school=*/ packet.readUInt32(); - uint32_t abs = packet.readUInt32(); - uint32_t res = packet.readUInt32(); - if (dmg > 0) - addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast(dmg), - spellId, isPlayerCaster); - if (abs > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(abs), - spellId, isPlayerCaster); - if (res > 0) - addCombatText(CombatTextEntry::RESIST, static_cast(res), - spellId, isPlayerCaster); - } else if (auraType == 8 || auraType == 124 || auraType == 45) { - // Classic/TBC: heal(4)+maxHeal(4)+overHeal(4) = 12 bytes - // WotLK 3.3.5a: heal(4)+maxHeal(4)+overHeal(4)+absorbed(4)+isCrit(1) = 17 bytes - const bool healWotlk = isActiveExpansion("wotlk"); - const size_t hotSz = healWotlk ? 17u : 12u; - if (packet.getSize() - packet.getReadPos() < hotSz) break; - uint32_t heal = packet.readUInt32(); - /*uint32_t max=*/ packet.readUInt32(); - /*uint32_t over=*/ packet.readUInt32(); - uint32_t hotAbs = 0; - if (healWotlk) { - hotAbs = packet.readUInt32(); - /*uint8_t isCrit=*/ packet.readUInt8(); - } - addCombatText(CombatTextEntry::PERIODIC_HEAL, static_cast(heal), - spellId, isPlayerCaster); - if (hotAbs > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(hotAbs), - spellId, isPlayerCaster); - } else if (auraType == 46 || auraType == 91) { - // OBS_MOD_POWER / PERIODIC_ENERGIZE: miscValue(powerType) + amount - // Common in WotLK: Replenishment, Mana Spring Totem, Divine Plea, etc. - if (packet.getSize() - packet.getReadPos() < 8) break; - /*uint32_t powerType =*/ packet.readUInt32(); - uint32_t amount = packet.readUInt32(); - if ((isPlayerVictim || isPlayerCaster) && amount > 0) - addCombatText(CombatTextEntry::ENERGIZE, static_cast(amount), - spellId, isPlayerCaster); - } else if (auraType == 98) { - // PERIODIC_MANA_LEECH: miscValue(powerType) + amount + float multiplier - if (packet.getSize() - packet.getReadPos() < 12) break; - /*uint32_t powerType =*/ packet.readUInt32(); - uint32_t amount = packet.readUInt32(); - /*float multiplier =*/ packet.readUInt32(); // read as raw uint32 (float bits) - // Show as periodic damage from victim's perspective (mana drained) - if (isPlayerVictim && amount > 0) - addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast(amount), - spellId, false); - } else { - // Unknown/untracked aura type — stop parsing this event safely - packet.setReadPos(packet.getSize()); - break; - } - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELLENERGIZELOG: { - // WotLK: packed_guid victim + packed_guid caster + uint32 spellId + uint8 powerType + int32 amount - // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint8 powerType + int32 amount - // Classic/Vanilla: packed_guid (same as WotLK) - const bool energizeTbc = isActiveExpansion("tbc"); - size_t rem = packet.getSize() - packet.getReadPos(); - if (rem < (energizeTbc ? 8u : 2u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t victimGuid = energizeTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - uint64_t casterGuid = energizeTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - rem = packet.getSize() - packet.getReadPos(); - if (rem < 6) { packet.setReadPos(packet.getSize()); break; } - uint32_t spellId = packet.readUInt32(); - /*uint8_t powerType =*/ packet.readUInt8(); - int32_t amount = static_cast(packet.readUInt32()); - bool isPlayerVictim = (victimGuid == playerGuid); - bool isPlayerCaster = (casterGuid == playerGuid); - if ((isPlayerVictim || isPlayerCaster) && amount > 0) - addCombatText(CombatTextEntry::ENERGIZE, amount, spellId, isPlayerCaster); - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_ENVIRONMENTAL_DAMAGE_LOG: { - // uint64 victimGuid + uint8 envDmgType + uint32 damage + uint32 absorbed + uint32 resisted - // envDmgType: 1=Exhausted(fatigue), 2=Drowning, 3=Fall, 4=Lava, 5=Slime, 6=Fire - if (packet.getSize() - packet.getReadPos() < 21) { packet.setReadPos(packet.getSize()); break; } - uint64_t victimGuid = packet.readUInt64(); - /*uint8_t envType =*/ packet.readUInt8(); - uint32_t dmg = packet.readUInt32(); - uint32_t envAbs = packet.readUInt32(); - uint32_t envRes = packet.readUInt32(); - if (victimGuid == playerGuid) { - if (dmg > 0) - addCombatText(CombatTextEntry::ENVIRONMENTAL, static_cast(dmg), 0, false); - if (envAbs > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(envAbs), 0, false); - if (envRes > 0) - addCombatText(CombatTextEntry::RESIST, static_cast(envRes), 0, false); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SET_PROFICIENCY: { - // uint8 itemClass + uint32 itemSubClassMask - if (packet.getSize() - packet.getReadPos() < 5) break; - uint8_t itemClass = packet.readUInt8(); - uint32_t mask = packet.readUInt32(); - if (itemClass == 2) { // Weapon - weaponProficiency_ = mask; - LOG_DEBUG("SMSG_SET_PROFICIENCY: weapon mask=0x", std::hex, mask, std::dec); - } else if (itemClass == 4) { // Armor - armorProficiency_ = mask; - LOG_DEBUG("SMSG_SET_PROFICIENCY: armor mask=0x", std::hex, mask, std::dec); - } - break; - } - - case Opcode::SMSG_ACTION_BUTTONS: { - // packed: bits 0-23 = actionId, bits 24-31 = type - // 0x00 = spell (when id != 0), 0x80 = item, 0x40 = macro (skip) - // Format differences: - // Classic 1.12: no mode byte, 120 slots (480 bytes) - // TBC 2.4.3: no mode byte, 132 slots (528 bytes) - // WotLK 3.3.5a: uint8 mode + 144 slots (577 bytes) - size_t rem = packet.getSize() - packet.getReadPos(); - const bool hasModeByteExp = isActiveExpansion("wotlk"); - int serverBarSlots; - if (isClassicLikeExpansion()) { - serverBarSlots = 120; - } else if (isActiveExpansion("tbc")) { - serverBarSlots = 132; - } else { - serverBarSlots = 144; - } - if (hasModeByteExp) { - if (rem < 1) break; - /*uint8_t mode =*/ packet.readUInt8(); - rem--; - } - for (int i = 0; i < serverBarSlots; ++i) { - if (rem < 4) break; - uint32_t packed = packet.readUInt32(); - rem -= 4; - if (i >= ACTION_BAR_SLOTS) continue; // only load bars 1 and 2 - if (packed == 0) { - // Empty slot — only clear if not already set to Attack/Hearthstone defaults - // so we don't wipe hardcoded fallbacks when the server sends zeros. - continue; - } - uint8_t type = static_cast((packed >> 24) & 0xFF); - uint32_t id = packed & 0x00FFFFFFu; - if (id == 0) continue; - ActionBarSlot slot; - switch (type) { - case 0x00: slot.type = ActionBarSlot::SPELL; slot.id = id; break; - case 0x80: slot.type = ActionBarSlot::ITEM; slot.id = id; break; - default: continue; // macro or unknown — leave as-is - } - actionBar[i] = slot; - } - LOG_INFO("SMSG_ACTION_BUTTONS: populated action bar from server"); - packet.setReadPos(packet.getSize()); - break; - } - - case Opcode::SMSG_LEVELUP_INFO: - case Opcode::SMSG_LEVELUP_INFO_ALT: { - // Server-authoritative level-up event. - // First field is always the new level in Classic/TBC/WotLK-era layouts. - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t newLevel = packet.readUInt32(); - if (newLevel > 0) { - uint32_t oldLevel = serverPlayerLevel_; - serverPlayerLevel_ = std::max(serverPlayerLevel_, newLevel); - for (auto& ch : characters) { - if (ch.guid == playerGuid) { - ch.level = serverPlayerLevel_; - break; - } - } - if (newLevel > oldLevel && levelUpCallback_) { - levelUpCallback_(newLevel); - } - } - } - // Remaining payload (hp/mana/stat deltas) is optional for our client. - packet.setReadPos(packet.getSize()); - break; - } - - case Opcode::SMSG_PLAY_SOUND: - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t soundId = packet.readUInt32(); - LOG_DEBUG("SMSG_PLAY_SOUND id=", soundId); - if (playSoundCallback_) playSoundCallback_(soundId); - } - break; - - case Opcode::SMSG_SERVER_MESSAGE: { - // uint32 type + string message - // Types: 1=shutdown_time, 2=restart_time, 3=string, 4=shutdown_cancelled, 5=restart_cancelled - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t msgType = packet.readUInt32(); - std::string msg = packet.readString(); - if (!msg.empty()) { - std::string prefix; - switch (msgType) { - case 1: prefix = "[Shutdown] "; addUIError("Server shutdown: " + msg); break; - case 2: prefix = "[Restart] "; addUIError("Server restart: " + msg); break; - case 4: prefix = "[Shutdown cancelled] "; break; - case 5: prefix = "[Restart cancelled] "; break; - default: prefix = "[Server] "; break; - } - addSystemChatMessage(prefix + msg); - } - } - break; - } - case Opcode::SMSG_CHAT_SERVER_MESSAGE: { - // uint32 type + string text - if (packet.getSize() - packet.getReadPos() >= 4) { - /*uint32_t msgType =*/ packet.readUInt32(); - std::string msg = packet.readString(); - if (!msg.empty()) addSystemChatMessage("[Announcement] " + msg); - } - break; - } - case Opcode::SMSG_AREA_TRIGGER_MESSAGE: { - // uint32 size, then string - if (packet.getSize() - packet.getReadPos() >= 4) { - /*uint32_t len =*/ packet.readUInt32(); - std::string msg = packet.readString(); - if (!msg.empty()) addSystemChatMessage(msg); - } - break; - } - case Opcode::SMSG_TRIGGER_CINEMATIC: - // uint32 cinematicId — we don't play cinematics; consume and skip. - packet.setReadPos(packet.getSize()); - LOG_DEBUG("SMSG_TRIGGER_CINEMATIC: skipped"); - break; - - case Opcode::SMSG_LOOT_MONEY_NOTIFY: { - // Format: uint32 money + uint8 soleLooter - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t amount = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() >= 1) { - /*uint8_t soleLooter =*/ packet.readUInt8(); - } - playerMoneyCopper_ += amount; - pendingMoneyDelta_ = amount; - pendingMoneyDeltaTimer_ = 2.0f; - LOG_INFO("Looted ", amount, " copper (total: ", playerMoneyCopper_, ")"); - uint64_t notifyGuid = pendingLootMoneyGuid_ != 0 ? pendingLootMoneyGuid_ : currentLoot.lootGuid; - pendingLootMoneyGuid_ = 0; - pendingLootMoneyAmount_ = 0; - pendingLootMoneyNotifyTimer_ = 0.0f; - bool alreadyAnnounced = false; - auto it = localLootState_.find(notifyGuid); - if (it != localLootState_.end()) { - alreadyAnnounced = it->second.moneyTaken; - it->second.moneyTaken = true; - } - if (!alreadyAnnounced) { - addSystemChatMessage("Looted: " + formatCopperAmount(amount)); - auto* renderer = core::Application::getInstance().getRenderer(); - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { - if (amount >= 10000) { - sfx->playLootCoinLarge(); - } else { - sfx->playLootCoinSmall(); - } - } - } - if (notifyGuid != 0) { - recentLootMoneyAnnounceCooldowns_[notifyGuid] = 1.5f; - } - } - } - break; } - case Opcode::SMSG_LOOT_CLEAR_MONEY: - case Opcode::SMSG_NPC_TEXT_UPDATE: - break; - case Opcode::SMSG_SELL_ITEM: { - // uint64 vendorGuid, uint64 itemGuid, uint8 result - if ((packet.getSize() - packet.getReadPos()) >= 17) { - uint64_t vendorGuid = packet.readUInt64(); - uint64_t itemGuid = packet.readUInt64(); // itemGuid - uint8_t result = packet.readUInt8(); - LOG_INFO("SMSG_SELL_ITEM: vendorGuid=0x", std::hex, vendorGuid, - " itemGuid=0x", itemGuid, std::dec, - " result=", static_cast(result)); - if (result == 0) { - pendingSellToBuyback_.erase(itemGuid); - } else { - bool removedPending = false; - auto it = pendingSellToBuyback_.find(itemGuid); - if (it != pendingSellToBuyback_.end()) { - for (auto bit = buybackItems_.begin(); bit != buybackItems_.end(); ++bit) { - if (bit->itemGuid == itemGuid) { - buybackItems_.erase(bit); - break; - } - } - pendingSellToBuyback_.erase(it); - removedPending = true; - } - if (!removedPending) { - // Some cores return a non-item GUID on sell failure; drop the newest - // optimistic entry if it is still pending so stale rows don't block buyback. - if (!buybackItems_.empty()) { - uint64_t frontGuid = buybackItems_.front().itemGuid; - if (pendingSellToBuyback_.erase(frontGuid) > 0) { - buybackItems_.pop_front(); - removedPending = true; - } - } - } - if (!removedPending && !pendingSellToBuyback_.empty()) { - // Last-resort desync recovery. - pendingSellToBuyback_.clear(); - buybackItems_.clear(); - } - static const char* sellErrors[] = { - "OK", "Can't find item", "Can't sell item", - "Can't find vendor", "You don't own that item", - "Unknown error", "Only empty bag" - }; - const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error"; - addSystemChatMessage(std::string("Sell failed: ") + msg); - LOG_WARNING("SMSG_SELL_ITEM error: ", (int)result, " (", msg, ")"); - } - } - break; - } - case Opcode::SMSG_INVENTORY_CHANGE_FAILURE: { - if ((packet.getSize() - packet.getReadPos()) >= 1) { - uint8_t error = packet.readUInt8(); - if (error != 0) { - LOG_WARNING("SMSG_INVENTORY_CHANGE_FAILURE: error=", (int)error); - // After error byte: item_guid1(8) + item_guid2(8) + bag_slot(1) = 17 bytes - uint32_t requiredLevel = 0; - if (packet.getSize() - packet.getReadPos() >= 17) { - packet.readUInt64(); // item_guid1 - packet.readUInt64(); // item_guid2 - packet.readUInt8(); // bag_slot - // Error 1 = EQUIP_ERR_LEVEL_REQ: server appends required level as uint32 - if (error == 1 && packet.getSize() - packet.getReadPos() >= 4) - requiredLevel = packet.readUInt32(); - } - // InventoryResult enum (AzerothCore 3.3.5a) - const char* errMsg = nullptr; - char levelBuf[64]; - switch (error) { - case 1: - if (requiredLevel > 0) { - std::snprintf(levelBuf, sizeof(levelBuf), - "You must reach level %u to use that item.", requiredLevel); - addSystemChatMessage(levelBuf); - } else { - addSystemChatMessage("You must reach a higher level to use that item."); - } - break; - case 2: errMsg = "You don't have the required skill."; break; - case 3: errMsg = "That item doesn't go in that slot."; break; - case 4: errMsg = "That bag is full."; break; - case 5: errMsg = "Can't put bags in bags."; break; - case 6: errMsg = "Can't trade equipped bags."; break; - case 7: errMsg = "That slot only holds ammo."; break; - case 8: errMsg = "You can't use that item."; break; - case 9: errMsg = "No equipment slot available."; break; - case 10: errMsg = "You can never use that item."; break; - case 11: errMsg = "You can never use that item."; break; - case 12: errMsg = "No equipment slot available."; break; - case 13: errMsg = "Can't equip with a two-handed weapon."; break; - case 14: errMsg = "Can't dual-wield."; break; - case 15: errMsg = "That item doesn't go in that bag."; break; - case 16: errMsg = "That item doesn't go in that bag."; break; - case 17: errMsg = "You can't carry any more of those."; break; - case 18: errMsg = "No equipment slot available."; break; - case 19: errMsg = "Can't stack those items."; break; - case 20: errMsg = "That item can't be equipped."; break; - case 21: errMsg = "Can't swap items."; break; - case 22: errMsg = "That slot is empty."; break; - case 23: errMsg = "Item not found."; break; - case 24: errMsg = "Can't drop soulbound items."; break; - case 25: errMsg = "Out of range."; break; - case 26: errMsg = "Need to split more than 1."; break; - case 27: errMsg = "Split failed."; break; - case 28: errMsg = "Not enough reagents."; break; - case 29: errMsg = "Not enough money."; break; - case 30: errMsg = "Not a bag."; break; - case 31: errMsg = "Can't destroy non-empty bag."; break; - case 32: errMsg = "You don't own that item."; break; - case 33: errMsg = "You can only have one quiver."; break; - case 34: errMsg = "No free bank slots."; break; - case 35: errMsg = "No bank here."; break; - case 36: errMsg = "Item is locked."; break; - case 37: errMsg = "You are stunned."; break; - case 38: errMsg = "You are dead."; break; - case 39: errMsg = "Can't do that right now."; break; - case 40: errMsg = "Internal bag error."; break; - case 49: errMsg = "Loot is gone."; break; - case 50: errMsg = "Inventory is full."; break; - case 51: errMsg = "Bank is full."; break; - case 52: errMsg = "That item is sold out."; break; - case 58: errMsg = "That object is busy."; break; - case 60: errMsg = "Can't do that in combat."; break; - case 61: errMsg = "Can't do that while disarmed."; break; - case 63: errMsg = "Requires a higher rank."; break; - case 64: errMsg = "Requires higher reputation."; break; - case 67: errMsg = "That item is unique-equipped."; break; - case 69: errMsg = "Not enough honor points."; break; - case 70: errMsg = "Not enough arena points."; break; - case 77: errMsg = "Too much gold."; break; - case 78: errMsg = "Can't do that during arena match."; break; - case 80: errMsg = "Requires a personal arena rating."; break; - case 87: errMsg = "Requires a higher level."; break; - case 88: errMsg = "Requires the right talent."; break; - default: break; - } - std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; - addSystemChatMessage(msg); - } - } - break; - } - case Opcode::SMSG_BUY_FAILED: { - // vendorGuid(8) + itemId(4) + errorCode(1) - if (packet.getSize() - packet.getReadPos() >= 13) { - uint64_t vendorGuid = packet.readUInt64(); - uint32_t itemIdOrSlot = packet.readUInt32(); - uint8_t errCode = packet.readUInt8(); - LOG_INFO("SMSG_BUY_FAILED: vendorGuid=0x", std::hex, vendorGuid, std::dec, - " item/slot=", itemIdOrSlot, - " err=", static_cast(errCode), - " pendingBuybackSlot=", pendingBuybackSlot_, - " pendingBuybackWireSlot=", pendingBuybackWireSlot_, - " pendingBuyItemId=", pendingBuyItemId_, - " pendingBuyItemSlot=", pendingBuyItemSlot_); - if (pendingBuybackSlot_ >= 0) { - // Some cores require probing absolute buyback slots until a live entry is found. - if (errCode == 0) { - constexpr uint16_t kWotlkCmsgBuybackItemOpcode = 0x290; - constexpr uint32_t kBuybackSlotEnd = 85; - if (pendingBuybackWireSlot_ >= 74 && pendingBuybackWireSlot_ < kBuybackSlotEnd && - socket && state == WorldState::IN_WORLD && currentVendorItems.vendorGuid != 0) { - ++pendingBuybackWireSlot_; - LOG_INFO("Buyback retry: vendorGuid=0x", std::hex, currentVendorItems.vendorGuid, - std::dec, " uiSlot=", pendingBuybackSlot_, - " wireSlot=", pendingBuybackWireSlot_); - network::Packet retry(kWotlkCmsgBuybackItemOpcode); - retry.writeUInt64(currentVendorItems.vendorGuid); - retry.writeUInt32(pendingBuybackWireSlot_); - socket->send(retry); - break; - } - // Exhausted slot probe: drop stale local row and advance. - if (pendingBuybackSlot_ < static_cast(buybackItems_.size())) { - buybackItems_.erase(buybackItems_.begin() + pendingBuybackSlot_); - } - pendingBuybackSlot_ = -1; - pendingBuybackWireSlot_ = 0; - if (currentVendorItems.vendorGuid != 0 && socket && state == WorldState::IN_WORLD) { - auto pkt = ListInventoryPacket::build(currentVendorItems.vendorGuid); - socket->send(pkt); - } - break; - } - pendingBuybackSlot_ = -1; - pendingBuybackWireSlot_ = 0; - } - - const char* msg = "Purchase failed."; - switch (errCode) { - case 0: msg = "Purchase failed: item not found."; break; - case 2: msg = "You don't have enough money."; break; - case 4: msg = "Seller is too far away."; break; - case 5: msg = "That item is sold out."; break; - case 6: msg = "You can't carry any more items."; break; - default: break; - } - addSystemChatMessage(msg); - } - break; - } - case Opcode::MSG_RAID_TARGET_UPDATE: { - // uint8 type: 0 = full update (8 × (uint8 icon + uint64 guid)), - // 1 = single update (uint8 icon + uint64 guid) - size_t remRTU = packet.getSize() - packet.getReadPos(); - if (remRTU < 1) break; - uint8_t rtuType = packet.readUInt8(); - if (rtuType == 0) { - // Full update: always 8 entries - for (uint32_t i = 0; i < kRaidMarkCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 9) break; - uint8_t icon = packet.readUInt8(); - uint64_t guid = packet.readUInt64(); - if (icon < kRaidMarkCount) - raidTargetGuids_[icon] = guid; - } - } else { - // Single update - if (packet.getSize() - packet.getReadPos() >= 9) { - uint8_t icon = packet.readUInt8(); - uint64_t guid = packet.readUInt64(); - if (icon < kRaidMarkCount) - raidTargetGuids_[icon] = guid; - } - } - LOG_DEBUG("MSG_RAID_TARGET_UPDATE: type=", static_cast(rtuType)); - break; - } - case Opcode::SMSG_BUY_ITEM: { - // uint64 vendorGuid + uint32 vendorSlot + int32 newCount + uint32 itemCount - // Confirms a successful CMSG_BUY_ITEM. The inventory update arrives via SMSG_UPDATE_OBJECT. - if (packet.getSize() - packet.getReadPos() >= 20) { - uint64_t vendorGuid = packet.readUInt64(); - uint32_t vendorSlot = packet.readUInt32(); - int32_t newCount = static_cast(packet.readUInt32()); - uint32_t itemCount = packet.readUInt32(); - LOG_DEBUG("SMSG_BUY_ITEM: vendorGuid=0x", std::hex, vendorGuid, std::dec, - " slot=", vendorSlot, " newCount=", newCount, " bought=", itemCount); - pendingBuyItemId_ = 0; - pendingBuyItemSlot_ = 0; - } - break; - } - case Opcode::SMSG_CRITERIA_UPDATE: { - // uint32 criteriaId + uint64 progress + uint32 elapsedTime + uint32 creationTime - if (packet.getSize() - packet.getReadPos() >= 20) { - uint32_t criteriaId = packet.readUInt32(); - uint64_t progress = packet.readUInt64(); - packet.readUInt32(); // elapsedTime - packet.readUInt32(); // creationTime - criteriaProgress_[criteriaId] = progress; - LOG_DEBUG("SMSG_CRITERIA_UPDATE: id=", criteriaId, " progress=", progress); - } - break; - } - case Opcode::SMSG_BARBER_SHOP_RESULT: { - // uint32 result (0 = success, 1 = no money, 2 = not barber, 3 = sitting) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t result = packet.readUInt32(); - if (result == 0) { - addSystemChatMessage("Hairstyle changed."); - } else { - const char* msg = (result == 1) ? "Not enough money for new hairstyle." - : (result == 2) ? "You are not at a barber shop." - : (result == 3) ? "You must stand up to use the barber shop." - : "Barber shop unavailable."; - addSystemChatMessage(msg); - } - LOG_DEBUG("SMSG_BARBER_SHOP_RESULT: result=", result); - } - break; - } - case Opcode::SMSG_OVERRIDE_LIGHT: { - // uint32 currentZoneLightId + uint32 overrideLightId + uint32 transitionMs - if (packet.getSize() - packet.getReadPos() >= 12) { - uint32_t zoneLightId = packet.readUInt32(); - uint32_t overrideLightId = packet.readUInt32(); - uint32_t transitionMs = packet.readUInt32(); - overrideLightId_ = overrideLightId; - overrideLightTransMs_ = transitionMs; - LOG_DEBUG("SMSG_OVERRIDE_LIGHT: zone=", zoneLightId, - " override=", overrideLightId, " transition=", transitionMs, "ms"); - } - break; - } - case Opcode::SMSG_WEATHER: { - // Classic 1.12: uint32 weatherType + float intensity (8 bytes, no isAbrupt) - // TBC 2.4.3 / WotLK 3.3.5a: uint32 weatherType + float intensity + uint8 isAbrupt (9 bytes) - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t wType = packet.readUInt32(); - float wIntensity = packet.readFloat(); - if (packet.getSize() - packet.getReadPos() >= 1) - /*uint8_t isAbrupt =*/ packet.readUInt8(); - weatherType_ = wType; - weatherIntensity_ = wIntensity; - const char* typeName = (wType == 1) ? "Rain" : (wType == 2) ? "Snow" : (wType == 3) ? "Storm" : "Clear"; - LOG_INFO("Weather changed: type=", wType, " (", typeName, "), intensity=", wIntensity); - } - break; - } - case Opcode::SMSG_SCRIPT_MESSAGE: { - // Server-script text message — display in system chat - std::string msg = packet.readString(); - if (!msg.empty()) { - addSystemChatMessage(msg); - LOG_INFO("SMSG_SCRIPT_MESSAGE: ", msg); - } - break; - } - case Opcode::SMSG_ENCHANTMENTLOG: { - // uint64 targetGuid + uint64 casterGuid + uint32 spellId + uint32 displayId + uint32 animType - if (packet.getSize() - packet.getReadPos() >= 28) { - /*uint64_t targetGuid =*/ packet.readUInt64(); - /*uint64_t casterGuid =*/ packet.readUInt64(); - uint32_t spellId = packet.readUInt32(); - /*uint32_t displayId =*/ packet.readUInt32(); - /*uint32_t animType =*/ packet.readUInt32(); - LOG_DEBUG("SMSG_ENCHANTMENTLOG: spellId=", spellId); - } - break; - } - case Opcode::SMSG_SOCKET_GEMS_RESULT: { - // uint64 itemGuid + uint32 result (0 = success) - if (packet.getSize() - packet.getReadPos() >= 12) { - /*uint64_t itemGuid =*/ packet.readUInt64(); - uint32_t result = packet.readUInt32(); - if (result == 0) { - addSystemChatMessage("Gems socketed successfully."); - } else { - addSystemChatMessage("Failed to socket gems."); - } - LOG_DEBUG("SMSG_SOCKET_GEMS_RESULT: result=", result); - } - break; - } - case Opcode::SMSG_ITEM_REFUND_RESULT: { - // uint64 itemGuid + uint32 result (0=success) - if (packet.getSize() - packet.getReadPos() >= 12) { - /*uint64_t itemGuid =*/ packet.readUInt64(); - uint32_t result = packet.readUInt32(); - if (result == 0) { - addSystemChatMessage("Item returned. Refund processed."); - } else { - addSystemChatMessage("Could not return item for refund."); - } - LOG_DEBUG("SMSG_ITEM_REFUND_RESULT: result=", result); - } - break; - } - case Opcode::SMSG_ITEM_TIME_UPDATE: { - // uint64 itemGuid + uint32 durationMs — item duration ticking down - if (packet.getSize() - packet.getReadPos() >= 12) { - /*uint64_t itemGuid =*/ packet.readUInt64(); - uint32_t durationMs = packet.readUInt32(); - LOG_DEBUG("SMSG_ITEM_TIME_UPDATE: remainingMs=", durationMs); - } - break; - } - case Opcode::SMSG_RESURRECT_FAILED: { - // uint32 reason — various resurrection failures - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t reason = packet.readUInt32(); - const char* msg = (reason == 1) ? "The target cannot be resurrected right now." - : (reason == 2) ? "Cannot resurrect in this area." - : "Resurrection failed."; - addSystemChatMessage(msg); - LOG_DEBUG("SMSG_RESURRECT_FAILED: reason=", reason); - } - break; - } - case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE: - handleGameObjectQueryResponse(packet); - break; - case Opcode::SMSG_GAMEOBJECT_PAGETEXT: - handleGameObjectPageText(packet); - break; - case Opcode::SMSG_GAMEOBJECT_CUSTOM_ANIM: { - if (packet.getSize() >= 12) { - uint64_t guid = packet.readUInt64(); - uint32_t animId = packet.readUInt32(); - if (gameObjectCustomAnimCallback_) { - gameObjectCustomAnimCallback_(guid, animId); - } - } - break; - } - case Opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE: - handlePageTextQueryResponse(packet); - break; - case Opcode::SMSG_QUESTGIVER_STATUS: { - if (packet.getSize() - packet.getReadPos() >= 9) { - uint64_t npcGuid = packet.readUInt64(); - uint8_t status = packetParsers_->readQuestGiverStatus(packet); - npcQuestStatus_[npcGuid] = static_cast(status); - LOG_DEBUG("SMSG_QUESTGIVER_STATUS: guid=0x", std::hex, npcGuid, std::dec, " status=", (int)status); - } - break; - } - case Opcode::SMSG_QUESTGIVER_STATUS_MULTIPLE: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t count = packet.readUInt32(); - for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < 9) break; - uint64_t npcGuid = packet.readUInt64(); - uint8_t status = packetParsers_->readQuestGiverStatus(packet); - npcQuestStatus_[npcGuid] = static_cast(status); - } - LOG_DEBUG("SMSG_QUESTGIVER_STATUS_MULTIPLE: ", count, " entries"); - } - break; - } - case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS: - handleQuestDetails(packet); - break; - case Opcode::SMSG_QUESTGIVER_QUEST_INVALID: { - // Quest query failed - parse failure reason - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t failReason = packet.readUInt32(); - pendingTurnInRewardRequest_ = false; - const char* reasonStr = "Unknown"; - switch (failReason) { - case 0: reasonStr = "Don't have quest"; break; - case 1: reasonStr = "Quest level too low"; break; - case 4: reasonStr = "Insufficient money"; break; - case 5: reasonStr = "Inventory full"; break; - case 13: reasonStr = "Already on that quest"; break; - case 18: reasonStr = "Already completed quest"; break; - case 19: reasonStr = "Can't take any more quests"; break; - } - LOG_WARNING("Quest invalid: reason=", failReason, " (", reasonStr, ")"); - if (!pendingQuestAcceptTimeouts_.empty()) { - std::vector pendingQuestIds; - pendingQuestIds.reserve(pendingQuestAcceptTimeouts_.size()); - for (const auto& pending : pendingQuestAcceptTimeouts_) { - pendingQuestIds.push_back(pending.first); - } - for (uint32_t questId : pendingQuestIds) { - const uint64_t npcGuid = pendingQuestAcceptNpcGuids_.count(questId) != 0 - ? pendingQuestAcceptNpcGuids_[questId] : 0; - if (failReason == 13) { - std::string fallbackTitle = "Quest #" + std::to_string(questId); - std::string fallbackObjectives; - if (currentQuestDetails.questId == questId) { - if (!currentQuestDetails.title.empty()) fallbackTitle = currentQuestDetails.title; - fallbackObjectives = currentQuestDetails.objectives; - } - addQuestToLocalLogIfMissing(questId, fallbackTitle, fallbackObjectives); - triggerQuestAcceptResync(questId, npcGuid, "already-on-quest"); - } else if (failReason == 18) { - triggerQuestAcceptResync(questId, npcGuid, "already-completed"); - } - clearPendingQuestAccept(questId); - } - } - // Only show error to user for real errors (not informational messages) - if (failReason != 13 && failReason != 18) { // Don't spam "already on/completed" - addSystemChatMessage(std::string("Quest unavailable: ") + reasonStr); - } - } - break; - } - case Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE: { - // Mark quest as complete in local log - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t questId = packet.readUInt32(); - LOG_INFO("Quest completed: questId=", questId); - if (pendingTurnInQuestId_ == questId) { - pendingTurnInQuestId_ = 0; - pendingTurnInNpcGuid_ = 0; - pendingTurnInRewardRequest_ = false; - } - for (auto it = questLog_.begin(); it != questLog_.end(); ++it) { - if (it->questId == questId) { - questLog_.erase(it); - LOG_INFO(" Removed quest ", questId, " from quest log"); - break; - } - } - } - // Re-query all nearby quest giver NPCs so markers refresh - if (socket) { - for (const auto& [guid, entity] : entityManager.getEntities()) { - if (entity->getType() != ObjectType::UNIT) continue; - auto unit = std::static_pointer_cast(entity); - if (unit->getNpcFlags() & 0x02) { - network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); - qsPkt.writeUInt64(guid); - socket->send(qsPkt); - } - } - } - break; - } - case Opcode::SMSG_QUESTUPDATE_ADD_KILL: { - // Quest kill count update - // Compatibility: some classic-family opcode tables swap ADD_KILL and COMPLETE. - size_t rem = packet.getSize() - packet.getReadPos(); - if (rem >= 12) { - uint32_t questId = packet.readUInt32(); - clearPendingQuestAccept(questId); - uint32_t entry = packet.readUInt32(); // Creature entry - uint32_t count = packet.readUInt32(); // Current kills - uint32_t reqCount = 0; - if (packet.getSize() - packet.getReadPos() >= 4) { - reqCount = packet.readUInt32(); // Required kills (if present) - } - - LOG_INFO("Quest kill update: questId=", questId, " entry=", entry, - " count=", count, "/", reqCount); - - // Update quest log with kill count - for (auto& quest : questLog_) { - if (quest.questId == questId) { - // Preserve prior required count if this packet variant omits it. - if (reqCount == 0) { - auto it = quest.killCounts.find(entry); - if (it != quest.killCounts.end()) reqCount = it->second.second; - } - // Fall back to killObjectives (parsed from SMSG_QUEST_QUERY_RESPONSE). - // Note: npcOrGoId < 0 means game object; server always sends entry as uint32 - // in QUESTUPDATE_ADD_KILL regardless of type, so match by absolute value. - if (reqCount == 0) { - for (const auto& obj : quest.killObjectives) { - if (obj.npcOrGoId == 0 || obj.required == 0) continue; - uint32_t objEntry = static_cast( - obj.npcOrGoId > 0 ? obj.npcOrGoId : -obj.npcOrGoId); - if (objEntry == entry) { - reqCount = obj.required; - break; - } - } - } - if (reqCount == 0) reqCount = count; // last-resort: avoid 0/0 display - quest.killCounts[entry] = {count, reqCount}; - - std::string creatureName = getCachedCreatureName(entry); - std::string progressMsg = quest.title + ": "; - if (!creatureName.empty()) { - progressMsg += creatureName + " "; - } - progressMsg += std::to_string(count) + "/" + std::to_string(reqCount); - addSystemChatMessage(progressMsg); - - LOG_INFO("Updated kill count for quest ", questId, ": ", - count, "/", reqCount); - break; - } - } - } else if (rem >= 4) { - // Swapped mapping fallback: treat as QUESTUPDATE_COMPLETE packet. - uint32_t questId = packet.readUInt32(); - clearPendingQuestAccept(questId); - LOG_INFO("Quest objectives completed (compat via ADD_KILL): questId=", questId); - for (auto& quest : questLog_) { - if (quest.questId == questId) { - quest.complete = true; - addSystemChatMessage("Quest Complete: " + quest.title); - break; - } - } - } - break; - } - case Opcode::SMSG_QUESTUPDATE_ADD_ITEM: { - // Quest item count update: itemId + count - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t itemId = packet.readUInt32(); - uint32_t count = packet.readUInt32(); - queryItemInfo(itemId, 0); - - std::string itemLabel = "item #" + std::to_string(itemId); - if (const ItemQueryResponseData* info = getItemInfo(itemId)) { - if (!info->name.empty()) itemLabel = info->name; - } - - bool updatedAny = false; - for (auto& quest : questLog_) { - if (quest.complete) continue; - bool tracksItem = - quest.requiredItemCounts.count(itemId) > 0 || - quest.itemCounts.count(itemId) > 0; - // Also check itemObjectives parsed from SMSG_QUEST_QUERY_RESPONSE in case - // requiredItemCounts hasn't been populated yet (race during quest accept). - if (!tracksItem) { - for (const auto& obj : quest.itemObjectives) { - if (obj.itemId == itemId && obj.required > 0) { - quest.requiredItemCounts.emplace(itemId, obj.required); - tracksItem = true; - break; - } - } - } - if (!tracksItem) continue; - quest.itemCounts[itemId] = count; - updatedAny = true; - } - addSystemChatMessage("Quest item: " + itemLabel + " (" + std::to_string(count) + ")"); - LOG_INFO("Quest item update: itemId=", itemId, " count=", count, - " trackedQuestsUpdated=", updatedAny); - } - break; - } - case Opcode::SMSG_QUESTUPDATE_COMPLETE: { - // Quest objectives completed - mark as ready to turn in. - // Compatibility: some classic-family opcode tables swap COMPLETE and ADD_KILL. - size_t rem = packet.getSize() - packet.getReadPos(); - if (rem >= 12) { - uint32_t questId = packet.readUInt32(); - clearPendingQuestAccept(questId); - uint32_t entry = packet.readUInt32(); - uint32_t count = packet.readUInt32(); - uint32_t reqCount = 0; - if (packet.getSize() - packet.getReadPos() >= 4) reqCount = packet.readUInt32(); - if (reqCount == 0) reqCount = count; - LOG_INFO("Quest kill update (compat via COMPLETE): questId=", questId, - " entry=", entry, " count=", count, "/", reqCount); - for (auto& quest : questLog_) { - if (quest.questId == questId) { - quest.killCounts[entry] = {count, reqCount}; - addSystemChatMessage(quest.title + ": " + std::to_string(count) + - "/" + std::to_string(reqCount)); - break; - } - } - } else if (rem >= 4) { - uint32_t questId = packet.readUInt32(); - clearPendingQuestAccept(questId); - LOG_INFO("Quest objectives completed: questId=", questId); - - for (auto& quest : questLog_) { - if (quest.questId == questId) { - quest.complete = true; - addSystemChatMessage("Quest Complete: " + quest.title); - LOG_INFO("Marked quest ", questId, " as complete"); - break; - } - } - } - break; - } - case Opcode::SMSG_QUEST_FORCE_REMOVE: { - // This opcode is aliased to SMSG_SET_REST_START in the opcode table - // because both share opcode 0x21E in WotLK 3.3.5a. - // In WotLK: payload = uint32 areaId (entering rest) or 0 (leaving rest). - // In Classic/TBC: payload = uint32 questId (force-remove a quest). - if (packet.getSize() - packet.getReadPos() < 4) { - LOG_WARNING("SMSG_QUEST_FORCE_REMOVE/SET_REST_START too short"); - break; - } - uint32_t value = packet.readUInt32(); - - // WotLK uses this opcode as SMSG_SET_REST_START: non-zero = entering - // a rest area (inn/city), zero = leaving. Classic/TBC use it for quest removal. - if (!isClassicLikeExpansion() && !isActiveExpansion("tbc")) { - // WotLK: treat as SET_REST_START - bool nowResting = (value != 0); - if (nowResting != isResting_) { - isResting_ = nowResting; - addSystemChatMessage(isResting_ ? "You are now resting." - : "You are no longer resting."); - } - break; - } - - // Classic/TBC: treat as QUEST_FORCE_REMOVE (uint32 questId) - uint32_t questId = value; - clearPendingQuestAccept(questId); - pendingQuestQueryIds_.erase(questId); - if (questId == 0) { - // Some servers emit a zero-id variant during world bootstrap. - // Treat as no-op to avoid false "Quest removed" spam. - break; - } - - bool removed = false; - std::string removedTitle; - for (auto it = questLog_.begin(); it != questLog_.end(); ++it) { - if (it->questId == questId) { - removedTitle = it->title; - questLog_.erase(it); - removed = true; - break; - } - } - if (currentQuestDetails.questId == questId) { - questDetailsOpen = false; - questDetailsOpenTime = std::chrono::steady_clock::time_point{}; - currentQuestDetails = QuestDetailsData{}; - removed = true; - } - if (currentQuestRequestItems_.questId == questId) { - questRequestItemsOpen_ = false; - currentQuestRequestItems_ = QuestRequestItemsData{}; - removed = true; - } - if (currentQuestOfferReward_.questId == questId) { - questOfferRewardOpen_ = false; - currentQuestOfferReward_ = QuestOfferRewardData{}; - removed = true; - } - if (removed) { - if (!removedTitle.empty()) { - addSystemChatMessage("Quest removed: " + removedTitle); - } else { - addSystemChatMessage("Quest removed (ID " + std::to_string(questId) + ")."); - } - } - break; - } - case Opcode::SMSG_QUEST_QUERY_RESPONSE: { - if (packet.getSize() < 8) { - LOG_WARNING("SMSG_QUEST_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)"); - break; - } - - uint32_t questId = packet.readUInt32(); - packet.readUInt32(); // questMethod - - // Classic/Turtle = stride 3, TBC = stride 4 — all use 40 fixed fields + 4 strings. - // WotLK = stride 5, uses 55 fixed fields + 5 strings. - const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() <= 4; - const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout); - const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout); - const QuestQueryRewards rwds = tryParseQuestRewards(packet.getData(), isClassicLayout); - - for (auto& q : questLog_) { - if (q.questId != questId) continue; - - const int existingScore = scoreQuestTitle(q.title); - const bool parsedStrong = isStrongQuestTitle(parsed.title); - const bool parsedLongEnough = parsed.title.size() >= 6; - const bool notShorterThanExisting = - isPlaceholderQuestTitle(q.title) || q.title.empty() || parsed.title.size() + 2 >= q.title.size(); - const bool shouldReplaceTitle = - parsed.score > -1000 && - parsedStrong && - parsedLongEnough && - notShorterThanExisting && - (isPlaceholderQuestTitle(q.title) || q.title.empty() || parsed.score >= existingScore + 12); - - if (shouldReplaceTitle && !parsed.title.empty()) { - q.title = parsed.title; - } - if (!parsed.objectives.empty() && - (q.objectives.empty() || q.objectives.size() < 16)) { - q.objectives = parsed.objectives; - } - - // Store structured kill/item objectives for later kill-count restoration. - if (objs.valid) { - for (int i = 0; i < 4; ++i) { - q.killObjectives[i].npcOrGoId = objs.kills[i].npcOrGoId; - q.killObjectives[i].required = objs.kills[i].required; - } - for (int i = 0; i < 6; ++i) { - q.itemObjectives[i].itemId = objs.items[i].itemId; - q.itemObjectives[i].required = objs.items[i].required; - } - // Now that we have the objective creature IDs, apply any packed kill - // counts from the player update fields that arrived at login. - applyPackedKillCountsFromFields(q); - // Pre-fetch creature/GO names and item info so objective display is - // populated by the time the player opens the quest log. - for (int i = 0; i < 4; ++i) { - int32_t id = objs.kills[i].npcOrGoId; - if (id == 0 || objs.kills[i].required == 0) continue; - if (id > 0) queryCreatureInfo(static_cast(id), 0); - else queryGameObjectInfo(static_cast(-id), 0); - } - for (int i = 0; i < 6; ++i) { - if (objs.items[i].itemId != 0 && objs.items[i].required != 0) - queryItemInfo(objs.items[i].itemId, 0); - } - LOG_DEBUG("Quest ", questId, " objectives parsed: kills=[", - objs.kills[0].npcOrGoId, "/", objs.kills[0].required, ", ", - objs.kills[1].npcOrGoId, "/", objs.kills[1].required, ", ", - objs.kills[2].npcOrGoId, "/", objs.kills[2].required, ", ", - objs.kills[3].npcOrGoId, "/", objs.kills[3].required, "]"); - } - - // Store reward data and pre-fetch item info for icons. - if (rwds.valid) { - q.rewardMoney = rwds.rewardMoney; - for (int i = 0; i < 4; ++i) { - q.rewardItems[i].itemId = rwds.itemId[i]; - q.rewardItems[i].count = (rwds.itemId[i] != 0) ? rwds.itemCount[i] : 0; - if (rwds.itemId[i] != 0) queryItemInfo(rwds.itemId[i], 0); - } - for (int i = 0; i < 6; ++i) { - q.rewardChoiceItems[i].itemId = rwds.choiceItemId[i]; - q.rewardChoiceItems[i].count = (rwds.choiceItemId[i] != 0) ? rwds.choiceItemCount[i] : 0; - if (rwds.choiceItemId[i] != 0) queryItemInfo(rwds.choiceItemId[i], 0); - } - } - break; - } - - pendingQuestQueryIds_.erase(questId); - break; - } - case Opcode::SMSG_QUESTLOG_FULL: - // Zero-payload notification: the player's quest log is full (25 quests). - addSystemChatMessage("Your quest log is full."); - LOG_INFO("SMSG_QUESTLOG_FULL: quest log is at capacity"); - break; - case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS: - handleQuestRequestItems(packet); - break; - case Opcode::SMSG_QUESTGIVER_OFFER_REWARD: - handleQuestOfferReward(packet); - break; - case Opcode::SMSG_GROUP_SET_LEADER: - LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec); - break; - - // ---- Teleport / Transfer ---- - case Opcode::MSG_MOVE_TELEPORT_ACK: - handleTeleportAck(packet); - break; - case Opcode::SMSG_TRANSFER_PENDING: { - // SMSG_TRANSFER_PENDING: uint32 mapId, then optional transport data - uint32_t pendingMapId = packet.readUInt32(); - LOG_INFO("SMSG_TRANSFER_PENDING: mapId=", pendingMapId); - // Optional: if remaining data, there's a transport entry + mapId - if (packet.getReadPos() + 8 <= packet.getSize()) { - uint32_t transportEntry = packet.readUInt32(); - uint32_t transportMapId = packet.readUInt32(); - LOG_INFO(" Transport entry=", transportEntry, " transportMapId=", transportMapId); - } - break; - } - case Opcode::SMSG_NEW_WORLD: - handleNewWorld(packet); - break; - case Opcode::SMSG_TRANSFER_ABORTED: { - uint32_t mapId = packet.readUInt32(); - uint8_t reason = (packet.getReadPos() < packet.getSize()) ? packet.readUInt8() : 0; - LOG_WARNING("SMSG_TRANSFER_ABORTED: mapId=", mapId, " reason=", (int)reason); - // Provide reason-specific feedback (WotLK TRANSFER_ABORT_* codes) - const char* abortMsg = nullptr; - switch (reason) { - case 0x01: abortMsg = "Transfer aborted: difficulty unavailable."; break; - case 0x02: abortMsg = "Transfer aborted: expansion required."; break; - case 0x03: abortMsg = "Transfer aborted: instance not found."; break; - case 0x04: abortMsg = "Transfer aborted: too many instances. Please wait before entering a new instance."; break; - case 0x06: abortMsg = "Transfer aborted: instance is full."; break; - case 0x07: abortMsg = "Transfer aborted: zone is in combat."; break; - case 0x08: abortMsg = "Transfer aborted: you are already in this instance."; break; - case 0x09: abortMsg = "Transfer aborted: not enough players."; break; - case 0x0C: abortMsg = "Transfer aborted."; break; - default: abortMsg = "Transfer aborted."; break; - } - addSystemChatMessage(abortMsg); - break; - } - - // ---- Taxi / Flight Paths ---- - case Opcode::SMSG_SHOWTAXINODES: - handleShowTaxiNodes(packet); - break; - case Opcode::SMSG_ACTIVATETAXIREPLY: - handleActivateTaxiReply(packet); - break; - case Opcode::SMSG_STANDSTATE_UPDATE: - // Server confirms stand state change (sit/stand/sleep/kneel) - if (packet.getSize() - packet.getReadPos() >= 1) { - standState_ = packet.readUInt8(); - LOG_INFO("Stand state updated: ", static_cast(standState_), - " (", standState_ == 0 ? "stand" : standState_ == 1 ? "sit" - : standState_ == 7 ? "dead" : standState_ == 8 ? "kneel" : "other", ")"); - if (standStateCallback_) { - standStateCallback_(standState_); - } - } - break; - case Opcode::SMSG_NEW_TAXI_PATH: - // Empty packet - server signals a new flight path was learned - // The actual node details come in the next SMSG_SHOWTAXINODES - addSystemChatMessage("New flight path discovered!"); - break; - - // ---- Arena / Battleground ---- - case Opcode::SMSG_BATTLEFIELD_STATUS: - handleBattlefieldStatus(packet); - break; - case Opcode::SMSG_BATTLEFIELD_LIST: - LOG_INFO("Received SMSG_BATTLEFIELD_LIST"); - break; - case Opcode::SMSG_BATTLEFIELD_PORT_DENIED: - addSystemChatMessage("Battlefield port denied."); - break; - case Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS: - // Optional map position updates for BG objectives/players. - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_REMOVED_FROM_PVP_QUEUE: - addSystemChatMessage("You have been removed from the PvP queue."); - break; - case Opcode::SMSG_GROUP_JOINED_BATTLEGROUND: - addSystemChatMessage("Your group has joined the battleground."); - break; - case Opcode::SMSG_JOINED_BATTLEGROUND_QUEUE: - addSystemChatMessage("You have joined the battleground queue."); - break; - case Opcode::SMSG_BATTLEGROUND_PLAYER_JOINED: - LOG_INFO("Battleground player joined"); - break; - case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT: - LOG_INFO("Battleground player left"); - break; - case Opcode::SMSG_INSTANCE_DIFFICULTY: - case Opcode::MSG_SET_DUNGEON_DIFFICULTY: - handleInstanceDifficulty(packet); - break; - case Opcode::SMSG_INSTANCE_SAVE_CREATED: - // Zero-payload: your instance save was just created on the server. - addSystemChatMessage("You are now saved to this instance."); - LOG_INFO("SMSG_INSTANCE_SAVE_CREATED"); - break; - case Opcode::SMSG_RAID_INSTANCE_MESSAGE: { - if (packet.getSize() - packet.getReadPos() >= 12) { - uint32_t msgType = packet.readUInt32(); - uint32_t mapId = packet.readUInt32(); - /*uint32_t diff =*/ packet.readUInt32(); - // type: 1=warning(time left), 2=saved, 3=welcome - if (msgType == 1 && packet.getSize() - packet.getReadPos() >= 4) { - uint32_t timeLeft = packet.readUInt32(); - uint32_t minutes = timeLeft / 60; - std::string msg = "Instance " + std::to_string(mapId) + - " will reset in " + std::to_string(minutes) + " minute(s)."; - addSystemChatMessage(msg); - } else if (msgType == 2) { - addSystemChatMessage("You have been saved to instance " + std::to_string(mapId) + "."); - } else if (msgType == 3) { - addSystemChatMessage("Welcome to instance " + std::to_string(mapId) + "."); - } - LOG_INFO("SMSG_RAID_INSTANCE_MESSAGE: type=", msgType, " map=", mapId); - } - break; - } - case Opcode::SMSG_INSTANCE_RESET: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t mapId = packet.readUInt32(); - // Remove matching lockout from local cache - auto it = std::remove_if(instanceLockouts_.begin(), instanceLockouts_.end(), - [mapId](const InstanceLockout& lo){ return lo.mapId == mapId; }); - instanceLockouts_.erase(it, instanceLockouts_.end()); - addSystemChatMessage("Instance " + std::to_string(mapId) + " has been reset."); - LOG_INFO("SMSG_INSTANCE_RESET: mapId=", mapId); - } - break; - } - case Opcode::SMSG_INSTANCE_RESET_FAILED: { - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t mapId = packet.readUInt32(); - uint32_t reason = packet.readUInt32(); - static const char* resetFailReasons[] = { - "Not max level.", "Offline party members.", "Party members inside.", - "Party members changing zone.", "Heroic difficulty only." - }; - const char* msg = (reason < 5) ? resetFailReasons[reason] : "Unknown reason."; - addSystemChatMessage("Cannot reset instance " + std::to_string(mapId) + ": " + msg); - LOG_INFO("SMSG_INSTANCE_RESET_FAILED: mapId=", mapId, " reason=", reason); - } - break; - } - case Opcode::SMSG_INSTANCE_LOCK_WARNING_QUERY: { - // Server asks player to confirm entering a saved instance. - // We auto-confirm with CMSG_INSTANCE_LOCK_RESPONSE. - if (socket && packet.getSize() - packet.getReadPos() >= 17) { - /*uint32_t mapId =*/ packet.readUInt32(); - /*uint32_t diff =*/ packet.readUInt32(); - /*uint32_t timeLeft =*/ packet.readUInt32(); - packet.readUInt32(); // unk - /*uint8_t locked =*/ packet.readUInt8(); - // Send acceptance - network::Packet resp(wireOpcode(Opcode::CMSG_INSTANCE_LOCK_RESPONSE)); - resp.writeUInt8(1); // 1=accept - socket->send(resp); - LOG_INFO("SMSG_INSTANCE_LOCK_WARNING_QUERY: auto-accepted"); - } - break; - } - - // ---- LFG / Dungeon Finder ---- - case Opcode::SMSG_LFG_JOIN_RESULT: - handleLfgJoinResult(packet); - break; - case Opcode::SMSG_LFG_QUEUE_STATUS: - handleLfgQueueStatus(packet); - break; - case Opcode::SMSG_LFG_PROPOSAL_UPDATE: - handleLfgProposalUpdate(packet); - break; - case Opcode::SMSG_LFG_ROLE_CHECK_UPDATE: - handleLfgRoleCheckUpdate(packet); - break; - case Opcode::SMSG_LFG_UPDATE_PLAYER: - case Opcode::SMSG_LFG_UPDATE_PARTY: - handleLfgUpdatePlayer(packet); - break; - case Opcode::SMSG_LFG_PLAYER_REWARD: - handleLfgPlayerReward(packet); - break; - case Opcode::SMSG_LFG_BOOT_PROPOSAL_UPDATE: - handleLfgBootProposalUpdate(packet); - break; - case Opcode::SMSG_LFG_TELEPORT_DENIED: - handleLfgTeleportDenied(packet); - break; - case Opcode::SMSG_LFG_DISABLED: - addSystemChatMessage("The Dungeon Finder is currently disabled."); - LOG_INFO("SMSG_LFG_DISABLED received"); - break; - case Opcode::SMSG_LFG_OFFER_CONTINUE: - addSystemChatMessage("Dungeon Finder: You may continue your dungeon."); - break; - case Opcode::SMSG_LFG_ROLE_CHOSEN: - case Opcode::SMSG_LFG_UPDATE_SEARCH: - case Opcode::SMSG_UPDATE_LFG_LIST: - case Opcode::SMSG_LFG_PLAYER_INFO: - case Opcode::SMSG_LFG_PARTY_INFO: - case Opcode::SMSG_OPEN_LFG_DUNGEON_FINDER: - // Informational LFG packets not yet surfaced in UI — consume silently. - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT: - handleArenaTeamCommandResult(packet); - break; - case Opcode::SMSG_ARENA_TEAM_QUERY_RESPONSE: - handleArenaTeamQueryResponse(packet); - break; - case Opcode::SMSG_ARENA_TEAM_ROSTER: - LOG_INFO("Received SMSG_ARENA_TEAM_ROSTER"); - break; - case Opcode::SMSG_ARENA_TEAM_INVITE: - handleArenaTeamInvite(packet); - break; - case Opcode::SMSG_ARENA_TEAM_EVENT: - handleArenaTeamEvent(packet); - break; - case Opcode::SMSG_ARENA_TEAM_STATS: - handleArenaTeamStats(packet); - break; - case Opcode::SMSG_ARENA_ERROR: - handleArenaError(packet); - break; - case Opcode::MSG_PVP_LOG_DATA: - LOG_INFO("Received MSG_PVP_LOG_DATA"); - break; - case Opcode::MSG_INSPECT_ARENA_TEAMS: - LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS"); - break; - case Opcode::MSG_TALENT_WIPE_CONFIRM: { - // Server sends: uint64 npcGuid + uint32 cost - // Client must respond with the same opcode containing uint64 npcGuid to confirm. - if (packet.getSize() - packet.getReadPos() < 12) { - packet.setReadPos(packet.getSize()); - break; - } - talentWipeNpcGuid_ = packet.readUInt64(); - talentWipeCost_ = packet.readUInt32(); - talentWipePending_ = true; - LOG_INFO("MSG_TALENT_WIPE_CONFIRM: npc=0x", std::hex, talentWipeNpcGuid_, - std::dec, " cost=", talentWipeCost_); - break; - } - - // ---- MSG_MOVE_* opcodes (server relays other players' movement) ---- - case Opcode::MSG_MOVE_START_FORWARD: - case Opcode::MSG_MOVE_START_BACKWARD: - case Opcode::MSG_MOVE_STOP: - case Opcode::MSG_MOVE_START_STRAFE_LEFT: - case Opcode::MSG_MOVE_START_STRAFE_RIGHT: - case Opcode::MSG_MOVE_STOP_STRAFE: - case Opcode::MSG_MOVE_JUMP: - case Opcode::MSG_MOVE_START_TURN_LEFT: - case Opcode::MSG_MOVE_START_TURN_RIGHT: - case Opcode::MSG_MOVE_STOP_TURN: - case Opcode::MSG_MOVE_SET_FACING: - case Opcode::MSG_MOVE_FALL_LAND: - case Opcode::MSG_MOVE_HEARTBEAT: - case Opcode::MSG_MOVE_START_SWIM: - case Opcode::MSG_MOVE_STOP_SWIM: - case Opcode::MSG_MOVE_SET_WALK_MODE: - case Opcode::MSG_MOVE_SET_RUN_MODE: - case Opcode::MSG_MOVE_START_PITCH_UP: - case Opcode::MSG_MOVE_START_PITCH_DOWN: - case Opcode::MSG_MOVE_STOP_PITCH: - case Opcode::MSG_MOVE_START_ASCEND: - case Opcode::MSG_MOVE_STOP_ASCEND: - case Opcode::MSG_MOVE_START_DESCEND: - case Opcode::MSG_MOVE_SET_PITCH: - case Opcode::MSG_MOVE_GRAVITY_CHNG: - case Opcode::MSG_MOVE_UPDATE_CAN_FLY: - case Opcode::MSG_MOVE_UPDATE_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: - case Opcode::MSG_MOVE_ROOT: - case Opcode::MSG_MOVE_UNROOT: - if (state == WorldState::IN_WORLD) { - handleOtherPlayerMovement(packet); - } - break; - - // ---- Mail ---- - case Opcode::SMSG_SHOW_MAILBOX: - handleShowMailbox(packet); - break; - case Opcode::SMSG_MAIL_LIST_RESULT: - handleMailListResult(packet); - break; - case Opcode::SMSG_SEND_MAIL_RESULT: - handleSendMailResult(packet); - break; - case Opcode::SMSG_RECEIVED_MAIL: - handleReceivedMail(packet); - break; - case Opcode::MSG_QUERY_NEXT_MAIL_TIME: - handleQueryNextMailTime(packet); - break; - case Opcode::SMSG_CHANNEL_LIST: { - // string channelName + uint8 flags + uint32 count + count×(uint64 guid + uint8 memberFlags) - std::string chanName = packet.readString(); - if (packet.getSize() - packet.getReadPos() < 5) break; - /*uint8_t chanFlags =*/ packet.readUInt8(); - uint32_t memberCount = packet.readUInt32(); - memberCount = std::min(memberCount, 200u); - addSystemChatMessage(chanName + " has " + std::to_string(memberCount) + " member(s):"); - for (uint32_t i = 0; i < memberCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 9) break; - uint64_t memberGuid = packet.readUInt64(); - uint8_t memberFlags = packet.readUInt8(); - // Look up the name from our entity manager - auto entity = entityManager.getEntity(memberGuid); - std::string name = "(unknown)"; - if (entity) { - auto player = std::dynamic_pointer_cast(entity); - if (player && !player->getName().empty()) name = player->getName(); - } - std::string entry = " " + name; - if (memberFlags & 0x01) entry += " [Moderator]"; - if (memberFlags & 0x02) entry += " [Muted]"; - addSystemChatMessage(entry); - LOG_DEBUG(" channel member: 0x", std::hex, memberGuid, std::dec, - " flags=", (int)memberFlags, " name=", name); - } - break; - } - case Opcode::SMSG_INSPECT_RESULTS_UPDATE: - handleInspectResults(packet); - break; - - // ---- Bank ---- - case Opcode::SMSG_SHOW_BANK: - handleShowBank(packet); - break; - case Opcode::SMSG_BUY_BANK_SLOT_RESULT: - handleBuyBankSlotResult(packet); - break; - - // ---- Guild Bank ---- - case Opcode::SMSG_GUILD_BANK_LIST: - handleGuildBankList(packet); - break; - - // ---- Auction House ---- - case Opcode::MSG_AUCTION_HELLO: - handleAuctionHello(packet); - break; - case Opcode::SMSG_AUCTION_LIST_RESULT: - handleAuctionListResult(packet); - break; - case Opcode::SMSG_AUCTION_OWNER_LIST_RESULT: - handleAuctionOwnerListResult(packet); - break; - case Opcode::SMSG_AUCTION_BIDDER_LIST_RESULT: - handleAuctionBidderListResult(packet); - break; - case Opcode::SMSG_AUCTION_COMMAND_RESULT: - handleAuctionCommandResult(packet); - break; - case Opcode::SMSG_AUCTION_OWNER_NOTIFICATION: { - // auctionId(u32) + action(u32) + error(u32) + itemEntry(u32) + ... - if (packet.getSize() - packet.getReadPos() >= 16) { - uint32_t auctionId = packet.readUInt32(); - uint32_t action = packet.readUInt32(); - uint32_t error = packet.readUInt32(); - uint32_t itemEntry = packet.readUInt32(); - (void)auctionId; (void)action; (void)error; - ensureItemInfo(itemEntry); - auto* info = getItemInfo(itemEntry); - std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry)); - addSystemChatMessage("Your auction of " + itemName + " has sold!"); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_AUCTION_BIDDER_NOTIFICATION: { - // auctionId(u32) + itemEntry(u32) + ... - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t auctionId = packet.readUInt32(); - uint32_t itemEntry = packet.readUInt32(); - (void)auctionId; - ensureItemInfo(itemEntry); - auto* info = getItemInfo(itemEntry); - std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry)); - addSystemChatMessage("You have been outbid on " + itemName + "."); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_AUCTION_REMOVED_NOTIFICATION: { - // uint32 auctionId + uint32 itemEntry + uint32 itemRandom — auction expired/cancelled - if (packet.getSize() - packet.getReadPos() >= 12) { - /*uint32_t auctionId =*/ packet.readUInt32(); - uint32_t itemEntry = packet.readUInt32(); - /*uint32_t itemRandom =*/ packet.readUInt32(); - ensureItemInfo(itemEntry); - auto* info = getItemInfo(itemEntry); - std::string itemName = info ? info->name : ("Item #" + std::to_string(itemEntry)); - addSystemChatMessage("Your auction of " + itemName + " has expired."); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_OPEN_CONTAINER: { - // uint64 containerGuid — tells client to open this container - // The actual items come via update packets; we just log this. - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t containerGuid = packet.readUInt64(); - LOG_DEBUG("SMSG_OPEN_CONTAINER: guid=0x", std::hex, containerGuid, std::dec); - } - break; - } - case Opcode::SMSG_GM_TICKET_STATUS_UPDATE: - // GM ticket status (new/updated); no ticket UI yet - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_PLAYER_VEHICLE_DATA: - // Vehicle data update for player in vehicle; no vehicle UI yet - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_SET_EXTRA_AURA_INFO_NEED_UPDATE: - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_TAXINODE_STATUS: { - // guid(8) + status(1): status 1 = NPC has available/new routes for this player - if (packet.getSize() - packet.getReadPos() >= 9) { - uint64_t npcGuid = packet.readUInt64(); - uint8_t status = packet.readUInt8(); - taxiNpcHasRoutes_[npcGuid] = (status != 0); - } - break; - } - case Opcode::SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE: - case Opcode::SMSG_SET_EXTRA_AURA_INFO_OBSOLETE: { - // TBC 2.4.3 aura tracking: replaces SMSG_AURA_UPDATE which doesn't exist in TBC. - // Format: uint64 targetGuid + uint8 count + N×{uint8 slot, uint32 spellId, - // uint8 effectIndex, uint8 flags, uint32 durationMs, uint32 maxDurationMs} - const bool isInit = (*logicalOp == Opcode::SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE); - auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (remaining() < 9) { packet.setReadPos(packet.getSize()); break; } - uint64_t auraTargetGuid = packet.readUInt64(); - uint8_t count = packet.readUInt8(); - - std::vector* auraList = nullptr; - if (auraTargetGuid == playerGuid) auraList = &playerAuras; - else if (auraTargetGuid == targetGuid) auraList = &targetAuras; - - if (auraList && isInit) auraList->clear(); - - uint64_t nowMs = static_cast( - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()).count()); - - for (uint8_t i = 0; i < count && remaining() >= 15; i++) { - uint8_t slot = packet.readUInt8(); // 1 byte - uint32_t spellId = packet.readUInt32(); // 4 bytes - (void) packet.readUInt8(); // effectIndex: 1 byte (unused for slot display) - uint8_t flags = packet.readUInt8(); // 1 byte - uint32_t durationMs = packet.readUInt32(); // 4 bytes - uint32_t maxDurMs = packet.readUInt32(); // 4 bytes — total 15 bytes per entry - - if (auraList) { - while (auraList->size() <= slot) auraList->push_back(AuraSlot{}); - AuraSlot& a = (*auraList)[slot]; - a.spellId = spellId; - a.flags = flags; - a.durationMs = (durationMs == 0xFFFFFFFF) ? -1 : static_cast(durationMs); - a.maxDurationMs= (maxDurMs == 0xFFFFFFFF) ? -1 : static_cast(maxDurMs); - a.receivedAtMs = nowMs; - } - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::MSG_MOVE_WORLDPORT_ACK: - // Client uses this outbound; treat inbound variant as no-op for robustness. - packet.setReadPos(packet.getSize()); - break; - case Opcode::MSG_MOVE_TIME_SKIPPED: - // Observed custom server packet (8 bytes). Safe-consume for now. - packet.setReadPos(packet.getSize()); - break; - - // ---- Logout cancel ACK ---- - case Opcode::SMSG_LOGOUT_CANCEL_ACK: - // loggingOut_ already cleared by cancelLogout(); this is server's confirmation - packet.setReadPos(packet.getSize()); - break; - - // ---- Guild decline ---- - case Opcode::SMSG_GUILD_DECLINE: { - if (packet.getReadPos() < packet.getSize()) { - std::string name = packet.readString(); - addSystemChatMessage(name + " declined your guild invitation."); - } - break; - } - - // ---- Talents involuntarily reset ---- - case Opcode::SMSG_TALENTS_INVOLUNTARILY_RESET: - addSystemChatMessage("Your talents have been reset by the server."); - packet.setReadPos(packet.getSize()); - break; - - // ---- Account data sync ---- - case Opcode::SMSG_UPDATE_ACCOUNT_DATA: - case Opcode::SMSG_UPDATE_ACCOUNT_DATA_COMPLETE: - packet.setReadPos(packet.getSize()); - break; - - // ---- Rest state ---- - case Opcode::SMSG_SET_REST_START: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t restTrigger = packet.readUInt32(); - isResting_ = (restTrigger > 0); - addSystemChatMessage(isResting_ ? "You are now resting." - : "You are no longer resting."); - } - break; - } - - // ---- Aura duration update ---- - case Opcode::SMSG_UPDATE_AURA_DURATION: { - if (packet.getSize() - packet.getReadPos() >= 5) { - uint8_t slot = packet.readUInt8(); - uint32_t durationMs = packet.readUInt32(); - handleUpdateAuraDuration(slot, durationMs); - } - break; - } - - // ---- Item name query response ---- - case Opcode::SMSG_ITEM_NAME_QUERY_RESPONSE: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t itemId = packet.readUInt32(); - std::string name = packet.readString(); - if (!itemInfoCache_.count(itemId) && !name.empty()) { - ItemQueryResponseData stub; - stub.entry = itemId; - stub.name = std::move(name); - stub.valid = true; - itemInfoCache_[itemId] = std::move(stub); - } - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Mount special animation ---- - case Opcode::SMSG_MOUNTSPECIAL_ANIM: - (void)UpdateObjectParser::readPackedGuid(packet); - break; - - // ---- Character customisation / faction change results ---- - case Opcode::SMSG_CHAR_CUSTOMIZE: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t result = packet.readUInt8(); - addSystemChatMessage(result == 0 ? "Character customization complete." - : "Character customization failed."); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_CHAR_FACTION_CHANGE: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t result = packet.readUInt8(); - addSystemChatMessage(result == 0 ? "Faction change complete." - : "Faction change failed."); - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Invalidate cached player data ---- - case Opcode::SMSG_INVALIDATE_PLAYER: { - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t guid = packet.readUInt64(); - playerNameCache.erase(guid); - } - break; - } - - // ---- Movie trigger ---- - case Opcode::SMSG_TRIGGER_MOVIE: - packet.setReadPos(packet.getSize()); - break; - - // ---- Equipment sets ---- - case Opcode::SMSG_EQUIPMENT_SET_LIST: - handleEquipmentSetList(packet); - break; - case Opcode::SMSG_EQUIPMENT_SET_USE_RESULT: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t result = packet.readUInt8(); - if (result != 0) addSystemChatMessage("Failed to equip item set."); - } - break; - } - - // ---- LFG informational (not yet surfaced in UI) ---- - case Opcode::SMSG_LFG_UPDATE: - case Opcode::SMSG_LFG_UPDATE_LFG: - case Opcode::SMSG_LFG_UPDATE_LFM: - case Opcode::SMSG_LFG_UPDATE_QUEUED: - case Opcode::SMSG_LFG_PENDING_INVITE: - case Opcode::SMSG_LFG_PENDING_MATCH: - case Opcode::SMSG_LFG_PENDING_MATCH_DONE: - packet.setReadPos(packet.getSize()); - break; - - // ---- GM Ticket responses ---- - case Opcode::SMSG_GMTICKET_CREATE: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t res = packet.readUInt8(); - addSystemChatMessage(res == 1 ? "GM ticket submitted." - : "Failed to submit GM ticket."); - } - break; - } - case Opcode::SMSG_GMTICKET_UPDATETEXT: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t res = packet.readUInt8(); - addSystemChatMessage(res == 1 ? "GM ticket updated." - : "Failed to update GM ticket."); - } - break; - } - case Opcode::SMSG_GMTICKET_DELETETICKET: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t res = packet.readUInt8(); - addSystemChatMessage(res == 9 ? "GM ticket deleted." - : "No ticket to delete."); - } - break; - } - case Opcode::SMSG_GMTICKET_GETTICKET: - case Opcode::SMSG_GMTICKET_SYSTEMSTATUS: - packet.setReadPos(packet.getSize()); - break; - - // ---- DK rune tracking ---- - case Opcode::SMSG_CONVERT_RUNE: { - // uint8 runeIndex + uint8 newRuneType (0=Blood,1=Unholy,2=Frost,3=Death) - if (packet.getSize() - packet.getReadPos() < 2) { - packet.setReadPos(packet.getSize()); - break; - } - uint8_t idx = packet.readUInt8(); - uint8_t type = packet.readUInt8(); - if (idx < 6) playerRunes_[idx].type = static_cast(type & 0x3); - break; - } - case Opcode::SMSG_RESYNC_RUNES: { - // uint8 runeReadyMask (bit i=1 → rune i is ready) - // uint8[6] cooldowns (0=ready, 255=just used → readyFraction = 1 - val/255) - if (packet.getSize() - packet.getReadPos() < 7) { - packet.setReadPos(packet.getSize()); - break; - } - uint8_t readyMask = packet.readUInt8(); - for (int i = 0; i < 6; i++) { - uint8_t cd = packet.readUInt8(); - playerRunes_[i].ready = (readyMask & (1u << i)) != 0; - playerRunes_[i].readyFraction = 1.0f - cd / 255.0f; - if (playerRunes_[i].ready) playerRunes_[i].readyFraction = 1.0f; - } - break; - } - case Opcode::SMSG_ADD_RUNE_POWER: { - // uint32 runeMask (bit i=1 → rune i just became ready) - if (packet.getSize() - packet.getReadPos() < 4) { - packet.setReadPos(packet.getSize()); - break; - } - uint32_t runeMask = packet.readUInt32(); - for (int i = 0; i < 6; i++) { - if (runeMask & (1u << i)) { - playerRunes_[i].ready = true; - playerRunes_[i].readyFraction = 1.0f; - } - } - break; - } - - // ---- Spell combat logs (consume) ---- - case Opcode::SMSG_AURACASTLOG: - case Opcode::SMSG_SPELLBREAKLOG: - case Opcode::SMSG_SPELLDAMAGESHIELD: { - // Classic/TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4) - // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) - const bool shieldClassicLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - const size_t shieldMinSz = shieldClassicLike ? 24u : 2u; - if (packet.getSize() - packet.getReadPos() < shieldMinSz) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t victimGuid = shieldClassicLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - uint64_t casterGuid = shieldClassicLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 12) { - packet.setReadPos(packet.getSize()); break; - } - /*uint32_t spellId =*/ packet.readUInt32(); - uint32_t damage = packet.readUInt32(); - if (!shieldClassicLike && packet.getSize() - packet.getReadPos() >= 4) - /*uint32_t absorbed =*/ packet.readUInt32(); - /*uint32_t school =*/ packet.readUInt32(); - // Show combat text: damage shield reflect - if (casterGuid == playerGuid) { - // We have a damage shield that reflected damage - addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(damage), 0, true); - } else if (victimGuid == playerGuid) { - // A damage shield hit us (e.g. target's Thorns) - addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(damage), 0, false); - } - break; - } - case Opcode::SMSG_SPELLORDAMAGE_IMMUNE: { - // WotLK: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType - // TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8 - const bool immuneTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - const size_t minSz = immuneTbcLike ? 21u : 2u; - if (packet.getSize() - packet.getReadPos() < minSz) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = immuneTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (immuneTbcLike ? 8u : 2u)) break; - uint64_t victimGuid = immuneTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 5) break; - /*uint32_t spellId =*/ packet.readUInt32(); - /*uint8_t saveType =*/ packet.readUInt8(); - // Show IMMUNE text when the player is the caster (we hit an immune target) - // or the victim (we are immune) - if (casterGuid == playerGuid || victimGuid == playerGuid) { - addCombatText(CombatTextEntry::IMMUNE, 0, 0, - casterGuid == playerGuid); - } - break; - } - case Opcode::SMSG_SPELLDISPELLOG: { - // WotLK: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen - // TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + ... - // + uint32 count + count × (uint32 dispelled_spellId + uint32 unk) - const bool dispelTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (dispelTbcLike ? 8u : 2u)) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = dispelTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (dispelTbcLike ? 8u : 2u)) break; - uint64_t victimGuid = dispelTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 9) break; - /*uint32_t dispelSpell =*/ packet.readUInt32(); - uint8_t isStolen = packet.readUInt8(); - uint32_t count = packet.readUInt32(); - // Show system message if player was victim or caster - if (victimGuid == playerGuid || casterGuid == playerGuid) { - const char* verb = isStolen ? "stolen" : "dispelled"; - // Collect first dispelled spell name for the message - // Each entry: uint32 spellId + uint8 isPositive (5 bytes in WotLK/TBC/Classic) - std::string firstSpellName; - for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 5; ++i) { - uint32_t dispelledId = packet.readUInt32(); - /*uint8_t isPositive =*/ packet.readUInt8(); - if (i == 0) { - const std::string& nm = getSpellName(dispelledId); - firstSpellName = nm.empty() ? ("spell " + std::to_string(dispelledId)) : nm; - } - } - if (!firstSpellName.empty()) { - char buf[256]; - if (victimGuid == playerGuid && casterGuid != playerGuid) - std::snprintf(buf, sizeof(buf), "%s was %s.", firstSpellName.c_str(), verb); - else if (casterGuid == playerGuid) - std::snprintf(buf, sizeof(buf), "You %s %s.", verb, firstSpellName.c_str()); - else - std::snprintf(buf, sizeof(buf), "%s %s.", firstSpellName.c_str(), verb); - addSystemChatMessage(buf); - } - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELLSTEALLOG: { - // Sent to the CASTER (Mage) when Spellsteal succeeds. - // Wire format mirrors SPELLDISPELLOG: - // WotLK: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count - // + count × (uint32 stolenSpellId + uint8 isPositive) - // TBC/Classic: full uint64 victim + full uint64 caster + same tail - const bool stealTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) { - packet.setReadPos(packet.getSize()); break; - } - /*uint64_t stealVictim =*/ stealTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t stealCaster = stealTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 9) { - packet.setReadPos(packet.getSize()); break; - } - /*uint32_t stealSpellId =*/ packet.readUInt32(); - /*uint8_t isStolen =*/ packet.readUInt8(); - uint32_t stealCount = packet.readUInt32(); - // Show feedback only when we are the caster (we stole something) - if (stealCaster == playerGuid) { - std::string stolenName; - for (uint32_t i = 0; i < stealCount && packet.getSize() - packet.getReadPos() >= 5; ++i) { - uint32_t stolenId = packet.readUInt32(); - /*uint8_t isPos =*/ packet.readUInt8(); - if (i == 0) { - const std::string& nm = getSpellName(stolenId); - stolenName = nm.empty() ? ("spell " + std::to_string(stolenId)) : nm; - } - } - if (!stolenName.empty()) { - char buf[256]; - std::snprintf(buf, sizeof(buf), "You stole %s.", stolenName.c_str()); - addSystemChatMessage(buf); - } - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELLINSTAKILLLOG: - case Opcode::SMSG_SPELLLOGEXECUTE: - case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: - case Opcode::SMSG_SPELL_CHANCE_RESIST_PUSHBACK: - case Opcode::SMSG_SPELL_UPDATE_CHAIN_TARGETS: - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_CLEAR_EXTRA_AURA_INFO: { - // TBC 2.4.3: clear a single aura slot for a unit - // Format: uint64 targetGuid + uint8 slot - if (packet.getSize() - packet.getReadPos() >= 9) { - uint64_t clearGuid = packet.readUInt64(); - uint8_t slot = packet.readUInt8(); - std::vector* auraList = nullptr; - if (clearGuid == playerGuid) auraList = &playerAuras; - else if (clearGuid == targetGuid) auraList = &targetAuras; - if (auraList && slot < auraList->size()) { - (*auraList)[slot] = AuraSlot{}; - } - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Misc consume ---- - case Opcode::SMSG_COMPLAIN_RESULT: - case Opcode::SMSG_ITEM_REFUND_INFO_RESPONSE: - case Opcode::SMSG_ITEM_ENCHANT_TIME_UPDATE: - case Opcode::SMSG_LOOT_LIST: - // Consume — not yet processed - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_RESUME_CAST_BAR: { - // WotLK: packed_guid caster + packed_guid target + uint32 spellId + uint32 remainingMs + uint32 totalMs + uint8 schoolMask - // TBC/Classic: uint64 caster + uint64 target + ... - const bool rcbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); - auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (remaining() < (rcbTbc ? 8u : 1u)) break; - uint64_t caster = rcbTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (remaining() < (rcbTbc ? 8u : 1u)) break; - if (rcbTbc) packet.readUInt64(); // target (discard) - else (void)UpdateObjectParser::readPackedGuid(packet); // target - if (remaining() < 12) break; - uint32_t spellId = packet.readUInt32(); - uint32_t remainMs = packet.readUInt32(); - uint32_t totalMs = packet.readUInt32(); - if (totalMs > 0) { - if (caster == playerGuid) { - casting = true; - castIsChannel = false; - currentCastSpellId = spellId; - castTimeTotal = totalMs / 1000.0f; - castTimeRemaining = remainMs / 1000.0f; - } else { - auto& s = unitCastStates_[caster]; - s.casting = true; - s.spellId = spellId; - s.timeTotal = totalMs / 1000.0f; - s.timeRemaining = remainMs / 1000.0f; - } - LOG_DEBUG("SMSG_RESUME_CAST_BAR: caster=0x", std::hex, caster, std::dec, - " spell=", spellId, " remaining=", remainMs, "ms total=", totalMs, "ms"); - } - break; - } - // ---- Channeled spell start/tick (WotLK: packed GUIDs; TBC/Classic: full uint64) ---- - case Opcode::MSG_CHANNEL_START: { - // casterGuid + uint32 spellId + uint32 totalDurationMs - const bool tbcOrClassic = isClassicLikeExpansion() || isActiveExpansion("tbc"); - uint64_t chanCaster = tbcOrClassic - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) - : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) break; - uint32_t chanSpellId = packet.readUInt32(); - uint32_t chanTotalMs = packet.readUInt32(); - if (chanTotalMs > 0 && chanCaster != 0) { - if (chanCaster == playerGuid) { - casting = true; - castIsChannel = true; - currentCastSpellId = chanSpellId; - castTimeTotal = chanTotalMs / 1000.0f; - castTimeRemaining = castTimeTotal; - } else { - auto& s = unitCastStates_[chanCaster]; - s.casting = true; - s.spellId = chanSpellId; - s.timeTotal = chanTotalMs / 1000.0f; - s.timeRemaining = s.timeTotal; - } - LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec, - " spell=", chanSpellId, " total=", chanTotalMs, "ms"); - } - break; - } - case Opcode::MSG_CHANNEL_UPDATE: { - // casterGuid + uint32 remainingMs - const bool tbcOrClassic2 = isClassicLikeExpansion() || isActiveExpansion("tbc"); - uint64_t chanCaster2 = tbcOrClassic2 - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) - : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t chanRemainMs = packet.readUInt32(); - if (chanCaster2 == playerGuid) { - castTimeRemaining = chanRemainMs / 1000.0f; - if (chanRemainMs == 0) { - casting = false; - castIsChannel = false; - currentCastSpellId = 0; - } - } else if (chanCaster2 != 0) { - auto it = unitCastStates_.find(chanCaster2); - if (it != unitCastStates_.end()) { - it->second.timeRemaining = chanRemainMs / 1000.0f; - if (chanRemainMs == 0) unitCastStates_.erase(it); - } - } - LOG_DEBUG("MSG_CHANNEL_UPDATE: caster=0x", std::hex, chanCaster2, std::dec, - " remaining=", chanRemainMs, "ms"); - break; - } - - case Opcode::SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT: { - // uint32 slot + packed_guid unit (0 packed = clear slot) - if (packet.getSize() - packet.getReadPos() < 5) { - packet.setReadPos(packet.getSize()); - break; - } - uint32_t slot = packet.readUInt32(); - uint64_t unit = UpdateObjectParser::readPackedGuid(packet); - if (slot < kMaxEncounterSlots) { - encounterUnitGuids_[slot] = unit; - LOG_DEBUG("SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT: slot=", slot, - " guid=0x", std::hex, unit, std::dec); - } - break; - } - case Opcode::SMSG_UPDATE_INSTANCE_OWNERSHIP: - case Opcode::SMSG_UPDATE_LAST_INSTANCE: - case Opcode::SMSG_SEND_ALL_COMBAT_LOG: - case Opcode::SMSG_SET_PROJECTILE_POSITION: - case Opcode::SMSG_AUCTION_LIST_PENDING_SALES: - packet.setReadPos(packet.getSize()); - break; - - // ---- Server-first achievement broadcast ---- - case Opcode::SMSG_SERVER_FIRST_ACHIEVEMENT: { - // charName (cstring) + guid (uint64) + achievementId (uint32) + ... - if (packet.getReadPos() < packet.getSize()) { - std::string charName = packet.readString(); - if (packet.getSize() - packet.getReadPos() >= 12) { - /*uint64_t guid =*/ packet.readUInt64(); - uint32_t achievementId = packet.readUInt32(); - loadAchievementNameCache(); - auto nit = achievementNameCache_.find(achievementId); - char buf[256]; - if (nit != achievementNameCache_.end() && !nit->second.empty()) { - std::snprintf(buf, sizeof(buf), - "%s is the first on the realm to earn: %s!", - charName.c_str(), nit->second.c_str()); - } else { - std::snprintf(buf, sizeof(buf), - "%s is the first on the realm to earn achievement #%u!", - charName.c_str(), achievementId); - } - addSystemChatMessage(buf); - } - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Forced faction reactions ---- - case Opcode::SMSG_SET_FORCED_REACTIONS: - handleSetForcedReactions(packet); - break; - - // ---- Spline speed changes for other units ---- - case Opcode::SMSG_SPLINE_SET_FLIGHT_SPEED: - case Opcode::SMSG_SPLINE_SET_FLIGHT_BACK_SPEED: - case Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED: - case Opcode::SMSG_SPLINE_SET_WALK_SPEED: - case Opcode::SMSG_SPLINE_SET_TURN_RATE: - case Opcode::SMSG_SPLINE_SET_PITCH_RATE: { - // Minimal parse: PackedGuid + float speed - if (packet.getSize() - packet.getReadPos() < 5) break; - uint64_t sGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) break; - float sSpeed = packet.readFloat(); - if (sGuid == playerGuid && std::isfinite(sSpeed) && sSpeed > 0.01f && sSpeed < 200.0f) { - if (*logicalOp == Opcode::SMSG_SPLINE_SET_FLIGHT_SPEED) - serverFlightSpeed_ = sSpeed; - else if (*logicalOp == Opcode::SMSG_SPLINE_SET_FLIGHT_BACK_SPEED) - serverFlightBackSpeed_ = sSpeed; - else if (*logicalOp == Opcode::SMSG_SPLINE_SET_SWIM_BACK_SPEED) - serverSwimBackSpeed_ = sSpeed; - else if (*logicalOp == Opcode::SMSG_SPLINE_SET_WALK_SPEED) - serverWalkSpeed_ = sSpeed; - else if (*logicalOp == Opcode::SMSG_SPLINE_SET_TURN_RATE) - serverTurnRate_ = sSpeed; // rad/s - } - break; - } - - // ---- Spline move flag changes for other units ---- - case Opcode::SMSG_SPLINE_MOVE_UNROOT: - case Opcode::SMSG_SPLINE_MOVE_UNSET_HOVER: - case Opcode::SMSG_SPLINE_MOVE_WATER_WALK: { - // Minimal parse: PackedGuid only — no animation-relevant state change. - if (packet.getSize() - packet.getReadPos() >= 1) { - (void)UpdateObjectParser::readPackedGuid(packet); - } - break; - } - case Opcode::SMSG_SPLINE_MOVE_UNSET_FLYING: { - // PackedGuid + synthesised move-flags=0 → clears flying animation. - if (packet.getSize() - packet.getReadPos() < 1) break; - uint64_t guid = UpdateObjectParser::readPackedGuid(packet); - if (guid == 0 || guid == playerGuid || !unitMoveFlagsCallback_) break; - unitMoveFlagsCallback_(guid, 0u); // clear flying/CAN_FLY - break; - } - - // ---- Quest failure notification ---- - case Opcode::SMSG_QUESTGIVER_QUEST_FAILED: { - // uint32 questId + uint32 reason - if (packet.getSize() - packet.getReadPos() >= 8) { - /*uint32_t questId =*/ packet.readUInt32(); - uint32_t reason = packet.readUInt32(); - const char* reasonStr = "Unknown reason"; - switch (reason) { - case 1: reasonStr = "Quest failed: failed conditions"; break; - case 2: reasonStr = "Quest failed: inventory full"; break; - case 3: reasonStr = "Quest failed: too far away"; break; - case 4: reasonStr = "Quest failed: another quest is blocking"; break; - case 5: reasonStr = "Quest failed: wrong time of day"; break; - case 6: reasonStr = "Quest failed: wrong race"; break; - case 7: reasonStr = "Quest failed: wrong class"; break; - } - addSystemChatMessage(reasonStr); - } - break; - } - - // ---- Suspend comms (requires ACK) ---- - case Opcode::SMSG_SUSPEND_COMMS: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t seqIdx = packet.readUInt32(); - if (socket) { - network::Packet ack(wireOpcode(Opcode::CMSG_SUSPEND_COMMS_ACK)); - ack.writeUInt32(seqIdx); - socket->send(ack); - } - } - break; - } - - // ---- Pre-resurrect state ---- - case Opcode::SMSG_PRE_RESURRECT: { - // packed GUID of the player to enter pre-resurrect - (void)UpdateObjectParser::readPackedGuid(packet); - break; - } - - // ---- Hearthstone bind error ---- - case Opcode::SMSG_PLAYERBINDERROR: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t error = packet.readUInt32(); - if (error == 0) - addSystemChatMessage("Your hearthstone is not bound."); - else - addSystemChatMessage("Hearthstone bind failed."); - } - break; - } - - // ---- Instance/raid errors ---- - case Opcode::SMSG_RAID_GROUP_ONLY: { - addSystemChatMessage("You must be in a raid group to enter this instance."); - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_RAID_READY_CHECK_ERROR: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t err = packet.readUInt8(); - if (err == 0) addSystemChatMessage("Ready check failed: not in a group."); - else if (err == 1) addSystemChatMessage("Ready check failed: in instance."); - else addSystemChatMessage("Ready check failed."); - } - break; - } - case Opcode::SMSG_RESET_FAILED_NOTIFY: { - addSystemChatMessage("Cannot reset instance: another player is still inside."); - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Realm split ---- - case Opcode::SMSG_REALM_SPLIT: { - // uint32 splitType + uint32 deferTime + string realmName - // Client must respond with CMSG_REALM_SPLIT to avoid session timeout on some servers. - uint32_t splitType = 0; - if (packet.getSize() - packet.getReadPos() >= 4) - splitType = packet.readUInt32(); - packet.setReadPos(packet.getSize()); - if (socket) { - network::Packet resp(wireOpcode(Opcode::CMSG_REALM_SPLIT)); - resp.writeUInt32(splitType); - resp.writeString("3.3.5"); - socket->send(resp); - LOG_DEBUG("SMSG_REALM_SPLIT splitType=", splitType, " — sent CMSG_REALM_SPLIT ack"); - } - break; - } - - // ---- Real group update (status flags) ---- - case Opcode::SMSG_REAL_GROUP_UPDATE: - packet.setReadPos(packet.getSize()); - break; - - // ---- Play music (WotLK standard opcode) ---- - case Opcode::SMSG_PLAY_MUSIC: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t soundId = packet.readUInt32(); - if (playMusicCallback_) playMusicCallback_(soundId); - } - break; - } - - // ---- Play object/spell sounds ---- - case Opcode::SMSG_PLAY_OBJECT_SOUND: - case Opcode::SMSG_PLAY_SPELL_IMPACT: - if (packet.getSize() - packet.getReadPos() >= 12) { - // uint32 soundId + uint64 sourceGuid - uint32_t soundId = packet.readUInt32(); - uint64_t srcGuid = packet.readUInt64(); - LOG_DEBUG("SMSG_PLAY_OBJECT_SOUND/SPELL_IMPACT id=", soundId, " src=0x", std::hex, srcGuid, std::dec); - if (playPositionalSoundCallback_) playPositionalSoundCallback_(soundId, srcGuid); - else if (playSoundCallback_) playSoundCallback_(soundId); - } else if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t soundId = packet.readUInt32(); - if (playSoundCallback_) playSoundCallback_(soundId); - } - packet.setReadPos(packet.getSize()); - break; - - // ---- Resistance/combat log ---- - case Opcode::SMSG_RESISTLOG: { - // WotLK: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId - // + float resistFactor + uint32 targetRes + uint32 resistedValue + ... - // TBC/Classic: same but full uint64 GUIDs - // Show RESIST combat text when player resists an incoming spell. - const bool rlTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - auto rl_rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } - /*uint32_t hitInfo =*/ packet.readUInt32(); - if (rl_rem() < (rlTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t attackerGuid = rlTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (rl_rem() < (rlTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t victimGuid = rlTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } - uint32_t spellId = packet.readUInt32(); - (void)attackerGuid; - // Show RESIST when player is the victim; show as caster-side MISS when player is attacker - if (victimGuid == playerGuid) { - addCombatText(CombatTextEntry::MISS, 0, spellId, false); - } else if (attackerGuid == playerGuid) { - addCombatText(CombatTextEntry::MISS, 0, spellId, true); - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Read item results ---- - case Opcode::SMSG_READ_ITEM_OK: - addSystemChatMessage("You read the item."); - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_READ_ITEM_FAILED: - addSystemChatMessage("You cannot read this item."); - packet.setReadPos(packet.getSize()); - break; - - // ---- Completed quests query ---- - case Opcode::SMSG_QUERY_QUESTS_COMPLETED_RESPONSE: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t count = packet.readUInt32(); - if (count <= 4096) { - for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t questId = packet.readUInt32(); - completedQuests_.insert(questId); - } - LOG_DEBUG("SMSG_QUERY_QUESTS_COMPLETED_RESPONSE: ", count, " completed quests"); - } - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- PVP quest kill update ---- - case Opcode::SMSG_QUESTUPDATE_ADD_PVP_KILL: { - // WotLK 3.3.5a format: uint64 guid + uint32 questId + uint32 count + uint32 reqCount - // Classic format: uint64 guid + uint32 questId + uint32 count (no reqCount) - if (packet.getSize() - packet.getReadPos() >= 16) { - /*uint64_t guid =*/ packet.readUInt64(); - uint32_t questId = packet.readUInt32(); - uint32_t count = packet.readUInt32(); - uint32_t reqCount = 0; - if (packet.getSize() - packet.getReadPos() >= 4) { - reqCount = packet.readUInt32(); - } - - // Update quest log kill counts (PvP kills use entry=0 as the key - // since there's no specific creature entry — one slot per quest). - constexpr uint32_t PVP_KILL_ENTRY = 0u; - for (auto& quest : questLog_) { - if (quest.questId != questId) continue; - - if (reqCount == 0) { - auto it = quest.killCounts.find(PVP_KILL_ENTRY); - if (it != quest.killCounts.end()) reqCount = it->second.second; - } - if (reqCount == 0) { - // Pull required count from kill objectives (npcOrGoId == 0 slot, if any) - for (const auto& obj : quest.killObjectives) { - if (obj.npcOrGoId == 0 && obj.required > 0) { - reqCount = obj.required; - break; - } - } - } - if (reqCount == 0) reqCount = count; - quest.killCounts[PVP_KILL_ENTRY] = {count, reqCount}; - - std::string progressMsg = quest.title + ": PvP kills " + - std::to_string(count) + "/" + std::to_string(reqCount); - addSystemChatMessage(progressMsg); - break; - } - } - break; - } - - // ---- NPC not responding ---- - case Opcode::SMSG_NPC_WONT_TALK: - addSystemChatMessage("That creature can't talk to you right now."); - packet.setReadPos(packet.getSize()); - break; - - // ---- Petition ---- - case Opcode::SMSG_OFFER_PETITION_ERROR: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t err = packet.readUInt32(); - if (err == 1) addSystemChatMessage("Player is already in a guild."); - else if (err == 2) addSystemChatMessage("Player already has a petition."); - else addSystemChatMessage("Cannot offer petition to that player."); - } - break; - } - case Opcode::SMSG_PETITION_QUERY_RESPONSE: - case Opcode::SMSG_PETITION_SHOW_SIGNATURES: - case Opcode::SMSG_PETITION_SIGN_RESULTS: - packet.setReadPos(packet.getSize()); - break; - - // ---- Pet system ---- - case Opcode::SMSG_PET_MODE: { - // uint64 petGuid, uint32 mode - // mode bits: low byte = command state, next byte = react state - if (packet.getSize() - packet.getReadPos() >= 12) { - uint64_t modeGuid = packet.readUInt64(); - uint32_t mode = packet.readUInt32(); - if (modeGuid == petGuid_) { - petCommand_ = static_cast(mode & 0xFF); - petReact_ = static_cast((mode >> 8) & 0xFF); - LOG_DEBUG("SMSG_PET_MODE: command=", (int)petCommand_, - " react=", (int)petReact_); - } - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_PET_BROKEN: - // Pet bond broken (died or forcibly dismissed) — clear pet state - petGuid_ = 0; - petSpellList_.clear(); - petAutocastSpells_.clear(); - memset(petActionSlots_, 0, sizeof(petActionSlots_)); - addSystemChatMessage("Your pet has died."); - LOG_INFO("SMSG_PET_BROKEN: pet bond broken"); - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_PET_LEARNED_SPELL: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t spellId = packet.readUInt32(); - petSpellList_.push_back(spellId); - LOG_DEBUG("SMSG_PET_LEARNED_SPELL: spellId=", spellId); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_PET_UNLEARNED_SPELL: { - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t spellId = packet.readUInt32(); - petSpellList_.erase( - std::remove(petSpellList_.begin(), petSpellList_.end(), spellId), - petSpellList_.end()); - petAutocastSpells_.erase(spellId); - LOG_DEBUG("SMSG_PET_UNLEARNED_SPELL: spellId=", spellId); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_PET_CAST_FAILED: { - if (packet.getSize() - packet.getReadPos() >= 5) { - uint8_t castCount = packet.readUInt8(); - uint32_t spellId = packet.readUInt32(); - uint32_t reason = (packet.getSize() - packet.getReadPos() >= 4) - ? packet.readUInt32() : 0; - LOG_DEBUG("SMSG_PET_CAST_FAILED: spell=", spellId, - " reason=", reason, " castCount=", (int)castCount); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_PET_GUIDS: - case Opcode::SMSG_PET_DISMISS_SOUND: - case Opcode::SMSG_PET_ACTION_SOUND: - case Opcode::SMSG_PET_UNLEARN_CONFIRM: - case Opcode::SMSG_PET_NAME_INVALID: - case Opcode::SMSG_PET_RENAMEABLE: - case Opcode::SMSG_PET_UPDATE_COMBO_POINTS: - packet.setReadPos(packet.getSize()); - break; - - // ---- Inspect (full character inspection) ---- - case Opcode::SMSG_INSPECT: - packet.setReadPos(packet.getSize()); - break; - - // ---- Multiple aggregated packets/moves ---- - case Opcode::SMSG_MULTIPLE_MOVES: - // Same wire format as SMSG_COMPRESSED_MOVES: uint8 size + uint16 opcode + payload[] - handleCompressedMoves(packet); - break; - - case Opcode::SMSG_MULTIPLE_PACKETS: { - // Each sub-packet uses the standard WotLK server wire format: - // uint16_be subSize (includes the 2-byte opcode; payload = subSize - 2) - // uint16_le subOpcode - // payload (subSize - 2 bytes) - const auto& pdata = packet.getData(); - size_t dataLen = pdata.size(); - size_t pos = packet.getReadPos(); - static uint32_t multiPktWarnCount = 0; - while (pos + 4 <= dataLen) { - uint16_t subSize = static_cast( - (static_cast(pdata[pos]) << 8) | pdata[pos + 1]); - if (subSize < 2) break; - size_t payloadLen = subSize - 2; - if (pos + 4 + payloadLen > dataLen) { - if (++multiPktWarnCount <= 10) { - LOG_WARNING("SMSG_MULTIPLE_PACKETS: sub-packet overruns buffer at pos=", - pos, " subSize=", subSize, " dataLen=", dataLen); - } - break; - } - uint16_t subOpcode = static_cast(pdata[pos + 2]) | - (static_cast(pdata[pos + 3]) << 8); - std::vector subPayload(pdata.begin() + pos + 4, - pdata.begin() + pos + 4 + payloadLen); - network::Packet subPacket(subOpcode, std::move(subPayload)); - handlePacket(subPacket); - pos += 4 + payloadLen; - } - packet.setReadPos(packet.getSize()); - break; - } - - // ---- Misc consume ---- - case Opcode::SMSG_SET_PLAYER_DECLINED_NAMES_RESULT: - case Opcode::SMSG_PROPOSE_LEVEL_GRANT: - case Opcode::SMSG_REFER_A_FRIEND_EXPIRED: - case Opcode::SMSG_REFER_A_FRIEND_FAILURE: - case Opcode::SMSG_REPORT_PVP_AFK_RESULT: - case Opcode::SMSG_REDIRECT_CLIENT: - case Opcode::SMSG_PVP_QUEUE_STATS: - case Opcode::SMSG_NOTIFY_DEST_LOC_SPELL_CAST: - case Opcode::SMSG_RESPOND_INSPECT_ACHIEVEMENTS: - case Opcode::SMSG_PLAYER_SKINNED: - case Opcode::SMSG_QUEST_POI_QUERY_RESPONSE: - handleQuestPoiQueryResponse(packet); - break; - case Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA: - case Opcode::SMSG_RESET_RANGED_COMBAT_TIMER: - case Opcode::SMSG_PROFILEDATA_RESPONSE: - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_PLAY_TIME_WARNING: { - // uint32 type (0=normal, 1=heavy, 2=tired/restricted) + uint32 minutes played - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t warnType = packet.readUInt32(); - uint32_t minutesPlayed = (packet.getSize() - packet.getReadPos() >= 4) - ? packet.readUInt32() : 0; - const char* severity = (warnType >= 2) ? "[Tired] " : "[Play Time] "; - char buf[128]; - if (minutesPlayed > 0) { - uint32_t h = minutesPlayed / 60; - uint32_t m = minutesPlayed % 60; - if (h > 0) - std::snprintf(buf, sizeof(buf), "%sYou have been playing for %uh %um.", severity, h, m); - else - std::snprintf(buf, sizeof(buf), "%sYou have been playing for %um.", severity, m); - } else { - std::snprintf(buf, sizeof(buf), "%sYou have been playing for a long time.", severity); - } - addSystemChatMessage(buf); - addUIError(buf); - } - break; - } - - // ---- Item query multiple (same format as single, re-use handler) ---- - case Opcode::SMSG_ITEM_QUERY_MULTIPLE_RESPONSE: - handleItemQueryResponse(packet); - break; - - // ---- Object position/rotation queries ---- - case Opcode::SMSG_QUERY_OBJECT_POSITION: - case Opcode::SMSG_QUERY_OBJECT_ROTATION: - case Opcode::SMSG_VOICESESSION_FULL: - packet.setReadPos(packet.getSize()); - break; - - // ---- Player movement flag changes (server-pushed) ---- - case Opcode::SMSG_MOVE_GRAVITY_DISABLE: - handleForceMoveFlagChange(packet, "GRAVITY_DISABLE", Opcode::CMSG_MOVE_GRAVITY_DISABLE_ACK, - static_cast(MovementFlags::LEVITATING), true); - break; - case Opcode::SMSG_MOVE_GRAVITY_ENABLE: - handleForceMoveFlagChange(packet, "GRAVITY_ENABLE", Opcode::CMSG_MOVE_GRAVITY_ENABLE_ACK, - static_cast(MovementFlags::LEVITATING), false); - break; - case Opcode::SMSG_MOVE_LAND_WALK: - handleForceMoveFlagChange(packet, "LAND_WALK", Opcode::CMSG_MOVE_WATER_WALK_ACK, - static_cast(MovementFlags::WATER_WALK), false); - break; - case Opcode::SMSG_MOVE_NORMAL_FALL: - handleForceMoveFlagChange(packet, "NORMAL_FALL", Opcode::CMSG_MOVE_FEATHER_FALL_ACK, - static_cast(MovementFlags::FEATHER_FALL), false); - break; - case Opcode::SMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: - handleForceMoveFlagChange(packet, "SET_CAN_TRANSITION_SWIM_FLY", - Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, true); - break; - case Opcode::SMSG_MOVE_UNSET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY: - handleForceMoveFlagChange(packet, "UNSET_CAN_TRANSITION_SWIM_FLY", - Opcode::CMSG_MOVE_SET_CAN_TRANSITION_BETWEEN_SWIM_AND_FLY_ACK, 0, false); - break; - case Opcode::SMSG_MOVE_SET_COLLISION_HGT: - handleMoveSetCollisionHeight(packet); - break; - case Opcode::SMSG_MOVE_SET_FLIGHT: - handleForceMoveFlagChange(packet, "SET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK, - static_cast(MovementFlags::FLYING), true); - break; - case Opcode::SMSG_MOVE_UNSET_FLIGHT: - handleForceMoveFlagChange(packet, "UNSET_FLIGHT", Opcode::CMSG_MOVE_FLIGHT_ACK, - static_cast(MovementFlags::FLYING), false); - break; - - default: - // In pre-world states we need full visibility (char create/login handshakes). - // In-world we keep de-duplication to avoid heavy log I/O in busy areas. - if (state != WorldState::IN_WORLD) { - static std::unordered_set loggedUnhandledByState; - const uint32_t key = (static_cast(static_cast(state)) << 16) | - static_cast(opcode); - if (loggedUnhandledByState.insert(key).second) { - LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec, - " state=", static_cast(state), - " size=", packet.getSize()); - const auto& data = packet.getData(); - std::string hex; - size_t limit = std::min(data.size(), 48); - hex.reserve(limit * 3); - for (size_t i = 0; i < limit; ++i) { - char b[4]; - snprintf(b, sizeof(b), "%02x ", data[i]); - hex += b; - } - LOG_INFO("Unhandled opcode payload hex (first ", limit, " bytes): ", hex); - } - } else { - static std::unordered_set loggedUnhandledOpcodes; - if (loggedUnhandledOpcodes.insert(static_cast(opcode)).second) { - LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec); - } - } - break; } } catch (const std::bad_alloc& e) { LOG_ERROR("OOM while handling world opcode=0x", std::hex, opcode, std::dec, @@ -6285,6 +7933,161 @@ void GameHandler::handlePacket(network::Packet& packet) { } } +void GameHandler::enqueueIncomingPacket(const network::Packet& packet) { + if (pendingIncomingPackets_.size() >= kMaxQueuedInboundPackets) { + LOG_ERROR("Inbound packet queue overflow (", pendingIncomingPackets_.size(), + " packets); dropping oldest packet to preserve responsiveness"); + pendingIncomingPackets_.pop_front(); + } + pendingIncomingPackets_.push_back(packet); + lastRxTime_ = std::chrono::steady_clock::now(); + rxSilenceLogged_ = false; +} + +void GameHandler::enqueueIncomingPacketFront(network::Packet&& packet) { + if (pendingIncomingPackets_.size() >= kMaxQueuedInboundPackets) { + LOG_ERROR("Inbound packet queue overflow while prepending (", pendingIncomingPackets_.size(), + " packets); dropping newest queued packet to preserve ordering"); + pendingIncomingPackets_.pop_back(); + } + pendingIncomingPackets_.emplace_front(std::move(packet)); +} + +void GameHandler::enqueueUpdateObjectWork(UpdateObjectData&& data) { + pendingUpdateObjectWork_.push_back(PendingUpdateObjectWork{std::move(data)}); +} + +void GameHandler::processPendingUpdateObjectWork(const std::chrono::steady_clock::time_point& start, + float budgetMs) { + if (pendingUpdateObjectWork_.empty()) { + return; + } + + const int maxBlocksThisUpdate = updateObjectBlocksBudgetPerUpdate(state); + int processedBlocks = 0; + + while (!pendingUpdateObjectWork_.empty() && processedBlocks < maxBlocksThisUpdate) { + float elapsedMs = std::chrono::duration( + std::chrono::steady_clock::now() - start).count(); + if (elapsedMs >= budgetMs) { + break; + } + + auto& work = pendingUpdateObjectWork_.front(); + if (!work.outOfRangeProcessed) { + auto outOfRangeStart = std::chrono::steady_clock::now(); + processOutOfRangeObjects(work.data.outOfRangeGuids); + float outOfRangeMs = std::chrono::duration( + std::chrono::steady_clock::now() - outOfRangeStart).count(); + if (outOfRangeMs > slowUpdateObjectBlockLogThresholdMs()) { + LOG_WARNING("SLOW update-object out-of-range handling: ", outOfRangeMs, + "ms guidCount=", work.data.outOfRangeGuids.size()); + } + work.outOfRangeProcessed = true; + } + + while (work.nextBlockIndex < work.data.blocks.size() && processedBlocks < maxBlocksThisUpdate) { + elapsedMs = std::chrono::duration( + std::chrono::steady_clock::now() - start).count(); + if (elapsedMs >= budgetMs) { + break; + } + + const UpdateBlock& block = work.data.blocks[work.nextBlockIndex]; + auto blockStart = std::chrono::steady_clock::now(); + applyUpdateObjectBlock(block, work.newItemCreated); + float blockMs = std::chrono::duration( + std::chrono::steady_clock::now() - blockStart).count(); + if (blockMs > slowUpdateObjectBlockLogThresholdMs()) { + LOG_WARNING("SLOW update-object block apply: ", blockMs, + "ms index=", work.nextBlockIndex, + " type=", static_cast(block.updateType), + " guid=0x", std::hex, block.guid, std::dec, + " objectType=", static_cast(block.objectType), + " fieldCount=", block.fields.size(), + " hasMovement=", block.hasMovement ? 1 : 0); + } + ++work.nextBlockIndex; + ++processedBlocks; + } + + if (work.nextBlockIndex >= work.data.blocks.size()) { + finalizeUpdateObjectBatch(work.newItemCreated); + pendingUpdateObjectWork_.pop_front(); + continue; + } + break; + } + + if (!pendingUpdateObjectWork_.empty()) { + const auto& work = pendingUpdateObjectWork_.front(); + LOG_DEBUG("GameHandler update-object budget reached (remainingBatches=", + pendingUpdateObjectWork_.size(), ", nextBlockIndex=", work.nextBlockIndex, + "/", work.data.blocks.size(), ", state=", worldStateName(state), ")"); + } +} + +void GameHandler::processQueuedIncomingPackets() { + if (pendingIncomingPackets_.empty() && pendingUpdateObjectWork_.empty()) { + return; + } + + const int maxPacketsThisUpdate = incomingPacketsBudgetPerUpdate(state); + const float budgetMs = incomingPacketBudgetMs(state); + const auto start = std::chrono::steady_clock::now(); + int processed = 0; + + while (processed < maxPacketsThisUpdate) { + float elapsedMs = std::chrono::duration( + std::chrono::steady_clock::now() - start).count(); + if (elapsedMs >= budgetMs) { + break; + } + + if (!pendingUpdateObjectWork_.empty()) { + processPendingUpdateObjectWork(start, budgetMs); + if (!pendingUpdateObjectWork_.empty()) { + break; + } + continue; + } + + if (pendingIncomingPackets_.empty()) { + break; + } + + network::Packet packet = std::move(pendingIncomingPackets_.front()); + pendingIncomingPackets_.pop_front(); + const uint16_t wireOp = packet.getOpcode(); + const auto logicalOp = opcodeTable_.fromWire(wireOp); + auto packetHandleStart = std::chrono::steady_clock::now(); + handlePacket(packet); + float packetMs = std::chrono::duration( + std::chrono::steady_clock::now() - packetHandleStart).count(); + if (packetMs > slowPacketLogThresholdMs()) { + const char* logicalName = logicalOp + ? OpcodeTable::logicalToName(*logicalOp) + : "UNKNOWN"; + LOG_WARNING("SLOW packet handler: ", packetMs, + "ms wire=0x", std::hex, wireOp, std::dec, + " logical=", logicalName, + " size=", packet.getSize(), + " state=", worldStateName(state)); + } + ++processed; + } + + if (!pendingUpdateObjectWork_.empty()) { + return; + } + + if (!pendingIncomingPackets_.empty()) { + LOG_DEBUG("GameHandler packet budget reached (processed=", processed, + ", remaining=", pendingIncomingPackets_.size(), + ", state=", worldStateName(state), ")"); + } +} + void GameHandler::handleAuthChallenge(network::Packet& packet) { LOG_INFO("Handling SMSG_AUTH_CHALLENGE"); @@ -6441,7 +8244,7 @@ void GameHandler::handleCharEnum(network::Packet& packet) { LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec); LOG_INFO(" ", getRaceName(character.race), " ", getClassName(character.characterClass)); - LOG_INFO(" Level ", (int)character.level); + LOG_INFO(" Level ", static_cast(character.level)); } } @@ -6599,7 +8402,7 @@ void GameHandler::handleCharLoginFailed(network::Packet& packet) { }; const char* msg = (reason < 9) ? reasonNames[reason] : "Unknown reason"; - LOG_ERROR("SMSG_CHARACTER_LOGIN_FAILED: reason=", (int)reason, " (", msg, ")"); + LOG_ERROR("SMSG_CHARACTER_LOGIN_FAILED: reason=", static_cast(reason), " (", msg, ")"); // Allow the player to re-select a character setState(WorldState::CHAR_LIST_RECEIVED); @@ -6611,7 +8414,7 @@ void GameHandler::handleCharLoginFailed(network::Packet& packet) { void GameHandler::selectCharacter(uint64_t characterGuid) { if (state != WorldState::CHAR_LIST_RECEIVED) { - LOG_WARNING("Cannot select character in state: ", (int)state); + LOG_WARNING("Cannot select character in state: ", static_cast(state)); return; } @@ -6628,7 +8431,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { for (const auto& character : characters) { if (character.guid == characterGuid) { LOG_INFO("Character: ", character.name); - LOG_INFO("Level ", (int)character.level, " ", + LOG_INFO("Level ", static_cast(character.level), " ", getRaceName(character.race), " ", getClassName(character.characterClass)); playerRace_ = character.race; @@ -6646,20 +8449,40 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { pendingItemQueries_.clear(); equipSlotGuids_ = {}; backpackSlotGuids_ = {}; + keyringSlotGuids_ = {}; invSlotBase_ = -1; packSlotBase_ = -1; lastPlayerFields_.clear(); onlineEquipDirty_ = false; playerMoneyCopper_ = 0; playerArmorRating_ = 0; + std::fill(std::begin(playerResistances_), std::end(playerResistances_), 0); std::fill(std::begin(playerStats_), std::end(playerStats_), -1); + playerMeleeAP_ = -1; + playerRangedAP_ = -1; + std::fill(std::begin(playerSpellDmgBonus_), std::end(playerSpellDmgBonus_), -1); + playerHealBonus_ = -1; + playerDodgePct_ = -1.0f; + playerParryPct_ = -1.0f; + playerBlockPct_ = -1.0f; + playerCritPct_ = -1.0f; + playerRangedCritPct_ = -1.0f; + std::fill(std::begin(playerSpellCritPct_), std::end(playerSpellCritPct_), -1.0f); + std::fill(std::begin(playerCombatRatings_), std::end(playerCombatRatings_), -1); knownSpells.clear(); spellCooldowns.clear(); + spellFlatMods_.clear(); + spellPctMods_.clear(); actionBar = {}; playerAuras.clear(); targetAuras.clear(); + unitAurasCache_.clear(); unitCastStates_.clear(); petGuid_ = 0; + stableWindowOpen_ = false; + stableMasterGuid_ = 0; + stableNumSlots_ = 0; + stabledPets_.clear(); playerXp_ = 0; playerNextLevelXp_ = 0; serverPlayerLevel_ = 1; @@ -6681,10 +8504,17 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { castIsChannel = false; currentCastSpellId = 0; pendingGameObjectInteractGuid_ = 0; + lastInteractedGoGuid_ = 0; castTimeRemaining = 0.0f; castTimeTotal = 0.0f; + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; playerDead_ = false; releasedSpirit_ = false; + corpseGuid_ = 0; + corpseReclaimAvailableMs_ = 0; targetGuid = 0; focusGuid = 0; lastTargetGuid = 0; @@ -6738,9 +8568,29 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { return; } + glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(data.x, data.y, data.z)); + const bool alreadyInWorld = (state == WorldState::IN_WORLD); + const bool sameMap = alreadyInWorld && (currentMapId_ == data.mapId); + const float dxCurrent = movementInfo.x - canonical.x; + const float dyCurrent = movementInfo.y - canonical.y; + const float dzCurrent = movementInfo.z - canonical.z; + const float distSqCurrent = dxCurrent * dxCurrent + dyCurrent * dyCurrent + dzCurrent * dzCurrent; + + // Some realms emit a late duplicate LOGIN_VERIFY_WORLD after the client is already + // in-world. Re-running full world-entry handling here can trigger an expensive + // same-map reload/reset path and starve networking for tens of seconds. + if (!initialWorldEntry && sameMap && distSqCurrent <= (5.0f * 5.0f)) { + LOG_INFO("Ignoring duplicate SMSG_LOGIN_VERIFY_WORLD while already in world: mapId=", + data.mapId, " dist=", std::sqrt(distSqCurrent)); + return; + } + // Successfully entered the world (or teleported) currentMapId_ = data.mapId; setState(WorldState::IN_WORLD); + if (socket) { + socket->tracePacketsFor(std::chrono::seconds(12), "login_verify_world"); + } LOG_INFO("========================================"); LOG_INFO(" SUCCESSFULLY ENTERED WORLD!"); @@ -6751,7 +8601,6 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { LOG_INFO("Player is now in the game world"); // Initialize movement info with world entry position (server → canonical) - glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(data.x, data.y, data.z)); LOG_DEBUG("LOGIN_VERIFY_WORLD: server=(", data.x, ", ", data.y, ", ", data.z, ") canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ") mapId=", data.mapId); movementInfo.x = canonical.x; @@ -6772,6 +8621,7 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { movementInfo.jumpXYSpeed = 0.0f; resurrectPending_ = false; resurrectRequestPending_ = false; + selfResAvailable_ = false; onTaxiFlight_ = false; taxiMountActive_ = false; taxiActivatePending_ = false; @@ -6781,6 +8631,7 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { taxiStartGrace_ = 0.0f; currentMountDisplayId_ = 0; taxiMountDisplayId_ = 0; + vehicleId_ = 0; if (mountCallback_) { mountCallback_(0); } @@ -6789,44 +8640,30 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { encounterUnitGuids_.fill(0); raidTargetGuids_.fill(0); - // Reset talent initialization so the first SMSG_TALENTS_INFO after login - // correctly sets the active spec (static locals don't reset across logins) - talentsInitialized_ = false; - learnedTalents_[0].clear(); - learnedTalents_[1].clear(); - unspentTalentPoints_[0] = 0; - unspentTalentPoints_[1] = 0; - activeTalentSpec_ = 0; - // Suppress area triggers on initial login — prevents exit portals from // immediately firing when spawning inside a dungeon/instance. activeAreaTriggers_.clear(); areaTriggerCheckTimer_ = -5.0f; areaTriggerSuppressFirst_ = true; - // Send CMSG_SET_ACTIVE_MOVER (required by some servers) + // Notify application to load terrain for this map/position (online mode) + if (worldEntryCallback_) { + worldEntryCallback_(data.mapId, data.x, data.y, data.z, initialWorldEntry); + } + + // Send CMSG_SET_ACTIVE_MOVER on initial world entry and world transfers. if (playerGuid != 0 && socket) { auto activeMoverPacket = SetActiveMoverPacket::build(playerGuid); socket->send(activeMoverPacket); LOG_INFO("Sent CMSG_SET_ACTIVE_MOVER for player 0x", std::hex, playerGuid, std::dec); } - // Notify application to load terrain for this map/position (online mode) - if (worldEntryCallback_) { - worldEntryCallback_(data.mapId, data.x, data.y, data.z, initialWorldEntry); - } - - // Auto-join default chat channels - autoJoinDefaultChannels(); - - // Auto-query guild info on login - const Character* activeChar = getActiveCharacter(); - if (activeChar && activeChar->hasGuild() && socket) { - auto gqPacket = GuildQueryPacket::build(activeChar->guildId); - socket->send(gqPacket); - auto grPacket = GuildRosterPacket::build(); - socket->send(grPacket); - LOG_INFO("Auto-queried guild info (guildId=", activeChar->guildId, ")"); + // Kick the first keepalive immediately on world entry. Classic-like realms + // can close the session before our default 30s ping cadence fires. + timeSinceLastPing = 0.0f; + if (socket) { + LOG_DEBUG("World entry keepalive: sending immediate ping after LOGIN_VERIFY_WORLD"); + sendPing(); } // If we disconnected mid-taxi, attempt to recover to destination after login. @@ -6844,6 +8681,33 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { } if (initialWorldEntry) { + // Clear inspect caches on world entry to avoid showing stale data. + inspectedPlayerAchievements_.clear(); + + // Reset talent initialization so the first SMSG_TALENTS_INFO after login + // correctly sets the active spec (static locals don't reset across logins). + talentsInitialized_ = false; + learnedTalents_[0].clear(); + learnedTalents_[1].clear(); + learnedGlyphs_[0].fill(0); + learnedGlyphs_[1].fill(0); + unspentTalentPoints_[0] = 0; + unspentTalentPoints_[1] = 0; + activeTalentSpec_ = 0; + + // Auto-join default chat channels only on first world entry. + autoJoinDefaultChannels(); + + // Auto-query guild info on login. + const Character* activeChar = getActiveCharacter(); + if (activeChar && activeChar->hasGuild() && socket) { + auto gqPacket = GuildQueryPacket::build(activeChar->guildId); + socket->send(gqPacket); + auto grPacket = GuildRosterPacket::build(); + socket->send(grPacket); + LOG_INFO("Auto-queried guild info (guildId=", activeChar->guildId, ")"); + } + pendingQuestAcceptTimeouts_.clear(); pendingQuestAcceptNpcGuids_.clear(); pendingQuestQueryIds_.clear(); @@ -6852,11 +8716,39 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { completedQuests_.clear(); LOG_INFO("Queued quest log resync for login (from server quest slots)"); - // Request completed quest IDs from server (populates completedQuests_ when response arrives) + // Request completed quest IDs when the expansion supports it. Classic-like + // opcode tables do not define this packet, and sending 0xFFFF during world + // entry can desync the early session handshake. if (socket) { - network::Packet cqcPkt(wireOpcode(Opcode::CMSG_QUERY_QUESTS_COMPLETED)); - socket->send(cqcPkt); - LOG_INFO("Sent CMSG_QUERY_QUESTS_COMPLETED"); + const uint16_t queryCompletedWire = wireOpcode(Opcode::CMSG_QUERY_QUESTS_COMPLETED); + if (queryCompletedWire != 0xFFFF) { + network::Packet cqcPkt(queryCompletedWire); + socket->send(cqcPkt); + LOG_INFO("Sent CMSG_QUERY_QUESTS_COMPLETED"); + } else { + LOG_INFO("Skipping CMSG_QUERY_QUESTS_COMPLETED: opcode not mapped for current expansion"); + } + } + + // Auto-request played time on login so the character Stats tab is + // populated immediately without requiring /played. + if (socket) { + auto ptPkt = RequestPlayedTimePacket::build(false); // false = don't show in chat + socket->send(ptPkt); + 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_) { + fireAddonEvent("PLAYER_ENTERING_WORLD", {initialWorldEntry ? "1" : "0"}); + // Also fire ZONE_CHANGED_NEW_AREA and UPDATE_WORLD_STATES so map/BG addons refresh + fireAddonEvent("ZONE_CHANGED_NEW_AREA", {}); + fireAddonEvent("UPDATE_WORLD_STATES", {}); + // PLAYER_LOGIN fires only on initial login (not teleports) + if (initialWorldEntry) { + fireAddonEvent("PLAYER_LOGIN", {}); } } } @@ -6934,7 +8826,7 @@ bool GameHandler::loadWardenCRFile(const std::string& moduleHashHex) { for (int i = 0; i < 9; i++) { char s[16]; snprintf(s, sizeof(s), "%s=0x%02X ", names[i], wardenCheckOpcodes_[i]); opcHex += s; } - LOG_DEBUG("Warden: Check opcodes: ", opcHex); + LOG_WARNING("Warden: Check opcodes: ", opcHex); } size_t entryCount = (static_cast(fileSize) - CR_HEADER_SIZE) / CR_ENTRY_SIZE; @@ -7133,6 +9025,18 @@ void GameHandler::handleWardenData(network::Packet& packet) { } std::vector seed(decrypted.begin() + 1, decrypted.begin() + 17); + auto applyWardenSeedRekey = [&](const std::vector& rekeySeed) { + // Derive new RC4 keys from the seed using SHA1Randx. + uint8_t newEncryptKey[16], newDecryptKey[16]; + WardenCrypto::sha1RandxGenerate(rekeySeed, newEncryptKey, newDecryptKey); + + std::vector ek(newEncryptKey, newEncryptKey + 16); + std::vector dk(newDecryptKey, newDecryptKey + 16); + wardenCrypto_->replaceKeys(ek, dk); + for (auto& b : newEncryptKey) b = 0; + for (auto& b : newDecryptKey) b = 0; + LOG_DEBUG("Warden: Derived and applied key update from seed"); + }; // --- Try CR lookup (pre-computed challenge/response entries) --- if (!wardenCREntries_.empty()) { @@ -7145,16 +9049,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { } if (match) { - LOG_DEBUG("Warden: Found matching CR entry for seed"); - - // Log the reply we're sending - { - std::string replyHex; - for (int i = 0; i < 20; i++) { - char s[4]; snprintf(s, 4, "%02x", match->reply[i]); replyHex += s; - } - LOG_DEBUG("Warden: Sending pre-computed reply=", replyHex); - } + LOG_WARNING("Warden: HASH_REQUEST — CR entry MATCHED, sending pre-computed reply"); // Send HASH_RESULT (opcode 0x04 + 20-byte reply) std::vector resp; @@ -7168,7 +9063,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { std::vector newDecryptKey(match->serverKey, match->serverKey + 16); wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey); - LOG_DEBUG("Warden: Switched to CR key set"); + LOG_WARNING("Warden: Switched to CR key set"); wardenState_ = WardenState::WAIT_CHECKS; break; @@ -7177,112 +9072,52 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } - // --- Fallback: compute hash from loaded module --- - LOG_WARNING("Warden: No CR match, computing hash from loaded module"); - - if (!wardenLoadedModule_ || !wardenLoadedModule_->isLoaded()) { - LOG_ERROR("Warden: No loaded module and no CR match — cannot compute hash"); - wardenState_ = WardenState::WAIT_CHECKS; - break; - } - + // --- No CR match: decide strategy based on server strictness --- { - const uint8_t* moduleImage = static_cast(wardenLoadedModule_->getModuleMemory()); - size_t moduleImageSize = wardenLoadedModule_->getModuleSize(); - const auto& decompressedData = wardenLoadedModule_->getDecompressedData(); + std::string seedHex; + for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; } - // --- Empirical test: try multiple SHA1 computations and check against first CR entry --- - if (!wardenCREntries_.empty()) { - const auto& firstCR = wardenCREntries_[0]; - std::string expectedHex; - for (int i = 0; i < 20; i++) { char s[4]; snprintf(s, 4, "%02x", firstCR.reply[i]); expectedHex += s; } - LOG_DEBUG("Warden: Empirical test — expected reply from CR[0]=", expectedHex); + bool isTurtle = isActiveExpansion("turtle"); + bool isClassic = (build <= 6005) && !isTurtle; - // Test 1: SHA1(moduleImage) - { - std::vector data(moduleImage, moduleImage + moduleImageSize); - auto h = auth::Crypto::sha1(data); - bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0); - std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; } - LOG_DEBUG("Warden: SHA1(moduleImage)=", hex, match ? " MATCH!" : ""); - } - // Test 2: SHA1(seed || moduleImage) - { - std::vector data; - data.insert(data.end(), seed.begin(), seed.end()); - data.insert(data.end(), moduleImage, moduleImage + moduleImageSize); - auto h = auth::Crypto::sha1(data); - bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0); - std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; } - LOG_DEBUG("Warden: SHA1(seed||image)=", hex, match ? " MATCH!" : ""); - } - // Test 3: SHA1(moduleImage || seed) - { - std::vector data(moduleImage, moduleImage + moduleImageSize); - data.insert(data.end(), seed.begin(), seed.end()); - auto h = auth::Crypto::sha1(data); - bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0); - std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; } - LOG_DEBUG("Warden: SHA1(image||seed)=", hex, match ? " MATCH!" : ""); - } - // Test 4: SHA1(decompressedData) - { - auto h = auth::Crypto::sha1(decompressedData); - bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0); - std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; } - LOG_DEBUG("Warden: SHA1(decompressed)=", hex, match ? " MATCH!" : ""); - } - // Test 5: SHA1(rawModuleData) - { - auto h = auth::Crypto::sha1(wardenModuleData_); - bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0); - std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; } - LOG_DEBUG("Warden: SHA1(rawModule)=", hex, match ? " MATCH!" : ""); - } - // Test 6: Check if all CR replies are the same (constant hash) - { - bool allSame = true; - for (size_t i = 1; i < wardenCREntries_.size(); i++) { - if (std::memcmp(wardenCREntries_[i].reply, firstCR.reply, 20) != 0) { - allSame = false; - break; - } - } - LOG_DEBUG("Warden: All ", wardenCREntries_.size(), " CR replies identical? ", allSame ? "YES" : "NO"); - } + if (!isTurtle && !isClassic) { + // WotLK/TBC (AzerothCore, etc.): strict servers BAN for wrong HASH_RESULT. + // Without a matching CR entry we cannot compute the correct hash + // (requires executing the module's native init function). + // Safest action: don't respond. Server will time-out and kick (not ban). + LOG_WARNING("Warden: HASH_REQUEST seed=", seedHex, + " — no CR match, SKIPPING response to avoid account ban"); + LOG_WARNING("Warden: To fix, provide a .cr file with the correct seed→reply entry for this module"); + // Stay in WAIT_HASH_REQUEST — server will eventually kick. + break; } - // --- Compute the hash: SHA1(moduleImage) is the most likely candidate --- - // The module's hash response is typically SHA1 of the loaded module image. - // This is a constant per module (seed is not used in the hash, only for key derivation). - std::vector imageData(moduleImage, moduleImage + moduleImageSize); - auto reply = auth::Crypto::sha1(imageData); + // Turtle/Classic: lenient servers (log-only penalties, no bans). + // Send a best-effort fallback hash so we can continue the handshake. + LOG_WARNING("Warden: No CR match (seed=", seedHex, + "), sending fallback hash (lenient server)"); - { - std::string hex; - for (auto b : reply) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; } - LOG_DEBUG("Warden: Sending SHA1(moduleImage)=", hex); + std::vector fallbackReply; + if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { + const uint8_t* moduleImage = static_cast(wardenLoadedModule_->getModuleMemory()); + size_t moduleImageSize = wardenLoadedModule_->getModuleSize(); + if (moduleImage && moduleImageSize > 0) { + std::vector imageData(moduleImage, moduleImage + moduleImageSize); + fallbackReply = auth::Crypto::sha1(imageData); + } + } + if (fallbackReply.empty()) { + if (!wardenModuleData_.empty()) + fallbackReply = auth::Crypto::sha1(wardenModuleData_); + else + fallbackReply.assign(20, 0); } - // Send HASH_RESULT (opcode 0x04 + 20-byte hash) std::vector resp; - resp.push_back(0x04); - resp.insert(resp.end(), reply.begin(), reply.end()); + resp.push_back(0x04); // WARDEN_CMSG_HASH_RESULT + resp.insert(resp.end(), fallbackReply.begin(), fallbackReply.end()); sendWardenResponse(resp); - - // Derive new RC4 keys from the seed using SHA1Randx - std::vector seedVec(seed.begin(), seed.end()); - // Pad seed to at least 2 bytes for SHA1Randx split - // SHA1Randx splits input in half: first_half and second_half - uint8_t newEncryptKey[16], newDecryptKey[16]; - WardenCrypto::sha1RandxGenerate(seedVec, newEncryptKey, newDecryptKey); - - std::vector ek(newEncryptKey, newEncryptKey + 16); - std::vector dk(newDecryptKey, newDecryptKey + 16); - wardenCrypto_->replaceKeys(ek, dk); - for (auto& b : newEncryptKey) b = 0; - for (auto& b : newDecryptKey) b = 0; - LOG_DEBUG("Warden: Derived and applied key update from seed"); + applyWardenSeedRekey(seed); } wardenState_ = WardenState::WAIT_CHECKS; @@ -7317,6 +9152,319 @@ void GameHandler::handleWardenData(network::Packet& packet) { uint8_t xorByte = decrypted.back(); LOG_DEBUG("Warden: XOR byte = 0x", [&]{ char s[4]; snprintf(s,4,"%02x",xorByte); return std::string(s); }()); + // Quick-scan for PAGE_A/PAGE_B checks (these trigger 5-second brute-force searches) + { + bool hasSlowChecks = false; + for (size_t i = pos; i < decrypted.size() - 1; i++) { + uint8_t d = decrypted[i] ^ xorByte; + if (d == wardenCheckOpcodes_[2] || d == wardenCheckOpcodes_[3]) { + hasSlowChecks = true; + break; + } + } + if (hasSlowChecks && !wardenResponsePending_) { + LOG_WARNING("Warden: PAGE_A/PAGE_B detected — building response async to avoid main-loop stall"); + // Ensure wardenMemory_ is loaded on main thread before launching async task + if (!wardenMemory_) { + wardenMemory_ = std::make_unique(); + if (!wardenMemory_->load(static_cast(build), isActiveExpansion("turtle"))) { + LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK"); + } + } + // Capture state by value (decrypted, strings) and launch async. + // The async task returns plaintext response bytes; main thread encrypts+sends in update(). + size_t capturedPos = pos; + wardenPendingEncrypted_ = std::async(std::launch::async, + [this, decrypted, strings, xorByte, capturedPos]() -> std::vector { + // This runs on a background thread — same logic as the synchronous path below. + // BEGIN: duplicated check processing (kept in sync with synchronous path) + enum CheckType { CT_MEM=0, CT_PAGE_A=1, CT_PAGE_B=2, CT_MPQ=3, CT_LUA=4, + CT_DRIVER=5, CT_TIMING=6, CT_PROC=7, CT_MODULE=8, CT_UNKNOWN=9 }; + size_t checkEnd = decrypted.size() - 1; + size_t pos = capturedPos; + + auto decodeCheckType = [&](uint8_t raw) -> CheckType { + uint8_t decoded = raw ^ xorByte; + if (decoded == wardenCheckOpcodes_[0]) return CT_MEM; + if (decoded == wardenCheckOpcodes_[1]) return CT_MODULE; + if (decoded == wardenCheckOpcodes_[2]) return CT_PAGE_A; + if (decoded == wardenCheckOpcodes_[3]) return CT_PAGE_B; + if (decoded == wardenCheckOpcodes_[4]) return CT_MPQ; + if (decoded == wardenCheckOpcodes_[5]) return CT_LUA; + if (decoded == wardenCheckOpcodes_[6]) return CT_PROC; + if (decoded == wardenCheckOpcodes_[7]) return CT_DRIVER; + if (decoded == wardenCheckOpcodes_[8]) return CT_TIMING; + return CT_UNKNOWN; + }; + auto resolveString = [&](uint8_t idx) -> std::string { + if (idx == 0) return {}; + size_t i = idx - 1; + return i < strings.size() ? strings[i] : std::string(); + }; + auto isKnownWantedCodeScan = [&](const uint8_t seed[4], const uint8_t hash[20], + uint32_t off, uint8_t len) -> bool { + auto tryMatch = [&](const uint8_t* pat, size_t patLen) { + uint8_t out[SHA_DIGEST_LENGTH]; unsigned int outLen = 0; + HMAC(EVP_sha1(), seed, 4, pat, patLen, out, &outLen); + return outLen == SHA_DIGEST_LENGTH && !std::memcmp(out, hash, SHA_DIGEST_LENGTH); + }; + static const uint8_t p1[] = {0x33,0xD2,0x33,0xC9,0xE8,0x87,0x07,0x1B,0x00,0xE8}; + if (off == 13856 && len == sizeof(p1) && tryMatch(p1, sizeof(p1))) return true; + static const uint8_t p2[] = {0x56,0x57,0xFC,0x8B,0x54,0x24,0x14,0x8B, + 0x74,0x24,0x10,0x8B,0x44,0x24,0x0C,0x8B,0xCA,0x8B,0xF8,0xC1, + 0xE9,0x02,0x74,0x02,0xF3,0xA5,0xB1,0x03,0x23,0xCA,0x74,0x02, + 0xF3,0xA4,0x5F,0x5E,0xC3}; + if (len == sizeof(p2) && tryMatch(p2, sizeof(p2))) return true; + return false; + }; + + std::vector resultData; + int checkCount = 0; + int checkTypeCounts[10] = {}; + + #define WARDEN_ASYNC_HANDLER 1 + // The check processing loop is identical to the synchronous path. + // See the synchronous case 0x02 below for the canonical version. + while (pos < checkEnd) { + CheckType ct = decodeCheckType(decrypted[pos]); + pos++; + checkCount++; + if (ct <= CT_UNKNOWN) checkTypeCounts[ct]++; + + switch (ct) { + case CT_TIMING: { + // Result byte: 0x01 = timing check ran successfully, + // 0x00 = timing check failed (Wine/VM — server skips anti-AFK). + // We return 0x01 so the server validates normally; our + // LastHardwareAction (now-2000) ensures a clean 2s delta. + resultData.push_back(0x01); + uint32_t ticks = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + resultData.push_back(ticks & 0xFF); + resultData.push_back((ticks >> 8) & 0xFF); + resultData.push_back((ticks >> 16) & 0xFF); + resultData.push_back((ticks >> 24) & 0xFF); + break; + } + case CT_MEM: { + if (pos + 6 > checkEnd) { pos = checkEnd; break; } + uint8_t strIdx = decrypted[pos++]; + std::string moduleName = resolveString(strIdx); + uint32_t offset = decrypted[pos] | (uint32_t(decrypted[pos+1])<<8) + | (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24); + pos += 4; + uint8_t readLen = decrypted[pos++]; + LOG_WARNING("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), + " len=", static_cast(readLen), + (strIdx ? " module=\"" + moduleName + "\"" : "")); + if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { + uint32_t now = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + wardenMemory_->writeLE32(0xCF0BC8, now - 2000); + } + std::vector memBuf(readLen, 0); + bool memOk = wardenMemory_ && wardenMemory_->isLoaded() && + wardenMemory_->readMemory(offset, readLen, memBuf.data()); + if (memOk) { + const char* region = "?"; + if (offset >= 0x7FFE0000 && offset < 0x7FFF0000) region = "KUSER"; + else if (offset >= 0x400000 && offset < 0x800000) region = ".text/.code"; + else if (offset >= 0x7FF000 && offset < 0x827000) region = ".rdata"; + else if (offset >= 0x827000 && offset < 0x883000) region = ".data(raw)"; + else if (offset >= 0x883000 && offset < 0xD06000) region = ".data(BSS)"; + bool allZero = true; + for (int i = 0; i < static_cast(readLen); i++) { if (memBuf[i] != 0) { allZero = false; break; } } + std::string hexDump; + for (int i = 0; i < static_cast(readLen); i++) { char hx[4]; snprintf(hx,4,"%02x ",memBuf[i]); hexDump += hx; } + LOG_WARNING("Warden: MEM_CHECK served: [", hexDump, "] region=", region, + (allZero && offset >= 0x883000 ? " \xe2\x98\x85""BSS_ZERO\xe2\x98\x85" : "")); + if (offset == 0x7FFE026C && readLen == 12) + LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet"); + resultData.push_back(0x00); + resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); + } else { + // Address not in PE/KUSER — return 0xE9 (not readable). + // Real 32-bit WoW can't read kernel space (>=0x80000000) + // or arbitrary unallocated user-space addresses. + LOG_WARNING("Warden: MEM_CHECK -> 0xE9 (unmapped 0x", + [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); + resultData.push_back(0xE9); + } + break; + } + case CT_PAGE_A: + case CT_PAGE_B: { + constexpr size_t kPageSize = 29; + const char* pageName = (ct == CT_PAGE_A) ? "PAGE_A" : "PAGE_B"; + bool isImageOnly = (ct == CT_PAGE_A); + if (pos + kPageSize > checkEnd) { pos = checkEnd; resultData.push_back(0x00); break; } + const uint8_t* p = decrypted.data() + pos; + const uint8_t* seed = p; + const uint8_t* sha1 = p + 4; + uint32_t off = uint32_t(p[24])|(uint32_t(p[25])<<8)|(uint32_t(p[26])<<16)|(uint32_t(p[27])<<24); + uint8_t patLen = p[28]; + bool found = false; + bool turtleFallback = false; + if (isKnownWantedCodeScan(seed, sha1, off, patLen)) { + found = true; + } else if (wardenMemory_ && wardenMemory_->isLoaded() && patLen > 0) { + // Hint + nearby window search (instant). + // Skip full brute-force for Turtle PAGE_A to avoid + // 25s delay that triggers response timeout. + bool hintOnly = (ct == CT_PAGE_A && isActiveExpansion("turtle")); + found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly, off, hintOnly); + if (!found && !hintOnly && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { + const uint8_t* modMem = static_cast(wardenLoadedModule_->getModuleMemory()); + size_t modSize = wardenLoadedModule_->getModuleSize(); + if (modMem && modSize >= patLen) { + for (size_t i = 0; i < modSize - patLen + 1; i++) { + uint8_t h[20]; unsigned int hl = 0; + HMAC(EVP_sha1(), seed, 4, modMem+i, patLen, h, &hl); + if (hl == 20 && !std::memcmp(h, sha1, 20)) { found = true; break; } + } + } + } + } + // Turtle PAGE_A fallback: patterns at runtime-patched + // offsets don't exist in the on-disk PE. The server + // expects "found" for these code integrity checks. + if (!found && ct == CT_PAGE_A && isActiveExpansion("turtle") && off < 0x600000) { + found = true; + turtleFallback = true; + } + uint8_t pageResult = found ? 0x4A : 0x00; + LOG_WARNING("Warden: ", pageName, " offset=0x", + [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}(), + " patLen=", static_cast(patLen), " found=", found ? "yes" : "no", + turtleFallback ? " (turtle-fallback)" : ""); + pos += kPageSize; + resultData.push_back(pageResult); + break; + } + case CT_MPQ: { + if (pos + 1 > checkEnd) { pos = checkEnd; break; } + uint8_t strIdx = decrypted[pos++]; + std::string filePath = resolveString(strIdx); + LOG_WARNING("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); + bool found = false; + std::vector hash(20, 0); + if (!filePath.empty()) { + std::string np = asciiLower(filePath); + std::replace(np.begin(), np.end(), '/', '\\'); + auto knownIt = knownDoorHashes().find(np); + if (knownIt != knownDoorHashes().end()) { found = true; hash.assign(knownIt->second.begin(), knownIt->second.end()); } + auto* am = core::Application::getInstance().getAssetManager(); + if (am && am->isInitialized() && !found) { + std::vector fd; + std::string rp = resolveCaseInsensitiveDataPath(am->getDataPath(), filePath); + if (!rp.empty()) fd = readFileBinary(rp); + if (fd.empty()) fd = am->readFile(filePath); + if (!fd.empty()) { found = true; hash = auth::Crypto::sha1(fd); } + } + } + LOG_WARNING("Warden: MPQ result=", (found ? "FOUND" : "NOT_FOUND")); + if (found) { resultData.push_back(0x00); resultData.insert(resultData.end(), hash.begin(), hash.end()); } + else { resultData.push_back(0x01); } + break; + } + case CT_LUA: { + if (pos + 1 > checkEnd) { pos = checkEnd; break; } + pos++; resultData.push_back(0x01); break; + } + case CT_DRIVER: { + if (pos + 25 > checkEnd) { pos = checkEnd; break; } + pos += 24; + uint8_t strIdx = decrypted[pos++]; + std::string dn = resolveString(strIdx); + LOG_WARNING("Warden: DRIVER=\"", (dn.empty() ? "?" : dn), "\" -> 0x00(not found)"); + resultData.push_back(0x00); break; + } + case CT_MODULE: { + if (pos + 24 > checkEnd) { pos = checkEnd; resultData.push_back(0x00); break; } + const uint8_t* p = decrypted.data() + pos; + uint8_t sb[4] = {p[0],p[1],p[2],p[3]}; + uint8_t rh[20]; std::memcpy(rh, p+4, 20); + pos += 24; + bool isWanted = hmacSha1Matches(sb, "KERNEL32.DLL", rh); + std::string mn = isWanted ? "KERNEL32.DLL" : "?"; + if (!isWanted) { + // Cheat modules (unwanted — report not found) + if (hmacSha1Matches(sb,"WPESPY.DLL",rh)) mn = "WPESPY.DLL"; + else if (hmacSha1Matches(sb,"TAMIA.DLL",rh)) mn = "TAMIA.DLL"; + else if (hmacSha1Matches(sb,"PRXDRVPE.DLL",rh)) mn = "PRXDRVPE.DLL"; + else if (hmacSha1Matches(sb,"SPEEDHACK-I386.DLL",rh)) mn = "SPEEDHACK-I386.DLL"; + else if (hmacSha1Matches(sb,"D3DHOOK.DLL",rh)) mn = "D3DHOOK.DLL"; + else if (hmacSha1Matches(sb,"NJUMD.DLL",rh)) mn = "NJUMD.DLL"; + // System DLLs (wanted — report found) + else if (hmacSha1Matches(sb,"USER32.DLL",rh)) { mn = "USER32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"NTDLL.DLL",rh)) { mn = "NTDLL.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WS2_32.DLL",rh)) { mn = "WS2_32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WSOCK32.DLL",rh)) { mn = "WSOCK32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"ADVAPI32.DLL",rh)) { mn = "ADVAPI32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"SHELL32.DLL",rh)) { mn = "SHELL32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"GDI32.DLL",rh)) { mn = "GDI32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"OPENGL32.DLL",rh)) { mn = "OPENGL32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WINMM.DLL",rh)) { mn = "WINMM.DLL"; isWanted = true; } + } + uint8_t mr = isWanted ? 0x4A : 0x00; + LOG_WARNING("Warden: MODULE \"", mn, "\" -> 0x", + [&]{char s[4];snprintf(s,4,"%02x",mr);return std::string(s);}(), + isWanted ? "(found)" : "(not found)"); + resultData.push_back(mr); break; + } + case CT_PROC: { + if (pos + 30 > checkEnd) { pos = checkEnd; break; } + pos += 30; resultData.push_back(0x01); break; + } + default: pos = checkEnd; break; + } + } + #undef WARDEN_ASYNC_HANDLER + + // Log summary + { + std::string summary; + const char* ctNames[] = {"MEM","PAGE_A","PAGE_B","MPQ","LUA","DRIVER","TIMING","PROC","MODULE","UNK"}; + for (int i = 0; i < 10; i++) { + if (checkTypeCounts[i] > 0) { + if (!summary.empty()) summary += " "; + summary += ctNames[i]; summary += "="; summary += std::to_string(checkTypeCounts[i]); + } + } + LOG_WARNING("Warden: (async) Parsed ", checkCount, " checks [", summary, + "] resultSize=", resultData.size()); + std::string fullHex; + for (size_t bi = 0; bi < resultData.size(); bi++) { + char hx[4]; snprintf(hx, 4, "%02x ", resultData[bi]); fullHex += hx; + if ((bi + 1) % 32 == 0 && bi + 1 < resultData.size()) fullHex += "\n "; + } + LOG_WARNING("Warden: RESPONSE_HEX [", fullHex, "]"); + } + + // Build plaintext response: [0x02][uint16 len][uint32 checksum][resultData] + auto resultHash = auth::Crypto::sha1(resultData); + uint32_t checksum = 0; + for (int i = 0; i < 5; i++) { + uint32_t word = resultHash[i*4] | (uint32_t(resultHash[i*4+1])<<8) + | (uint32_t(resultHash[i*4+2])<<16) | (uint32_t(resultHash[i*4+3])<<24); + checksum ^= word; + } + uint16_t rl = static_cast(resultData.size()); + std::vector resp; + resp.push_back(0x02); + resp.push_back(rl & 0xFF); resp.push_back((rl >> 8) & 0xFF); + resp.push_back(checksum & 0xFF); resp.push_back((checksum >> 8) & 0xFF); + resp.push_back((checksum >> 16) & 0xFF); resp.push_back((checksum >> 24) & 0xFF); + resp.insert(resp.end(), resultData.begin(), resultData.end()); + return resp; // plaintext; main thread will encrypt + send + }); + wardenResponsePending_ = true; + break; // exit case 0x02 — response will be sent from update() + } + } + // Check type enum indices enum CheckType { CT_MEM=0, CT_PAGE_A=1, CT_PAGE_B=2, CT_MPQ=3, CT_LUA=4, CT_DRIVER=5, CT_TIMING=6, CT_PROC=7, CT_MODULE=8, CT_UNKNOWN=9 }; @@ -7439,7 +9587,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { switch (ct) { case CT_TIMING: { // No additional request data - // Response: [uint8 result=1][uint32 ticks] + // Response: [uint8 result][uint32 ticks] + // 0x01 = timing check ran successfully (server validates anti-AFK) + // 0x00 = timing failed (Wine/VM — server skips check but flags client) resultData.push_back(0x01); uint32_t ticks = static_cast( std::chrono::duration_cast( @@ -7448,6 +9598,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { resultData.push_back((ticks >> 8) & 0xFF); resultData.push_back((ticks >> 16) & 0xFF); resultData.push_back((ticks >> 24) & 0xFF); + LOG_WARNING("Warden: (sync) TIMING ticks=", ticks); break; } case CT_MEM: { @@ -7459,30 +9610,39 @@ void GameHandler::handleWardenData(network::Packet& packet) { | (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24); pos += 4; uint8_t readLen = decrypted[pos++]; - LOG_DEBUG("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), - " len=", (int)readLen); - if (!moduleName.empty()) { - LOG_DEBUG("Warden: MEM module=\"", moduleName, "\""); - } + LOG_WARNING("Warden: (sync) MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), + " len=", static_cast(readLen), + moduleName.empty() ? "" : (" module=\"" + moduleName + "\"")); // Lazy-load WoW.exe PE image on first MEM_CHECK if (!wardenMemory_) { wardenMemory_ = std::make_unique(); - if (!wardenMemory_->load(static_cast(build))) { + if (!wardenMemory_->load(static_cast(build), isActiveExpansion("turtle"))) { LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK"); } } + // Dynamically update LastHardwareAction before reading + // (anti-AFK scan compares this timestamp against TIMING ticks) + if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { + uint32_t now = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + wardenMemory_->writeLE32(0xCF0BC8, now - 2000); + } + // Read bytes from PE image (includes patched runtime globals) std::vector memBuf(readLen, 0); if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) { LOG_DEBUG("Warden: MEM_CHECK served from PE image"); + resultData.push_back(0x00); + resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); } else { - LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x", - [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}()); + // Address not in PE/KUSER — return 0xE9 (not readable). + LOG_WARNING("Warden: (sync) MEM_CHECK -> 0xE9 (unmapped 0x", + [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); + resultData.push_back(0xE9); } - resultData.push_back(0x00); - resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); break; } case CT_PAGE_A: { @@ -7526,11 +9686,31 @@ void GameHandler::handleWardenData(network::Packet& packet) { (uint32_t(p[26]) << 16) | (uint32_t(p[27]) << 24); uint8_t len = p[28]; if (isKnownWantedCodeScan(seedBytes, reqHash, off, len)) { - pageResult = 0x4A; // PatternFound + pageResult = 0x4A; + } else if (wardenMemory_ && wardenMemory_->isLoaded() && len > 0) { + if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true, off)) + pageResult = 0x4A; + } + // Turtle PAGE_A fallback: runtime-patched offsets aren't in the + // on-disk PE. Server expects "found" for code integrity checks. + if (pageResult == 0x00 && isActiveExpansion("turtle") && off < 0x600000) { + pageResult = 0x4A; + LOG_WARNING("Warden: PAGE_A turtle-fallback for offset=0x", + [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}()); } } - LOG_DEBUG("Warden: PAGE_A request bytes=", consume, - " result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + if (consume >= 29) { + uint32_t off2 = uint32_t((decrypted.data()+pos)[24]) | (uint32_t((decrypted.data()+pos)[25])<<8) | + (uint32_t((decrypted.data()+pos)[26])<<16) | (uint32_t((decrypted.data()+pos)[27])<<24); + uint8_t len2 = (decrypted.data()+pos)[28]; + LOG_WARNING("Warden: (sync) PAGE_A offset=0x", + [&]{char s[12];snprintf(s,12,"%08x",off2);return std::string(s);}(), + " patLen=", static_cast(len2), + " result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + } else { + LOG_WARNING("Warden: (sync) PAGE_A (short ", consume, "b) result=0x", + [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + } pos += consume; resultData.push_back(pageResult); break; @@ -7579,7 +9759,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (pos + 1 > checkEnd) { pos = checkEnd; break; } uint8_t strIdx = decrypted[pos++]; std::string filePath = resolveWardenString(strIdx); - LOG_DEBUG("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); + LOG_WARNING("Warden: (sync) MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); bool found = false; std::vector hash(20, 0); @@ -7614,10 +9794,15 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } - // Response: [uint8 result][20 sha1] - // result=0 => found/success, result=1 => not found/failure - resultData.push_back(found ? 0x00 : 0x01); - resultData.insert(resultData.end(), hash.begin(), hash.end()); + // Response: result=0 + 20-byte SHA1 if found; result=1 (no hash) if not found. + // Server only reads 20 hash bytes when result==0; extra bytes corrupt parsing. + if (found) { + resultData.push_back(0x00); + resultData.insert(resultData.end(), hash.begin(), hash.end()); + } else { + resultData.push_back(0x01); + } + LOG_WARNING("Warden: (sync) MPQ result=", found ? "FOUND" : "NOT_FOUND"); break; } case CT_LUA: { @@ -7625,7 +9810,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (pos + 1 > checkEnd) { pos = checkEnd; break; } uint8_t strIdx = decrypted[pos++]; std::string luaVar = resolveWardenString(strIdx); - LOG_DEBUG("Warden: LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\""); + LOG_WARNING("Warden: (sync) LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\""); // Response: [uint8 result=0][uint16 len=0] // Lua string doesn't exist resultData.push_back(0x01); // not found @@ -7637,9 +9822,10 @@ void GameHandler::handleWardenData(network::Packet& packet) { pos += 24; // skip seed + sha1 uint8_t strIdx = decrypted[pos++]; std::string driverName = resolveWardenString(strIdx); - LOG_DEBUG("Warden: DRIVER=\"", (driverName.empty() ? "?" : driverName), "\""); - // Response: [uint8 result=1] (driver NOT found = clean) - resultData.push_back(0x01); + LOG_WARNING("Warden: (sync) DRIVER=\"", (driverName.empty() ? "?" : driverName), "\" -> 0x00(not found)"); + // Response: [uint8 result=0] (driver NOT found = clean) + // VMaNGOS: result != 0 means "found". 0x01 would mean VM driver detected! + resultData.push_back(0x00); break; } case CT_MODULE: { @@ -7657,23 +9843,34 @@ void GameHandler::handleWardenData(network::Packet& packet) { std::memcpy(reqHash, p + 4, 20); pos += moduleSize; - // CMaNGOS uppercases module names before hashing. - // DB module scans: - // KERNEL32.DLL (wanted=true) - // WPESPY.DLL / SPEEDHACK-I386.DLL / TAMIA.DLL (wanted=false) bool shouldReportFound = false; - if (hmacSha1Matches(seedBytes, "KERNEL32.DLL", reqHash)) { - shouldReportFound = true; - } else if (hmacSha1Matches(seedBytes, "WPESPY.DLL", reqHash) || - hmacSha1Matches(seedBytes, "SPEEDHACK-I386.DLL", reqHash) || - hmacSha1Matches(seedBytes, "TAMIA.DLL", reqHash)) { - shouldReportFound = false; - } - resultData.push_back(shouldReportFound ? 0x4A : 0x01); + std::string modName = "?"; + // Wanted system modules + if (hmacSha1Matches(seedBytes, "KERNEL32.DLL", reqHash)) { modName = "KERNEL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "USER32.DLL", reqHash)) { modName = "USER32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "NTDLL.DLL", reqHash)) { modName = "NTDLL.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WS2_32.DLL", reqHash)) { modName = "WS2_32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WSOCK32.DLL", reqHash)) { modName = "WSOCK32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "ADVAPI32.DLL", reqHash)) { modName = "ADVAPI32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "SHELL32.DLL", reqHash)) { modName = "SHELL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "GDI32.DLL", reqHash)) { modName = "GDI32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "OPENGL32.DLL", reqHash)) { modName = "OPENGL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WINMM.DLL", reqHash)) { modName = "WINMM.DLL"; shouldReportFound = true; } + // Unwanted cheat modules + else if (hmacSha1Matches(seedBytes, "WPESPY.DLL", reqHash)) modName = "WPESPY.DLL"; + else if (hmacSha1Matches(seedBytes, "SPEEDHACK-I386.DLL", reqHash)) modName = "SPEEDHACK-I386.DLL"; + else if (hmacSha1Matches(seedBytes, "TAMIA.DLL", reqHash)) modName = "TAMIA.DLL"; + else if (hmacSha1Matches(seedBytes, "PRXDRVPE.DLL", reqHash)) modName = "PRXDRVPE.DLL"; + else if (hmacSha1Matches(seedBytes, "D3DHOOK.DLL", reqHash)) modName = "D3DHOOK.DLL"; + else if (hmacSha1Matches(seedBytes, "NJUMD.DLL", reqHash)) modName = "NJUMD.DLL"; + LOG_WARNING("Warden: (sync) MODULE \"", modName, + "\" -> 0x", [&]{char s[4];snprintf(s,4,"%02x",shouldReportFound?0x4A:0x00);return std::string(s);}(), + "(", shouldReportFound ? "found" : "not found", ")"); + resultData.push_back(shouldReportFound ? 0x4A : 0x00); break; } // Truncated module request fallback: module NOT loaded = clean - resultData.push_back(0x01); + resultData.push_back(0x00); break; } case CT_PROC: { @@ -7682,19 +9879,39 @@ void GameHandler::handleWardenData(network::Packet& packet) { int procSize = 30; if (pos + procSize > checkEnd) { pos = checkEnd; break; } pos += procSize; + LOG_WARNING("Warden: (sync) PROC check -> 0x01(not found)"); // Response: [uint8 result=1] (proc NOT found = clean) resultData.push_back(0x01); break; } default: { - LOG_WARNING("Warden: Unknown check type, cannot parse remaining"); + uint8_t rawByte = decrypted[pos - 1]; + uint8_t decoded = rawByte ^ xorByte; + LOG_WARNING("Warden: Unknown check type raw=0x", + [&]{char s[4];snprintf(s,4,"%02x",rawByte);return std::string(s);}(), + " decoded=0x", + [&]{char s[4];snprintf(s,4,"%02x",decoded);return std::string(s);}(), + " xorByte=0x", + [&]{char s[4];snprintf(s,4,"%02x",xorByte);return std::string(s);}(), + " opcodes=[", + [&]{std::string r;for(int i=0;i<9;i++){char s[6];snprintf(s,6,"0x%02x ",wardenCheckOpcodes_[i]);r+=s;}return r;}(), + "] pos=", pos, "/", checkEnd); pos = checkEnd; // stop parsing break; } } } - LOG_DEBUG("Warden: Parsed ", checkCount, " checks, result data size=", resultData.size()); + // Log synchronous round summary at WARNING level for diagnostics + { + LOG_WARNING("Warden: (sync) Parsed ", checkCount, " checks, resultSize=", resultData.size()); + std::string fullHex; + for (size_t bi = 0; bi < resultData.size(); bi++) { + char hx[4]; snprintf(hx, 4, "%02x ", resultData[bi]); fullHex += hx; + if ((bi + 1) % 32 == 0 && bi + 1 < resultData.size()) fullHex += "\n "; + } + LOG_WARNING("Warden: (sync) RESPONSE_HEX [", fullHex, "]"); + } // --- Compute checksum: XOR of 5 uint32s from SHA1(resultData) --- auto resultHash = auth::Crypto::sha1(resultData); @@ -7730,8 +9947,8 @@ void GameHandler::handleWardenData(network::Packet& packet) { break; default: - LOG_DEBUG("Warden: Unknown opcode 0x", std::hex, (int)wardenOpcode, std::dec, - " (state=", (int)wardenState_, ", size=", decrypted.size(), ")"); + LOG_DEBUG("Warden: Unknown opcode 0x", std::hex, static_cast(wardenOpcode), std::dec, + " (state=", static_cast(wardenState_), ", size=", decrypted.size(), ")"); break; } } @@ -7793,8 +10010,8 @@ void GameHandler::sendPing() { // Increment sequence number pingSequence++; - LOG_DEBUG("Sending CMSG_PING (heartbeat)"); - LOG_DEBUG(" Sequence: ", pingSequence); + LOG_DEBUG("Sending CMSG_PING: sequence=", pingSequence, + " latencyHintMs=", lastLatency); // Record send time for RTT measurement pingTimestamp_ = std::chrono::steady_clock::now(); @@ -7804,6 +10021,125 @@ void GameHandler::sendPing() { socket->send(packet); } +void GameHandler::sendRequestVehicleExit() { + if (state != WorldState::IN_WORLD || vehicleId_ == 0) return; + // CMSG_REQUEST_VEHICLE_EXIT has no payload — opcode only + network::Packet pkt(wireOpcode(Opcode::CMSG_REQUEST_VEHICLE_EXIT)); + socket->send(pkt); + vehicleId_ = 0; // Optimistically clear; server will confirm via SMSG_PLAYER_VEHICLE_DATA(0) +} + +bool GameHandler::supportsEquipmentSets() const { + return wireOpcode(Opcode::CMSG_EQUIPMENT_SET_SAVE) != 0xFFFF; +} + +void GameHandler::useEquipmentSet(uint32_t setId) { + if (!isInWorld()) return; + uint16_t wire = wireOpcode(Opcode::CMSG_EQUIPMENT_SET_USE); + if (wire == 0xFFFF) { addUIError("Equipment sets not supported."); return; } + // Find the equipment set to get target item GUIDs per slot + const EquipmentSet* es = nullptr; + for (const auto& s : equipmentSets_) { + if (s.setId == setId) { es = &s; break; } + } + if (!es) { + addUIError("Equipment set not found."); + return; + } + // CMSG_EQUIPMENT_SET_USE: 19 × (PackedGuid itemGuid + uint8 srcBag + uint8 srcSlot) + network::Packet pkt(wire); + for (int slot = 0; slot < 19; ++slot) { + uint64_t itemGuid = es->itemGuids[slot]; + pkt.writePackedGuid(itemGuid); + uint8_t srcBag = 0xFF; + uint8_t srcSlot = 0; + if (itemGuid != 0) { + bool found = false; + // Check if item is already in an equipment slot + for (int eq = 0; eq < 19 && !found; ++eq) { + if (getEquipSlotGuid(eq) == itemGuid) { + srcBag = 0xFF; // INVENTORY_SLOT_BAG_0 + srcSlot = static_cast(eq); + found = true; + } + } + // Check backpack (slots 23-38 in the body container) + for (int bp = 0; bp < 16 && !found; ++bp) { + if (getBackpackItemGuid(bp) == itemGuid) { + srcBag = 0xFF; + srcSlot = static_cast(23 + bp); + found = true; + } + } + // Check extra bags (bag indices 19-22) + for (int bag = 0; bag < 4 && !found; ++bag) { + int bagSize = inventory.getBagSize(bag); + for (int s = 0; s < bagSize && !found; ++s) { + if (getBagItemGuid(bag, s) == itemGuid) { + srcBag = static_cast(19 + bag); + srcSlot = static_cast(s); + found = true; + } + } + } + } + pkt.writeUInt8(srcBag); + pkt.writeUInt8(srcSlot); + } + socket->send(pkt); + LOG_INFO("CMSG_EQUIPMENT_SET_USE: setId=", setId); +} + +void GameHandler::saveEquipmentSet(const std::string& name, const std::string& iconName, + uint64_t existingGuid, uint32_t setIndex) { + if (state != WorldState::IN_WORLD) return; + uint16_t wire = wireOpcode(Opcode::CMSG_EQUIPMENT_SET_SAVE); + if (wire == 0xFFFF) { addUIError("Equipment sets not supported."); return; } + // CMSG_EQUIPMENT_SET_SAVE: uint64 setGuid + uint32 setIndex + string name + string iconName + // + 19 × PackedGuid itemGuid (one per equipment slot, 0–18) + if (setIndex == 0xFFFFFFFF) { + // Auto-assign next free index + setIndex = 0; + for (const auto& es : equipmentSets_) { + if (es.setId >= setIndex) setIndex = es.setId + 1; + } + } + network::Packet pkt(wire); + pkt.writeUInt64(existingGuid); // 0 = create new, nonzero = update + pkt.writeUInt32(setIndex); + pkt.writeString(name); + pkt.writeString(iconName); + for (int slot = 0; slot < 19; ++slot) { + uint64_t guid = getEquipSlotGuid(slot); + pkt.writePackedGuid(guid); + } + // Track pending save so SMSG_EQUIPMENT_SET_SAVED can add the new set locally + pendingSaveSetName_ = name; + pendingSaveSetIcon_ = iconName; + socket->send(pkt); + LOG_INFO("CMSG_EQUIPMENT_SET_SAVE: name=\"", name, "\" guid=", existingGuid, " index=", setIndex); +} + +void GameHandler::deleteEquipmentSet(uint64_t setGuid) { + if (state != WorldState::IN_WORLD || setGuid == 0) return; + uint16_t wire = wireOpcode(Opcode::CMSG_DELETEEQUIPMENT_SET); + if (wire == 0xFFFF) { addUIError("Equipment sets not supported."); return; } + // CMSG_DELETEEQUIPMENT_SET: uint64 setGuid + network::Packet pkt(wire); + pkt.writeUInt64(setGuid); + socket->send(pkt); + // Remove locally so UI updates immediately + equipmentSets_.erase( + std::remove_if(equipmentSets_.begin(), equipmentSets_.end(), + [setGuid](const EquipmentSet& es) { return es.setGuid == setGuid; }), + equipmentSets_.end()); + equipmentSetInfo_.erase( + std::remove_if(equipmentSetInfo_.begin(), equipmentSetInfo_.end(), + [setGuid](const EquipmentSetInfo& es) { return es.setGuid == setGuid; }), + equipmentSetInfo_.end()); + LOG_INFO("CMSG_DELETEEQUIPMENT_SET: guid=", setGuid); +} + void GameHandler::sendMinimapPing(float wowX, float wowY) { if (state != WorldState::IN_WORLD) return; @@ -7848,7 +10184,8 @@ void GameHandler::handlePong(network::Packet& packet) { lastLatency = static_cast( std::chrono::duration_cast(rtt).count()); - LOG_DEBUG("Heartbeat acknowledged (sequence: ", data.sequence, ", latency: ", lastLatency, "ms)"); + LOG_DEBUG("SMSG_PONG acknowledged: sequence=", data.sequence, + " latencyMs=", lastLatency); } uint32_t GameHandler::nextMovementTimestampMs() { @@ -7875,7 +10212,7 @@ uint32_t GameHandler::nextMovementTimestampMs() { void GameHandler::sendMovement(Opcode opcode) { if (state != WorldState::IN_WORLD) { - LOG_WARNING("Cannot send movement in state: ", (int)state); + LOG_WARNING("Cannot send movement in state: ", static_cast(state)); return; } @@ -7892,7 +10229,43 @@ void GameHandler::sendMovement(Opcode opcode) { if (resurrectPending_ && !taxiAllowed) return; // Always send a strictly increasing non-zero client movement clock value. - movementInfo.time = nextMovementTimestampMs(); + const uint32_t movementTime = nextMovementTimestampMs(); + movementInfo.time = movementTime; + + if (opcode == Opcode::MSG_MOVE_SET_FACING && + (isPreWotlk())) { + const float facingDelta = core::coords::normalizeAngleRad( + movementInfo.orientation - lastFacingSentOrientation_); + const uint32_t sinceLastFacingMs = + lastFacingSendTimeMs_ != 0 && movementTime >= lastFacingSendTimeMs_ + ? (movementTime - lastFacingSendTimeMs_) + : std::numeric_limits::max(); + if (std::abs(facingDelta) < 0.02f && sinceLastFacingMs < 200U) { + return; + } + } + + // Track movement state transition for PLAYER_STARTED/STOPPED_MOVING events + const uint32_t kMoveMask = static_cast(MovementFlags::FORWARD) | + static_cast(MovementFlags::BACKWARD) | + static_cast(MovementFlags::STRAFE_LEFT) | + static_cast(MovementFlags::STRAFE_RIGHT); + const bool wasMoving = (movementInfo.flags & kMoveMask) != 0; + + // Cancel any timed (non-channeled) cast the moment the player starts moving. + // Channeled spells end via MSG_CHANNEL_UPDATE / SMSG_CHANNEL_NOTIFY from the server. + // Turning (MSG_MOVE_START_TURN_*) is allowed while casting. + if (casting && !castIsChannel) { + const bool isPositionalMove = + opcode == Opcode::MSG_MOVE_START_FORWARD || + opcode == Opcode::MSG_MOVE_START_BACKWARD || + opcode == Opcode::MSG_MOVE_START_STRAFE_LEFT || + opcode == Opcode::MSG_MOVE_START_STRAFE_RIGHT || + opcode == Opcode::MSG_MOVE_JUMP; + if (isPositionalMove) { + cancelCast(); + } + } // Update movement flags based on opcode switch (opcode) { @@ -7966,6 +10339,7 @@ void GameHandler::sendMovement(Opcode opcode) { break; case Opcode::MSG_MOVE_HEARTBEAT: // No flag changes — just sends current position + timeSinceLastMoveHeartbeat_ = 0.0f; break; case Opcode::MSG_MOVE_START_ASCEND: movementInfo.flags |= static_cast(MovementFlags::ASCENDING); @@ -7982,6 +10356,20 @@ void GameHandler::sendMovement(Opcode opcode) { break; } + // Fire PLAYER_STARTED/STOPPED_MOVING on movement state transitions + { + const bool isMoving = (movementInfo.flags & kMoveMask) != 0; + if (isMoving && !wasMoving) + fireAddonEvent("PLAYER_STARTED_MOVING", {}); + else if (!isMoving && wasMoving) + fireAddonEvent("PLAYER_STOPPED_MOVING", {}); + } + + if (opcode == Opcode::MSG_MOVE_SET_FACING) { + lastFacingSendTimeMs_ = movementInfo.time; + lastFacingSentOrientation_ = movementInfo.orientation; + } + // Keep fallTime current: it must equal the elapsed milliseconds since FALLING // was set, so the server can compute fall damage correctly. if (isFalling_ && movementInfo.hasFlag(MovementFlags::FALLING)) { @@ -8004,8 +10392,17 @@ void GameHandler::sendMovement(Opcode opcode) { sanitizeMovementForTaxi(); } - // Add transport data if player is on a transport - if (isOnTransport()) { + bool includeTransportInWire = isOnTransport(); + if (includeTransportInWire && transportManager_) { + if (auto* tr = transportManager_->getTransport(playerTransportGuid_); tr && tr->isM2) { + // Client-detected M2 elevators/trams are not always server-recognized transports. + // Sending ONTRANSPORT for these can trigger bad fall-state corrections server-side. + includeTransportInWire = false; + } + } + + // Add transport data if player is on a server-recognized transport + if (includeTransportInWire) { // Keep authoritative world position synchronized to parent transport transform // so heartbeats/corrections don't drag the passenger through geometry. if (transportManager_) { @@ -8045,9 +10442,52 @@ void GameHandler::sendMovement(Opcode opcode) { movementInfo.transportSeat = -1; } + if (opcode == Opcode::MSG_MOVE_HEARTBEAT && isClassicLikeExpansion()) { + const uint32_t locomotionFlags = + static_cast(MovementFlags::FORWARD) | + static_cast(MovementFlags::BACKWARD) | + static_cast(MovementFlags::STRAFE_LEFT) | + static_cast(MovementFlags::STRAFE_RIGHT) | + static_cast(MovementFlags::TURN_LEFT) | + static_cast(MovementFlags::TURN_RIGHT) | + static_cast(MovementFlags::ASCENDING) | + static_cast(MovementFlags::FALLING) | + static_cast(MovementFlags::FALLINGFAR) | + static_cast(MovementFlags::SWIMMING); + const bool stationaryIdle = + !onTaxiFlight_ && + !taxiMountActive_ && + !taxiActivatePending_ && + !taxiClientActive_ && + !includeTransportInWire && + (movementInfo.flags & locomotionFlags) == 0; + const uint32_t sinceLastHeartbeatMs = + lastHeartbeatSendTimeMs_ != 0 && movementTime >= lastHeartbeatSendTimeMs_ + ? (movementTime - lastHeartbeatSendTimeMs_) + : std::numeric_limits::max(); + const bool unchangedState = + std::abs(movementInfo.x - lastHeartbeatX_) < 0.01f && + std::abs(movementInfo.y - lastHeartbeatY_) < 0.01f && + std::abs(movementInfo.z - lastHeartbeatZ_) < 0.01f && + movementInfo.flags == lastHeartbeatFlags_ && + movementInfo.transportGuid == lastHeartbeatTransportGuid_; + if (stationaryIdle && unchangedState && sinceLastHeartbeatMs < 1500U) { + timeSinceLastMoveHeartbeat_ = 0.0f; + return; + } + const uint32_t sinceLastNonHeartbeatMoveMs = + lastNonHeartbeatMoveSendTimeMs_ != 0 && movementTime >= lastNonHeartbeatMoveSendTimeMs_ + ? (movementTime - lastNonHeartbeatMoveSendTimeMs_) + : std::numeric_limits::max(); + if (sinceLastNonHeartbeatMoveMs < 350U) { + timeSinceLastMoveHeartbeat_ = 0.0f; + return; + } + } + LOG_DEBUG("Sending movement packet: opcode=0x", std::hex, wireOpcode(opcode), std::dec, - (isOnTransport() ? " ONTRANSPORT" : "")); + (includeTransportInWire ? " ONTRANSPORT" : "")); // Convert canonical → server coordinates for the wire MovementInfo wireInfo = movementInfo; @@ -8060,7 +10500,7 @@ void GameHandler::sendMovement(Opcode opcode) { wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation); // Also convert transport local position to server coordinates if on transport - if (isOnTransport()) { + if (includeTransportInWire) { glm::vec3 serverTransportPos = core::coords::canonicalToServer( glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ)); wireInfo.transportX = serverTransportPos.x; @@ -8075,6 +10515,17 @@ void GameHandler::sendMovement(Opcode opcode) { ? packetParsers_->buildMovementPacket(opcode, wireInfo, playerGuid) : MovementPacket::build(opcode, wireInfo, playerGuid); socket->send(packet); + + if (opcode == Opcode::MSG_MOVE_HEARTBEAT) { + lastHeartbeatSendTimeMs_ = movementInfo.time; + lastHeartbeatX_ = movementInfo.x; + lastHeartbeatY_ = movementInfo.y; + lastHeartbeatZ_ = movementInfo.z; + lastHeartbeatFlags_ = movementInfo.flags; + lastHeartbeatTransportGuid_ = movementInfo.transportGuid; + } else { + lastNonHeartbeatMoveSendTimeMs_ = movementInfo.time; + } } void GameHandler::sanitizeMovementForTaxi() { @@ -8115,10 +10566,14 @@ void GameHandler::forceClearTaxiAndMovementState() { taxiMountActive_ = false; taxiMountDisplayId_ = 0; currentMountDisplayId_ = 0; + vehicleId_ = 0; resurrectPending_ = false; resurrectRequestPending_ = false; + selfResAvailable_ = false; playerDead_ = false; releasedSpirit_ = false; + corpseGuid_ = 0; + corpseReclaimAvailableMs_ = 0; repopPending_ = false; pendingSpiritHealerGuid_ = 0; resurrectCasterGuid_ = 0; @@ -8150,7 +10605,6 @@ void GameHandler::setOrientation(float orientation) { } void GameHandler::handleUpdateObject(network::Packet& packet) { - static const bool kVerboseUpdateObject = envFlagEnabled("WOWEE_LOG_UPDATE_OBJECT_VERBOSE", false); UpdateObjectData data; if (!packetParsers_->parseUpdateObject(packet, data)) { static int updateObjErrors = 0; @@ -8160,6 +10614,61 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Fall through: process any blocks that were successfully parsed before the failure. } + enqueueUpdateObjectWork(std::move(data)); +} + +void GameHandler::processOutOfRangeObjects(const std::vector& guids) { + // Process out-of-range objects first + for (uint64_t guid : guids) { + auto entity = entityManager.getEntity(guid); + if (!entity) continue; + + const bool isKnownTransport = transportGuids_.count(guid) > 0; + if (isKnownTransport) { + // Keep transports alive across out-of-range flapping. + // Boats/zeppelins are global movers and removing them here can make + // them disappear until a later movement snapshot happens to recreate them. + const bool playerAboardNow = (playerTransportGuid_ == guid); + const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f); + const bool movementSaysAboard = (movementInfo.transportGuid == guid); + LOG_INFO("Preserving transport on out-of-range: 0x", + std::hex, guid, std::dec, + " now=", playerAboardNow, + " sticky=", stickyAboard, + " movement=", movementSaysAboard); + continue; + } + + LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec); + // Trigger despawn callbacks before removing entity + if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) { + creatureDespawnCallback_(guid); + } else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) { + playerDespawnCallback_(guid); + otherPlayerVisibleItemEntries_.erase(guid); + otherPlayerVisibleDirty_.erase(guid); + otherPlayerMoveTimeMs_.erase(guid); + inspectedPlayerItemEntries_.erase(guid); + pendingAutoInspect_.erase(guid); + // Clear pending name query so the query is re-sent when this player + // comes back into range (entity is recreated as a new object). + pendingNameQueries.erase(guid); + } else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) { + gameObjectDespawnCallback_(guid); + } + transportGuids_.erase(guid); + serverUpdatedTransportGuids_.erase(guid); + clearTransportAttachment(guid); + if (playerTransportGuid_ == guid) { + clearPlayerTransport(); + } + entityManager.removeEntity(guid); + } + +} + +void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated) { + static const bool kVerboseUpdateObject = envFlagEnabled("WOWEE_LOG_UPDATE_OBJECT_VERBOSE", false); auto extractPlayerAppearance = [&](const std::map& fields, uint8_t& outRace, uint8_t& outGender, @@ -8281,941 +10790,633 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { pendingMoneyDeltaTimer_ = 0.0f; }; - // Process out-of-range objects first - for (uint64_t guid : data.outOfRangeGuids) { - auto entity = entityManager.getEntity(guid); - if (!entity) continue; + switch (block.updateType) { + case UpdateType::CREATE_OBJECT: + case UpdateType::CREATE_OBJECT2: { + // Create new entity + std::shared_ptr entity; - const bool isKnownTransport = transportGuids_.count(guid) > 0; - if (isKnownTransport) { - // Keep transports alive across out-of-range flapping. - // Boats/zeppelins are global movers and removing them here can make - // them disappear until a later movement snapshot happens to recreate them. - const bool playerAboardNow = (playerTransportGuid_ == guid); - const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f); - const bool movementSaysAboard = (movementInfo.transportGuid == guid); - LOG_INFO("Preserving transport on out-of-range: 0x", - std::hex, guid, std::dec, - " now=", playerAboardNow, - " sticky=", stickyAboard, - " movement=", movementSaysAboard); - continue; - } + switch (block.objectType) { + case ObjectType::PLAYER: + entity = std::make_shared(block.guid); + break; - LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec); - // Trigger despawn callbacks before removing entity - if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) { - creatureDespawnCallback_(guid); - } else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) { - playerDespawnCallback_(guid); - otherPlayerVisibleItemEntries_.erase(guid); - otherPlayerVisibleDirty_.erase(guid); - otherPlayerMoveTimeMs_.erase(guid); - inspectedPlayerItemEntries_.erase(guid); - pendingAutoInspect_.erase(guid); - // Clear pending name query so the query is re-sent when this player - // comes back into range (entity is recreated as a new object). - pendingNameQueries.erase(guid); - } else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) { - gameObjectDespawnCallback_(guid); - } - transportGuids_.erase(guid); - serverUpdatedTransportGuids_.erase(guid); - clearTransportAttachment(guid); - if (playerTransportGuid_ == guid) { - clearPlayerTransport(); - } - entityManager.removeEntity(guid); - } + case ObjectType::UNIT: + entity = std::make_shared(block.guid); + break; - // Process update blocks - bool newItemCreated = false; - for (const auto& block : data.blocks) { - switch (block.updateType) { - case UpdateType::CREATE_OBJECT: - case UpdateType::CREATE_OBJECT2: { - // Create new entity - std::shared_ptr entity; + case ObjectType::GAMEOBJECT: + entity = std::make_shared(block.guid); + break; - switch (block.objectType) { - case ObjectType::PLAYER: - entity = std::make_shared(block.guid); - break; + default: + entity = std::make_shared(block.guid); + entity->setType(block.objectType); + break; + } - case ObjectType::UNIT: - entity = std::make_shared(block.guid); - break; - - case ObjectType::GAMEOBJECT: - entity = std::make_shared(block.guid); - break; - - default: - entity = std::make_shared(block.guid); - entity->setType(block.objectType); - break; + // Set position from movement block (server → canonical) + if (block.hasMovement) { + glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); + float oCanonical = core::coords::serverToCanonicalYaw(block.orientation); + entity->setPosition(pos.x, pos.y, pos.z, oCanonical); + LOG_DEBUG(" Position: (", pos.x, ", ", pos.y, ", ", pos.z, ")"); + if (block.guid == playerGuid && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { + serverRunSpeed_ = block.runSpeed; } - - // Set position from movement block (server → canonical) - if (block.hasMovement) { - glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); - float oCanonical = core::coords::serverToCanonicalYaw(block.orientation); - entity->setPosition(pos.x, pos.y, pos.z, oCanonical); - LOG_DEBUG(" Position: (", pos.x, ", ", pos.y, ", ", pos.z, ")"); - if (block.guid == playerGuid && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { - serverRunSpeed_ = block.runSpeed; - } - // Track player-on-transport state - if (block.guid == playerGuid) { - if (block.onTransport) { - setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f)); - // Convert transport offset from server → canonical coordinates - glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); - playerTransportOffset_ = core::coords::serverToCanonical(serverOffset); - if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) { - glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); - entity->setPosition(composed.x, composed.y, composed.z, oCanonical); - movementInfo.x = composed.x; - movementInfo.y = composed.y; - movementInfo.z = composed.z; - } - LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec, - " offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")"); - } else { - // Don't clear client-side M2 transport boarding (trams) — - // the server doesn't know about client-detected transport attachment. - bool isClientM2Transport = false; - if (playerTransportGuid_ != 0 && transportManager_) { - auto* tr = transportManager_->getTransport(playerTransportGuid_); - isClientM2Transport = (tr && tr->isM2); - } - if (playerTransportGuid_ != 0 && !isClientM2Transport) { - LOG_INFO("Player left transport"); - clearPlayerTransport(); - } + // Track player-on-transport state + if (block.guid == playerGuid) { + if (block.onTransport) { + // Convert transport offset from server → canonical coordinates + glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); + glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset); + setPlayerOnTransport(block.transportGuid, canonicalOffset); + if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) { + glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); + entity->setPosition(composed.x, composed.y, composed.z, oCanonical); + movementInfo.x = composed.x; + movementInfo.y = composed.y; + movementInfo.z = composed.z; } - } - - // Track transport-relative children so they follow parent transport motion. - if (block.guid != playerGuid && - (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::GAMEOBJECT)) { - if (block.onTransport && block.transportGuid != 0) { - glm::vec3 localOffset = core::coords::serverToCanonical( - glm::vec3(block.transportX, block.transportY, block.transportZ)); - const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING - float localOriCanonical = core::coords::normalizeAngleRad(-block.transportO); - setTransportAttachment(block.guid, block.objectType, block.transportGuid, - localOffset, hasLocalOrientation, localOriCanonical); - if (transportManager_ && transportManager_->getTransport(block.transportGuid)) { - glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset); - entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation()); - } - } else { - clearTransportAttachment(block.guid); + LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec, + " offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")"); + } else { + // Don't clear client-side M2 transport boarding (trams) — + // the server doesn't know about client-detected transport attachment. + bool isClientM2Transport = false; + if (playerTransportGuid_ != 0 && transportManager_) { + auto* tr = transportManager_->getTransport(playerTransportGuid_); + isClientM2Transport = (tr && tr->isM2); + } + if (playerTransportGuid_ != 0 && !isClientM2Transport) { + LOG_INFO("Player left transport"); + clearPlayerTransport(); } } } - // Set fields - for (const auto& field : block.fields) { - entity->setField(field.first, field.second); - } - - // Add to manager - entityManager.addEntity(block.guid, entity); - - // For the local player, capture the full initial field state (CREATE_OBJECT carries the - // large baseline update-field set, including visible item fields on many cores). - // Later VALUES updates often only include deltas and may never touch visible item fields. - if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { - lastPlayerFields_ = entity->getFields(); - maybeDetectVisibleItemLayout(); - } - - // Auto-query names (Phase 1) - if (block.objectType == ObjectType::PLAYER) { - queryPlayerName(block.guid); - if (block.guid != playerGuid) { - updateOtherPlayerVisibleItems(block.guid, entity->getFields()); - } - } else if (block.objectType == ObjectType::UNIT) { - auto it = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); - if (it != block.fields.end() && it->second != 0) { - auto unit = std::static_pointer_cast(entity); - unit->setEntry(it->second); - // Set name from cache immediately if available - std::string cached = getCachedCreatureName(it->second); - if (!cached.empty()) { - unit->setName(cached); + // Track transport-relative children so they follow parent transport motion. + if (block.guid != playerGuid && + (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::GAMEOBJECT)) { + if (block.onTransport && block.transportGuid != 0) { + glm::vec3 localOffset = core::coords::serverToCanonical( + glm::vec3(block.transportX, block.transportY, block.transportZ)); + const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING + float localOriCanonical = core::coords::normalizeAngleRad(-block.transportO); + setTransportAttachment(block.guid, block.objectType, block.transportGuid, + localOffset, hasLocalOrientation, localOriCanonical); + if (transportManager_ && transportManager_->getTransport(block.transportGuid)) { + glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset); + entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation()); } - queryCreatureInfo(it->second, block.guid); + } else { + clearTransportAttachment(block.guid); } } + } - // Extract health/mana/power from fields (Phase 2) — single pass - if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) { + // Set fields + for (const auto& field : block.fields) { + entity->setField(field.first, field.second); + } + + // Add to manager + entityManager.addEntity(block.guid, entity); + + // For the local player, capture the full initial field state (CREATE_OBJECT carries the + // large baseline update-field set, including visible item fields on many cores). + // Later VALUES updates often only include deltas and may never touch visible item fields. + if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { + lastPlayerFields_ = entity->getFields(); + maybeDetectVisibleItemLayout(); + } + + // Auto-query names (Phase 1) + if (block.objectType == ObjectType::PLAYER) { + queryPlayerName(block.guid); + if (block.guid != playerGuid) { + updateOtherPlayerVisibleItems(block.guid, entity->getFields()); + } + } else if (block.objectType == ObjectType::UNIT) { + auto it = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); + if (it != block.fields.end() && it->second != 0) { auto unit = std::static_pointer_cast(entity); - constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; - constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; - bool unitInitiallyDead = false; - const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH); - const uint16_t ufPowerBase = fieldIndex(UF::UNIT_FIELD_POWER1); - const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH); - const uint16_t ufMaxPowerBase = fieldIndex(UF::UNIT_FIELD_MAXPOWER1); - const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); - const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE); - const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS); - const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS); - const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID); - const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID); - const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS); - const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0); - for (const auto& [key, val] : block.fields) { - // Check all specific fields BEFORE power/maxpower range checks. - // In Classic, power indices (23-27) are adjacent to maxHealth (28), - // and maxPower indices (29-33) are adjacent to level (34) and faction (35). - // A range check like "key >= powerBase && key < powerBase+7" would - // incorrectly capture maxHealth/level/faction in Classic's tight layout. - if (key == ufHealth) { - unit->setHealth(val); - if (block.objectType == ObjectType::UNIT && val == 0) { - unitInitiallyDead = true; - } - if (block.guid == playerGuid && val == 0) { - playerDead_ = true; - LOG_INFO("Player logged in dead"); - } - } else if (key == ufMaxHealth) { unit->setMaxHealth(val); } - else if (key == ufLevel) { - unit->setLevel(val); - } else if (key == ufFaction) { unit->setFactionTemplate(val); } - else if (key == ufFlags) { unit->setUnitFlags(val); } - else if (key == ufBytes0) { - unit->setPowerType(static_cast((val >> 24) & 0xFF)); - } else if (key == ufDisplayId) { unit->setDisplayId(val); } - else if (key == ufNpcFlags) { unit->setNpcFlags(val); } - else if (key == ufDynFlags) { - unit->setDynamicFlags(val); - if (block.objectType == ObjectType::UNIT && - ((val & UNIT_DYNFLAG_DEAD) != 0 || (val & UNIT_DYNFLAG_LOOTABLE) != 0)) { - unitInitiallyDead = true; - } + unit->setEntry(it->second); + // Set name from cache immediately if available + std::string cached = getCachedCreatureName(it->second); + if (!cached.empty()) { + unit->setName(cached); + } + queryCreatureInfo(it->second, block.guid); + } + } + + // Extract health/mana/power from fields (Phase 2) — single pass + if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) { + auto unit = std::static_pointer_cast(entity); + constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; + constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; + bool unitInitiallyDead = false; + const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH); + const uint16_t ufPowerBase = fieldIndex(UF::UNIT_FIELD_POWER1); + const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH); + const uint16_t ufMaxPowerBase = fieldIndex(UF::UNIT_FIELD_MAXPOWER1); + const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); + const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE); + const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS); + const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS); + const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID); + const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID); + const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS); + const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0); + for (const auto& [key, val] : block.fields) { + // Check all specific fields BEFORE power/maxpower range checks. + // In Classic, power indices (23-27) are adjacent to maxHealth (28), + // and maxPower indices (29-33) are adjacent to level (34) and faction (35). + // A range check like "key >= powerBase && key < powerBase+7" would + // incorrectly capture maxHealth/level/faction in Classic's tight layout. + if (key == ufHealth) { + unit->setHealth(val); + if (block.objectType == ObjectType::UNIT && val == 0) { + unitInitiallyDead = true; } - // Power/maxpower range checks AFTER all specific fields - else if (key >= ufPowerBase && key < ufPowerBase + 7) { - unit->setPowerByType(static_cast(key - ufPowerBase), val); - } else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { - unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); + if (block.guid == playerGuid && val == 0) { + playerDead_ = true; + LOG_INFO("Player logged in dead"); } - else if (key == ufMountDisplayId) { - if (block.guid == playerGuid) { - uint32_t old = currentMountDisplayId_; - currentMountDisplayId_ = val; - if (val != old && mountCallback_) mountCallback_(val); - if (old == 0 && val != 0) { - // Just mounted — find the mount aura (indefinite duration, self-cast) - mountAuraSpellId_ = 0; - for (const auto& a : playerAuras) { - if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) { - mountAuraSpellId_ = a.spellId; - } + } else if (key == ufMaxHealth) { unit->setMaxHealth(val); } + else if (key == ufLevel) { + unit->setLevel(val); + } else if (key == ufFaction) { + unit->setFactionTemplate(val); + if (addonEventCallback_) { + auto uid = guidToUnitId(block.guid); + if (!uid.empty()) + fireAddonEvent("UNIT_FACTION", {uid}); + } + } + else if (key == ufFlags) { + unit->setUnitFlags(val); + if (addonEventCallback_) { + auto uid = guidToUnitId(block.guid); + if (!uid.empty()) + fireAddonEvent("UNIT_FLAGS", {uid}); + } + } + else if (key == ufBytes0) { + unit->setPowerType(static_cast((val >> 24) & 0xFF)); + } else if (key == ufDisplayId) { + unit->setDisplayId(val); + if (addonEventCallback_) { + auto uid = guidToUnitId(block.guid); + if (!uid.empty()) + fireAddonEvent("UNIT_MODEL_CHANGED", {uid}); + } + } + else if (key == ufNpcFlags) { unit->setNpcFlags(val); } + else if (key == ufDynFlags) { + unit->setDynamicFlags(val); + if (block.objectType == ObjectType::UNIT && + ((val & UNIT_DYNFLAG_DEAD) != 0 || (val & UNIT_DYNFLAG_LOOTABLE) != 0)) { + unitInitiallyDead = true; + } + } + // Power/maxpower range checks AFTER all specific fields + else if (key >= ufPowerBase && key < ufPowerBase + 7) { + unit->setPowerByType(static_cast(key - ufPowerBase), val); + } else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { + unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); + } + else if (key == ufMountDisplayId) { + if (block.guid == playerGuid) { + uint32_t old = currentMountDisplayId_; + currentMountDisplayId_ = val; + if (val != old && mountCallback_) mountCallback_(val); + if (val != old) + fireAddonEvent("UNIT_MODEL_CHANGED", {"player"}); + if (old == 0 && val != 0) { + // Just mounted — find the mount aura (indefinite duration, self-cast) + mountAuraSpellId_ = 0; + for (const auto& a : playerAuras) { + if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) { + mountAuraSpellId_ = a.spellId; } - // Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block - if (mountAuraSpellId_ == 0) { - const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); - if (ufAuras != 0xFFFF) { - for (const auto& [fk, fv] : block.fields) { - if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) { - mountAuraSpellId_ = fv; - break; - } + } + // Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block + if (mountAuraSpellId_ == 0) { + const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); + if (ufAuras != 0xFFFF) { + for (const auto& [fk, fv] : block.fields) { + if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) { + mountAuraSpellId_ = fv; + break; } } } - LOG_INFO("Mount detected: displayId=", val, " auraSpellId=", mountAuraSpellId_); - } - if (old != 0 && val == 0) { - mountAuraSpellId_ = 0; - for (auto& a : playerAuras) - if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{}; } + LOG_INFO("Mount detected: displayId=", val, " auraSpellId=", mountAuraSpellId_); + } + if (old != 0 && val == 0) { + mountAuraSpellId_ = 0; + for (auto& a : playerAuras) + if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{}; } - unit->setMountDisplayId(val); - } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } - } - if (block.guid == playerGuid) { - constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; - if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) { - onTaxiFlight_ = true; - taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f); - sanitizeMovementForTaxi(); - applyTaxiMountForCurrentNode(); } + unit->setMountDisplayId(val); } - if (block.guid == playerGuid && - (unit->getDynamicFlags() & UNIT_DYNFLAG_DEAD) != 0) { + } + if (block.guid == playerGuid) { + constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; + if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) { + onTaxiFlight_ = true; + taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f); + sanitizeMovementForTaxi(); + applyTaxiMountForCurrentNode(); + } + } + if (block.guid == playerGuid && + (unit->getDynamicFlags() & UNIT_DYNFLAG_DEAD) != 0) { + playerDead_ = true; + LOG_INFO("Player logged in dead (dynamic flags)"); + } + // Detect ghost state on login via PLAYER_FLAGS + if (block.guid == playerGuid) { + constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; + auto pfIt = block.fields.find(fieldIndex(UF::PLAYER_FLAGS)); + if (pfIt != block.fields.end() && (pfIt->second & PLAYER_FLAGS_GHOST) != 0) { + releasedSpirit_ = true; playerDead_ = true; - LOG_INFO("Player logged in dead (dynamic flags)"); - } - // Detect ghost state on login via PLAYER_FLAGS - if (block.guid == playerGuid) { - constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; - auto pfIt = block.fields.find(fieldIndex(UF::PLAYER_FLAGS)); - if (pfIt != block.fields.end() && (pfIt->second & PLAYER_FLAGS_GHOST) != 0) { - releasedSpirit_ = true; - playerDead_ = true; - LOG_INFO("Player logged in as ghost (PLAYER_FLAGS)"); - if (ghostStateCallback_) ghostStateCallback_(true); - } - } - // Determine hostility from faction template for online creatures. - // Always call isHostileFaction — factionTemplate=0 defaults to hostile - // in the lookup rather than silently staying at the struct default (false). - unit->setHostile(isHostileFaction(unit->getFactionTemplate())); - // Trigger creature spawn callback for units/players with displayId - if (block.objectType == ObjectType::UNIT && unit->getDisplayId() == 0) { - LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, - " has displayId=0 — no spawn (entry=", unit->getEntry(), - " at ", unit->getX(), ",", unit->getY(), ",", unit->getZ(), ")"); - } - if ((block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) && unit->getDisplayId() != 0) { - if (block.objectType == ObjectType::PLAYER && block.guid == playerGuid) { - // Skip local player — spawned separately via spawnPlayerCharacter() - } else if (block.objectType == ObjectType::PLAYER) { - if (playerSpawnCallback_) { - uint8_t race = 0, gender = 0, facial = 0; - uint32_t appearanceBytes = 0; - // Use the entity's accumulated field state, not just this block's changed fields. - if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) { - playerSpawnCallback_(block.guid, unit->getDisplayId(), race, gender, - appearanceBytes, facial, - unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); - } else { - LOG_WARNING("[Spawn] PLAYER guid=0x", std::hex, block.guid, std::dec, - " displayId=", unit->getDisplayId(), " appearance extraction failed — model will not render"); - } - } - } else if (creatureSpawnCallback_) { - LOG_DEBUG("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, - " displayId=", unit->getDisplayId(), " at (", - unit->getX(), ",", unit->getY(), ",", unit->getZ(), ")"); - float unitScale = 1.0f; - { - uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); - if (scaleIdx != 0xFFFF) { - uint32_t raw = entity->getField(scaleIdx); - if (raw != 0) { - std::memcpy(&unitScale, &raw, sizeof(float)); - if (unitScale <= 0.01f || unitScale > 100.0f) unitScale = 1.0f; - } - } - } - creatureSpawnCallback_(block.guid, unit->getDisplayId(), - unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale); - if (unitInitiallyDead && npcDeathCallback_) { - npcDeathCallback_(block.guid); - } - } - // Initialise swim/walk state from spawn-time movement flags (cold-join fix). - // Without this, an entity already swimming/walking when the client joins - // won't get its animation state set until the next MSG_MOVE_* heartbeat. - if (block.hasMovement && block.moveFlags != 0 && unitMoveFlagsCallback_ && - block.guid != playerGuid) { - unitMoveFlagsCallback_(block.guid, block.moveFlags); - } - // Query quest giver status for NPCs with questgiver flag (0x02) - if (block.objectType == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { - network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); - qsPkt.writeUInt64(block.guid); - socket->send(qsPkt); + LOG_INFO("Player logged in as ghost (PLAYER_FLAGS)"); + if (ghostStateCallback_) ghostStateCallback_(true); + // Query corpse position so minimap marker is accurate on reconnect + if (socket) { + network::Packet cq(wireOpcode(Opcode::MSG_CORPSE_QUERY)); + socket->send(cq); } } } - // Extract displayId and entry for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8) - if (block.objectType == ObjectType::GAMEOBJECT) { - auto go = std::static_pointer_cast(entity); - auto itDisp = block.fields.find(fieldIndex(UF::GAMEOBJECT_DISPLAYID)); - if (itDisp != block.fields.end()) { - go->setDisplayId(itDisp->second); - } - auto itEntry = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); - if (itEntry != block.fields.end() && itEntry->second != 0) { - go->setEntry(itEntry->second); - auto cacheIt = gameObjectInfoCache_.find(itEntry->second); - if (cacheIt != gameObjectInfoCache_.end()) { - go->setName(cacheIt->second.name); + // Classic: rebuild playerAuras from UNIT_FIELD_AURAS on initial object create + if (block.guid == playerGuid && isClassicLikeExpansion()) { + const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); + const uint16_t ufAuraFlags = fieldIndex(UF::UNIT_FIELD_AURAFLAGS); + if (ufAuras != 0xFFFF) { + bool hasAuraField = false; + for (const auto& [fk, fv] : block.fields) { + if (fk >= ufAuras && fk < ufAuras + 48) { hasAuraField = true; break; } + } + if (hasAuraField) { + playerAuras.clear(); + playerAuras.resize(48); + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto& allFields = entity->getFields(); + for (int slot = 0; slot < 48; ++slot) { + auto it = allFields.find(static_cast(ufAuras + slot)); + if (it != allFields.end() && it->second != 0) { + AuraSlot& a = playerAuras[slot]; + a.spellId = it->second; + // Read aura flag byte: packed 4-per-uint32 at ufAuraFlags + // Classic flags: 0x01=cancelable, 0x02=harmful, 0x04=helpful + // Normalize to WotLK convention: 0x80 = negative (debuff) + uint8_t classicFlag = 0; + if (ufAuraFlags != 0xFFFF) { + auto fit = allFields.find(static_cast(ufAuraFlags + slot / 4)); + if (fit != allFields.end()) + classicFlag = static_cast((fit->second >> ((slot % 4) * 8)) & 0xFF); + } + // Map Classic harmful bit (0x02) → WotLK debuff bit (0x80) + a.flags = (classicFlag & 0x02) ? 0x80u : 0u; + a.durationMs = -1; + a.maxDurationMs = -1; + a.casterGuid = playerGuid; + a.receivedAtMs = nowMs; + } + } + LOG_DEBUG("[Classic] Rebuilt playerAuras from UNIT_FIELD_AURAS (CREATE_OBJECT)"); + fireAddonEvent("UNIT_AURA", {"player"}); } - queryGameObjectInfo(itEntry->second, block.guid); } - // Detect transport GameObjects via UPDATEFLAG_TRANSPORT (0x0002) - LOG_DEBUG("GameObject CREATE: guid=0x", std::hex, block.guid, std::dec, - " entry=", go->getEntry(), " displayId=", go->getDisplayId(), - " updateFlags=0x", std::hex, block.updateFlags, std::dec, - " pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")"); - if (block.updateFlags & 0x0002) { - transportGuids_.insert(block.guid); - LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec, - " entry=", go->getEntry(), - " displayId=", go->getDisplayId(), - " pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")"); - // Note: TransportSpawnCallback will be invoked from Application after WMO instance is created - } - if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) { - float goScale = 1.0f; + } + // Determine hostility from faction template for online creatures. + // Always call isHostileFaction — factionTemplate=0 defaults to hostile + // in the lookup rather than silently staying at the struct default (false). + unit->setHostile(isHostileFaction(unit->getFactionTemplate())); + // Trigger creature spawn callback for units/players with displayId + if (block.objectType == ObjectType::UNIT && unit->getDisplayId() == 0) { + LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, + " has displayId=0 — no spawn (entry=", unit->getEntry(), + " at ", unit->getX(), ",", unit->getY(), ",", unit->getZ(), ")"); + } + if ((block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) && unit->getDisplayId() != 0) { + if (block.objectType == ObjectType::PLAYER && block.guid == playerGuid) { + // Skip local player — spawned separately via spawnPlayerCharacter() + } else if (block.objectType == ObjectType::PLAYER) { + if (playerSpawnCallback_) { + uint8_t race = 0, gender = 0, facial = 0; + uint32_t appearanceBytes = 0; + // Use the entity's accumulated field state, not just this block's changed fields. + if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) { + playerSpawnCallback_(block.guid, unit->getDisplayId(), race, gender, + appearanceBytes, facial, + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + } else { + LOG_WARNING("[Spawn] PLAYER guid=0x", std::hex, block.guid, std::dec, + " displayId=", unit->getDisplayId(), " appearance extraction failed — model will not render"); + } + } + if (unitInitiallyDead && npcDeathCallback_) { + npcDeathCallback_(block.guid); + } + } else if (creatureSpawnCallback_) { + LOG_DEBUG("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, + " displayId=", unit->getDisplayId(), " at (", + unit->getX(), ",", unit->getY(), ",", unit->getZ(), ")"); + float unitScale = 1.0f; { uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); if (scaleIdx != 0xFFFF) { uint32_t raw = entity->getField(scaleIdx); if (raw != 0) { - std::memcpy(&goScale, &raw, sizeof(float)); - if (goScale <= 0.01f || goScale > 100.0f) goScale = 1.0f; + std::memcpy(&unitScale, &raw, sizeof(float)); + if (unitScale <= 0.01f || unitScale > 100.0f) unitScale = 1.0f; } } } - gameObjectSpawnCallback_(block.guid, go->getEntry(), go->getDisplayId(), - go->getX(), go->getY(), go->getZ(), go->getOrientation(), goScale); - } - // Fire transport move callback for transports (position update on re-creation) - if (transportGuids_.count(block.guid) && transportMoveCallback_) { - serverUpdatedTransportGuids_.insert(block.guid); - transportMoveCallback_(block.guid, - go->getX(), go->getY(), go->getZ(), go->getOrientation()); - } - } - // Track online item objects (CONTAINER = bags, also tracked as items) - if (block.objectType == ObjectType::ITEM || block.objectType == ObjectType::CONTAINER) { - auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); - auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT)); - auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY)); - auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY)); - if (entryIt != block.fields.end() && entryIt->second != 0) { - // Preserve existing info when doing partial updates - OnlineItemInfo info = onlineItems_.count(block.guid) - ? onlineItems_[block.guid] : OnlineItemInfo{}; - info.entry = entryIt->second; - if (stackIt != block.fields.end()) info.stackCount = stackIt->second; - if (durIt != block.fields.end()) info.curDurability = durIt->second; - if (maxDurIt!= block.fields.end()) info.maxDurability = maxDurIt->second; - bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end()); - onlineItems_[block.guid] = info; - if (isNew) newItemCreated = true; - queryItemInfo(info.entry, block.guid); - } - // Extract container slot GUIDs for bags - if (block.objectType == ObjectType::CONTAINER) { - extractContainerFields(block.guid, block.fields); - } - } - - // Extract XP / inventory slot / skill fields for player entity - if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { - // Auto-detect coinage index using the previous snapshot vs this full snapshot. - maybeDetectCoinageIndex(lastPlayerFields_, block.fields); - - lastPlayerFields_ = block.fields; - detectInventorySlotBases(block.fields); - - if (kVerboseUpdateObject) { - uint16_t maxField = 0; - for (const auto& [key, _val] : block.fields) { - if (key > maxField) maxField = key; + creatureSpawnCallback_(block.guid, unit->getDisplayId(), + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale); + if (unitInitiallyDead && npcDeathCallback_) { + npcDeathCallback_(block.guid); } - LOG_INFO("Player update with ", block.fields.size(), - " fields (max index=", maxField, ")"); } - - bool slotsChanged = false; - const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP); - const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP); - const uint16_t ufPlayerRestedXp = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE); - const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); - const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); - const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); - const uint16_t ufPBytes2 = fieldIndex(UF::PLAYER_BYTES_2); - const uint16_t ufStats[5] = { - fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1), - fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3), - fieldIndex(UF::UNIT_FIELD_STAT4) - }; - for (const auto& [key, val] : block.fields) { - if (key == ufPlayerXp) { playerXp_ = val; } - else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; } - else if (ufPlayerRestedXp != 0xFFFF && key == ufPlayerRestedXp) { playerRestedXp_ = val; } - else if (key == ufPlayerLevel) { - serverPlayerLevel_ = val; - for (auto& ch : characters) { - if (ch.guid == playerGuid) { ch.level = val; break; } + // Initialise swim/walk state from spawn-time movement flags (cold-join fix). + // Without this, an entity already swimming/walking when the client joins + // won't get its animation state set until the next MSG_MOVE_* heartbeat. + if (block.hasMovement && block.moveFlags != 0 && unitMoveFlagsCallback_ && + block.guid != playerGuid) { + unitMoveFlagsCallback_(block.guid, block.moveFlags); + } + // Query quest giver status for NPCs with questgiver flag (0x02) + if (block.objectType == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { + network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + qsPkt.writeUInt64(block.guid); + socket->send(qsPkt); + } + } + } + // Extract displayId and entry for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8) + if (block.objectType == ObjectType::GAMEOBJECT) { + auto go = std::static_pointer_cast(entity); + auto itDisp = block.fields.find(fieldIndex(UF::GAMEOBJECT_DISPLAYID)); + if (itDisp != block.fields.end()) { + go->setDisplayId(itDisp->second); + } + auto itEntry = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); + if (itEntry != block.fields.end() && itEntry->second != 0) { + go->setEntry(itEntry->second); + auto cacheIt = gameObjectInfoCache_.find(itEntry->second); + if (cacheIt != gameObjectInfoCache_.end()) { + go->setName(cacheIt->second.name); + } + queryGameObjectInfo(itEntry->second, block.guid); + } + // Detect transport GameObjects via UPDATEFLAG_TRANSPORT (0x0002) + LOG_DEBUG("GameObject CREATE: guid=0x", std::hex, block.guid, std::dec, + " entry=", go->getEntry(), " displayId=", go->getDisplayId(), + " updateFlags=0x", std::hex, block.updateFlags, std::dec, + " pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")"); + if (block.updateFlags & 0x0002) { + transportGuids_.insert(block.guid); + LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec, + " entry=", go->getEntry(), + " displayId=", go->getDisplayId(), + " pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")"); + // Note: TransportSpawnCallback will be invoked from Application after WMO instance is created + } + if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) { + float goScale = 1.0f; + { + uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); + if (scaleIdx != 0xFFFF) { + uint32_t raw = entity->getField(scaleIdx); + if (raw != 0) { + std::memcpy(&goScale, &raw, sizeof(float)); + if (goScale <= 0.01f || goScale > 100.0f) goScale = 1.0f; } } - else if (key == ufCoinage) { - playerMoneyCopper_ = val; - LOG_DEBUG("Money set from update fields: ", val, " copper"); - } - else if (ufArmor != 0xFFFF && key == ufArmor) { - playerArmorRating_ = static_cast(val); - LOG_DEBUG("Armor rating from update fields: ", playerArmorRating_); - } - else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) { - uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); - LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec, - " bankBagSlots=", static_cast(bankBagSlots)); - inventory.setPurchasedBankBagSlots(bankBagSlots); - // Byte 3 (bits 24-31): REST_STATE - // 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY - uint8_t restStateByte = static_cast((val >> 24) & 0xFF); - isResting_ = (restStateByte != 0); - } - else { - for (int si = 0; si < 5; ++si) { - if (ufStats[si] != 0xFFFF && key == ufStats[si]) { - playerStats_[si] = static_cast(val); - break; - } - } - } - // Do not synthesize quest-log entries from raw update-field slots. - // Slot layouts differ on some classic-family realms and can produce - // phantom "already accepted" quests that block quest acceptance. } - if (applyInventoryFields(block.fields)) slotsChanged = true; - if (slotsChanged) rebuildOnlineInventory(); - maybeDetectVisibleItemLayout(); - extractSkillFields(lastPlayerFields_); - extractExploredZoneFields(lastPlayerFields_); - applyQuestStateFromFields(lastPlayerFields_); + gameObjectSpawnCallback_(block.guid, go->getEntry(), go->getDisplayId(), + go->getX(), go->getY(), go->getZ(), go->getOrientation(), goScale); + } + // Fire transport move callback for transports (position update on re-creation) + if (transportGuids_.count(block.guid) && transportMoveCallback_) { + serverUpdatedTransportGuids_.insert(block.guid); + transportMoveCallback_(block.guid, + go->getX(), go->getY(), go->getZ(), go->getOrientation()); + } + } + // Detect player's own corpse object so we have the position even when + // SMSG_DEATH_RELEASE_LOC hasn't been received (e.g. login as ghost). + if (block.objectType == ObjectType::CORPSE && block.hasMovement) { + // CORPSE_FIELD_OWNER is at index 6 (uint64, low word at 6, high at 7) + uint16_t ownerLowIdx = 6; + auto ownerLowIt = block.fields.find(ownerLowIdx); + uint32_t ownerLow = (ownerLowIt != block.fields.end()) ? ownerLowIt->second : 0; + auto ownerHighIt = block.fields.find(ownerLowIdx + 1); + uint32_t ownerHigh = (ownerHighIt != block.fields.end()) ? ownerHighIt->second : 0; + uint64_t ownerGuid = (static_cast(ownerHigh) << 32) | ownerLow; + if (ownerGuid == playerGuid || ownerLow == static_cast(playerGuid)) { + // Server coords from movement block + corpseGuid_ = block.guid; + corpseX_ = block.x; + corpseY_ = block.y; + corpseZ_ = block.z; + corpseMapId_ = currentMapId_; + LOG_INFO("Corpse object detected: guid=0x", std::hex, corpseGuid_, std::dec, + " server=(", block.x, ", ", block.y, ", ", block.z, + ") map=", corpseMapId_); } - break; } - case UpdateType::VALUES: { - // Update existing entity fields - auto entity = entityManager.getEntity(block.guid); - if (entity) { - if (block.hasMovement) { - glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); - float oCanonical = core::coords::serverToCanonicalYaw(block.orientation); - entity->setPosition(pos.x, pos.y, pos.z, oCanonical); - - if (block.guid != playerGuid && - (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::GAMEOBJECT)) { - if (block.onTransport && block.transportGuid != 0) { - glm::vec3 localOffset = core::coords::serverToCanonical( - glm::vec3(block.transportX, block.transportY, block.transportZ)); - const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING - float localOriCanonical = core::coords::normalizeAngleRad(-block.transportO); - setTransportAttachment(block.guid, entity->getType(), block.transportGuid, - localOffset, hasLocalOrientation, localOriCanonical); - if (transportManager_ && transportManager_->getTransport(block.transportGuid)) { - glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset); - entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation()); - } - } else { - clearTransportAttachment(block.guid); - } - } - } - - for (const auto& field : block.fields) { - entity->setField(field.first, field.second); - } - - if (entity->getType() == ObjectType::PLAYER && block.guid != playerGuid) { - updateOtherPlayerVisibleItems(block.guid, entity->getFields()); - } - - // Update cached health/mana/power values (Phase 2) — single pass - if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { - auto unit = std::static_pointer_cast(entity); - constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; - constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; - uint32_t oldDisplayId = unit->getDisplayId(); - bool displayIdChanged = false; - bool npcDeathNotified = false; - bool npcRespawnNotified = false; - const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH); - const uint16_t ufPowerBase = fieldIndex(UF::UNIT_FIELD_POWER1); - const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH); - const uint16_t ufMaxPowerBase = fieldIndex(UF::UNIT_FIELD_MAXPOWER1); - const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); - const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE); - const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS); - const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS); - const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID); - const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID); - const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS); - const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0); - for (const auto& [key, val] : block.fields) { - if (key == ufHealth) { - uint32_t oldHealth = unit->getHealth(); - unit->setHealth(val); - if (val == 0) { - if (block.guid == autoAttackTarget) { - stopAutoAttack(); - } - hostileAttackers_.erase(block.guid); - if (block.guid == playerGuid) { - playerDead_ = true; - releasedSpirit_ = false; - stopAutoAttack(); - LOG_INFO("Player died!"); - } - if (entity->getType() == ObjectType::UNIT && npcDeathCallback_) { - npcDeathCallback_(block.guid); - npcDeathNotified = true; - } - } else if (oldHealth == 0 && val > 0) { - if (block.guid == playerGuid) { - playerDead_ = false; - if (!releasedSpirit_) { - LOG_INFO("Player resurrected!"); - } else { - LOG_INFO("Player entered ghost form"); - } - } - if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) { - npcRespawnCallback_(block.guid); - npcRespawnNotified = true; - } - } - // Specific fields checked BEFORE power/maxpower range checks - // (Classic packs maxHealth/level/faction adjacent to power indices) - } else if (key == ufMaxHealth) { unit->setMaxHealth(val); } - else if (key == ufBytes0) { - unit->setPowerType(static_cast((val >> 24) & 0xFF)); - } else if (key == ufFlags) { unit->setUnitFlags(val); } - else if (key == ufDynFlags) { - uint32_t oldDyn = unit->getDynamicFlags(); - unit->setDynamicFlags(val); - if (block.guid == playerGuid) { - bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0; - bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; - if (!wasDead && nowDead) { - playerDead_ = true; - releasedSpirit_ = false; - LOG_INFO("Player died (dynamic flags)"); - } else if (wasDead && !nowDead) { - playerDead_ = false; - releasedSpirit_ = false; - LOG_INFO("Player resurrected (dynamic flags)"); - } - } else if (entity->getType() == ObjectType::UNIT) { - bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0; - bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; - if (!wasDead && nowDead) { - if (!npcDeathNotified && npcDeathCallback_) { - npcDeathCallback_(block.guid); - npcDeathNotified = true; - } - } else if (wasDead && !nowDead) { - if (!npcRespawnNotified && npcRespawnCallback_) { - npcRespawnCallback_(block.guid); - npcRespawnNotified = true; - } - } - } - } else if (key == ufLevel) { - uint32_t oldLvl = unit->getLevel(); - unit->setLevel(val); - if (block.guid != playerGuid && - entity->getType() == ObjectType::PLAYER && - val > oldLvl && oldLvl > 0 && - otherPlayerLevelUpCallback_) { - otherPlayerLevelUpCallback_(block.guid, val); - } - } - else if (key == ufFaction) { - unit->setFactionTemplate(val); - unit->setHostile(isHostileFaction(val)); - } else if (key == ufDisplayId) { - if (val != unit->getDisplayId()) { - unit->setDisplayId(val); - displayIdChanged = true; - } - } else if (key == ufMountDisplayId) { - if (block.guid == playerGuid) { - uint32_t old = currentMountDisplayId_; - currentMountDisplayId_ = val; - if (val != old && mountCallback_) mountCallback_(val); - if (old == 0 && val != 0) { - mountAuraSpellId_ = 0; - for (const auto& a : playerAuras) { - if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) { - mountAuraSpellId_ = a.spellId; - } - } - // Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block - if (mountAuraSpellId_ == 0) { - const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); - if (ufAuras != 0xFFFF) { - for (const auto& [fk, fv] : block.fields) { - if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) { - mountAuraSpellId_ = fv; - break; - } - } - } - } - LOG_INFO("Mount detected (values update): displayId=", val, " auraSpellId=", mountAuraSpellId_); - } - if (old != 0 && val == 0) { - mountAuraSpellId_ = 0; - for (auto& a : playerAuras) - if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{}; - } - } - unit->setMountDisplayId(val); - } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } - // Power/maxpower range checks AFTER all specific fields - else if (key >= ufPowerBase && key < ufPowerBase + 7) { - unit->setPowerByType(static_cast(key - ufPowerBase), val); - } else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { - unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); - } - } - - // Some units/players are created without displayId and get it later via VALUES. - if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && - displayIdChanged && - unit->getDisplayId() != 0 && - unit->getDisplayId() != oldDisplayId) { - if (entity->getType() == ObjectType::PLAYER && block.guid == playerGuid) { - // Skip local player — spawned separately - } else if (entity->getType() == ObjectType::PLAYER) { - if (playerSpawnCallback_) { - uint8_t race = 0, gender = 0, facial = 0; - uint32_t appearanceBytes = 0; - // Use the entity's accumulated field state, not just this block's changed fields. - if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) { - playerSpawnCallback_(block.guid, unit->getDisplayId(), race, gender, - appearanceBytes, facial, - unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); - } else { - LOG_WARNING("[Spawn] PLAYER guid=0x", std::hex, block.guid, std::dec, - " displayId=", unit->getDisplayId(), " appearance extraction failed (VALUES update) — model will not render"); - } - } - } else if (creatureSpawnCallback_) { - float unitScale2 = 1.0f; - { - uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); - if (scaleIdx != 0xFFFF) { - uint32_t raw = entity->getField(scaleIdx); - if (raw != 0) { - std::memcpy(&unitScale2, &raw, sizeof(float)); - if (unitScale2 <= 0.01f || unitScale2 > 100.0f) unitScale2 = 1.0f; - } - } - } - creatureSpawnCallback_(block.guid, unit->getDisplayId(), - unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale2); - bool isDeadNow = (unit->getHealth() == 0) || - ((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0); - if (isDeadNow && !npcDeathNotified && npcDeathCallback_) { - npcDeathCallback_(block.guid); - npcDeathNotified = true; - } - } - if (entity->getType() == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { - network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); - qsPkt.writeUInt64(block.guid); - socket->send(qsPkt); - } - } - } - // Update XP / inventory slot / skill fields for player entity - if (block.guid == playerGuid) { - const bool needCoinageDetectSnapshot = - (pendingMoneyDelta_ != 0 && pendingMoneyDeltaTimer_ > 0.0f); - std::map oldFieldsSnapshot; - if (needCoinageDetectSnapshot) { - oldFieldsSnapshot = lastPlayerFields_; - } - if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { - serverRunSpeed_ = block.runSpeed; - // Some server dismount paths update run speed without updating mount display field. - if (!onTaxiFlight_ && !taxiMountActive_ && - currentMountDisplayId_ != 0 && block.runSpeed <= 8.5f) { - LOG_INFO("Auto-clearing mount from movement speed update: speed=", block.runSpeed, - " displayId=", currentMountDisplayId_); - currentMountDisplayId_ = 0; - if (mountCallback_) { - mountCallback_(0); - } - } - } - auto mergeHint = lastPlayerFields_.end(); - for (const auto& [key, val] : block.fields) { - mergeHint = lastPlayerFields_.insert_or_assign(mergeHint, key, val); - } - if (needCoinageDetectSnapshot) { - maybeDetectCoinageIndex(oldFieldsSnapshot, lastPlayerFields_); - } - maybeDetectVisibleItemLayout(); - detectInventorySlotBases(block.fields); - bool slotsChanged = false; - const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP); - const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP); - const uint16_t ufPlayerRestedXpV = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE); - const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); - const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); - const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS); - const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); - const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2); - const uint16_t ufStatsV[5] = { - fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1), - fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3), - fieldIndex(UF::UNIT_FIELD_STAT4) - }; - for (const auto& [key, val] : block.fields) { - if (key == ufPlayerXp) { - playerXp_ = val; - LOG_DEBUG("XP updated: ", val); - } - else if (key == ufPlayerNextXp) { - playerNextLevelXp_ = val; - LOG_DEBUG("Next level XP updated: ", val); - } - else if (ufPlayerRestedXpV != 0xFFFF && key == ufPlayerRestedXpV) { - playerRestedXp_ = val; - } - else if (key == ufPlayerLevel) { - serverPlayerLevel_ = val; - LOG_DEBUG("Level updated: ", val); - for (auto& ch : characters) { - if (ch.guid == playerGuid) { - ch.level = val; - break; - } - } - } - else if (key == ufCoinage) { - playerMoneyCopper_ = val; - LOG_DEBUG("Money updated via VALUES: ", val, " copper"); - } - else if (ufArmor != 0xFFFF && key == ufArmor) { - playerArmorRating_ = static_cast(val); - } - else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) { - uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); - LOG_WARNING("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, - " bankBagSlots=", static_cast(bankBagSlots)); - inventory.setPurchasedBankBagSlots(bankBagSlots); - // Byte 3 (bits 24-31): REST_STATE - // 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY - uint8_t restStateByte = static_cast((val >> 24) & 0xFF); - isResting_ = (restStateByte != 0); - } - else if (key == ufPlayerFlags) { - constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; - bool wasGhost = releasedSpirit_; - bool nowGhost = (val & PLAYER_FLAGS_GHOST) != 0; - if (!wasGhost && nowGhost) { - releasedSpirit_ = true; - LOG_INFO("Player entered ghost form (PLAYER_FLAGS)"); - if (ghostStateCallback_) ghostStateCallback_(true); - } else if (wasGhost && !nowGhost) { - releasedSpirit_ = false; - playerDead_ = false; - repopPending_ = false; - resurrectPending_ = false; - LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)"); - if (ghostStateCallback_) ghostStateCallback_(false); - } - } - else { - for (int si = 0; si < 5; ++si) { - if (ufStatsV[si] != 0xFFFF && key == ufStatsV[si]) { - playerStats_[si] = static_cast(val); - break; - } - } - } - } - // Do not auto-create quests from VALUES quest-log slot fields for the - // same reason as CREATE_OBJECT2 above (can be misaligned per realm). - if (applyInventoryFields(block.fields)) slotsChanged = true; - if (slotsChanged) rebuildOnlineInventory(); - extractSkillFields(lastPlayerFields_); - extractExploredZoneFields(lastPlayerFields_); - applyQuestStateFromFields(lastPlayerFields_); - } - - // Update item stack count / durability for online items - if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) { - bool inventoryChanged = false; - const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT); - const uint16_t itemDurField = fieldIndex(UF::ITEM_FIELD_DURABILITY); - const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY); - const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS); - const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1); - - auto it = onlineItems_.find(block.guid); - bool isItemInInventory = (it != onlineItems_.end()); - - for (const auto& [key, val] : block.fields) { - if (key == itemStackField && isItemInInventory) { - if (it->second.stackCount != val) { - it->second.stackCount = val; - inventoryChanged = true; - } - } else if (key == itemDurField && isItemInInventory) { - if (it->second.curDurability != val) { - it->second.curDurability = val; - inventoryChanged = true; - } - } else if (key == itemMaxDurField && isItemInInventory) { - if (it->second.maxDurability != val) { - it->second.maxDurability = val; - inventoryChanged = true; - } - } - } - // Update container slot GUIDs on bag content changes - if (entity->getType() == ObjectType::CONTAINER) { - for (const auto& [key, _] : block.fields) { - if ((containerNumSlotsField != 0xFFFF && key == containerNumSlotsField) || - (containerSlot1Field != 0xFFFF && key >= containerSlot1Field && key < containerSlot1Field + 72)) { - inventoryChanged = true; - break; - } - } - extractContainerFields(block.guid, block.fields); - } - if (inventoryChanged) { - rebuildOnlineInventory(); - } - } - if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) { - if (transportGuids_.count(block.guid) && transportMoveCallback_) { - serverUpdatedTransportGuids_.insert(block.guid); - transportMoveCallback_(block.guid, entity->getX(), entity->getY(), - entity->getZ(), entity->getOrientation()); - } else if (gameObjectMoveCallback_) { - gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(), - entity->getZ(), entity->getOrientation()); - } - } - - LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec); - } else { + // Track online item objects (CONTAINER = bags, also tracked as items) + if (block.objectType == ObjectType::ITEM || block.objectType == ObjectType::CONTAINER) { + auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY)); + auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT)); + auto durIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_DURABILITY)); + auto maxDurIt= block.fields.find(fieldIndex(UF::ITEM_FIELD_MAXDURABILITY)); + const uint16_t enchBase = (fieldIndex(UF::ITEM_FIELD_STACK_COUNT) != 0xFFFF) + ? static_cast(fieldIndex(UF::ITEM_FIELD_STACK_COUNT) + 8u) : 0xFFFFu; + auto permEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase) : block.fields.end(); + auto tempEnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 3u) : block.fields.end(); + auto sock1EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 6u) : block.fields.end(); + auto sock2EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 9u) : block.fields.end(); + auto sock3EnchIt = (enchBase != 0xFFFF) ? block.fields.find(enchBase + 12u) : block.fields.end(); + if (entryIt != block.fields.end() && entryIt->second != 0) { + // Preserve existing info when doing partial updates + OnlineItemInfo info = onlineItems_.count(block.guid) + ? onlineItems_[block.guid] : OnlineItemInfo{}; + info.entry = entryIt->second; + if (stackIt != block.fields.end()) info.stackCount = stackIt->second; + if (durIt != block.fields.end()) info.curDurability = durIt->second; + if (maxDurIt != block.fields.end()) info.maxDurability = maxDurIt->second; + if (permEnchIt != block.fields.end()) info.permanentEnchantId = permEnchIt->second; + if (tempEnchIt != block.fields.end()) info.temporaryEnchantId = tempEnchIt->second; + if (sock1EnchIt != block.fields.end()) info.socketEnchantIds[0] = sock1EnchIt->second; + if (sock2EnchIt != block.fields.end()) info.socketEnchantIds[1] = sock2EnchIt->second; + if (sock3EnchIt != block.fields.end()) info.socketEnchantIds[2] = sock3EnchIt->second; + bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end()); + onlineItems_[block.guid] = info; + if (isNew) newItemCreated = true; + queryItemInfo(info.entry, block.guid); + } + // Extract container slot GUIDs for bags + if (block.objectType == ObjectType::CONTAINER) { + extractContainerFields(block.guid, block.fields); } - break; } - case UpdateType::MOVEMENT: { - // Diagnostic: Log if we receive MOVEMENT blocks for transports - if (transportGuids_.count(block.guid)) { - LOG_INFO("MOVEMENT update for transport 0x", std::hex, block.guid, std::dec, - " pos=(", block.x, ", ", block.y, ", ", block.z, ")"); + // Extract XP / inventory slot / skill fields for player entity + if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) { + // Auto-detect coinage index using the previous snapshot vs this full snapshot. + maybeDetectCoinageIndex(lastPlayerFields_, block.fields); + + lastPlayerFields_ = block.fields; + detectInventorySlotBases(block.fields); + + if (kVerboseUpdateObject) { + uint16_t maxField = 0; + for (const auto& [key, _val] : block.fields) { + if (key > maxField) maxField = key; + } + LOG_INFO("Player update with ", block.fields.size(), + " fields (max index=", maxField, ")"); } - // Update entity position (server → canonical) - auto entity = entityManager.getEntity(block.guid); - if (entity) { + bool slotsChanged = false; + const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP); + const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP); + const uint16_t ufPlayerRestedXp = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE); + const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); + const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); + const uint16_t ufHonor = fieldIndex(UF::PLAYER_FIELD_HONOR_CURRENCY); + const uint16_t ufArena = fieldIndex(UF::PLAYER_FIELD_ARENA_CURRENCY); + const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); + const uint16_t ufPBytes2 = fieldIndex(UF::PLAYER_BYTES_2); + const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE); + const uint16_t ufStats[5] = { + fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1), + fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3), + fieldIndex(UF::UNIT_FIELD_STAT4) + }; + const uint16_t ufMeleeAP = fieldIndex(UF::UNIT_FIELD_ATTACK_POWER); + const uint16_t ufRangedAP = fieldIndex(UF::UNIT_FIELD_RANGED_ATTACK_POWER); + const uint16_t ufSpDmg1 = fieldIndex(UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS); + const uint16_t ufHealBonus = fieldIndex(UF::PLAYER_FIELD_MOD_HEALING_DONE_POS); + const uint16_t ufBlockPct = fieldIndex(UF::PLAYER_BLOCK_PERCENTAGE); + const uint16_t ufDodgePct = fieldIndex(UF::PLAYER_DODGE_PERCENTAGE); + const uint16_t ufParryPct = fieldIndex(UF::PLAYER_PARRY_PERCENTAGE); + const uint16_t ufCritPct = fieldIndex(UF::PLAYER_CRIT_PERCENTAGE); + const uint16_t ufRCritPct = fieldIndex(UF::PLAYER_RANGED_CRIT_PERCENTAGE); + const uint16_t ufSCrit1 = fieldIndex(UF::PLAYER_SPELL_CRIT_PERCENTAGE1); + const uint16_t ufRating1 = fieldIndex(UF::PLAYER_FIELD_COMBAT_RATING_1); + for (const auto& [key, val] : block.fields) { + if (key == ufPlayerXp) { playerXp_ = val; } + else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; } + else if (ufPlayerRestedXp != 0xFFFF && key == ufPlayerRestedXp) { playerRestedXp_ = val; } + else if (key == ufPlayerLevel) { + serverPlayerLevel_ = val; + for (auto& ch : characters) { + if (ch.guid == playerGuid) { ch.level = val; break; } + } + } + else if (key == ufCoinage) { + uint64_t oldMoney = playerMoneyCopper_; + playerMoneyCopper_ = val; + LOG_DEBUG("Money set from update fields: ", val, " copper"); + if (val != oldMoney) + fireAddonEvent("PLAYER_MONEY", {}); + } + else if (ufHonor != 0xFFFF && key == ufHonor) { + playerHonorPoints_ = val; + LOG_DEBUG("Honor points from update fields: ", val); + } + else if (ufArena != 0xFFFF && key == ufArena) { + playerArenaPoints_ = val; + LOG_DEBUG("Arena points from update fields: ", val); + } + else if (ufArmor != 0xFFFF && key == ufArmor) { + playerArmorRating_ = static_cast(val); + LOG_DEBUG("Armor rating from update fields: ", playerArmorRating_); + } + else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) { + playerResistances_[key - ufArmor - 1] = static_cast(val); + } + else if (ufPBytes2 != 0xFFFF && key == ufPBytes2) { + uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); + LOG_WARNING("PLAYER_BYTES_2 (CREATE): raw=0x", std::hex, val, std::dec, + " bankBagSlots=", static_cast(bankBagSlots)); + inventory.setPurchasedBankBagSlots(bankBagSlots); + // Byte 3 (bits 24-31): REST_STATE + // 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY + uint8_t restStateByte = static_cast((val >> 24) & 0xFF); + bool wasResting = isResting_; + isResting_ = (restStateByte != 0); + if (isResting_ != wasResting) { + fireAddonEvent("UPDATE_EXHAUSTION", {}); + fireAddonEvent("PLAYER_UPDATE_RESTING", {}); + } + } + else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) { + chosenTitleBit_ = static_cast(val); + LOG_DEBUG("PLAYER_CHOSEN_TITLE from update fields: ", chosenTitleBit_); + } + else if (ufMeleeAP != 0xFFFF && key == ufMeleeAP) { playerMeleeAP_ = static_cast(val); } + else if (ufRangedAP != 0xFFFF && key == ufRangedAP) { playerRangedAP_ = static_cast(val); } + else if (ufSpDmg1 != 0xFFFF && key >= ufSpDmg1 && key < ufSpDmg1 + 7) { + playerSpellDmgBonus_[key - ufSpDmg1] = static_cast(val); + } + else if (ufHealBonus != 0xFFFF && key == ufHealBonus) { playerHealBonus_ = static_cast(val); } + else if (ufBlockPct != 0xFFFF && key == ufBlockPct) { std::memcpy(&playerBlockPct_, &val, 4); } + else if (ufDodgePct != 0xFFFF && key == ufDodgePct) { std::memcpy(&playerDodgePct_, &val, 4); } + else if (ufParryPct != 0xFFFF && key == ufParryPct) { std::memcpy(&playerParryPct_, &val, 4); } + else if (ufCritPct != 0xFFFF && key == ufCritPct) { std::memcpy(&playerCritPct_, &val, 4); } + else if (ufRCritPct != 0xFFFF && key == ufRCritPct) { std::memcpy(&playerRangedCritPct_, &val, 4); } + else if (ufSCrit1 != 0xFFFF && key >= ufSCrit1 && key < ufSCrit1 + 7) { + std::memcpy(&playerSpellCritPct_[key - ufSCrit1], &val, 4); + } + else if (ufRating1 != 0xFFFF && key >= ufRating1 && key < ufRating1 + 25) { + playerCombatRatings_[key - ufRating1] = static_cast(val); + } + else { + for (int si = 0; si < 5; ++si) { + if (ufStats[si] != 0xFFFF && key == ufStats[si]) { + playerStats_[si] = static_cast(val); + break; + } + } + } + // Do not synthesize quest-log entries from raw update-field slots. + // Slot layouts differ on some classic-family realms and can produce + // phantom "already accepted" quests that block quest acceptance. + } + if (applyInventoryFields(block.fields)) slotsChanged = true; + if (slotsChanged) rebuildOnlineInventory(); + maybeDetectVisibleItemLayout(); + extractSkillFields(lastPlayerFields_); + extractExploredZoneFields(lastPlayerFields_); + applyQuestStateFromFields(lastPlayerFields_); + } + break; + } + + case UpdateType::VALUES: { + // Update existing entity fields + auto entity = entityManager.getEntity(block.guid); + if (entity) { + if (block.hasMovement) { glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); float oCanonical = core::coords::serverToCanonicalYaw(block.orientation); entity->setPosition(pos.x, pos.y, pos.z, oCanonical); - LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec); if (block.guid != playerGuid && (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::GAMEOBJECT)) { @@ -9234,78 +11435,763 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { clearTransportAttachment(block.guid); } } + } - if (block.guid == playerGuid) { - movementInfo.orientation = oCanonical; + for (const auto& field : block.fields) { + entity->setField(field.first, field.second); + } - // Track player-on-transport state from MOVEMENT updates - if (block.onTransport) { - setPlayerOnTransport(block.transportGuid, glm::vec3(0.0f)); - // Convert transport offset from server → canonical coordinates - glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); - playerTransportOffset_ = core::coords::serverToCanonical(serverOffset); - if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) { - glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); - entity->setPosition(composed.x, composed.y, composed.z, oCanonical); - movementInfo.x = composed.x; - movementInfo.y = composed.y; - movementInfo.z = composed.z; - } else { - movementInfo.x = pos.x; - movementInfo.y = pos.y; - movementInfo.z = pos.z; + if (entity->getType() == ObjectType::PLAYER && block.guid != playerGuid) { + updateOtherPlayerVisibleItems(block.guid, entity->getFields()); + } + + // Update cached health/mana/power values (Phase 2) — single pass + if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { + auto unit = std::static_pointer_cast(entity); + constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; + constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; + uint32_t oldDisplayId = unit->getDisplayId(); + bool displayIdChanged = false; + bool npcDeathNotified = false; + bool npcRespawnNotified = false; + bool healthChanged = false; + bool powerChanged = false; + const uint16_t ufHealth = fieldIndex(UF::UNIT_FIELD_HEALTH); + const uint16_t ufPowerBase = fieldIndex(UF::UNIT_FIELD_POWER1); + const uint16_t ufMaxHealth = fieldIndex(UF::UNIT_FIELD_MAXHEALTH); + const uint16_t ufMaxPowerBase = fieldIndex(UF::UNIT_FIELD_MAXPOWER1); + const uint16_t ufLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); + const uint16_t ufFaction = fieldIndex(UF::UNIT_FIELD_FACTIONTEMPLATE); + const uint16_t ufFlags = fieldIndex(UF::UNIT_FIELD_FLAGS); + const uint16_t ufDynFlags = fieldIndex(UF::UNIT_DYNAMIC_FLAGS); + const uint16_t ufDisplayId = fieldIndex(UF::UNIT_FIELD_DISPLAYID); + const uint16_t ufMountDisplayId = fieldIndex(UF::UNIT_FIELD_MOUNTDISPLAYID); + const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS); + const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0); + const uint16_t ufBytes1 = fieldIndex(UF::UNIT_FIELD_BYTES_1); + for (const auto& [key, val] : block.fields) { + if (key == ufHealth) { + uint32_t oldHealth = unit->getHealth(); + unit->setHealth(val); + healthChanged = true; + if (val == 0) { + if (block.guid == autoAttackTarget) { + stopAutoAttack(); + } + hostileAttackers_.erase(block.guid); + if (block.guid == playerGuid) { + playerDead_ = true; + releasedSpirit_ = false; + stopAutoAttack(); + // Cache death position as corpse location. + // Classic WoW does not send SMSG_DEATH_RELEASE_LOC, so + // this is the primary source for canReclaimCorpse(). + // movementInfo is canonical (x=north, y=west); corpseX_/Y_ + // are raw server coords (x=west, y=north) — swap axes. + corpseX_ = movementInfo.y; // canonical west = server X + corpseY_ = movementInfo.x; // canonical north = server Y + corpseZ_ = movementInfo.z; + corpseMapId_ = currentMapId_; + LOG_INFO("Player died! Corpse position cached at server=(", + corpseX_, ",", corpseY_, ",", corpseZ_, + ") map=", corpseMapId_); + fireAddonEvent("PLAYER_DEAD", {}); + } + if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && npcDeathCallback_) { + npcDeathCallback_(block.guid); + npcDeathNotified = true; + } + } else if (oldHealth == 0 && val > 0) { + if (block.guid == playerGuid) { + bool wasGhost = releasedSpirit_; + playerDead_ = false; + if (!wasGhost) { + LOG_INFO("Player resurrected!"); + fireAddonEvent("PLAYER_ALIVE", {}); + } else { + LOG_INFO("Player entered ghost form"); + releasedSpirit_ = false; + fireAddonEvent("PLAYER_UNGHOST", {}); + } + } + if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && npcRespawnCallback_) { + npcRespawnCallback_(block.guid); + npcRespawnNotified = true; + } } - LOG_INFO("Player on transport (MOVEMENT): 0x", std::hex, playerTransportGuid_, std::dec); - } else { - movementInfo.x = pos.x; - movementInfo.y = pos.y; - movementInfo.z = pos.z; - // Don't clear client-side M2 transport boarding - bool isClientM2Transport = false; - if (playerTransportGuid_ != 0 && transportManager_) { - auto* tr = transportManager_->getTransport(playerTransportGuid_); - isClientM2Transport = (tr && tr->isM2); + // Specific fields checked BEFORE power/maxpower range checks + // (Classic packs maxHealth/level/faction adjacent to power indices) + } else if (key == ufMaxHealth) { unit->setMaxHealth(val); healthChanged = true; } + else if (key == ufBytes0) { + uint8_t oldPT = unit->getPowerType(); + unit->setPowerType(static_cast((val >> 24) & 0xFF)); + if (unit->getPowerType() != oldPT) { + auto uid = guidToUnitId(block.guid); + if (!uid.empty()) + fireAddonEvent("UNIT_DISPLAYPOWER", {uid}); } - if (playerTransportGuid_ != 0 && !isClientM2Transport) { - LOG_INFO("Player left transport (MOVEMENT)"); - clearPlayerTransport(); + } else if (key == ufFlags) { unit->setUnitFlags(val); } + else if (ufBytes1 != 0xFFFF && key == ufBytes1 && block.guid == playerGuid) { + uint8_t newForm = static_cast((val >> 24) & 0xFF); + if (newForm != shapeshiftFormId_) { + shapeshiftFormId_ = newForm; + LOG_INFO("Shapeshift form changed: ", static_cast(newForm)); + fireAddonEvent("UPDATE_SHAPESHIFT_FORM", {}); + fireAddonEvent("UPDATE_SHAPESHIFT_FORMS", {}); + } + } + else if (key == ufDynFlags) { + uint32_t oldDyn = unit->getDynamicFlags(); + unit->setDynamicFlags(val); + if (block.guid == playerGuid) { + bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0; + bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; + if (!wasDead && nowDead) { + playerDead_ = true; + releasedSpirit_ = false; + corpseX_ = movementInfo.y; + corpseY_ = movementInfo.x; + corpseZ_ = movementInfo.z; + corpseMapId_ = currentMapId_; + LOG_INFO("Player died (dynamic flags). Corpse cached map=", corpseMapId_); + } else if (wasDead && !nowDead) { + playerDead_ = false; + releasedSpirit_ = false; + selfResAvailable_ = false; + LOG_INFO("Player resurrected (dynamic flags)"); + } + } else if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { + bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0; + bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; + if (!wasDead && nowDead) { + if (!npcDeathNotified && npcDeathCallback_) { + npcDeathCallback_(block.guid); + npcDeathNotified = true; + } + } else if (wasDead && !nowDead) { + if (!npcRespawnNotified && npcRespawnCallback_) { + npcRespawnCallback_(block.guid); + npcRespawnNotified = true; + } + } + } + } else if (key == ufLevel) { + uint32_t oldLvl = unit->getLevel(); + unit->setLevel(val); + if (val != oldLvl) { + auto uid = guidToUnitId(block.guid); + if (!uid.empty()) + fireAddonEvent("UNIT_LEVEL", {uid}); + } + if (block.guid != playerGuid && + entity->getType() == ObjectType::PLAYER && + val > oldLvl && oldLvl > 0 && + otherPlayerLevelUpCallback_) { + otherPlayerLevelUpCallback_(block.guid, val); + } + } + else if (key == ufFaction) { + unit->setFactionTemplate(val); + unit->setHostile(isHostileFaction(val)); + } else if (key == ufDisplayId) { + if (val != unit->getDisplayId()) { + unit->setDisplayId(val); + displayIdChanged = true; + } + } else if (key == ufMountDisplayId) { + if (block.guid == playerGuid) { + uint32_t old = currentMountDisplayId_; + currentMountDisplayId_ = val; + if (val != old && mountCallback_) mountCallback_(val); + if (val != old) + fireAddonEvent("UNIT_MODEL_CHANGED", {"player"}); + if (old == 0 && val != 0) { + mountAuraSpellId_ = 0; + for (const auto& a : playerAuras) { + if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == playerGuid) { + mountAuraSpellId_ = a.spellId; + } + } + // Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block + if (mountAuraSpellId_ == 0) { + const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); + if (ufAuras != 0xFFFF) { + for (const auto& [fk, fv] : block.fields) { + if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) { + mountAuraSpellId_ = fv; + break; + } + } + } + } + LOG_INFO("Mount detected (values update): displayId=", val, " auraSpellId=", mountAuraSpellId_); + } + if (old != 0 && val == 0) { + mountAuraSpellId_ = 0; + for (auto& a : playerAuras) + if (!a.isEmpty() && a.maxDurationMs < 0) a = AuraSlot{}; + } + } + unit->setMountDisplayId(val); + } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } + // Power/maxpower range checks AFTER all specific fields + else if (key >= ufPowerBase && key < ufPowerBase + 7) { + unit->setPowerByType(static_cast(key - ufPowerBase), val); + powerChanged = true; + } else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { + unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); + powerChanged = true; + } + } + + // Fire UNIT_HEALTH / UNIT_POWER events for Lua addons + if ((healthChanged || powerChanged)) { + auto unitId = guidToUnitId(block.guid); + if (!unitId.empty()) { + if (healthChanged) fireAddonEvent("UNIT_HEALTH", {unitId}); + if (powerChanged) { + fireAddonEvent("UNIT_POWER", {unitId}); + // When player power changes, action bar usability may change + if (block.guid == playerGuid) { + fireAddonEvent("ACTIONBAR_UPDATE_USABLE", {}); + fireAddonEvent("SPELL_UPDATE_USABLE", {}); + } } } } - // Fire transport move callback if this is a known transport + // Classic: sync playerAuras from UNIT_FIELD_AURAS when those fields are updated + if (block.guid == playerGuid && isClassicLikeExpansion()) { + const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); + const uint16_t ufAuraFlags = fieldIndex(UF::UNIT_FIELD_AURAFLAGS); + if (ufAuras != 0xFFFF) { + bool hasAuraUpdate = false; + for (const auto& [fk, fv] : block.fields) { + if (fk >= ufAuras && fk < ufAuras + 48) { hasAuraUpdate = true; break; } + } + if (hasAuraUpdate) { + playerAuras.clear(); + playerAuras.resize(48); + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + const auto& allFields = entity->getFields(); + for (int slot = 0; slot < 48; ++slot) { + auto it = allFields.find(static_cast(ufAuras + slot)); + if (it != allFields.end() && it->second != 0) { + AuraSlot& a = playerAuras[slot]; + a.spellId = it->second; + // Read aura flag byte: packed 4-per-uint32 at ufAuraFlags + uint8_t aFlag = 0; + if (ufAuraFlags != 0xFFFF) { + auto fit = allFields.find(static_cast(ufAuraFlags + slot / 4)); + if (fit != allFields.end()) + aFlag = static_cast((fit->second >> ((slot % 4) * 8)) & 0xFF); + } + a.flags = aFlag; + a.durationMs = -1; + a.maxDurationMs = -1; + a.casterGuid = playerGuid; + a.receivedAtMs = nowMs; + } + } + LOG_DEBUG("[Classic] Rebuilt playerAuras from UNIT_FIELD_AURAS (VALUES)"); + fireAddonEvent("UNIT_AURA", {"player"}); + } + } + } + + // Some units/players are created without displayId and get it later via VALUES. + if ((entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) && + displayIdChanged && + unit->getDisplayId() != 0 && + unit->getDisplayId() != oldDisplayId) { + if (entity->getType() == ObjectType::PLAYER && block.guid == playerGuid) { + // Skip local player — spawned separately + } else if (entity->getType() == ObjectType::PLAYER) { + if (playerSpawnCallback_) { + uint8_t race = 0, gender = 0, facial = 0; + uint32_t appearanceBytes = 0; + // Use the entity's accumulated field state, not just this block's changed fields. + if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) { + playerSpawnCallback_(block.guid, unit->getDisplayId(), race, gender, + appearanceBytes, facial, + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation()); + } else { + LOG_WARNING("[Spawn] PLAYER guid=0x", std::hex, block.guid, std::dec, + " displayId=", unit->getDisplayId(), " appearance extraction failed (VALUES update) — model will not render"); + } + } + bool isDeadNow = (unit->getHealth() == 0) || + ((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0); + if (isDeadNow && !npcDeathNotified && npcDeathCallback_) { + npcDeathCallback_(block.guid); + npcDeathNotified = true; + } + } else if (creatureSpawnCallback_) { + float unitScale2 = 1.0f; + { + uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X); + if (scaleIdx != 0xFFFF) { + uint32_t raw = entity->getField(scaleIdx); + if (raw != 0) { + std::memcpy(&unitScale2, &raw, sizeof(float)); + if (unitScale2 <= 0.01f || unitScale2 > 100.0f) unitScale2 = 1.0f; + } + } + } + creatureSpawnCallback_(block.guid, unit->getDisplayId(), + unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation(), unitScale2); + bool isDeadNow = (unit->getHealth() == 0) || + ((unit->getDynamicFlags() & (UNIT_DYNFLAG_DEAD | UNIT_DYNFLAG_LOOTABLE)) != 0); + if (isDeadNow && !npcDeathNotified && npcDeathCallback_) { + npcDeathCallback_(block.guid); + npcDeathNotified = true; + } + } + if (entity->getType() == ObjectType::UNIT && (unit->getNpcFlags() & 0x02) && socket) { + network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + 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()) + fireAddonEvent("UNIT_MODEL_CHANGED", {uid}); + } + } + } + // Update XP / inventory slot / skill fields for player entity + if (block.guid == playerGuid) { + const bool needCoinageDetectSnapshot = + (pendingMoneyDelta_ != 0 && pendingMoneyDeltaTimer_ > 0.0f); + std::map oldFieldsSnapshot; + if (needCoinageDetectSnapshot) { + oldFieldsSnapshot = lastPlayerFields_; + } + if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { + serverRunSpeed_ = block.runSpeed; + // Some server dismount paths update run speed without updating mount display field. + if (!onTaxiFlight_ && !taxiMountActive_ && + currentMountDisplayId_ != 0 && block.runSpeed <= 8.5f) { + LOG_INFO("Auto-clearing mount from movement speed update: speed=", block.runSpeed, + " displayId=", currentMountDisplayId_); + currentMountDisplayId_ = 0; + if (mountCallback_) { + mountCallback_(0); + } + } + } + auto mergeHint = lastPlayerFields_.end(); + for (const auto& [key, val] : block.fields) { + mergeHint = lastPlayerFields_.insert_or_assign(mergeHint, key, val); + } + if (needCoinageDetectSnapshot) { + maybeDetectCoinageIndex(oldFieldsSnapshot, lastPlayerFields_); + } + maybeDetectVisibleItemLayout(); + detectInventorySlotBases(block.fields); + bool slotsChanged = false; + const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP); + const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP); + const uint16_t ufPlayerRestedXpV = fieldIndex(UF::PLAYER_REST_STATE_EXPERIENCE); + const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); + const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); + const uint16_t ufHonorV = fieldIndex(UF::PLAYER_FIELD_HONOR_CURRENCY); + const uint16_t ufArenaV = fieldIndex(UF::PLAYER_FIELD_ARENA_CURRENCY); + const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS); + const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES); + const uint16_t ufPBytesV = fieldIndex(UF::PLAYER_BYTES); + const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2); + const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE); + const uint16_t ufStatsV[5] = { + fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1), + fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3), + fieldIndex(UF::UNIT_FIELD_STAT4) + }; + const uint16_t ufMeleeAPV = fieldIndex(UF::UNIT_FIELD_ATTACK_POWER); + const uint16_t ufRangedAPV = fieldIndex(UF::UNIT_FIELD_RANGED_ATTACK_POWER); + const uint16_t ufSpDmg1V = fieldIndex(UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS); + const uint16_t ufHealBonusV= fieldIndex(UF::PLAYER_FIELD_MOD_HEALING_DONE_POS); + const uint16_t ufBlockPctV = fieldIndex(UF::PLAYER_BLOCK_PERCENTAGE); + const uint16_t ufDodgePctV = fieldIndex(UF::PLAYER_DODGE_PERCENTAGE); + const uint16_t ufParryPctV = fieldIndex(UF::PLAYER_PARRY_PERCENTAGE); + const uint16_t ufCritPctV = fieldIndex(UF::PLAYER_CRIT_PERCENTAGE); + const uint16_t ufRCritPctV = fieldIndex(UF::PLAYER_RANGED_CRIT_PERCENTAGE); + const uint16_t ufSCrit1V = fieldIndex(UF::PLAYER_SPELL_CRIT_PERCENTAGE1); + const uint16_t ufRating1V = fieldIndex(UF::PLAYER_FIELD_COMBAT_RATING_1); + for (const auto& [key, val] : block.fields) { + if (key == ufPlayerXp) { + playerXp_ = val; + LOG_DEBUG("XP updated: ", val); + fireAddonEvent("PLAYER_XP_UPDATE", {std::to_string(val)}); + } + else if (key == ufPlayerNextXp) { + playerNextLevelXp_ = val; + LOG_DEBUG("Next level XP updated: ", val); + } + else if (ufPlayerRestedXpV != 0xFFFF && key == ufPlayerRestedXpV) { + playerRestedXp_ = val; + fireAddonEvent("UPDATE_EXHAUSTION", {}); + } + else if (key == ufPlayerLevel) { + serverPlayerLevel_ = val; + LOG_DEBUG("Level updated: ", val); + for (auto& ch : characters) { + if (ch.guid == playerGuid) { + ch.level = val; + break; + } + } + } + else if (key == ufCoinage) { + uint64_t oldM = playerMoneyCopper_; + playerMoneyCopper_ = val; + LOG_DEBUG("Money updated via VALUES: ", val, " copper"); + if (val != oldM) + fireAddonEvent("PLAYER_MONEY", {}); + } + else if (ufHonorV != 0xFFFF && key == ufHonorV) { + playerHonorPoints_ = val; + LOG_DEBUG("Honor points updated: ", val); + } + else if (ufArenaV != 0xFFFF && key == ufArenaV) { + playerArenaPoints_ = val; + LOG_DEBUG("Arena points updated: ", val); + } + else if (ufArmor != 0xFFFF && key == ufArmor) { + playerArmorRating_ = static_cast(val); + } + else if (ufArmor != 0xFFFF && key > ufArmor && key <= ufArmor + 6) { + playerResistances_[key - ufArmor - 1] = static_cast(val); + } + else if (ufPBytesV != 0xFFFF && key == ufPBytesV) { + // PLAYER_BYTES changed (barber shop, polymorph, etc.) + // Update the Character struct so inventory preview refreshes + for (auto& ch : characters) { + if (ch.guid == playerGuid) { + ch.appearanceBytes = val; + break; + } + } + if (appearanceChangedCallback_) + appearanceChangedCallback_(); + } + else if (ufPBytes2v != 0xFFFF && key == ufPBytes2v) { + // Byte 0 (bits 0-7): facial hair / piercings + uint8_t facialHair = static_cast(val & 0xFF); + for (auto& ch : characters) { + if (ch.guid == playerGuid) { + ch.facialFeatures = facialHair; + break; + } + } + uint8_t bankBagSlots = static_cast((val >> 16) & 0xFF); + LOG_DEBUG("PLAYER_BYTES_2 (VALUES): raw=0x", std::hex, val, std::dec, + " bankBagSlots=", static_cast(bankBagSlots), + " facial=", static_cast(facialHair)); + inventory.setPurchasedBankBagSlots(bankBagSlots); + // Byte 3 (bits 24-31): REST_STATE + // 0 = not resting, 1 = REST_TYPE_IN_TAVERN, 2 = REST_TYPE_IN_CITY + uint8_t restStateByte = static_cast((val >> 24) & 0xFF); + isResting_ = (restStateByte != 0); + if (appearanceChangedCallback_) + appearanceChangedCallback_(); + } + else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) { + chosenTitleBit_ = static_cast(val); + LOG_DEBUG("PLAYER_CHOSEN_TITLE updated: ", chosenTitleBit_); + } + else if (key == ufPlayerFlags) { + constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; + bool wasGhost = releasedSpirit_; + bool nowGhost = (val & PLAYER_FLAGS_GHOST) != 0; + if (!wasGhost && nowGhost) { + releasedSpirit_ = true; + LOG_INFO("Player entered ghost form (PLAYER_FLAGS)"); + if (ghostStateCallback_) ghostStateCallback_(true); + } else if (wasGhost && !nowGhost) { + releasedSpirit_ = false; + playerDead_ = false; + repopPending_ = false; + resurrectPending_ = false; + selfResAvailable_ = false; + corpseMapId_ = 0; // corpse reclaimed + corpseGuid_ = 0; + corpseReclaimAvailableMs_ = 0; + LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)"); + fireAddonEvent("PLAYER_ALIVE", {}); + if (ghostStateCallback_) ghostStateCallback_(false); + } + fireAddonEvent("PLAYER_FLAGS_CHANGED", {}); + } + else if (ufMeleeAPV != 0xFFFF && key == ufMeleeAPV) { playerMeleeAP_ = static_cast(val); } + else if (ufRangedAPV != 0xFFFF && key == ufRangedAPV) { playerRangedAP_ = static_cast(val); } + else if (ufSpDmg1V != 0xFFFF && key >= ufSpDmg1V && key < ufSpDmg1V + 7) { + playerSpellDmgBonus_[key - ufSpDmg1V] = static_cast(val); + } + else if (ufHealBonusV != 0xFFFF && key == ufHealBonusV) { playerHealBonus_ = static_cast(val); } + else if (ufBlockPctV != 0xFFFF && key == ufBlockPctV) { std::memcpy(&playerBlockPct_, &val, 4); } + else if (ufDodgePctV != 0xFFFF && key == ufDodgePctV) { std::memcpy(&playerDodgePct_, &val, 4); } + else if (ufParryPctV != 0xFFFF && key == ufParryPctV) { std::memcpy(&playerParryPct_, &val, 4); } + else if (ufCritPctV != 0xFFFF && key == ufCritPctV) { std::memcpy(&playerCritPct_, &val, 4); } + else if (ufRCritPctV != 0xFFFF && key == ufRCritPctV) { std::memcpy(&playerRangedCritPct_, &val, 4); } + else if (ufSCrit1V != 0xFFFF && key >= ufSCrit1V && key < ufSCrit1V + 7) { + std::memcpy(&playerSpellCritPct_[key - ufSCrit1V], &val, 4); + } + else if (ufRating1V != 0xFFFF && key >= ufRating1V && key < ufRating1V + 25) { + playerCombatRatings_[key - ufRating1V] = static_cast(val); + } + else { + for (int si = 0; si < 5; ++si) { + if (ufStatsV[si] != 0xFFFF && key == ufStatsV[si]) { + playerStats_[si] = static_cast(val); + break; + } + } + } + } + // Do not auto-create quests from VALUES quest-log slot fields for the + // same reason as CREATE_OBJECT2 above (can be misaligned per realm). + if (applyInventoryFields(block.fields)) slotsChanged = true; + if (slotsChanged) { + rebuildOnlineInventory(); + fireAddonEvent("PLAYER_EQUIPMENT_CHANGED", {}); + } + extractSkillFields(lastPlayerFields_); + extractExploredZoneFields(lastPlayerFields_); + applyQuestStateFromFields(lastPlayerFields_); + } + + // Update item stack count / durability for online items + if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) { + bool inventoryChanged = false; + const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT); + const uint16_t itemDurField = fieldIndex(UF::ITEM_FIELD_DURABILITY); + const uint16_t itemMaxDurField = fieldIndex(UF::ITEM_FIELD_MAXDURABILITY); + const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS); + const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1); + // ITEM_FIELD_ENCHANTMENT starts 8 fields after ITEM_FIELD_STACK_COUNT (fixed offset + // across all expansions: +DURATION, +5×SPELL_CHARGES, +FLAGS = +8). + // Slot 0 = permanent (field +0), slot 1 = temp (+3), slots 2-4 = sockets (+6,+9,+12). + const uint16_t itemEnchBase = (itemStackField != 0xFFFF) ? (itemStackField + 8u) : 0xFFFF; + const uint16_t itemPermEnchField = itemEnchBase; + const uint16_t itemTempEnchField = (itemEnchBase != 0xFFFF) ? (itemEnchBase + 3u) : 0xFFFF; + const uint16_t itemSock1EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 6u) : 0xFFFF; + const uint16_t itemSock2EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 9u) : 0xFFFF; + const uint16_t itemSock3EnchField= (itemEnchBase != 0xFFFF) ? (itemEnchBase + 12u) : 0xFFFF; + + auto it = onlineItems_.find(block.guid); + bool isItemInInventory = (it != onlineItems_.end()); + + for (const auto& [key, val] : block.fields) { + if (key == itemStackField && isItemInInventory) { + if (it->second.stackCount != val) { + it->second.stackCount = val; + inventoryChanged = true; + } + } else if (key == itemDurField && isItemInInventory) { + if (it->second.curDurability != val) { + const uint32_t prevDur = it->second.curDurability; + it->second.curDurability = val; + inventoryChanged = true; + // Warn once when durability drops below 20% for an equipped item. + const uint32_t maxDur = it->second.maxDurability; + if (maxDur > 0 && val < maxDur / 5u && prevDur >= maxDur / 5u) { + // Check if this item is in an equip slot (not bag inventory). + bool isEquipped = false; + for (uint64_t slotGuid : equipSlotGuids_) { + if (slotGuid == block.guid) { isEquipped = true; break; } + } + if (isEquipped) { + std::string itemName; + const auto* info = getItemInfo(it->second.entry); + if (info) itemName = info->name; + char buf[128]; + if (!itemName.empty()) + std::snprintf(buf, sizeof(buf), "%s is about to break!", itemName.c_str()); + else + std::snprintf(buf, sizeof(buf), "An equipped item is about to break!"); + addUIError(buf); + addSystemChatMessage(buf); + } + } + } + } else if (key == itemMaxDurField && isItemInInventory) { + if (it->second.maxDurability != val) { + it->second.maxDurability = val; + inventoryChanged = true; + } + } else if (isItemInInventory && itemPermEnchField != 0xFFFF && key == itemPermEnchField) { + if (it->second.permanentEnchantId != val) { + it->second.permanentEnchantId = val; + inventoryChanged = true; + } + } else if (isItemInInventory && itemTempEnchField != 0xFFFF && key == itemTempEnchField) { + if (it->second.temporaryEnchantId != val) { + it->second.temporaryEnchantId = val; + inventoryChanged = true; + } + } else if (isItemInInventory && itemSock1EnchField != 0xFFFF && key == itemSock1EnchField) { + if (it->second.socketEnchantIds[0] != val) { + it->second.socketEnchantIds[0] = val; + inventoryChanged = true; + } + } else if (isItemInInventory && itemSock2EnchField != 0xFFFF && key == itemSock2EnchField) { + if (it->second.socketEnchantIds[1] != val) { + it->second.socketEnchantIds[1] = val; + inventoryChanged = true; + } + } else if (isItemInInventory && itemSock3EnchField != 0xFFFF && key == itemSock3EnchField) { + if (it->second.socketEnchantIds[2] != val) { + it->second.socketEnchantIds[2] = val; + inventoryChanged = true; + } + } + } + // Update container slot GUIDs on bag content changes + if (entity->getType() == ObjectType::CONTAINER) { + for (const auto& [key, _] : block.fields) { + if ((containerNumSlotsField != 0xFFFF && key == containerNumSlotsField) || + (containerSlot1Field != 0xFFFF && key >= containerSlot1Field && key < containerSlot1Field + 72)) { + inventoryChanged = true; + break; + } + } + extractContainerFields(block.guid, block.fields); + } + if (inventoryChanged) { + rebuildOnlineInventory(); + fireAddonEvent("BAG_UPDATE", {}); + fireAddonEvent("UNIT_INVENTORY_CHANGED", {"player"}); + } + } + if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) { if (transportGuids_.count(block.guid) && transportMoveCallback_) { serverUpdatedTransportGuids_.insert(block.guid); - transportMoveCallback_(block.guid, pos.x, pos.y, pos.z, oCanonical); - } - // Fire move callback for non-transport gameobjects. - if (entity->getType() == ObjectType::GAMEOBJECT && - transportGuids_.count(block.guid) == 0 && - gameObjectMoveCallback_) { + transportMoveCallback_(block.guid, entity->getX(), entity->getY(), + entity->getZ(), entity->getOrientation()); + } else if (gameObjectMoveCallback_) { gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(), entity->getZ(), entity->getOrientation()); } - // Fire move callback for non-player units (creatures). - // SMSG_MONSTER_MOVE handles smooth interpolated movement, but many - // servers (especially vanilla/Turtle WoW) communicate NPC positions - // via MOVEMENT blocks instead. Use duration=0 for an instant snap. - if (block.guid != playerGuid && - entity->getType() == ObjectType::UNIT && - transportGuids_.count(block.guid) == 0 && - creatureMoveCallback_) { - creatureMoveCallback_(block.guid, pos.x, pos.y, pos.z, 0); - } - } else { - LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec); } - break; + + LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec); + } else { + } + break; + } + + case UpdateType::MOVEMENT: { + // Diagnostic: Log if we receive MOVEMENT blocks for transports + if (transportGuids_.count(block.guid)) { + LOG_INFO("MOVEMENT update for transport 0x", std::hex, block.guid, std::dec, + " pos=(", block.x, ", ", block.y, ", ", block.z, ")"); } - default: - break; - } - } + // Update entity position (server → canonical) + auto entity = entityManager.getEntity(block.guid); + if (entity) { + glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); + float oCanonical = core::coords::serverToCanonicalYaw(block.orientation); + entity->setPosition(pos.x, pos.y, pos.z, oCanonical); + LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec); + if (block.guid != playerGuid && + (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::GAMEOBJECT)) { + if (block.onTransport && block.transportGuid != 0) { + glm::vec3 localOffset = core::coords::serverToCanonical( + glm::vec3(block.transportX, block.transportY, block.transportZ)); + const bool hasLocalOrientation = (block.updateFlags & 0x0020) != 0; // UPDATEFLAG_LIVING + float localOriCanonical = core::coords::normalizeAngleRad(-block.transportO); + setTransportAttachment(block.guid, entity->getType(), block.transportGuid, + localOffset, hasLocalOrientation, localOriCanonical); + if (transportManager_ && transportManager_->getTransport(block.transportGuid)) { + glm::vec3 composed = transportManager_->getPlayerWorldPosition(block.transportGuid, localOffset); + entity->setPosition(composed.x, composed.y, composed.z, entity->getOrientation()); + } + } else { + clearTransportAttachment(block.guid); + } + } + + if (block.guid == playerGuid) { + movementInfo.orientation = oCanonical; + + // Track player-on-transport state from MOVEMENT updates + if (block.onTransport) { + // Convert transport offset from server → canonical coordinates + glm::vec3 serverOffset(block.transportX, block.transportY, block.transportZ); + glm::vec3 canonicalOffset = core::coords::serverToCanonical(serverOffset); + setPlayerOnTransport(block.transportGuid, canonicalOffset); + if (transportManager_ && transportManager_->getTransport(playerTransportGuid_)) { + glm::vec3 composed = transportManager_->getPlayerWorldPosition(playerTransportGuid_, playerTransportOffset_); + entity->setPosition(composed.x, composed.y, composed.z, oCanonical); + movementInfo.x = composed.x; + movementInfo.y = composed.y; + movementInfo.z = composed.z; + } else { + movementInfo.x = pos.x; + movementInfo.y = pos.y; + movementInfo.z = pos.z; + } + LOG_INFO("Player on transport (MOVEMENT): 0x", std::hex, playerTransportGuid_, std::dec); + } else { + movementInfo.x = pos.x; + movementInfo.y = pos.y; + movementInfo.z = pos.z; + // Don't clear client-side M2 transport boarding + bool isClientM2Transport = false; + if (playerTransportGuid_ != 0 && transportManager_) { + auto* tr = transportManager_->getTransport(playerTransportGuid_); + isClientM2Transport = (tr && tr->isM2); + } + if (playerTransportGuid_ != 0 && !isClientM2Transport) { + LOG_INFO("Player left transport (MOVEMENT)"); + clearPlayerTransport(); + } + } + } + + // Fire transport move callback if this is a known transport + if (transportGuids_.count(block.guid) && transportMoveCallback_) { + serverUpdatedTransportGuids_.insert(block.guid); + transportMoveCallback_(block.guid, pos.x, pos.y, pos.z, oCanonical); + } + // Fire move callback for non-transport gameobjects. + if (entity->getType() == ObjectType::GAMEOBJECT && + transportGuids_.count(block.guid) == 0 && + gameObjectMoveCallback_) { + gameObjectMoveCallback_(block.guid, entity->getX(), entity->getY(), + entity->getZ(), entity->getOrientation()); + } + // Fire move callback for non-player units (creatures). + // SMSG_MONSTER_MOVE handles smooth interpolated movement, but many + // servers (especially vanilla/Turtle WoW) communicate NPC positions + // via MOVEMENT blocks instead. Use duration=0 for an instant snap. + if (block.guid != playerGuid && + entity->getType() == ObjectType::UNIT && + transportGuids_.count(block.guid) == 0 && + creatureMoveCallback_) { + creatureMoveCallback_(block.guid, pos.x, pos.y, pos.z, 0); + } + } else { + LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec); + } + break; + } + + default: + break; + } +} + +void GameHandler::finalizeUpdateObjectBatch(bool newItemCreated) { tabCycleStale = true; // Entity count logging disabled @@ -9338,13 +12224,15 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) { uint32_t decompressedSize = packet.readUInt32(); LOG_DEBUG(" Decompressed size: ", decompressedSize); - if (decompressedSize == 0 || decompressedSize > 1024 * 1024) { + // Capital cities and large raids can produce very large update packets. + // The real WoW client handles up to ~10MB; 5MB covers all practical cases. + if (decompressedSize == 0 || decompressedSize > 5 * 1024 * 1024) { LOG_WARNING("Invalid decompressed size: ", decompressedSize); return; } // Remaining data is zlib compressed - size_t compressedSize = packet.getSize() - packet.getReadPos(); + size_t compressedSize = packet.getRemainingSize(); const uint8_t* compressedData = packet.getData().data() + packet.getReadPos(); // Decompress @@ -9437,13 +12325,26 @@ void GameHandler::handleDestroyObject(network::Packet& packet) { // Clean up quest giver status npcQuestStatus_.erase(data.guid); + // Remove combat text entries referencing the destroyed entity so floating + // damage numbers don't linger after the source/target despawns. + combatText.erase( + std::remove_if(combatText.begin(), combatText.end(), + [&data](const CombatTextEntry& e) { + return e.dstGuid == data.guid; + }), + combatText.end()); + + // Clean up unit cast state (cast bar) for the destroyed unit + unitCastStates_.erase(data.guid); + // Clean up cached auras + unitAurasCache_.erase(data.guid); + tabCycleStale = true; - // Entity count logging disabled } void GameHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) { if (state != WorldState::IN_WORLD) { - LOG_WARNING("Cannot send chat in state: ", (int)state); + LOG_WARNING("Cannot send chat in state: ", static_cast(state)); return; } @@ -9468,10 +12369,7 @@ void GameHandler::sendChatMessage(ChatType type, const std::string& message, con echo.message = message; // Look up player name - auto nameIt = playerNameCache.find(playerGuid); - if (nameIt != playerNameCache.end()) { - echo.senderName = nameIt->second; - } + echo.senderName = lookupName(playerGuid); if (type == ChatType::WHISPER) { echo.type = ChatType::WHISPER_INFORM; @@ -9507,27 +12405,7 @@ void GameHandler::handleMessageChat(network::Packet& packet) { // Resolve sender name from entity/cache if not already set by parser if (data.senderName.empty() && data.senderGuid != 0) { - // Check player name cache first - auto nameIt = playerNameCache.find(data.senderGuid); - if (nameIt != playerNameCache.end()) { - data.senderName = nameIt->second; - } else { - // Try entity name - auto entity = entityManager.getEntity(data.senderGuid); - if (entity) { - if (entity->getType() == ObjectType::PLAYER) { - auto player = std::dynamic_pointer_cast(entity); - if (player && !player->getName().empty()) { - data.senderName = player->getName(); - } - } else if (entity->getType() == ObjectType::UNIT) { - auto unit = std::dynamic_pointer_cast(entity); - if (unit && !unit->getName().empty()) { - data.senderName = unit->getName(); - } - } - } - } + data.senderName = lookupName(data.senderGuid); // If still unknown, proactively query the server so the UI can show names soon after. if (data.senderName.empty()) { @@ -9546,6 +12424,15 @@ void GameHandler::handleMessageChat(network::Packet& packet) { // Track whisper sender for /r command if (data.type == ChatType::WHISPER && !data.senderName.empty()) { lastWhisperSender_ = data.senderName; + + // Auto-reply if AFK or DND + if (afkStatus_ && !data.senderName.empty()) { + std::string reply = afkMessage_.empty() ? "Away from Keyboard" : afkMessage_; + sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); + } else if (dndStatus_ && !data.senderName.empty()) { + std::string reply = dndMessage_.empty() ? "Do Not Disturb" : dndMessage_; + sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); + } } // Trigger chat bubble for SAY/YELL messages from others @@ -9574,10 +12461,57 @@ void GameHandler::handleMessageChat(network::Packet& packet) { } LOG_DEBUG("[", getChatTypeString(data.type), "] ", channelInfo, senderInfo, ": ", data.message); + + // Detect addon messages: format is "prefix\ttext" in the message body. + // Only treat as addon message if prefix is short (<=16 chars, WoW limit), + // contains no spaces (real prefixes are identifiers like "DBM4" or "BigWigs"), + // and the message isn't a SAY/YELL/EMOTE (those are always player chat). + if (addonEventCallback_ && + data.type != ChatType::SAY && data.type != ChatType::YELL && + data.type != ChatType::EMOTE && data.type != ChatType::TEXT_EMOTE && + data.type != ChatType::MONSTER_SAY && data.type != ChatType::MONSTER_YELL) { + auto tabPos = data.message.find('\t'); + if (tabPos != std::string::npos && tabPos > 0 && tabPos <= 16 && + tabPos < data.message.size() - 1) { + std::string prefix = data.message.substr(0, tabPos); + // Addon prefixes are identifier-like: no spaces + if (prefix.find(' ') == std::string::npos) { + std::string body = data.message.substr(tabPos + 1); + std::string channel = getChatTypeString(data.type); + fireAddonEvent("CHAT_MSG_ADDON", {prefix, body, channel, data.senderName}); + return; + } + } + } + + // Fire CHAT_MSG_* addon events so Lua chat frames and addons receive messages. + // WoW event args: message, senderName, language, channelName + if (addonEventCallback_) { + std::string eventName = "CHAT_MSG_"; + eventName += getChatTypeString(data.type); + std::string lang = std::to_string(static_cast(data.language)); + // Format sender GUID as hex string for addons that need it + char guidBuf[32]; + snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)data.senderGuid); + fireAddonEvent(eventName, { + data.message, + data.senderName, + lang, + data.channelName, + senderInfo, // arg5: displayName + "", // arg6: specialFlags + "0", // arg7: zoneChannelID + "0", // arg8: channelIndex + "", // arg9: channelBaseName + "0", // arg10: unused + "0", // arg11: lineID + guidBuf // arg12: senderGUID + }); + } } void GameHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = TextEmotePacket::build(textEmoteId, targetGuid); socket->send(packet); } @@ -9585,7 +12519,7 @@ void GameHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) { void GameHandler::handleTextEmote(network::Packet& packet) { // Classic 1.12 and TBC 2.4.3 send: textEmoteId(u32) + emoteNum(u32) + senderGuid(u64) + nameLen(u32) + name // WotLK 3.3.5a reversed this to: senderGuid(u64) + textEmoteId(u32) + emoteNum(u32) + nameLen(u32) + name - const bool legacyFormat = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool legacyFormat = isPreWotlk(); TextEmoteData data; if (!TextEmoteParser::parse(packet, data, legacyFormat)) { LOG_WARNING("Failed to parse SMSG_TEXT_EMOTE"); @@ -9598,17 +12532,7 @@ void GameHandler::handleTextEmote(network::Packet& packet) { } // Resolve sender name - std::string senderName; - auto nameIt = playerNameCache.find(data.senderGuid); - if (nameIt != playerNameCache.end()) { - senderName = nameIt->second; - } else { - auto entity = entityManager.getEntity(data.senderGuid); - if (entity) { - auto unit = std::dynamic_pointer_cast(entity); - if (unit) senderName = unit->getName(); - } - } + std::string senderName = lookupName(data.senderGuid); if (senderName.empty()) { senderName = "Unknown"; queryPlayerName(data.senderGuid); @@ -9631,10 +12555,7 @@ void GameHandler::handleTextEmote(network::Packet& packet) { chatMsg.senderName = senderName; chatMsg.message = emoteText; - chatHistory.push_back(chatMsg); - if (chatHistory.size() > maxChatHistory) { - chatHistory.erase(chatHistory.begin()); - } + addLocalChatMessage(chatMsg); // Trigger emote animation on sender's entity via callback uint32_t animId = rendering::Renderer::getEmoteAnimByDbcId(data.textEmoteId); @@ -9646,7 +12567,7 @@ void GameHandler::handleTextEmote(network::Packet& packet) { } void GameHandler::joinChannel(const std::string& channelName, const std::string& password) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = packetParsers_ ? packetParsers_->buildJoinChannel(channelName, password) : JoinChannelPacket::build(channelName, password); socket->send(packet); @@ -9654,7 +12575,7 @@ void GameHandler::joinChannel(const std::string& channelName, const std::string& } void GameHandler::leaveChannel(const std::string& channelName) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = packetParsers_ ? packetParsers_->buildLeaveChannel(channelName) : LeaveChannelPacket::build(channelName); socket->send(packet); @@ -9721,10 +12642,86 @@ void GameHandler::handleChannelNotify(network::Packet& packet) { } break; } - case ChannelNotifyType::NOT_IN_AREA: { + case ChannelNotifyType::NOT_IN_AREA: + addSystemChatMessage("You must be in the area to join '" + data.channelName + "'."); LOG_DEBUG("Cannot join channel ", data.channelName, " (not in area)"); break; - } + case ChannelNotifyType::WRONG_PASSWORD: + addSystemChatMessage("Wrong password for channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::NOT_MEMBER: + addSystemChatMessage("You are not in channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::NOT_MODERATOR: + addSystemChatMessage("You are not a moderator of '" + data.channelName + "'."); + break; + case ChannelNotifyType::MUTED: + addSystemChatMessage("You are muted in channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::BANNED: + addSystemChatMessage("You are banned from channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::THROTTLED: + addSystemChatMessage("Channel '" + data.channelName + "' is throttled. Please wait."); + break; + case ChannelNotifyType::NOT_IN_LFG: + addSystemChatMessage("You must be in a LFG queue to join '" + data.channelName + "'."); + break; + case ChannelNotifyType::PLAYER_KICKED: + addSystemChatMessage("A player was kicked from '" + data.channelName + "'."); + break; + case ChannelNotifyType::PASSWORD_CHANGED: + addSystemChatMessage("Password for '" + data.channelName + "' changed."); + break; + case ChannelNotifyType::OWNER_CHANGED: + addSystemChatMessage("Owner of '" + data.channelName + "' changed."); + break; + case ChannelNotifyType::NOT_OWNER: + addSystemChatMessage("You are not the owner of '" + data.channelName + "'."); + break; + case ChannelNotifyType::INVALID_NAME: + addSystemChatMessage("Invalid channel name '" + data.channelName + "'."); + break; + case ChannelNotifyType::PLAYER_NOT_FOUND: + addSystemChatMessage("Player not found."); + break; + case ChannelNotifyType::ANNOUNCEMENTS_ON: + addSystemChatMessage("Channel '" + data.channelName + "': announcements enabled."); + break; + case ChannelNotifyType::ANNOUNCEMENTS_OFF: + addSystemChatMessage("Channel '" + data.channelName + "': announcements disabled."); + break; + case ChannelNotifyType::MODERATION_ON: + addSystemChatMessage("Channel '" + data.channelName + "' is now moderated."); + break; + case ChannelNotifyType::MODERATION_OFF: + addSystemChatMessage("Channel '" + data.channelName + "' is no longer moderated."); + break; + case ChannelNotifyType::PLAYER_BANNED: + addSystemChatMessage("A player was banned from '" + data.channelName + "'."); + break; + case ChannelNotifyType::PLAYER_UNBANNED: + addSystemChatMessage("A player was unbanned from '" + data.channelName + "'."); + break; + case ChannelNotifyType::PLAYER_NOT_BANNED: + addSystemChatMessage("That player is not banned from '" + data.channelName + "'."); + break; + case ChannelNotifyType::INVITE: + addSystemChatMessage("You have been invited to join channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::INVITE_WRONG_FACTION: + case ChannelNotifyType::WRONG_FACTION: + addSystemChatMessage("Wrong faction for channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::NOT_MODERATED: + addSystemChatMessage("Channel '" + data.channelName + "' is not moderated."); + break; + case ChannelNotifyType::PLAYER_INVITED: + addSystemChatMessage("Player invited to channel '" + data.channelName + "'."); + break; + case ChannelNotifyType::PLAYER_INVITE_BANNED: + addSystemChatMessage("That player is banned from '" + data.channelName + "'."); + break; default: LOG_DEBUG("Channel notify type ", static_cast(data.notifyType), " for channel ", data.channelName); @@ -9761,7 +12758,7 @@ void GameHandler::setTarget(uint64_t guid) { // (the new target's cast state is naturally fetched from unitCastStates_ by GUID) // Inform server of target selection (Phase 1) - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { auto packet = SetSelectionPacket::build(guid); socket->send(packet); } @@ -9769,11 +12766,13 @@ void GameHandler::setTarget(uint64_t guid) { if (guid != 0) { LOG_INFO("Target set: 0x", std::hex, guid, std::dec); } + fireAddonEvent("PLAYER_TARGET_CHANGED", {}); } void GameHandler::clearTarget() { if (targetGuid != 0) { LOG_INFO("Target cleared"); + fireAddonEvent("PLAYER_TARGET_CHANGED", {}); } targetGuid = 0; tabCycleIndex = -1; @@ -9787,16 +12786,17 @@ std::shared_ptr GameHandler::getTarget() const { void GameHandler::setFocus(uint64_t guid) { focusGuid = guid; + fireAddonEvent("PLAYER_FOCUS_CHANGED", {}); if (guid != 0) { auto entity = entityManager.getEntity(guid); if (entity) { - std::string name = "Unknown"; - if (entity->getType() == ObjectType::PLAYER) { - auto player = std::dynamic_pointer_cast(entity); - if (player && !player->getName().empty()) { - name = player->getName(); - } + std::string name; + auto unit = std::dynamic_pointer_cast(entity); + if (unit && !unit->getName().empty()) { + name = unit->getName(); } + if (name.empty()) name = lookupName(guid); + if (name.empty()) name = "Unknown"; addSystemChatMessage("Focus set: " + name); LOG_INFO("Focus set: 0x", std::hex, guid, std::dec); } @@ -9809,6 +12809,14 @@ void GameHandler::clearFocus() { LOG_INFO("Focus cleared"); } focusGuid = 0; + fireAddonEvent("PLAYER_FOCUS_CHANGED", {}); +} + +void GameHandler::setMouseoverGuid(uint64_t guid) { + if (mouseoverGuid_ != guid) { + mouseoverGuid_ = guid; + fireAddonEvent("UPDATE_MOUSEOVER_UNIT", {}); + } } std::shared_ptr GameHandler::getFocus() const { @@ -9835,9 +12843,8 @@ void GameHandler::targetEnemy(bool reverse) { for (const auto& [guid, entity] : entities) { if (entity->getType() == ObjectType::UNIT) { - // Check if hostile (this is simplified - would need faction checking) auto unit = std::dynamic_pointer_cast(entity); - if (unit && guid != playerGuid) { + if (unit && guid != playerGuid && unit->isHostile()) { hostiles.push_back(guid); } } @@ -9915,7 +12922,7 @@ void GameHandler::targetFriend(bool reverse) { } void GameHandler::inspectTarget() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot inspect: not in world or not connected"); return; } @@ -9934,6 +12941,12 @@ void GameHandler::inspectTarget() { auto packet = InspectPacket::build(targetGuid); socket->send(packet); + // WotLK: also query the player's achievement data so the inspect UI can display it + if (isActiveExpansion("wotlk")) { + auto achPkt = QueryInspectAchievementsPacket::build(targetGuid); + socket->send(achPkt); + } + auto player = std::static_pointer_cast(target); std::string name = player->getName().empty() ? "Target" : player->getName(); addSystemChatMessage("Inspecting " + name + "..."); @@ -9941,7 +12954,7 @@ void GameHandler::inspectTarget() { } void GameHandler::queryServerTime() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot query time: not in world or not connected"); return; } @@ -9952,7 +12965,7 @@ void GameHandler::queryServerTime() { } void GameHandler::requestPlayedTime() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot request played time: not in world or not connected"); return; } @@ -9963,7 +12976,7 @@ void GameHandler::requestPlayedTime() { } void GameHandler::queryWho(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot query who: not in world or not connected"); return; } @@ -9974,7 +12987,7 @@ void GameHandler::queryWho(const std::string& playerName) { } void GameHandler::addFriend(const std::string& playerName, const std::string& note) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot add friend: not in world or not connected"); return; } @@ -9991,7 +13004,7 @@ void GameHandler::addFriend(const std::string& playerName, const std::string& no } void GameHandler::removeFriend(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot remove friend: not in world or not connected"); return; } @@ -10016,7 +13029,7 @@ void GameHandler::removeFriend(const std::string& playerName) { } void GameHandler::setFriendNote(const std::string& playerName, const std::string& note) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot set friend note: not in world or not connected"); return; } @@ -10040,7 +13053,7 @@ void GameHandler::setFriendNote(const std::string& playerName, const std::string } void GameHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot roll: not in world or not connected"); return; } @@ -10059,7 +13072,7 @@ void GameHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) { } void GameHandler::addIgnore(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot add ignore: not in world or not connected"); return; } @@ -10076,7 +13089,7 @@ void GameHandler::addIgnore(const std::string& playerName) { } void GameHandler::removeIgnore(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot remove ignore: not in world or not connected"); return; } @@ -10132,23 +13145,24 @@ void GameHandler::cancelLogout() { auto packet = LogoutCancelPacket::build(); socket->send(packet); loggingOut_ = false; + logoutCountdown_ = 0.0f; addSystemChatMessage("Logout cancelled."); LOG_INFO("Cancelled logout"); } void GameHandler::setStandState(uint8_t standState) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot change stand state: not in world or not connected"); return; } auto packet = StandStateChangePacket::build(standState); socket->send(packet); - LOG_INFO("Changed stand state to: ", (int)standState); + LOG_INFO("Changed stand state to: ", static_cast(standState)); } void GameHandler::toggleHelm() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot toggle helm: not in world or not connected"); return; } @@ -10161,7 +13175,7 @@ void GameHandler::toggleHelm() { } void GameHandler::toggleCloak() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot toggle cloak: not in world or not connected"); return; } @@ -10207,6 +13221,17 @@ void GameHandler::followTarget() { addSystemChatMessage("Now following " + targetName + "."); LOG_INFO("Following target: ", targetName, " (GUID: 0x", std::hex, targetGuid, std::dec, ")"); + fireAddonEvent("AUTOFOLLOW_BEGIN", {}); +} + +void GameHandler::cancelFollow() { + if (followTargetGuid_ == 0) { + addSystemChatMessage("You are not following anyone."); + return; + } + followTargetGuid_ = 0; + addSystemChatMessage("You stop following."); + fireAddonEvent("AUTOFOLLOW_END", {}); } void GameHandler::assistTarget() { @@ -10262,7 +13287,7 @@ void GameHandler::assistTarget() { } void GameHandler::togglePvp() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot toggle PvP: not in world or not connected"); return; } @@ -10286,7 +13311,7 @@ void GameHandler::togglePvp() { } void GameHandler::requestGuildInfo() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot request guild info: not in world or not connected"); return; } @@ -10297,7 +13322,7 @@ void GameHandler::requestGuildInfo() { } void GameHandler::requestGuildRoster() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot request guild roster: not in world or not connected"); return; } @@ -10309,7 +13334,7 @@ void GameHandler::requestGuildRoster() { } void GameHandler::setGuildMotd(const std::string& motd) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot set guild MOTD: not in world or not connected"); return; } @@ -10321,7 +13346,7 @@ void GameHandler::setGuildMotd(const std::string& motd) { } void GameHandler::promoteGuildMember(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot promote guild member: not in world or not connected"); return; } @@ -10338,7 +13363,7 @@ void GameHandler::promoteGuildMember(const std::string& playerName) { } void GameHandler::demoteGuildMember(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot demote guild member: not in world or not connected"); return; } @@ -10355,7 +13380,7 @@ void GameHandler::demoteGuildMember(const std::string& playerName) { } void GameHandler::leaveGuild() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot leave guild: not in world or not connected"); return; } @@ -10367,7 +13392,7 @@ void GameHandler::leaveGuild() { } void GameHandler::inviteToGuild(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot invite to guild: not in world or not connected"); return; } @@ -10384,7 +13409,7 @@ void GameHandler::inviteToGuild(const std::string& playerName) { } void GameHandler::initiateReadyCheck() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot initiate ready check: not in world or not connected"); return; } @@ -10401,7 +13426,7 @@ void GameHandler::initiateReadyCheck() { } void GameHandler::respondToReadyCheck(bool ready) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot respond to ready check: not in world or not connected"); return; } @@ -10422,7 +13447,7 @@ void GameHandler::acceptDuel() { } void GameHandler::forfeitDuel() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot forfeit duel: not in world or not connected"); return; } @@ -10434,8 +13459,8 @@ void GameHandler::forfeitDuel() { } void GameHandler::handleDuelRequested(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 16) { - packet.setReadPos(packet.getSize()); + if (!packet.hasRemaining(16)) { + packet.skipAll(); return; } duelChallengerGuid_ = packet.readUInt64(); @@ -10443,10 +13468,12 @@ void GameHandler::handleDuelRequested(network::Packet& packet) { // Resolve challenger name from entity list duelChallengerName_.clear(); - auto entity = entityManager.getEntity(duelChallengerGuid_); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(duelChallengerGuid_)) { duelChallengerName_ = unit->getName(); } + if (duelChallengerName_.empty()) { + duelChallengerName_ = lookupName(duelChallengerGuid_); + } if (duelChallengerName_.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", @@ -10456,30 +13483,39 @@ void GameHandler::handleDuelRequested(network::Packet& packet) { pendingDuelRequest_ = true; addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!"); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playTargetSelect(); }); LOG_INFO("SMSG_DUEL_REQUESTED: challenger=0x", std::hex, duelChallengerGuid_, " flag=0x", duelFlagGuid_, std::dec, " name=", duelChallengerName_); + fireAddonEvent("DUEL_REQUESTED", {duelChallengerName_}); } void GameHandler::handleDuelComplete(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; uint8_t started = packet.readUInt8(); // started=1: duel began, started=0: duel was cancelled before starting pendingDuelRequest_ = false; + duelCountdownMs_ = 0; // clear countdown once duel is resolved if (!started) { addSystemChatMessage("The duel was cancelled."); } LOG_INFO("SMSG_DUEL_COMPLETE: started=", static_cast(started)); + fireAddonEvent("DUEL_FINISHED", {}); } void GameHandler::handleDuelWinner(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 3) return; - /*uint8_t type =*/ packet.readUInt8(); // 0=normal, 1=flee + if (!packet.hasRemaining(3)) return; + uint8_t duelType = packet.readUInt8(); // 0=normal win, 1=opponent fled duel area std::string winner = packet.readString(); std::string loser = packet.readString(); - std::string msg = winner + " has defeated " + loser + " in a duel!"; + std::string msg; + if (duelType == 1) { + msg = loser + " has fled from the duel. " + winner + " wins!"; + } else { + msg = winner + " has defeated " + loser + " in a duel!"; + } addSystemChatMessage(msg); - LOG_INFO("SMSG_DUEL_WINNER: winner=", winner, " loser=", loser); + LOG_INFO("SMSG_DUEL_WINNER: winner=", winner, " loser=", loser, " type=", static_cast(duelType)); } void GameHandler::toggleAfk(const std::string& message) { @@ -10529,7 +13565,7 @@ void GameHandler::toggleDnd(const std::string& message) { } void GameHandler::replyToLastWhisper(const std::string& message) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot send whisper: not in world or not connected"); return; } @@ -10550,7 +13586,7 @@ void GameHandler::replyToLastWhisper(const std::string& message) { } void GameHandler::uninvitePlayer(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot uninvite player: not in world or not connected"); return; } @@ -10567,7 +13603,7 @@ void GameHandler::uninvitePlayer(const std::string& playerName) { } void GameHandler::leaveParty() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot leave party: not in world or not connected"); return; } @@ -10579,7 +13615,7 @@ void GameHandler::leaveParty() { } void GameHandler::setMainTank(uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot set main tank: not in world or not connected"); return; } @@ -10597,7 +13633,7 @@ void GameHandler::setMainTank(uint64_t targetGuid) { } void GameHandler::setMainAssist(uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot set main assist: not in world or not connected"); return; } @@ -10615,7 +13651,7 @@ void GameHandler::setMainAssist(uint64_t targetGuid) { } void GameHandler::clearMainTank() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot clear main tank: not in world or not connected"); return; } @@ -10628,7 +13664,7 @@ void GameHandler::clearMainTank() { } void GameHandler::clearMainAssist() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot clear main assist: not in world or not connected"); return; } @@ -10641,7 +13677,7 @@ void GameHandler::clearMainAssist() { } void GameHandler::setRaidMark(uint64_t guid, uint8_t icon) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; static const char* kMarkNames[] = { "Star", "Circle", "Diamond", "Triangle", "Moon", "Square", "Cross", "Skull" @@ -10664,7 +13700,7 @@ void GameHandler::setRaidMark(uint64_t guid, uint8_t icon) { } void GameHandler::requestRaidInfo() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot request raid info: not in world or not connected"); return; } @@ -10676,7 +13712,7 @@ void GameHandler::requestRaidInfo() { } void GameHandler::proposeDuel(uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot propose duel: not in world or not connected"); return; } @@ -10693,7 +13729,7 @@ void GameHandler::proposeDuel(uint64_t targetGuid) { } void GameHandler::initiateTrade(uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot initiate trade: not in world or not connected"); return; } @@ -10710,7 +13746,7 @@ void GameHandler::initiateTrade(uint64_t targetGuid) { } void GameHandler::stopCasting() { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("Cannot stop casting: not in world or not connected"); return; } @@ -10725,13 +13761,18 @@ void GameHandler::stopCasting() { socket->send(packet); } - // Reset casting state + // Reset casting state and clear any queued spell so it doesn't fire later casting = false; castIsChannel = false; currentCastSpellId = 0; pendingGameObjectInteractGuid_ = 0; + lastInteractedGoGuid_ = 0; castTimeRemaining = 0.0f; castTimeTotal = 0.0f; + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; LOG_INFO("Cancelled spell cast"); } @@ -10745,33 +13786,67 @@ void GameHandler::releaseSpirit() { } auto packet = RepopRequestPacket::build(); socket->send(packet); - releasedSpirit_ = true; + // Do NOT set releasedSpirit_ = true here. Setting it optimistically races + // with PLAYER_FLAGS field updates that arrive before the server processes + // CMSG_REPOP_REQUEST: the PLAYER_FLAGS handler sees wasGhost=true/nowGhost=false + // and fires the "ghost cleared" path, wiping corpseMapId_/corpseGuid_. + // Let the server drive ghost state via PLAYER_FLAGS_GHOST (field update path). + selfResAvailable_ = false; // self-res window closes when spirit is released repopPending_ = true; lastRepopRequestMs_ = static_cast(now); LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)"); + // Query server for authoritative corpse position (response updates corpseX_/Y_/Z_) + network::Packet cq(wireOpcode(Opcode::MSG_CORPSE_QUERY)); + socket->send(cq); } } bool GameHandler::canReclaimCorpse() const { - if (!releasedSpirit_ || corpseMapId_ == 0) return false; - // Only if ghost is on the same map as their corpse + // Need: ghost state + corpse object GUID (required by CMSG_RECLAIM_CORPSE) + + // corpse map known + same map + within 40 yards. + if (!releasedSpirit_ || corpseGuid_ == 0 || corpseMapId_ == 0) return false; if (currentMapId_ != corpseMapId_) return false; - // Must be within 40 yards (server also validates proximity) - float dx = movementInfo.x - corpseX_; - float dy = movementInfo.y - corpseY_; + // movementInfo.x/y are canonical (x=north=server_y, y=west=server_x). + // corpseX_/Y_ are raw server coords (x=west, y=north). + float dx = movementInfo.x - corpseY_; // canonical north - server.y + float dy = movementInfo.y - corpseX_; // canonical west - server.x float dz = movementInfo.z - corpseZ_; return (dx*dx + dy*dy + dz*dz) <= (40.0f * 40.0f); } +float GameHandler::getCorpseReclaimDelaySec() const { + if (corpseReclaimAvailableMs_ == 0) return 0.0f; + auto nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + if (nowMs >= corpseReclaimAvailableMs_) return 0.0f; + return static_cast(corpseReclaimAvailableMs_ - nowMs) / 1000.0f; +} + void GameHandler::reclaimCorpse() { if (!canReclaimCorpse() || !socket) return; - network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE)); + // CMSG_RECLAIM_CORPSE requires the corpse object's own GUID. + // Servers look up the corpse by this GUID; sending the player GUID silently fails. + if (corpseGuid_ == 0) { + LOG_WARNING("reclaimCorpse: corpse GUID not yet known (corpse object not received); cannot reclaim"); + return; + } + auto packet = ReclaimCorpsePacket::build(corpseGuid_); socket->send(packet); - LOG_INFO("Sent CMSG_RECLAIM_CORPSE"); + LOG_INFO("Sent CMSG_RECLAIM_CORPSE for corpse guid=0x", std::hex, corpseGuid_, std::dec); +} + +void GameHandler::useSelfRes() { + if (!selfResAvailable_ || !socket) return; + // CMSG_SELF_RES: empty body — server confirms resurrection via SMSG_UPDATE_OBJECT. + network::Packet pkt(wireOpcode(Opcode::CMSG_SELF_RES)); + socket->send(pkt); + selfResAvailable_ = false; + LOG_INFO("Sent CMSG_SELF_RES (Reincarnation / Twisting Nether)"); } void GameHandler::activateSpiritHealer(uint64_t npcGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingSpiritHealerGuid_ = npcGuid; auto packet = SpiritHealerActivatePacket::build(npcGuid); socket->send(packet); @@ -10889,6 +13964,25 @@ void GameHandler::addLocalChatMessage(const MessageChatData& msg) { if (chatHistory.size() > maxChatHistory) { chatHistory.pop_front(); } + if (addonChatCallback_) addonChatCallback_(msg); + + // Fire CHAT_MSG_* for local echoes (player's own messages, system messages) + // so Lua chat frame addons display them. + if (addonEventCallback_) { + std::string eventName = "CHAT_MSG_"; + eventName += getChatTypeString(msg.type); + const Character* ac = getActiveCharacter(); + std::string senderName = msg.senderName.empty() + ? (ac ? ac->name : std::string{}) : msg.senderName; + char guidBuf[32]; + snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", + (unsigned long long)(msg.senderGuid != 0 ? msg.senderGuid : playerGuid)); + fireAddonEvent(eventName, { + msg.message, senderName, + std::to_string(static_cast(msg.language)), + msg.channelName, senderName, "", "0", "0", "", "0", "0", guidBuf + }); + } } // ============================================================ @@ -10910,7 +14004,7 @@ void GameHandler::queryPlayerName(uint64_t guid) { return; } if (pendingNameQueries.count(guid)) return; - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_INFO("queryPlayerName: skipped guid=0x", std::hex, guid, std::dec, " state=", worldStateName(state), " socket=", (socket ? "yes" : "no")); return; @@ -10924,7 +14018,7 @@ void GameHandler::queryPlayerName(uint64_t guid) { void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) { if (creatureInfoCache.count(entry) || pendingCreatureQueries.count(entry)) return; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingCreatureQueries.insert(entry); auto packet = CreatureQueryPacket::build(entry, guid); @@ -10933,7 +14027,7 @@ void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) { void GameHandler::queryGameObjectInfo(uint32_t entry, uint64_t guid) { if (gameObjectInfoCache_.count(entry) || pendingGameObjectQueries_.count(entry)) return; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingGameObjectQueries_.insert(entry); auto packet = GameObjectQueryPacket::build(entry, guid); @@ -10941,8 +14035,7 @@ void GameHandler::queryGameObjectInfo(uint32_t entry, uint64_t guid) { } std::string GameHandler::getCachedPlayerName(uint64_t guid) const { - auto it = playerNameCache.find(guid); - return (it != playerNameCache.end()) ? it->second : ""; + return std::string(lookupName(guid)); } std::string GameHandler::getCachedCreatureName(uint32_t entry) const { @@ -10960,11 +14053,15 @@ void GameHandler::handleNameQueryResponse(network::Packet& packet) { pendingNameQueries.erase(data.guid); LOG_INFO("Name query response: guid=0x", std::hex, data.guid, std::dec, - " found=", (int)data.found, " name='", data.name, "'", - " race=", (int)data.race, " class=", (int)data.classId); + " found=", static_cast(data.found), " name='", data.name, "'", + " race=", static_cast(data.race), " class=", static_cast(data.classId)); if (data.isValid()) { playerNameCache[data.guid] = data.name; + // Cache class/race from name query for UnitClass/UnitRace fallback + if (data.classId != 0 || data.race != 0) { + playerClassRaceCache_[data.guid] = {data.classId, data.race}; + } // Update entity name auto entity = entityManager.getEntity(data.guid); if (entity && entity->getType() == ObjectType::PLAYER) { @@ -10991,6 +14088,16 @@ void GameHandler::handleNameQueryResponse(network::Packet& packet) { if (friendGuids_.count(data.guid)) { friendsCache[data.name] = data.guid; } + + // Fire UNIT_NAME_UPDATE so nameplate/unit frame addons know the name is available + if (addonEventCallback_) { + std::string unitId; + if (data.guid == targetGuid) unitId = "target"; + else if (data.guid == focusGuid) unitId = "focus"; + else if (data.guid == playerGuid) unitId = "player"; + if (!unitId.empty()) + fireAddonEvent("UNIT_NAME_UPDATE", {unitId}); + } } } @@ -11054,7 +14161,7 @@ void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) { } void GameHandler::handleGameObjectPageText(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; uint64_t guid = packet.readUInt64(); auto entity = entityManager.getEntity(guid); if (!entity || entity->getType() != ObjectType::GAMEOBJECT) return; @@ -11078,6 +14185,7 @@ void GameHandler::handleGameObjectPageText(network::Packet& packet) { else if (info.type == 10) pageId = info.data[7]; if (pageId != 0 && socket && state == WorldState::IN_WORLD) { + bookPages_.clear(); // start a fresh book for this interaction auto req = PageTextQueryPacket::build(pageId, guid); socket->send(req); return; @@ -11092,19 +14200,31 @@ void GameHandler::handlePageTextQueryResponse(network::Packet& packet) { PageTextQueryResponseData data; if (!PageTextQueryResponseParser::parse(packet, data)) return; - if (!data.text.empty()) { - std::istringstream iss(data.text); - std::string line; - bool wrote = false; - while (std::getline(iss, line)) { - if (line.empty()) continue; - addSystemChatMessage(line); - wrote = true; + if (!data.isValid()) return; + + // Append page if not already collected + bool alreadyHave = false; + for (const auto& bp : bookPages_) { + if (bp.pageId == data.pageId) { alreadyHave = true; break; } + } + if (!alreadyHave) { + bookPages_.push_back({data.pageId, data.text}); + } + + // Follow the chain: if there's a next page we haven't fetched yet, request it + if (data.nextPageId != 0) { + bool nextHave = false; + for (const auto& bp : bookPages_) { + if (bp.pageId == data.nextPageId) { nextHave = true; break; } } - if (!wrote) { - addSystemChatMessage(data.text); + if (!nextHave && socket && state == WorldState::IN_WORLD) { + auto req = PageTextQueryPacket::build(data.nextPageId, playerGuid); + socket->send(req); } } + LOG_DEBUG("handlePageTextQueryResponse: pageId=", data.pageId, + " nextPage=", data.nextPageId, + " totalPages=", bookPages_.size()); } // ============================================================ @@ -11113,7 +14233,7 @@ void GameHandler::handlePageTextQueryResponse(network::Packet& packet) { void GameHandler::queryItemInfo(uint32_t entry, uint64_t guid) { if (itemInfoCache_.count(entry) || pendingItemQueries_.count(entry)) return; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingItemQueries_.insert(entry); // Some cores reject CMSG_ITEM_QUERY_SINGLE when the GUID is 0. @@ -11147,6 +14267,22 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) { rebuildOnlineInventory(); maybeDetectVisibleItemLayout(); + // Flush any deferred loot notifications waiting on this item's name/quality. + for (auto it = pendingItemPushNotifs_.begin(); it != pendingItemPushNotifs_.end(); ) { + if (it->itemId == data.entry) { + std::string itemName = data.name.empty() ? ("item #" + std::to_string(data.entry)) : data.name; + std::string link = buildItemLink(data.entry, data.quality, itemName); + std::string msg = "Received: " + link; + if (it->count > 1) msg += " x" + std::to_string(it->count); + addSystemChatMessage(msg); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLootItem(); }); + if (itemLootCallback_) itemLootCallback_(data.entry, it->count, data.quality, itemName); + it = pendingItemPushNotifs_.erase(it); + } else { + ++it; + } + } + // Selectively re-emit only players whose equipment references this item entry const uint32_t resolvedEntry = data.entry; for (const auto& [guid, entries] : otherPlayerVisibleItemEntries_) { @@ -11187,14 +14323,14 @@ void GameHandler::handleInspectResults(network::Packet& packet) { // If type==1: PackedGUID of inspected player // Then: uint32 unspentTalents, uint8 talentGroupCount, uint8 activeTalentGroup // Per talent group: uint8 talentCount, [talentId(u32) + rank(u8)]..., uint8 glyphCount, [glyphId(u16)]... - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; uint8_t talentType = packet.readUInt8(); if (talentType == 0) { // Own talent info (type 0): uint32 unspentTalents, uint8 groupCount, uint8 activeGroup // Per group: uint8 talentCount, [talentId(4)+rank(1)]..., uint8 glyphCount, [glyphId(2)]... - if (packet.getSize() - packet.getReadPos() < 6) { + if (!packet.hasRemaining(6)) { LOG_DEBUG("SMSG_TALENTS_INFO type=0: too short"); return; } @@ -11206,20 +14342,22 @@ void GameHandler::handleInspectResults(network::Packet& packet) { activeTalentSpec_ = activeTalentGroup; for (uint8_t g = 0; g < talentGroupCount && g < 2; ++g) { - if (packet.getSize() - packet.getReadPos() < 1) break; + if (!packet.hasRemaining(1)) break; uint8_t talentCount = packet.readUInt8(); learnedTalents_[g].clear(); for (uint8_t t = 0; t < talentCount; ++t) { - if (packet.getSize() - packet.getReadPos() < 5) break; + if (!packet.hasRemaining(5)) break; uint32_t talentId = packet.readUInt32(); uint8_t rank = packet.readUInt8(); - learnedTalents_[g][talentId] = rank; + learnedTalents_[g][talentId] = rank + 1u; // wire sends 0-indexed; store 1-indexed } - if (packet.getSize() - packet.getReadPos() < 1) break; + if (!packet.hasRemaining(1)) break; + learnedGlyphs_[g].fill(0); uint8_t glyphCount = packet.readUInt8(); for (uint8_t gl = 0; gl < glyphCount; ++gl) { - if (packet.getSize() - packet.getReadPos() < 2) break; - packet.readUInt16(); // glyphId (skip) + if (!packet.hasRemaining(2)) break; + uint16_t glyphId = packet.readUInt16(); + if (gl < MAX_GLYPH_SLOTS) learnedGlyphs_[g][gl] = glyphId; } } @@ -11235,21 +14373,21 @@ void GameHandler::handleInspectResults(network::Packet& packet) { } LOG_INFO("SMSG_TALENTS_INFO type=0: unspent=", unspentTalents, - " groups=", (int)talentGroupCount, " active=", (int)activeTalentGroup, + " groups=", static_cast(talentGroupCount), " active=", static_cast(activeTalentGroup), " learned=", learnedTalents_[activeTalentGroup].size()); return; } // talentType == 1: inspect result // WotLK: packed GUID; TBC: full uint64 - const bool talentTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (talentTbc ? 8u : 2u)) return; + const bool talentTbc = isPreWotlk(); + if (!packet.hasRemaining(talentTbc ? 8u : 2u) ) return; uint64_t guid = talentTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + ? packet.readUInt64() : packet.readPackedGuid(); if (guid == 0) return; - size_t bytesLeft = packet.getSize() - packet.getReadPos(); + size_t bytesLeft = packet.getRemainingSize(); if (bytesLeft < 6) { LOG_WARNING("SMSG_TALENTS_INFO: too short after guid, ", bytesLeft, " bytes"); auto entity = entityManager.getEntity(guid); @@ -11277,37 +14415,38 @@ void GameHandler::handleInspectResults(network::Packet& packet) { // Parse talent groups uint32_t totalTalents = 0; for (uint8_t g = 0; g < talentGroupCount && g < 2; ++g) { - bytesLeft = packet.getSize() - packet.getReadPos(); + bytesLeft = packet.getRemainingSize(); if (bytesLeft < 1) break; uint8_t talentCount = packet.readUInt8(); for (uint8_t t = 0; t < talentCount; ++t) { - bytesLeft = packet.getSize() - packet.getReadPos(); + bytesLeft = packet.getRemainingSize(); if (bytesLeft < 5) break; packet.readUInt32(); // talentId packet.readUInt8(); // rank totalTalents++; } - bytesLeft = packet.getSize() - packet.getReadPos(); + bytesLeft = packet.getRemainingSize(); if (bytesLeft < 1) break; uint8_t glyphCount = packet.readUInt8(); for (uint8_t gl = 0; gl < glyphCount; ++gl) { - bytesLeft = packet.getSize() - packet.getReadPos(); + bytesLeft = packet.getRemainingSize(); if (bytesLeft < 2) break; packet.readUInt16(); // glyphId } } // Parse enchantment slot mask + enchant IDs - bytesLeft = packet.getSize() - packet.getReadPos(); + std::array enchantIds{}; + bytesLeft = packet.getRemainingSize(); if (bytesLeft >= 4) { uint32_t slotMask = packet.readUInt32(); for (int slot = 0; slot < 19; ++slot) { if (slotMask & (1u << slot)) { - bytesLeft = packet.getSize() - packet.getReadPos(); + bytesLeft = packet.getRemainingSize(); if (bytesLeft < 2) break; - packet.readUInt16(); // enchantId + enchantIds[slot] = packet.readUInt16(); } } } @@ -11319,6 +14458,7 @@ void GameHandler::handleInspectResults(network::Packet& packet) { inspectResult_.unspentTalents = unspentTalents; inspectResult_.talentGroups = talentGroupCount; inspectResult_.activeTalentGroup = activeTalentGroup; + inspectResult_.enchantIds = enchantIds; // Merge any gear we already have from a prior inspect request auto gearIt = inspectedPlayerItemEntries_.find(guid); @@ -11329,7 +14469,12 @@ void GameHandler::handleInspectResults(network::Packet& packet) { } LOG_INFO("Inspect results for ", playerName, ": ", totalTalents, " talents, ", - unspentTalents, " unspent, ", (int)talentGroupCount, " specs"); + unspentTalents, " unspent, ", static_cast(talentGroupCount), " specs"); + if (addonEventCallback_) { + char guidBuf[32]; + snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)guid); + fireAddonEvent("INSPECT_READY", {guidBuf}); + } } uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const { @@ -11429,6 +14574,21 @@ bool GameHandler::applyInventoryFields(const std::map& field bool slotsChanged = false; int equipBase = (invSlotBase_ >= 0) ? invSlotBase_ : static_cast(fieldIndex(UF::PLAYER_FIELD_INV_SLOT_HEAD)); int packBase = (packSlotBase_ >= 0) ? packSlotBase_ : static_cast(fieldIndex(UF::PLAYER_FIELD_PACK_SLOT_1)); + int bankBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1)); + int bankBagBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1)); + + // Derive slot counts from field gap (Classic=24/6, TBC/WotLK=28/7). + if (bankBase != 0xFFFF && bankBagBase != 0xFFFF) { + effectiveBankSlots_ = std::min((bankBagBase - bankBase) / 2, 28); + effectiveBankBagSlots_ = (effectiveBankSlots_ <= 24) ? 6 : 7; + } + + int keyringBase = static_cast(fieldIndex(UF::PLAYER_FIELD_KEYRING_SLOT_1)); + if (keyringBase == 0xFFFF && bankBagBase != 0xFFFF) { + // Layout fallback for profiles that don't define PLAYER_FIELD_KEYRING_SLOT_1. + // Bank bag slots are followed by 12 vendor buyback slots (24 fields), then keyring. + keyringBase = bankBagBase + (effectiveBankBagSlots_ * 2) + 24; + } for (const auto& [key, val] : fields) { if (key >= equipBase && key <= equipBase + (game::Inventory::NUM_EQUIP_SLOTS * 2 - 1)) { @@ -11449,15 +14609,17 @@ bool GameHandler::applyInventoryFields(const std::map& field else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); slotsChanged = true; } - } - - // Bank slots starting at PLAYER_FIELD_BANK_SLOT_1 - int bankBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANK_SLOT_1)); - int bankBagBase = static_cast(fieldIndex(UF::PLAYER_FIELD_BANKBAG_SLOT_1)); - // Derive slot counts from field gap (Classic=24/6, TBC/WotLK=28/7) - if (bankBase != 0xFFFF && bankBagBase != 0xFFFF) { - effectiveBankSlots_ = std::min((bankBagBase - bankBase) / 2, 28); - effectiveBankBagSlots_ = (effectiveBankSlots_ <= 24) ? 6 : 7; + } else if (keyringBase != 0xFFFF && + key >= keyringBase && + key <= keyringBase + (game::Inventory::KEYRING_SLOTS * 2 - 1)) { + int slotIndex = (key - keyringBase) / 2; + bool isLow = ((key - keyringBase) % 2 == 0); + if (slotIndex < static_cast(keyringSlotGuids_.size())) { + uint64_t& guid = keyringSlotGuids_[slotIndex]; + if (isLow) guid = (guid & 0xFFFFFFFF00000000ULL) | val; + else guid = (guid & 0x00000000FFFFFFFFULL) | (uint64_t(val) << 32); + slotsChanged = true; + } } if (bankBase != 0xFFFF && key >= static_cast(bankBase) && key <= static_cast(bankBase) + (effectiveBankSlots_ * 2 - 1)) { @@ -11618,6 +14780,55 @@ void GameHandler::rebuildOnlineInventory() { inventory.setBackpackSlot(i, def); } + // Keyring slots + for (int i = 0; i < game::Inventory::KEYRING_SLOTS; i++) { + uint64_t guid = keyringSlotGuids_[i]; + if (guid == 0) continue; + + auto itemIt = onlineItems_.find(guid); + if (itemIt == onlineItems_.end()) continue; + + ItemDef def; + def.itemId = itemIt->second.entry; + def.stackCount = itemIt->second.stackCount; + def.curDurability = itemIt->second.curDurability; + def.maxDurability = itemIt->second.maxDurability; + def.maxStack = 1; + + auto infoIt = itemInfoCache_.find(itemIt->second.entry); + if (infoIt != itemInfoCache_.end()) { + def.name = infoIt->second.name; + def.quality = static_cast(infoIt->second.quality); + def.inventoryType = infoIt->second.inventoryType; + def.maxStack = std::max(1, infoIt->second.maxStack); + def.displayInfoId = infoIt->second.displayInfoId; + def.subclassName = infoIt->second.subclassName; + def.damageMin = infoIt->second.damageMin; + def.damageMax = infoIt->second.damageMax; + def.delayMs = infoIt->second.delayMs; + def.armor = infoIt->second.armor; + def.stamina = infoIt->second.stamina; + def.strength = infoIt->second.strength; + def.agility = infoIt->second.agility; + def.intellect = infoIt->second.intellect; + def.spirit = infoIt->second.spirit; + def.sellPrice = infoIt->second.sellPrice; + def.itemLevel = infoIt->second.itemLevel; + def.requiredLevel = infoIt->second.requiredLevel; + def.bindType = infoIt->second.bindType; + def.description = infoIt->second.description; + def.startQuestId = infoIt->second.startQuestId; + def.extraStats.clear(); + for (const auto& es : infoIt->second.extraStats) + def.extraStats.push_back({es.statType, es.statValue}); + } else { + def.name = "Item " + std::to_string(def.itemId); + queryItemInfo(def.itemId, guid); + } + + inventory.setKeyringSlot(i, def); + } + // Bag contents (BAG1-BAG4 are equip slots 19-22) for (int bagIdx = 0; bagIdx < 4; bagIdx++) { uint64_t bagGuid = equipSlotGuids_[19 + bagIdx]; @@ -11861,6 +15072,8 @@ void GameHandler::rebuildOnlineInventory() { int c = 0; for (auto g : equipSlotGuids_) if (g) c++; return c; }(), " backpack=", [&](){ int c = 0; for (auto g : backpackSlotGuids_) if (g) c++; return c; + }(), " keyring=", [&](){ + int c = 0; for (auto g : keyringSlotGuids_) if (g) c++; return c; }()); } @@ -12062,6 +15275,7 @@ void GameHandler::startAutoAttack(uint64_t targetGuid) { } autoAttackRequested_ = true; + autoAttackRetryPending_ = true; // Keep combat animation/state server-authoritative. We only flip autoAttacking // on SMSG_ATTACKSTART where attackerGuid == playerGuid. autoAttacking = false; @@ -12070,7 +15284,7 @@ void GameHandler::startAutoAttack(uint64_t targetGuid) { autoAttackOutOfRangeTime_ = 0.0f; autoAttackResendTimer_ = 0.0f; autoAttackFacingSyncTimer_ = 0.0f; - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { auto packet = AttackSwingPacket::build(targetGuid); socket->send(packet); } @@ -12081,26 +15295,111 @@ void GameHandler::stopAutoAttack() { if (!autoAttacking && !autoAttackRequested_) return; autoAttackRequested_ = false; autoAttacking = false; + autoAttackRetryPending_ = false; autoAttackTarget = 0; autoAttackOutOfRange_ = false; autoAttackOutOfRangeTime_ = 0.0f; autoAttackResendTimer_ = 0.0f; autoAttackFacingSyncTimer_ = 0.0f; - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { auto packet = AttackStopPacket::build(); socket->send(packet); } LOG_INFO("Stopping auto-attack"); + fireAddonEvent("PLAYER_LEAVE_COMBAT", {}); } -void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource) { +void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType, + uint64_t srcGuid, uint64_t dstGuid) { CombatTextEntry entry; entry.type = type; entry.amount = amount; entry.spellId = spellId; entry.age = 0.0f; entry.isPlayerSource = isPlayerSource; + entry.powerType = powerType; + entry.srcGuid = srcGuid; + entry.dstGuid = dstGuid; + // Random horizontal stagger so simultaneous hits don't stack vertically + static std::mt19937 rng(std::random_device{}()); + std::uniform_real_distribution dist(-1.0f, 1.0f); + entry.xSeed = dist(rng); combatText.push_back(entry); + + // Persistent combat log — use explicit GUIDs if provided, else fall back to + // player/current-target (the old behaviour for events without specific participants). + CombatLogEntry log; + log.type = type; + log.amount = amount; + log.spellId = spellId; + log.isPlayerSource = isPlayerSource; + log.powerType = powerType; + log.timestamp = std::time(nullptr); + // If the caller provided an explicit destination GUID but left source GUID as 0, + // preserve "unknown/no source" (e.g. environmental damage) instead of + // backfilling from current target. + uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid + : ((dstGuid != 0) ? 0 : (isPlayerSource ? playerGuid : targetGuid)); + uint64_t effectiveDst = (dstGuid != 0) ? dstGuid + : (isPlayerSource ? targetGuid : playerGuid); + log.sourceName = lookupName(effectiveSrc); + log.targetName = (effectiveDst != 0) ? lookupName(effectiveDst) : std::string{}; + if (combatLog_.size() >= MAX_COMBAT_LOG) + combatLog_.pop_front(); + combatLog_.push_back(std::move(log)); + + // Fire COMBAT_LOG_EVENT_UNFILTERED for Lua addons + // Args: subevent, sourceGUID, sourceName, 0 (sourceFlags), destGUID, destName, 0 (destFlags), spellId, spellName, amount + if (addonEventCallback_) { + static const char* kSubevents[] = { + "SWING_DAMAGE", "SPELL_DAMAGE", "SPELL_HEAL", "SWING_MISSED", "SWING_MISSED", + "SWING_MISSED", "SWING_MISSED", "SWING_MISSED", "SPELL_DAMAGE", "SPELL_HEAL", + "SPELL_PERIODIC_DAMAGE", "SPELL_PERIODIC_HEAL", "ENVIRONMENTAL_DAMAGE", + "SPELL_ENERGIZE", "SPELL_DRAIN", "PARTY_KILL", "SPELL_MISSED", "SPELL_ABSORBED", + "SPELL_MISSED", "SPELL_MISSED", "SPELL_MISSED", "SPELL_AURA_APPLIED", + "SPELL_DISPEL", "SPELL_STOLEN", "SPELL_INTERRUPT", "SPELL_INSTAKILL", + "PARTY_KILL", "SWING_DAMAGE", "SWING_DAMAGE" + }; + const char* subevent = (type < sizeof(kSubevents)/sizeof(kSubevents[0])) + ? kSubevents[type] : "UNKNOWN"; + char srcBuf[32], dstBuf[32]; + snprintf(srcBuf, sizeof(srcBuf), "0x%016llX", (unsigned long long)effectiveSrc); + snprintf(dstBuf, sizeof(dstBuf), "0x%016llX", (unsigned long long)effectiveDst); + std::string spellName = (spellId != 0) ? getSpellName(spellId) : std::string{}; + std::string timestamp = std::to_string(static_cast(std::time(nullptr))); + fireAddonEvent("COMBAT_LOG_EVENT_UNFILTERED", { + timestamp, subevent, + srcBuf, log.sourceName, "0", + dstBuf, log.targetName, "0", + std::to_string(spellId), spellName, + std::to_string(amount) + }); + } +} + +bool GameHandler::shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId) { + if (spellId == 0) return false; + + const auto now = std::chrono::steady_clock::now(); + constexpr auto kRecentWindow = std::chrono::seconds(1); + while (!recentSpellstealLogs_.empty() && + now - recentSpellstealLogs_.front().timestamp > kRecentWindow) { + recentSpellstealLogs_.pop_front(); + } + + for (auto it = recentSpellstealLogs_.begin(); it != recentSpellstealLogs_.end(); ++it) { + if (it->casterGuid == casterGuid && + it->victimGuid == victimGuid && + it->spellId == spellId) { + recentSpellstealLogs_.erase(it); + return false; + } + } + + if (recentSpellstealLogs_.size() >= MAX_RECENT_SPELLSTEAL_LOGS) + recentSpellstealLogs_.pop_front(); + recentSpellstealLogs_.push_back({casterGuid, victimGuid, spellId, now}); + return true; } void GameHandler::updateCombatText(float deltaTime) { @@ -12127,7 +15426,9 @@ void GameHandler::handleAttackStart(network::Packet& packet) { if (data.attackerGuid == playerGuid) { autoAttackRequested_ = true; autoAttacking = true; + autoAttackRetryPending_ = false; autoAttackTarget = data.victimGuid; + fireAddonEvent("PLAYER_ENTER_COMBAT", {}); } else if (data.victimGuid == playerGuid && data.attackerGuid != 0) { hostileAttackers_.insert(data.attackerGuid); autoTargetAttacker(data.attackerGuid); @@ -12164,13 +15465,9 @@ void GameHandler::handleAttackStop(network::Packet& packet) { // Keep intent, but clear server-confirmed active state until ATTACKSTART resumes. if (data.attackerGuid == playerGuid) { autoAttacking = false; + autoAttackRetryPending_ = autoAttackRequested_; + autoAttackResendTimer_ = 0.0f; LOG_DEBUG("SMSG_ATTACKSTOP received (keeping auto-attack intent)"); - if (autoAttackRequested_ && autoAttackTarget != 0 && socket) { - // Classic-family servers may emit transient ATTACKSTOP when range/facing jitters. - // Reassert melee intent immediately instead of waiting for periodic resend. - auto pkt = AttackSwingPacket::build(autoAttackTarget); - socket->send(pkt); - } } else if (data.victimGuid == playerGuid) { hostileAttackers_.erase(data.attackerGuid); } @@ -12218,9 +15515,9 @@ void GameHandler::dismount() { void GameHandler::handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage) { // WotLK: packed GUID; TBC/Classic: full uint64 - const bool fscTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool fscTbcLike = isPreWotlk(); uint64_t guid = fscTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + ? packet.readUInt64() : packet.readPackedGuid(); // uint32 counter uint32_t counter = packet.readUInt32(); @@ -12228,7 +15525,7 @@ void GameHandler::handleForceSpeedChange(network::Packet& packet, const char* na // 5 bytes remaining = uint8(1) + float(4) — standard 3.3.5a // 8 bytes remaining = uint32(4) + float(4) — some forks // 4 bytes remaining = float(4) — no unknown field - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining >= 8) { packet.readUInt32(); // unknown (extended format) } else if (remaining >= 5) { @@ -12251,7 +15548,7 @@ void GameHandler::handleForceSpeedChange(network::Packet& packet, const char* na if (legacyGuidAck) { ack.writeUInt64(playerGuid); } else { - MovementPacket::writePackedGuid(ack, playerGuid); + ack.writePackedGuid(playerGuid); } ack.writeUInt32(counter); @@ -12311,11 +15608,11 @@ void GameHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) // WotLK: packed GUID + uint32 counter + [optional unknown field(s)] // TBC/Classic: full uint64 + uint32 counter // We always ACK with current movement state, same pattern as speed-change ACKs. - const bool rootTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (rootTbc ? 8u : 2u)) return; + const bool rootTbc = isPreWotlk(); + if (!packet.hasRemaining(rootTbc ? 8u : 2u) ) return; uint64_t guid = rootTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) return; + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; uint32_t counter = packet.readUInt32(); LOG_INFO(rooted ? "SMSG_FORCE_MOVE_ROOT" : "SMSG_FORCE_MOVE_UNROOT", @@ -12341,7 +15638,7 @@ void GameHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) if (legacyGuidAck) { ack.writeUInt64(playerGuid); // CMaNGOS expects full GUID for root/unroot ACKs } else { - MovementPacket::writePackedGuid(ack, playerGuid); + ack.writePackedGuid(playerGuid); } ack.writeUInt32(counter); @@ -12371,11 +15668,11 @@ void GameHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) void GameHandler::handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set) { // WotLK: packed GUID; TBC/Classic: full uint64 - const bool fmfTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (fmfTbcLike ? 8u : 2u)) return; + const bool fmfTbcLike = isPreWotlk(); + if (!packet.hasRemaining(fmfTbcLike ? 8u : 2u) ) return; uint64_t guid = fmfTbcLike - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) return; + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; uint32_t counter = packet.readUInt32(); LOG_INFO("SMSG_FORCE_", name, ": guid=0x", std::hex, guid, std::dec, " counter=", counter); @@ -12401,7 +15698,7 @@ void GameHandler::handleForceMoveFlagChange(network::Packet& packet, const char* if (legacyGuidAck) { ack.writeUInt64(playerGuid); } else { - MovementPacket::writePackedGuid(ack, playerGuid); + ack.writePackedGuid(playerGuid); } ack.writeUInt32(counter); @@ -12431,10 +15728,10 @@ void GameHandler::handleForceMoveFlagChange(network::Packet& packet, const char* void GameHandler::handleMoveSetCollisionHeight(network::Packet& packet) { // SMSG_MOVE_SET_COLLISION_HGT: packed guid + counter + float (height) // ACK: CMSG_MOVE_SET_COLLISION_HGT_ACK = packed guid + counter + movement block + float (height) - const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (legacyGuid ? 8u : 2u)) return; - uint64_t guid = legacyGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) return; // counter(4) + height(4) + const bool legacyGuid = isPreWotlk(); + if (!packet.hasRemaining(legacyGuid ? 8u : 2u) ) return; + uint64_t guid = legacyGuid ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) return; // counter(4) + height(4) uint32_t counter = packet.readUInt32(); float height = packet.readFloat(); @@ -12452,7 +15749,7 @@ void GameHandler::handleMoveSetCollisionHeight(network::Packet& packet) { if (legacyGuidAck) { ack.writeUInt64(playerGuid); } else { - MovementPacket::writePackedGuid(ack, playerGuid); + ack.writePackedGuid(playerGuid); } ack.writeUInt32(counter); @@ -12471,11 +15768,11 @@ void GameHandler::handleMoveSetCollisionHeight(network::Packet& packet) { void GameHandler::handleMoveKnockBack(network::Packet& packet) { // WotLK: packed GUID; TBC/Classic: full uint64 - const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (mkbTbc ? 8u : 2u)) return; + const bool mkbTbc = isPreWotlk(); + if (!packet.hasRemaining(mkbTbc ? 8u : 2u) ) return; uint64_t guid = mkbTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 20) return; // counter(4) + vcos(4) + vsin(4) + hspeed(4) + vspeed(4) + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(20)) return; // counter(4) + vcos(4) + vsin(4) + hspeed(4) + vspeed(4) uint32_t counter = packet.readUInt32(); float vcos = packet.readFloat(); float vsin = packet.readFloat(); @@ -12504,7 +15801,7 @@ void GameHandler::handleMoveKnockBack(network::Packet& packet) { if (legacyGuidAck) { ack.writeUInt64(playerGuid); } else { - MovementPacket::writePackedGuid(ack, playerGuid); + ack.writePackedGuid(playerGuid); } ack.writeUInt32(counter); @@ -12546,7 +15843,7 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { // queueSlot(4) arenaType(1) unk(1) bgTypeId(4) unk2(2) instanceId(4) isRated(1) statusId(4) [status fields...] // STATUS_NONE sends only: queueSlot(4) arenaType(1) - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t queueSlot = packet.readUInt32(); const bool classicFormat = isClassicLikeExpansion(); @@ -12555,63 +15852,93 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { if (!classicFormat) { // TBC/WotLK: arenaType(1) + unk(1) before bgTypeId // STATUS_NONE sends only queueSlot + arenaType - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { LOG_INFO("Battlefield status: queue slot ", queueSlot, " cleared"); return; } arenaType = packet.readUInt8(); - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; packet.readUInt8(); // unk } else { // Classic STATUS_NONE sends only queueSlot + bgTypeId (4 bytes) - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_INFO("Battlefield status: queue slot ", queueSlot, " cleared"); return; } } - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t bgTypeId = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < 2) return; + if (!packet.hasRemaining(2)) return; uint16_t unk2 = packet.readUInt16(); (void)unk2; - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t clientInstanceId = packet.readUInt32(); (void)clientInstanceId; - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; uint8_t isRatedArena = packet.readUInt8(); (void)isRatedArena; - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t statusId = packet.readUInt32(); - std::string bgName = "Battleground #" + std::to_string(bgTypeId); + // Map BG type IDs to their names (stable across all three expansions) + // BattlemasterList.dbc IDs (3.3.5a) + static const std::pair kBgNames[] = { + {1, "Alterac Valley"}, + {2, "Warsong Gulch"}, + {3, "Arathi Basin"}, + {4, "Nagrand Arena"}, + {5, "Blade's Edge Arena"}, + {6, "All Arenas"}, + {7, "Eye of the Storm"}, + {8, "Ruins of Lordaeron"}, + {9, "Strand of the Ancients"}, + {10, "Dalaran Sewers"}, + {11, "Ring of Valor"}, + {30, "Isle of Conquest"}, + {32, "Random Battleground"}, + }; + std::string bgName = "Battleground"; + for (const auto& kv : kBgNames) { + if (kv.first == bgTypeId) { bgName = kv.second; break; } + } + if (bgName == "Battleground") + bgName = "Battleground #" + std::to_string(bgTypeId); if (arenaType > 0) { bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena"; + // If bgTypeId matches a named arena, prefer that name + for (const auto& kv : kBgNames) { + if (kv.first == bgTypeId) { + bgName += " (" + std::string(kv.second) + ")"; + break; + } + } } // Parse status-specific fields uint32_t inviteTimeout = 80; // default WoW BG invite window (seconds) + uint32_t avgWaitSec = 0, timeInQueueSec = 0; if (statusId == 1) { // STATUS_WAIT_QUEUE: avgWaitTime(4) + timeInQueue(4) - if (packet.getSize() - packet.getReadPos() >= 8) { - /*uint32_t avgWait =*/ packet.readUInt32(); - /*uint32_t inQueue =*/ packet.readUInt32(); + if (packet.hasRemaining(8)) { + avgWaitSec = packet.readUInt32() / 1000; // ms → seconds + timeInQueueSec = packet.readUInt32() / 1000; } } else if (statusId == 2) { // STATUS_WAIT_JOIN: timeout(4) + mapId(4) - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasRemaining(4)) { inviteTimeout = packet.readUInt32(); } - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasRemaining(4)) { /*uint32_t mapId =*/ packet.readUInt32(); } } else if (statusId == 3) { // STATUS_IN_PROGRESS: mapId(4) + timeSinceStart(4) - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { /*uint32_t mapId =*/ packet.readUInt32(); /*uint32_t elapsed =*/ packet.readUInt32(); } @@ -12624,6 +15951,11 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { bgQueues_[queueSlot].bgTypeId = bgTypeId; bgQueues_[queueSlot].arenaType = arenaType; bgQueues_[queueSlot].statusId = statusId; + bgQueues_[queueSlot].bgName = bgName; + if (statusId == 1) { + bgQueues_[queueSlot].avgWaitTimeSec = avgWaitSec; + bgQueues_[queueSlot].timeInQueueSec = timeInQueueSec; + } if (statusId == 2 && !wasInvite) { bgQueues_[queueSlot].inviteTimeout = inviteTimeout; bgQueues_[queueSlot].inviteReceivedTime = std::chrono::steady_clock::now(); @@ -12655,6 +15987,81 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { LOG_INFO("Battlefield status: unknown (", statusId, ") for ", bgName); break; } + fireAddonEvent("UPDATE_BATTLEFIELD_STATUS", {std::to_string(statusId)}); +} + +void GameHandler::handleBattlefieldList(network::Packet& packet) { + // SMSG_BATTLEFIELD_LIST wire format by expansion: + // + // Classic 1.12 (vmangos/cmangos): + // bgTypeId(4) isRegistered(1) count(4) [instanceId(4)...] + // + // TBC 2.4.3: + // bgTypeId(4) isRegistered(1) isHoliday(1) count(4) [instanceId(4)...] + // + // WotLK 3.3.5a: + // bgTypeId(4) isRegistered(1) isHoliday(1) minLevel(4) maxLevel(4) count(4) [instanceId(4)...] + + if (!packet.hasRemaining(5)) return; + + AvailableBgInfo info; + info.bgTypeId = packet.readUInt32(); + info.isRegistered = packet.readUInt8() != 0; + + const bool isWotlk = isActiveExpansion("wotlk"); + const bool isTbc = isActiveExpansion("tbc"); + + if (isTbc || isWotlk) { + if (!packet.hasRemaining(1)) return; + info.isHoliday = packet.readUInt8() != 0; + } + + if (isWotlk) { + if (!packet.hasRemaining(8)) return; + info.minLevel = packet.readUInt32(); + info.maxLevel = packet.readUInt32(); + } + + if (!packet.hasRemaining(4)) return; + uint32_t count = packet.readUInt32(); + + // Sanity cap to avoid OOM from malformed packets + constexpr uint32_t kMaxInstances = 256; + count = std::min(count, kMaxInstances); + info.instanceIds.reserve(count); + + for (uint32_t i = 0; i < count; ++i) { + if (!packet.hasRemaining(4)) break; + info.instanceIds.push_back(packet.readUInt32()); + } + + // Update or append the entry for this BG type + bool updated = false; + for (auto& existing : availableBgs_) { + if (existing.bgTypeId == info.bgTypeId) { + existing = std::move(info); + updated = true; + break; + } + } + if (!updated) { + availableBgs_.push_back(std::move(info)); + } + + const auto& stored = availableBgs_.back(); + static const std::unordered_map kBgNames = { + {1, "Alterac Valley"}, {2, "Warsong Gulch"}, {3, "Arathi Basin"}, + {4, "Nagrand Arena"}, {5, "Blade's Edge Arena"}, {6, "All Arenas"}, + {7, "Eye of the Storm"}, {8, "Ruins of Lordaeron"}, + {9, "Strand of the Ancients"}, {10, "Dalaran Sewers"}, + {11, "The Ring of Valor"}, {30, "Isle of Conquest"}, + }; + auto nameIt = kBgNames.find(stored.bgTypeId); + const char* bgName = (nameIt != kBgNames.end()) ? nameIt->second : "Unknown Battleground"; + + LOG_INFO("SMSG_BATTLEFIELD_LIST: ", bgName, " bgType=", stored.bgTypeId, + " registered=", stored.isRegistered ? "yes" : "no", + " instances=", stored.instanceIds.size()); } void GameHandler::declineBattlefield(uint32_t queueSlot) { @@ -12748,7 +16155,7 @@ void GameHandler::handleRaidInstanceInfo(network::Packet& packet) { const bool isClassic = isClassicLikeExpansion(); const bool useTbcFormat = isTbc || isClassic; - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t count = packet.readUInt32(); instanceLockouts_.clear(); @@ -12756,7 +16163,7 @@ void GameHandler::handleRaidInstanceInfo(network::Packet& packet) { const size_t kEntrySize = useTbcFormat ? (4 + 4 + 4 + 1) : (4 + 4 + 8 + 1 + 1); for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < kEntrySize) break; + if (!packet.hasRemaining(kEntrySize)) break; InstanceLockout lo; lo.mapId = packet.readUInt32(); lo.difficulty = packet.readUInt32(); @@ -12779,8 +16186,9 @@ void GameHandler::handleRaidInstanceInfo(network::Packet& packet) { void GameHandler::handleInstanceDifficulty(network::Packet& packet) { // SMSG_INSTANCE_DIFFICULTY: uint32 difficulty, uint32 heroic (8 bytes) // MSG_SET_DUNGEON_DIFFICULTY: uint32 difficulty[, uint32 isInGroup, uint32 savedBool] (4 or 12 bytes) - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 4) return; + uint32_t prevDifficulty = instanceDifficulty_; instanceDifficulty_ = packet.readUInt32(); if (rem() >= 4) { uint32_t secondField = packet.readUInt32(); @@ -12798,7 +16206,17 @@ void GameHandler::handleInstanceDifficulty(network::Packet& packet) { } else { instanceIsHeroic_ = (instanceDifficulty_ == 1); } + inInstance_ = true; LOG_INFO("Instance difficulty: ", instanceDifficulty_, " heroic=", instanceIsHeroic_); + + // Announce difficulty change to the player (only when it actually changes) + // difficulty values: 0=Normal, 1=Heroic, 2=25-Man Normal, 3=25-Man Heroic + if (instanceDifficulty_ != prevDifficulty) { + static const char* kDiffLabels[] = {"Normal", "Heroic", "25-Man Normal", "25-Man Heroic"}; + const char* diffLabel = (instanceDifficulty_ < 4) ? kDiffLabels[instanceDifficulty_] : nullptr; + if (diffLabel) + addSystemChatMessage(std::string("Dungeon difficulty set to ") + diffLabel + "."); + } } // --------------------------------------------------------------------------- @@ -12840,7 +16258,7 @@ static const char* lfgTeleportDeniedString(uint8_t reason) { } void GameHandler::handleLfgJoinResult(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 2) return; uint8_t result = packet.readUInt8(); @@ -12850,10 +16268,17 @@ void GameHandler::handleLfgJoinResult(network::Packet& packet) { // Success — state tells us what phase we're entering lfgState_ = static_cast(state); LOG_INFO("SMSG_LFG_JOIN_RESULT: success, state=", static_cast(state)); - addSystemChatMessage("Dungeon Finder: Joined the queue."); + { + std::string dName = getLfgDungeonName(lfgDungeonId_); + if (!dName.empty()) + addSystemChatMessage("Dungeon Finder: Joined the queue for " + dName + "."); + else + addSystemChatMessage("Dungeon Finder: Joined the queue."); + } } else { const char* msg = lfgJoinResultString(result); std::string errMsg = std::string("Dungeon Finder: ") + (msg ? msg : "Join failed."); + addUIError(errMsg); addSystemChatMessage(errMsg); LOG_INFO("SMSG_LFG_JOIN_RESULT: result=", static_cast(result), " state=", static_cast(state)); @@ -12861,7 +16286,7 @@ void GameHandler::handleLfgJoinResult(network::Packet& packet) { } void GameHandler::handleLfgQueueStatus(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 4 + 6 * 4 + 1 + 4) return; // dungeonId + 6 int32 + uint8 + uint32 lfgDungeonId_ = packet.readUInt32(); @@ -12881,7 +16306,7 @@ void GameHandler::handleLfgQueueStatus(network::Packet& packet) { } void GameHandler::handleLfgProposalUpdate(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 16) return; uint32_t dungeonId = packet.readUInt32(); @@ -12899,17 +16324,28 @@ void GameHandler::handleLfgProposalUpdate(network::Packet& packet) { case 0: lfgState_ = LfgState::Queued; lfgProposalId_ = 0; + addUIError("Dungeon Finder: Group proposal failed."); addSystemChatMessage("Dungeon Finder: Group proposal failed."); break; - case 1: + case 1: { lfgState_ = LfgState::InDungeon; lfgProposalId_ = 0; - addSystemChatMessage("Dungeon Finder: Group found! Entering dungeon..."); + std::string dName = getLfgDungeonName(dungeonId); + if (!dName.empty()) + addSystemChatMessage("Dungeon Finder: Group found for " + dName + "! Entering dungeon..."); + else + addSystemChatMessage("Dungeon Finder: Group found! Entering dungeon..."); break; - case 2: + } + case 2: { lfgState_ = LfgState::Proposal; - addSystemChatMessage("Dungeon Finder: A group has been found. Accept or decline."); + std::string dName = getLfgDungeonName(dungeonId); + if (!dName.empty()) + addSystemChatMessage("Dungeon Finder: A group has been found for " + dName + ". Accept or decline."); + else + addSystemChatMessage("Dungeon Finder: A group has been found. Accept or decline."); break; + } default: break; } @@ -12919,7 +16355,7 @@ void GameHandler::handleLfgProposalUpdate(network::Packet& packet) { } void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 6) return; /*uint32_t dungeonId =*/ packet.readUInt32(); @@ -12932,6 +16368,7 @@ void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) { LOG_INFO("LFG role check finished"); } else if (roleCheckState == 3) { lfgState_ = LfgState::None; + addUIError("Dungeon Finder: Role check failed — missing required role."); addSystemChatMessage("Dungeon Finder: Role check failed — missing required role."); } else if (roleCheckState == 2) { lfgState_ = LfgState::RoleCheck; @@ -12943,7 +16380,7 @@ void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) { void GameHandler::handleLfgUpdatePlayer(network::Packet& packet) { // SMSG_LFG_UPDATE_PLAYER and SMSG_LFG_UPDATE_PARTY share the same layout. - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 1) return; uint8_t updateType = packet.readUInt8(); @@ -12953,7 +16390,7 @@ void GameHandler::handleLfgUpdatePlayer(network::Packet& packet) { // 9=proposal_failed, 10=proposal_declined, 15=leave_queue, 17=member_offline, 18=group_disband bool hasExtra = (updateType != 0 && updateType != 1 && updateType != 15 && updateType != 17 && updateType != 18); - if (!hasExtra || packet.getSize() - packet.getReadPos() < 3) { + if (!hasExtra || !packet.hasRemaining(3)) { switch (updateType) { case 8: lfgState_ = LfgState::None; addSystemChatMessage("Dungeon Finder: Removed from queue."); break; @@ -12975,9 +16412,9 @@ void GameHandler::handleLfgUpdatePlayer(network::Packet& packet) { packet.readUInt8(); // unk1 packet.readUInt8(); // unk2 - if (packet.getSize() - packet.getReadPos() >= 1) { + if (packet.hasRemaining(1)) { uint8_t count = packet.readUInt8(); - for (uint8_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 4; ++i) { + for (uint8_t i = 0; i < count && packet.hasRemaining(4); ++i) { uint32_t dungeonEntry = packet.readUInt32(); if (i == 0) lfgDungeonId_ = dungeonEntry; } @@ -13000,8 +16437,7 @@ void GameHandler::handleLfgUpdatePlayer(network::Packet& packet) { } void GameHandler::handleLfgPlayerReward(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); - if (remaining < 4 + 4 + 1 + 4 + 4 + 4) return; + if (!packet.hasRemaining(4 + 4 + 1 + 4 + 4 + 4)) return; /*uint32_t randomDungeonEntry =*/ packet.readUInt32(); /*uint32_t dungeonEntry =*/ packet.readUInt32(); @@ -13024,14 +16460,20 @@ void GameHandler::handleLfgPlayerReward(network::Packet& packet) { std::string rewardMsg = std::string("Dungeon Finder reward: ") + moneyBuf + ", " + std::to_string(xp) + " XP"; - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasRemaining(4)) { uint32_t rewardCount = packet.readUInt32(); - for (uint32_t i = 0; i < rewardCount && packet.getSize() - packet.getReadPos() >= 9; ++i) { + for (uint32_t i = 0; i < rewardCount && packet.hasRemaining(9); ++i) { uint32_t itemId = packet.readUInt32(); uint32_t itemCount = packet.readUInt32(); packet.readUInt8(); // unk if (i == 0) { - rewardMsg += ", item #" + std::to_string(itemId); + std::string itemLabel = "item #" + std::to_string(itemId); + uint32_t lfgItemQuality = 1; + if (const ItemQueryResponseData* info = getItemInfo(itemId)) { + if (!info->name.empty()) itemLabel = info->name; + lfgItemQuality = info->quality; + } + rewardMsg += ", " + buildItemLink(itemId, lfgItemQuality, itemLabel); if (itemCount > 1) rewardMsg += " x" + std::to_string(itemCount); } } @@ -13043,31 +16485,38 @@ void GameHandler::handleLfgPlayerReward(network::Packet& packet) { } void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); - if (remaining < 7 + 4 + 4 + 4 + 4) return; + if (!packet.hasRemaining(7 + 4 + 4 + 4 + 4)) return; bool inProgress = packet.readUInt8() != 0; - bool myVote = packet.readUInt8() != 0; - bool myAnswer = packet.readUInt8() != 0; + /*bool myVote =*/ packet.readUInt8(); // whether local player has voted + /*bool myAnswer =*/ packet.readUInt8(); // local player's vote (yes/no) — unused; result derived from counts uint32_t totalVotes = packet.readUInt32(); uint32_t bootVotes = packet.readUInt32(); uint32_t timeLeft = packet.readUInt32(); uint32_t votesNeeded = packet.readUInt32(); - (void)myVote; - lfgBootVotes_ = bootVotes; lfgBootTotal_ = totalVotes; lfgBootTimeLeft_ = timeLeft; lfgBootNeeded_ = votesNeeded; + // Optional: reason string and target name (null-terminated) follow the fixed fields + if (packet.hasData()) + lfgBootReason_ = packet.readString(); + if (packet.hasData()) + lfgBootTargetName_ = packet.readString(); + if (inProgress) { lfgState_ = LfgState::Boot; } else { - // Boot vote ended — return to InDungeon state regardless of outcome + // Boot vote ended — pass/fail determined by whether enough yes votes were cast, + // not by the local player's own vote (myAnswer = what *I* voted, not the result). + const bool bootPassed = (bootVotes >= votesNeeded); lfgBootVotes_ = lfgBootTotal_ = lfgBootTimeLeft_ = lfgBootNeeded_ = 0; + lfgBootTargetName_.clear(); + lfgBootReason_.clear(); lfgState_ = LfgState::InDungeon; - if (myAnswer) { + if (bootPassed) { addSystemChatMessage("Dungeon Finder: Vote kick passed — member removed."); } else { addSystemChatMessage("Dungeon Finder: Vote kick failed."); @@ -13075,11 +16524,12 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { } LOG_INFO("SMSG_LFG_BOOT_PROPOSAL_UPDATE: inProgress=", inProgress, - " bootVotes=", bootVotes, "/", totalVotes); + " bootVotes=", bootVotes, "/", totalVotes, + " target=", lfgBootTargetName_, " reason=", lfgBootReason_); } void GameHandler::handleLfgTeleportDenied(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; uint8_t reason = packet.readUInt8(); const char* msg = lfgTeleportDeniedString(reason); addSystemChatMessage(std::string("Dungeon Finder: ") + msg); @@ -13091,7 +16541,7 @@ void GameHandler::handleLfgTeleportDenied(network::Packet& packet) { // --------------------------------------------------------------------------- void GameHandler::lfgJoin(uint32_t dungeonId, uint8_t roles) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_JOIN)); pkt.writeUInt8(roles); @@ -13119,6 +16569,17 @@ void GameHandler::lfgLeave() { LOG_INFO("Sent CMSG_LFG_LEAVE"); } +void GameHandler::lfgSetRoles(uint8_t roles) { + if (!isInWorld()) return; + const uint32_t wire = wireOpcode(Opcode::CMSG_LFG_SET_ROLES); + if (wire == 0xFFFF) return; + + network::Packet pkt(static_cast(wire)); + pkt.writeUInt8(roles); + socket->send(pkt); + LOG_INFO("Sent CMSG_LFG_SET_ROLES: roles=", static_cast(roles)); +} + void GameHandler::lfgAcceptProposal(uint32_t proposalId, bool accept) { if (!socket) return; @@ -13186,7 +16647,7 @@ void GameHandler::loadAreaTriggerDbc() { } void GameHandler::checkAreaTriggers() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; if (onTaxiFlight_ || taxiClientActive_) return; loadAreaTriggerDbc(); @@ -13276,7 +16737,7 @@ void GameHandler::checkAreaTriggers() { } void GameHandler::handleArenaTeamCommandResult(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; uint32_t command = packet.readUInt32(); std::string name = packet.readString(); uint32_t error = packet.readUInt32(); @@ -13295,10 +16756,92 @@ void GameHandler::handleArenaTeamCommandResult(network::Packet& packet) { } void GameHandler::handleArenaTeamQueryResponse(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t teamId = packet.readUInt32(); std::string teamName = packet.readString(); - LOG_INFO("Arena team query response: id=", teamId, " name=", teamName); + uint32_t teamType = 0; + if (packet.hasRemaining(4)) + teamType = packet.readUInt32(); + LOG_INFO("Arena team query response: id=", teamId, " name=", teamName, " type=", teamType); + + // Store name and type in matching ArenaTeamStats entry + for (auto& s : arenaTeamStats_) { + if (s.teamId == teamId) { + s.teamName = teamName; + s.teamType = teamType; + return; + } + } + // No stats entry yet — create a placeholder so we can show the name + ArenaTeamStats stub; + stub.teamId = teamId; + stub.teamName = teamName; + stub.teamType = teamType; + arenaTeamStats_.push_back(std::move(stub)); +} + +void GameHandler::handleArenaTeamRoster(network::Packet& packet) { + // SMSG_ARENA_TEAM_ROSTER (WotLK 3.3.5a): + // uint32 teamId + // uint8 unk (0 = not captainship packet) + // uint32 memberCount + // For each member: + // uint64 guid + // uint8 online (1=online, 0=offline) + // string name (null-terminated) + // uint32 gamesWeek + // uint32 winsWeek + // uint32 gamesSeason + // uint32 winsSeason + // uint32 personalRating + // float modDay (unused here) + // float modWeek (unused here) + if (!packet.hasRemaining(9)) return; + + uint32_t teamId = packet.readUInt32(); + /*uint8_t unk =*/ packet.readUInt8(); + uint32_t memberCount = packet.readUInt32(); + + // Sanity cap to avoid huge allocations from malformed packets + if (memberCount > 100) memberCount = 100; + + ArenaTeamRoster roster; + roster.teamId = teamId; + roster.members.reserve(memberCount); + + for (uint32_t i = 0; i < memberCount; ++i) { + if (!packet.hasRemaining(12)) break; + + ArenaTeamMember m; + m.guid = packet.readUInt64(); + m.online = (packet.readUInt8() != 0); + m.name = packet.readString(); + if (!packet.hasRemaining(20)) break; + m.weekGames = packet.readUInt32(); + m.weekWins = packet.readUInt32(); + m.seasonGames = packet.readUInt32(); + m.seasonWins = packet.readUInt32(); + m.personalRating = packet.readUInt32(); + // skip 2 floats (modDay, modWeek) + if (packet.hasRemaining(8)) { + packet.readFloat(); + packet.readFloat(); + } + roster.members.push_back(std::move(m)); + } + + // Replace existing roster for this team or append + for (auto& r : arenaTeamRosters_) { + if (r.teamId == teamId) { + r = std::move(roster); + LOG_INFO("SMSG_ARENA_TEAM_ROSTER: updated teamId=", teamId, + " members=", r.members.size()); + return; + } + } + LOG_INFO("SMSG_ARENA_TEAM_ROSTER: new teamId=", teamId, + " members=", roster.members.size()); + arenaTeamRosters_.push_back(std::move(roster)); } void GameHandler::handleArenaTeamInvite(network::Packet& packet) { @@ -13309,18 +16852,12 @@ void GameHandler::handleArenaTeamInvite(network::Packet& packet) { } void GameHandler::handleArenaTeamEvent(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; uint8_t event = packet.readUInt8(); - static const char* events[] = { - "joined", "left", "removed", "leader changed", - "disbanded", "created" - }; - std::string eventName = (event < 6) ? events[event] : "unknown event"; - // Read string params (up to 3) uint8_t strCount = 0; - if (packet.getSize() - packet.getReadPos() >= 1) { + if (packet.hasRemaining(1)) { strCount = packet.readUInt8(); } @@ -13328,18 +16865,52 @@ void GameHandler::handleArenaTeamEvent(network::Packet& packet) { if (strCount >= 1 && packet.getSize() > packet.getReadPos()) param1 = packet.readString(); if (strCount >= 2 && packet.getSize() > packet.getReadPos()) param2 = packet.readString(); - std::string msg = "Arena team " + eventName; - if (!param1.empty()) msg += ": " + param1; - if (!param2.empty()) msg += " (" + param2 + ")"; + // Build natural-language message based on event type + // Event params: 0=joined(name), 1=left(name), 2=removed(name,kicker), + // 3=leader_changed(new,old), 4=disbanded, 5=created(name) + std::string msg; + switch (event) { + case 0: // joined + msg = param1.empty() ? "A player has joined your arena team." + : param1 + " has joined your arena team."; + break; + case 1: // left + msg = param1.empty() ? "A player has left the arena team." + : param1 + " has left the arena team."; + break; + case 2: // removed + if (!param1.empty() && !param2.empty()) + msg = param1 + " has been removed from the arena team by " + param2 + "."; + else if (!param1.empty()) + msg = param1 + " has been removed from the arena team."; + else + msg = "A player has been removed from the arena team."; + break; + case 3: // leader changed + msg = param1.empty() ? "The arena team captain has changed." + : param1 + " is now the arena team captain."; + break; + case 4: // disbanded + msg = "Your arena team has been disbanded."; + break; + case 5: // created + msg = param1.empty() ? "Your arena team has been created." + : "Arena team \"" + param1 + "\" has been created."; + break; + default: + msg = "Arena team event " + std::to_string(event); + if (!param1.empty()) msg += ": " + param1; + break; + } addSystemChatMessage(msg); - LOG_INFO("Arena team event: ", eventName, " ", param1, " ", param2); + LOG_INFO("Arena team event: ", static_cast(event), " ", param1, " ", param2); } void GameHandler::handleArenaTeamStats(network::Packet& packet) { // SMSG_ARENA_TEAM_STATS (WotLK 3.3.5a): // uint32 teamId, uint32 rating, uint32 weekGames, uint32 weekWins, // uint32 seasonGames, uint32 seasonWins, uint32 rank - if (packet.getSize() - packet.getReadPos() < 28) return; + if (!packet.hasRemaining(28)) return; ArenaTeamStats stats; stats.teamId = packet.readUInt32(); @@ -13350,22 +16921,33 @@ void GameHandler::handleArenaTeamStats(network::Packet& packet) { stats.seasonWins = packet.readUInt32(); stats.rank = packet.readUInt32(); - // Update or insert for this team + // Update or insert for this team (preserve name/type from query response) for (auto& s : arenaTeamStats_) { if (s.teamId == stats.teamId) { - s = stats; - LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", stats.teamId, - " rating=", stats.rating, " rank=", stats.rank); + stats.teamName = std::move(s.teamName); + stats.teamType = s.teamType; + s = std::move(stats); + LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", s.teamId, + " rating=", s.rating, " rank=", s.rank); return; } } - arenaTeamStats_.push_back(stats); - LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", stats.teamId, - " rating=", stats.rating, " rank=", stats.rank); + arenaTeamStats_.push_back(std::move(stats)); + LOG_INFO("SMSG_ARENA_TEAM_STATS: teamId=", arenaTeamStats_.back().teamId, + " rating=", arenaTeamStats_.back().rating, + " rank=", arenaTeamStats_.back().rank); +} + +void GameHandler::requestArenaTeamRoster(uint32_t teamId) { + if (!socket) return; + network::Packet pkt(wireOpcode(Opcode::CMSG_ARENA_TEAM_ROSTER)); + pkt.writeUInt32(teamId); + socket->send(pkt); + LOG_INFO("Requesting arena team roster for teamId=", teamId); } void GameHandler::handleArenaError(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t error = packet.readUInt32(); std::string msg; @@ -13380,11 +16962,135 @@ void GameHandler::handleArenaError(network::Packet& packet) { LOG_INFO("Arena error: ", error, " - ", msg); } +void GameHandler::requestPvpLog() { + if (!isInWorld()) return; + // MSG_PVP_LOG_DATA is bidirectional: client sends an empty packet to request + network::Packet pkt(wireOpcode(Opcode::MSG_PVP_LOG_DATA)); + socket->send(pkt); + LOG_INFO("Requested PvP log data"); +} + +void GameHandler::handlePvpLogData(network::Packet& packet) { + auto remaining = [&]() { return packet.getRemainingSize(); }; + if (remaining() < 1) return; + + bgScoreboard_ = BgScoreboardData{}; + bgScoreboard_.isArena = (packet.readUInt8() != 0); + + if (bgScoreboard_.isArena) { + // WotLK 3.3.5a MSG_PVP_LOG_DATA arena header: + // two team blocks × (uint32 ratingChange + uint32 newRating + uint32 unk1 + uint32 unk2 + uint32 unk3 + CString teamName) + // After both team blocks: same player list and winner fields as battleground. + for (int t = 0; t < 2; ++t) { + if (remaining() < 20) { packet.skipAll(); return; } + bgScoreboard_.arenaTeams[t].ratingChange = packet.readUInt32(); + bgScoreboard_.arenaTeams[t].newRating = packet.readUInt32(); + packet.readUInt32(); // unk1 + packet.readUInt32(); // unk2 + packet.readUInt32(); // unk3 + bgScoreboard_.arenaTeams[t].teamName = remaining() > 0 ? packet.readString() : ""; + } + // Fall through to parse player list and winner fields below (same layout as BG) + } + + if (remaining() < 4) return; + uint32_t playerCount = packet.readUInt32(); + bgScoreboard_.players.reserve(playerCount); + + for (uint32_t i = 0; i < playerCount && remaining() >= 13; ++i) { + BgPlayerScore ps; + ps.guid = packet.readUInt64(); + ps.team = packet.readUInt8(); + ps.killingBlows = packet.readUInt32(); + ps.honorableKills = packet.readUInt32(); + ps.deaths = packet.readUInt32(); + ps.bonusHonor = packet.readUInt32(); + + // Resolve player name from entity manager + { + auto ent = entityManager.getEntity(ps.guid); + if (ent && (ent->getType() == game::ObjectType::PLAYER || + ent->getType() == game::ObjectType::UNIT)) { + auto u = std::static_pointer_cast(ent); + if (!u->getName().empty()) ps.name = u->getName(); + } + } + + // BG-specific stat blocks: uint32 count + N × (string fieldName + uint32 value) + if (remaining() < 4) { bgScoreboard_.players.push_back(std::move(ps)); break; } + uint32_t statCount = packet.readUInt32(); + for (uint32_t s = 0; s < statCount && remaining() >= 5; ++s) { + std::string fieldName; + while (remaining() > 0) { + char c = static_cast(packet.readUInt8()); + if (c == '\0') break; + fieldName += c; + } + uint32_t val = (remaining() >= 4) ? packet.readUInt32() : 0; + ps.bgStats.emplace_back(std::move(fieldName), val); + } + + bgScoreboard_.players.push_back(std::move(ps)); + } + + if (remaining() >= 1) { + bgScoreboard_.hasWinner = (packet.readUInt8() != 0); + if (bgScoreboard_.hasWinner && remaining() >= 1) + bgScoreboard_.winner = packet.readUInt8(); + } + + if (bgScoreboard_.isArena) { + LOG_INFO("Arena log: ", bgScoreboard_.players.size(), " players, hasWinner=", + bgScoreboard_.hasWinner, " winner=", static_cast(bgScoreboard_.winner), + " team0='", bgScoreboard_.arenaTeams[0].teamName, + "' ratingChange=", static_cast(bgScoreboard_.arenaTeams[0].ratingChange), + " team1='", bgScoreboard_.arenaTeams[1].teamName, + "' ratingChange=", static_cast(bgScoreboard_.arenaTeams[1].ratingChange)); + } else { + LOG_INFO("PvP log: ", bgScoreboard_.players.size(), " players, hasWinner=", + bgScoreboard_.hasWinner, " winner=", static_cast(bgScoreboard_.winner)); + } +} + +void GameHandler::handleMoveSetSpeed(network::Packet& packet) { + // MSG_MOVE_SET_*_SPEED: PackedGuid (WotLK) / full uint64 (Classic/TBC) + MovementInfo + float speed. + // The MovementInfo block is variable-length; rather than fully parsing it, we read the + // fixed prefix, skip over optional blocks by consuming remaining bytes until 4 remain, + // then read the speed float. This is safe because the speed is always the last field. + const bool useFull = isPreWotlk(); + uint64_t moverGuid = useFull + ? packet.readUInt64() : packet.readPackedGuid(); + + // Skip to the last 4 bytes — the speed float — by advancing past the MovementInfo. + // This avoids duplicating the full variable-length MovementInfo parser here. + const size_t remaining = packet.getRemainingSize(); + if (remaining < 4) return; + if (remaining > 4) { + // Advance past all MovementInfo bytes (flags, time, position, optional blocks). + // Speed is always the last 4 bytes in the packet. + packet.setReadPos(packet.getSize() - 4); + } + + float speed = packet.readFloat(); + if (!std::isfinite(speed) || speed <= 0.01f || speed > 200.0f) return; + + // Update local player speed state if this broadcast targets us. + if (moverGuid != playerGuid) return; + const uint16_t wireOp = packet.getOpcode(); + if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_RUN_SPEED)) serverRunSpeed_ = speed; + else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_RUN_BACK_SPEED)) serverRunBackSpeed_ = speed; + else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_WALK_SPEED)) serverWalkSpeed_ = speed; + else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_SWIM_SPEED)) serverSwimSpeed_ = speed; + else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED)) serverSwimBackSpeed_ = speed; + else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_FLIGHT_SPEED)) serverFlightSpeed_ = speed; + else if (wireOp == wireOpcode(Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED))serverFlightBackSpeed_= speed; +} + void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { // Server relays MSG_MOVE_* for other players: packed GUID (WotLK) or full uint64 (TBC/Classic) - const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool otherMoveTbc = isPreWotlk(); uint64_t moverGuid = otherMoveTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + ? packet.readUInt64() : packet.readPackedGuid(); if (moverGuid == playerGuid || moverGuid == 0) { return; // Skip our own echoes } @@ -13404,6 +17110,32 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { info.z = packet.readFloat(); info.orientation = packet.readFloat(); + // Read transport data if the on-transport flag is set in wire-format move flags. + // The flag bit position differs between expansions (0x200 for WotLK/TBC, 0x02000000 for Classic/Turtle). + const uint32_t wireTransportFlag = packetParsers_ ? packetParsers_->wireOnTransportFlag() : 0x00000200; + const bool onTransport = (info.flags & wireTransportFlag) != 0; + uint64_t transportGuid = 0; + float tLocalX = 0, tLocalY = 0, tLocalZ = 0, tLocalO = 0; + if (onTransport) { + transportGuid = packet.readPackedGuid(); + tLocalX = packet.readFloat(); + tLocalY = packet.readFloat(); + tLocalZ = packet.readFloat(); + tLocalO = packet.readFloat(); + // TBC and WotLK include a transport timestamp; Classic does not. + if (flags2Size >= 1) { + /*uint32_t transportTime =*/ packet.readUInt32(); + } + // WotLK adds a transport seat byte. + if (flags2Size >= 2) { + /*int8_t transportSeat =*/ packet.readUInt8(); + // Optional second transport time for interpolated movement. + if (info.flags2 & 0x0200) { + /*uint32_t transportTime2 =*/ packet.readUInt32(); + } + } + } + // Update entity position in entity manager auto entity = entityManager.getEntity(moverGuid); if (!entity) { @@ -13413,6 +17145,20 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { // Convert server coords to canonical glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(info.x, info.y, info.z)); float canYaw = core::coords::serverToCanonicalYaw(info.orientation); + + // Handle transport attachment: attach/detach the entity so it follows the transport + // smoothly between movement updates via updateAttachedTransportChildren(). + if (onTransport && transportGuid != 0 && transportManager_) { + glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(tLocalX, tLocalY, tLocalZ)); + setTransportAttachment(moverGuid, entity->getType(), transportGuid, localCanonical, true, + core::coords::serverToCanonicalYaw(tLocalO)); + // Derive world position from transport system for best accuracy. + glm::vec3 worldPos = transportManager_->getPlayerWorldPosition(transportGuid, localCanonical); + canonical = worldPos; + } else if (!onTransport) { + // Player left transport — clear any stale attachment. + clearTransportAttachment(moverGuid); + } // Compute a smoothed interpolation window for this player. // Using a raw packet delta causes jitter when timing spikes (e.g. 50ms then 300ms). // An exponential moving average of intervals gives a stable playback speed that @@ -13471,12 +17217,48 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { } void GameHandler::handleCompressedMoves(network::Packet& packet) { - // Vanilla/Classic SMSG_COMPRESSED_MOVES: raw concatenated sub-packets, NOT zlib. - // Evidence: observed 1-byte "00" packets which are not valid zlib streams. - // Each sub-packet: uint8 size (of opcode[2]+payload), uint16 opcode, uint8[] payload. - // size=0 → invalid/empty, signals end of batch. - const auto& data = packet.getData(); - size_t dataLen = data.size(); + // Vanilla-family SMSG_COMPRESSED_MOVES carries concatenated movement sub-packets. + // Turtle can additionally wrap the batch in the same uint32 decompressedSize + zlib + // envelope used by other compressed world packets. + // + // Within the decompressed stream, some realms encode the leading uint8 size as: + // - opcode(2) + payload bytes + // - payload bytes only + // Try both framing modes and use the one that cleanly consumes the batch. + std::vector decompressedStorage; + const std::vector* dataPtr = &packet.getData(); + + const auto& rawData = packet.getData(); + const bool hasCompressedWrapper = + rawData.size() >= 6 && + rawData[4] == 0x78 && + (rawData[5] == 0x01 || rawData[5] == 0x9C || + rawData[5] == 0xDA || rawData[5] == 0x5E); + if (hasCompressedWrapper) { + uint32_t decompressedSize = static_cast(rawData[0]) | + (static_cast(rawData[1]) << 8) | + (static_cast(rawData[2]) << 16) | + (static_cast(rawData[3]) << 24); + if (decompressedSize == 0 || decompressedSize > 65536) { + LOG_WARNING("SMSG_COMPRESSED_MOVES: bad decompressedSize=", decompressedSize); + return; + } + + decompressedStorage.resize(decompressedSize); + uLongf destLen = decompressedSize; + int ret = uncompress(decompressedStorage.data(), &destLen, + rawData.data() + 4, rawData.size() - 4); + if (ret != Z_OK) { + LOG_WARNING("SMSG_COMPRESSED_MOVES: zlib error ", ret); + return; + } + + decompressedStorage.resize(destLen); + dataPtr = &decompressedStorage; + } + + const auto& data = *dataPtr; + const size_t dataLen = data.size(); // Wire opcodes for sub-packet routing uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE); @@ -13516,47 +17298,136 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) { wireOpcode(Opcode::MSG_MOVE_UNROOT), }; + struct CompressedMoveSubPacket { + uint16_t opcode = 0; + std::vector payload; + }; + struct DecodeResult { + bool ok = false; + bool overrun = false; + bool usedPayloadOnlySize = false; + size_t endPos = 0; + size_t recognizedCount = 0; + size_t subPacketCount = 0; + std::vector packets; + }; + + auto isRecognizedSubOpcode = [&](uint16_t subOpcode) { + return subOpcode == monsterMoveWire || + subOpcode == monsterMoveTransportWire || + std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), subOpcode) != kMoveOpcodes.end(); + }; + + auto decodeSubPackets = [&](bool payloadOnlySize) -> DecodeResult { + DecodeResult result; + result.usedPayloadOnlySize = payloadOnlySize; + size_t pos = 0; + while (pos < dataLen) { + if (pos + 1 > dataLen) break; + uint8_t subSize = data[pos]; + if (subSize == 0) { + result.ok = true; + result.endPos = pos + 1; + return result; + } + + const size_t payloadLen = payloadOnlySize + ? static_cast(subSize) + : (subSize >= 2 ? static_cast(subSize) - 2 : 0); + if (!payloadOnlySize && subSize < 2) { + result.endPos = pos; + return result; + } + + const size_t packetLen = 1 + 2 + payloadLen; + if (pos + packetLen > dataLen) { + result.overrun = true; + result.endPos = pos; + return result; + } + + uint16_t subOpcode = static_cast(data[pos + 1]) | + (static_cast(data[pos + 2]) << 8); + size_t payloadStart = pos + 3; + + CompressedMoveSubPacket subPacket; + subPacket.opcode = subOpcode; + subPacket.payload.assign(data.begin() + payloadStart, + data.begin() + payloadStart + payloadLen); + result.packets.push_back(std::move(subPacket)); + ++result.subPacketCount; + if (isRecognizedSubOpcode(subOpcode)) { + ++result.recognizedCount; + } + + pos += packetLen; + } + result.ok = (result.endPos == 0 || result.endPos == dataLen); + result.endPos = dataLen; + return result; + }; + + DecodeResult decoded = decodeSubPackets(false); + if (!decoded.ok || decoded.overrun) { + DecodeResult payloadOnlyDecoded = decodeSubPackets(true); + const bool preferPayloadOnly = + payloadOnlyDecoded.ok && + (!decoded.ok || decoded.overrun || payloadOnlyDecoded.recognizedCount > decoded.recognizedCount); + if (preferPayloadOnly) { + decoded = std::move(payloadOnlyDecoded); + static uint32_t payloadOnlyFallbackCount = 0; + ++payloadOnlyFallbackCount; + if (payloadOnlyFallbackCount <= 10 || (payloadOnlyFallbackCount % 100) == 0) { + LOG_WARNING("SMSG_COMPRESSED_MOVES decoded via payload-only size fallback", + " (occurrence=", payloadOnlyFallbackCount, ")"); + } + } + } + + if (!decoded.ok || decoded.overrun) { + LOG_WARNING("SMSG_COMPRESSED_MOVES: sub-packet overruns buffer at pos=", decoded.endPos); + return; + } + // Track unhandled sub-opcodes once per compressed packet (avoid log spam) std::unordered_set unhandledSeen; - size_t pos = 0; - while (pos < dataLen) { - if (pos + 1 > dataLen) break; - uint8_t subSize = data[pos]; - if (subSize < 2) break; // size=0 or 1 → empty/end-of-batch sentinel - if (pos + 1 + subSize > dataLen) { - LOG_WARNING("SMSG_COMPRESSED_MOVES: sub-packet overruns buffer at pos=", pos); - break; - } - uint16_t subOpcode = static_cast(data[pos + 1]) | - (static_cast(data[pos + 2]) << 8); - size_t payloadLen = subSize - 2; - size_t payloadStart = pos + 3; + for (const auto& entry : decoded.packets) { + network::Packet subPacket(entry.opcode, entry.payload); - std::vector subPayload(data.begin() + payloadStart, - data.begin() + payloadStart + payloadLen); - network::Packet subPacket(subOpcode, subPayload); - - if (subOpcode == monsterMoveWire) { + if (entry.opcode == monsterMoveWire) { handleMonsterMove(subPacket); - } else if (subOpcode == monsterMoveTransportWire) { + } else if (entry.opcode == monsterMoveTransportWire) { handleMonsterMoveTransport(subPacket); } else if (state == WorldState::IN_WORLD && - std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), subOpcode) != kMoveOpcodes.end()) { + std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), entry.opcode) != kMoveOpcodes.end()) { // Player/NPC movement update packed in SMSG_MULTIPLE_MOVES handleOtherPlayerMovement(subPacket); } else { - if (unhandledSeen.insert(subOpcode).second) { + if (unhandledSeen.insert(entry.opcode).second) { LOG_INFO("SMSG_COMPRESSED_MOVES: unhandled sub-opcode 0x", - std::hex, subOpcode, std::dec, " payloadLen=", payloadLen); + std::hex, entry.opcode, std::dec, " payloadLen=", entry.payload.size()); } } - - pos = payloadStart + payloadLen; } } void GameHandler::handleMonsterMove(network::Packet& packet) { + if (isActiveExpansion("classic") || isActiveExpansion("turtle")) { + constexpr uint32_t kMaxMonsterMovesPerTick = 256; + ++monsterMovePacketsThisTick_; + if (monsterMovePacketsThisTick_ > kMaxMonsterMovesPerTick) { + ++monsterMovePacketsDroppedThisTick_; + if (monsterMovePacketsDroppedThisTick_ <= 3 || + (monsterMovePacketsDroppedThisTick_ % 100) == 0) { + LOG_WARNING("SMSG_MONSTER_MOVE: per-tick cap exceeded, dropping packet", + " (processed=", monsterMovePacketsThisTick_, + " dropped=", monsterMovePacketsDroppedThisTick_, ")"); + } + return; + } + } + MonsterMoveData data; auto logMonsterMoveParseFailure = [&](const std::string& msg) { static uint32_t failCount = 0; @@ -13565,6 +17436,14 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { LOG_WARNING(msg, " (occurrence=", failCount, ")"); } }; + auto logWrappedUncompressedFallbackUsed = [&]() { + static uint32_t wrappedUncompressedFallbackCount = 0; + ++wrappedUncompressedFallbackCount; + if (wrappedUncompressedFallbackCount <= 10 || (wrappedUncompressedFallbackCount % 100) == 0) { + LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback", + " (occurrence=", wrappedUncompressedFallbackCount, ")"); + } + }; auto stripWrappedSubpacket = [&](const std::vector& bytes, std::vector& stripped) -> bool { if (bytes.size() < 3) return false; uint8_t subSize = bytes[0]; @@ -13606,22 +17485,30 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { std::vector stripped; bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped); - // Try unwrapped payload first (common form), then wrapped-subpacket fallback. - network::Packet decompPacket(packet.getOpcode(), decompressed); - if (!packetParsers_->parseMonsterMove(decompPacket, data)) { - if (!hasWrappedForm) { - logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " + - std::to_string(destLen) + " bytes)"); - return; - } + bool parsed = false; + if (hasWrappedForm) { network::Packet wrappedPacket(packet.getOpcode(), stripped); - if (!packetParsers_->parseMonsterMove(wrappedPacket, data)) { + if (packetParsers_->parseMonsterMove(wrappedPacket, data)) { + parsed = true; + } + } + if (!parsed) { + network::Packet decompPacket(packet.getOpcode(), decompressed); + if (packetParsers_->parseMonsterMove(decompPacket, data)) { + parsed = true; + } + } + + if (!parsed) { + if (hasWrappedForm) { logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " + std::to_string(destLen) + " bytes, wrapped payload " + std::to_string(stripped.size()) + " bytes)"); - return; + } else { + logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " + + std::to_string(destLen) + " bytes)"); } - LOG_WARNING("SMSG_MONSTER_MOVE parsed via wrapped-subpacket fallback"); + return; } } else if (!packetParsers_->parseMonsterMove(packet, data)) { // Some realms occasionally embed an extra [size|opcode] wrapper even when the @@ -13630,7 +17517,7 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { if (stripWrappedSubpacket(rawData, stripped)) { network::Packet wrappedPacket(packet.getOpcode(), stripped); if (packetParsers_->parseMonsterMove(wrappedPacket, data)) { - LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback"); + logWrappedUncompressedFallbackUsed(); } else { logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE"); return; @@ -13721,6 +17608,27 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { creatureMoveCallback_(data.guid, posCanonical.x, posCanonical.y, posCanonical.z, 0); } + } else if (data.moveType == 4) { + // FacingAngle without movement — rotate NPC in place + float orientation = core::coords::serverToCanonicalYaw(data.facingAngle); + glm::vec3 posCanonical = core::coords::serverToCanonical( + glm::vec3(data.x, data.y, data.z)); + entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z, orientation); + if (creatureMoveCallback_) { + creatureMoveCallback_(data.guid, + posCanonical.x, posCanonical.y, posCanonical.z, 0); + } + } else if (data.moveType == 3 && data.facingTarget != 0) { + // FacingTarget without movement — rotate NPC to face a target + auto target = entityManager.getEntity(data.facingTarget); + if (target) { + float dx = target->getX() - entity->getX(); + float dy = target->getY() - entity->getY(); + if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { + float orientation = std::atan2(-dy, dx); + entity->setOrientation(orientation); + } + } } } @@ -13728,7 +17636,7 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) { // Parse transport-relative creature movement (NPCs on boats/zeppelins) // Packet: moverGuid(8) + unk(1) + transportGuid(8) + localX/Y/Z(12) + spline data - if (packet.getSize() - packet.getReadPos() < 8 + 1 + 8 + 12) return; + if (!packet.hasRemaining(8) + 1 + 8 + 12) return; uint64_t moverGuid = packet.readUInt64(); /*uint8_t unk =*/ packet.readUInt8(); uint64_t transportGuid = packet.readUInt64(); @@ -13742,7 +17650,7 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) { if (!entity) return; // ---- Spline data (same format as SMSG_MONSTER_MOVE, transport-local coords) ---- - if (packet.getReadPos() + 5 > packet.getSize()) { + if (!packet.hasRemaining(5)) { // No spline data — snap to start position if (transportManager_) { glm::vec3 localCanonical = core::coords::serverToCanonical(glm::vec3(localX, localY, localZ)); @@ -13774,12 +17682,12 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) { // Facing data based on moveType float facingAngle = entity->getOrientation(); if (moveType == 2) { // FacingSpot - if (packet.getReadPos() + 12 > packet.getSize()) return; + if (!packet.hasRemaining(12)) return; float sx = packet.readFloat(), sy = packet.readFloat(), sz = packet.readFloat(); facingAngle = std::atan2(-(sy - localY), sx - localX); (void)sz; } else if (moveType == 3) { // FacingTarget - if (packet.getReadPos() + 8 > packet.getSize()) return; + if (!packet.hasRemaining(8)) return; uint64_t tgtGuid = packet.readUInt64(); if (auto tgt = entityManager.getEntity(tgtGuid)) { float dx = tgt->getX() - entity->getX(); @@ -13788,28 +17696,34 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) { facingAngle = std::atan2(-dy, dx); } } else if (moveType == 4) { // FacingAngle - if (packet.getReadPos() + 4 > packet.getSize()) return; + if (!packet.hasRemaining(4)) return; facingAngle = core::coords::serverToCanonicalYaw(packet.readFloat()); } - if (packet.getReadPos() + 4 > packet.getSize()) return; + if (!packet.hasRemaining(4)) return; uint32_t splineFlags = packet.readUInt32(); if (splineFlags & 0x00400000) { // Animation - if (packet.getReadPos() + 5 > packet.getSize()) return; + if (!packet.hasRemaining(5)) return; packet.readUInt8(); packet.readUInt32(); } - if (packet.getReadPos() + 4 > packet.getSize()) return; + if (!packet.hasRemaining(4)) return; uint32_t duration = packet.readUInt32(); if (splineFlags & 0x00000800) { // Parabolic - if (packet.getReadPos() + 8 > packet.getSize()) return; + if (!packet.hasRemaining(8)) return; packet.readFloat(); packet.readUInt32(); } - if (packet.getReadPos() + 4 > packet.getSize()) return; + if (!packet.hasRemaining(4)) return; uint32_t pointCount = packet.readUInt32(); + constexpr uint32_t kMaxTransportSplinePoints = 1000; + if (pointCount > kMaxTransportSplinePoints) { + LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: pointCount=", pointCount, + " clamped to ", kMaxTransportSplinePoints); + pointCount = kMaxTransportSplinePoints; + } // Read destination point (transport-local server coords) float destLocalX = localX, destLocalY = localY, destLocalZ = localZ; @@ -13818,17 +17732,17 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) { const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0; if (uncompressed) { for (uint32_t i = 0; i < pointCount - 1; ++i) { - if (packet.getReadPos() + 12 > packet.getSize()) break; + if (!packet.hasRemaining(12)) break; packet.readFloat(); packet.readFloat(); packet.readFloat(); } - if (packet.getReadPos() + 12 <= packet.getSize()) { + if (packet.hasRemaining(12)) { destLocalX = packet.readFloat(); destLocalY = packet.readFloat(); destLocalZ = packet.readFloat(); hasDest = true; } } else { - if (packet.getReadPos() + 12 <= packet.getSize()) { + if (packet.hasRemaining(12)) { destLocalX = packet.readFloat(); destLocalY = packet.readFloat(); destLocalZ = packet.readFloat(); @@ -13883,8 +17797,11 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { bool isPlayerTarget = (data.targetGuid == playerGuid); if (!isPlayerAttacker && !isPlayerTarget) return; // Not our combat - if (isPlayerAttacker && meleeSwingCallback_) { - meleeSwingCallback_(); + if (isPlayerAttacker) { + lastMeleeSwingMs_ = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + if (meleeSwingCallback_) meleeSwingCallback_(); } if (!isPlayerAttacker && npcSwingCallback_) { npcSwingCallback_(data.attackerGuid); @@ -13922,28 +17839,36 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { } if (data.isMiss()) { - addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker); + addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 1) { - addCombatText(CombatTextEntry::DODGE, 0, 0, isPlayerAttacker); + addCombatText(CombatTextEntry::DODGE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 2) { - addCombatText(CombatTextEntry::PARRY, 0, 0, isPlayerAttacker); + addCombatText(CombatTextEntry::PARRY, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 4) { // VICTIMSTATE_BLOCKS: show reduced damage and the blocked amount if (data.totalDamage > 0) - addCombatText(CombatTextEntry::MELEE_DAMAGE, data.totalDamage, 0, isPlayerAttacker); - addCombatText(CombatTextEntry::BLOCK, static_cast(data.blocked), 0, isPlayerAttacker); + addCombatText(CombatTextEntry::MELEE_DAMAGE, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); + addCombatText(CombatTextEntry::BLOCK, static_cast(data.blocked), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 5) { - // VICTIMSTATE_EVADE: NPC evaded (out of combat zone). Show as miss. - addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker); + // VICTIMSTATE_EVADE: NPC evaded (out of combat zone). + addCombatText(CombatTextEntry::EVADE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 6) { // VICTIMSTATE_IS_IMMUNE: Target is immune to this attack. - addCombatText(CombatTextEntry::IMMUNE, 0, 0, isPlayerAttacker); + addCombatText(CombatTextEntry::IMMUNE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 7) { // VICTIMSTATE_DEFLECT: Attack was deflected (e.g. shield slam reflect). - addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker); + addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else { - auto type = data.isCrit() ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::MELEE_DAMAGE; - addCombatText(type, data.totalDamage, 0, isPlayerAttacker); + CombatTextEntry::Type type; + if (data.isCrit()) + type = CombatTextEntry::CRIT_DAMAGE; + else if (data.isCrushing()) + type = CombatTextEntry::CRUSHING; + else if (data.isGlancing()) + type = CombatTextEntry::GLANCING; + else + type = CombatTextEntry::MELEE_DAMAGE; + addCombatText(type, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); // Show partial absorb/resist from sub-damage entries uint32_t totalAbsorbed = 0, totalResisted = 0; for (const auto& sub : data.subDamages) { @@ -13951,9 +17876,9 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { totalResisted += sub.resisted; } if (totalAbsorbed > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(totalAbsorbed), 0, isPlayerAttacker); + addCombatText(CombatTextEntry::ABSORB, static_cast(totalAbsorbed), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); if (totalResisted > 0) - addCombatText(CombatTextEntry::RESIST, static_cast(totalResisted), 0, isPlayerAttacker); + addCombatText(CombatTextEntry::RESIST, static_cast(totalResisted), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } (void)isPlayerTarget; @@ -13974,11 +17899,11 @@ void GameHandler::handleSpellDamageLog(network::Packet& packet) { auto type = data.isCrit ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::SPELL_DAMAGE; if (data.damage > 0) - addCombatText(type, static_cast(data.damage), data.spellId, isPlayerSource); + addCombatText(type, static_cast(data.damage), data.spellId, isPlayerSource, 0, data.attackerGuid, data.targetGuid); if (data.absorbed > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(data.absorbed), data.spellId, isPlayerSource); + addCombatText(CombatTextEntry::ABSORB, static_cast(data.absorbed), data.spellId, isPlayerSource, 0, data.attackerGuid, data.targetGuid); if (data.resisted > 0) - addCombatText(CombatTextEntry::RESIST, static_cast(data.resisted), data.spellId, isPlayerSource); + addCombatText(CombatTextEntry::RESIST, static_cast(data.resisted), data.spellId, isPlayerSource, 0, data.attackerGuid, data.targetGuid); } void GameHandler::handleSpellHealLog(network::Packet& packet) { @@ -13990,9 +17915,9 @@ void GameHandler::handleSpellHealLog(network::Packet& packet) { if (!isPlayerSource && !isPlayerTarget) return; // Not our combat auto type = data.isCrit ? CombatTextEntry::CRIT_HEAL : CombatTextEntry::HEAL; - addCombatText(type, static_cast(data.heal), data.spellId, isPlayerSource); + addCombatText(type, static_cast(data.heal), data.spellId, isPlayerSource, 0, data.casterGuid, data.targetGuid); if (data.absorbed > 0) - addCombatText(CombatTextEntry::ABSORB, static_cast(data.absorbed), data.spellId, isPlayerSource); + addCombatText(CombatTextEntry::ABSORB, static_cast(data.absorbed), data.spellId, isPlayerSource, 0, data.casterGuid, data.targetGuid); } // ============================================================ @@ -14013,7 +17938,7 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { return; } - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // Casting any spell while mounted → dismount instead if (isMounted()) { @@ -14021,7 +17946,17 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { return; } - if (casting) return; // Already casting + if (casting) { + // Spell queue: if we're within 400ms of the cast completing (and not channeling), + // store the spell so it fires automatically when the cast finishes. + if (!castIsChannel && castTimeRemaining > 0.0f && castTimeRemaining <= 0.4f) { + queuedSpellId_ = spellId; + queuedSpellTarget_ = targetGuid != 0 ? targetGuid : this->targetGuid; + LOG_INFO("Spell queue: queued spellId=", spellId, " (", castTimeRemaining * 1000.0f, + "ms remaining)"); + } + return; + } // Hearthstone: cast spell directly (server checks item in inventory) // Using CMSG_CAST_SPELL is more reliable than CMSG_USE_ITEM which @@ -14068,13 +18003,7 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { // Detected via physical school mask (1) from DBC cache — covers warrior, rogue, DK, paladin, // feral druid, and hunter melee abilities generically. { - loadSpellNameCache(); - bool isMeleeAbility = false; - auto cacheIt = spellNameCache_.find(spellId); - if (cacheIt != spellNameCache_.end() && cacheIt->second.schoolMask == 1) { - // Physical school and no cast time (instant) — treat as melee ability - isMeleeAbility = true; - } + bool isMeleeAbility = (getSpellSchoolMask(spellId) == 1); if (isMeleeAbility && target != 0) { auto entity = entityManager.getEntity(target); if (entity) { @@ -14099,32 +18028,78 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { : CastSpellPacket::build(spellId, target, ++castCount); socket->send(packet); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); + + // Fire UNIT_SPELLCAST_SENT for cast bar addons (fires on client intent, before server confirms) + if (addonEventCallback_) { + std::string targetName; + if (target != 0) targetName = lookupName(target); + fireAddonEvent("UNIT_SPELLCAST_SENT", {"player", targetName, std::to_string(spellId)}); + } + + // Optimistically start GCD immediately on cast, but do not restart it while + // already active (prevents timeout animation reset on repeated key presses). + if (!isGCDActive()) { + gcdTotal_ = 1.5f; + gcdStartedAt_ = std::chrono::steady_clock::now(); + } } void GameHandler::cancelCast() { if (!casting) return; // GameObject interaction cast is client-side timing only. if (pendingGameObjectInteractGuid_ == 0 && - state == WorldState::IN_WORLD && socket && + isInWorld() && currentCastSpellId != 0) { auto packet = CancelCastPacket::build(currentCastSpellId); socket->send(packet); } pendingGameObjectInteractGuid_ = 0; + lastInteractedGoGuid_ = 0; casting = false; castIsChannel = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; + // Cancel craft queue and spell queue when player manually cancels cast + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; + fireAddonEvent("UNIT_SPELLCAST_STOP", {"player"}); +} + +void GameHandler::startCraftQueue(uint32_t spellId, int count) { + craftQueueSpellId_ = spellId; + craftQueueRemaining_ = count; + // Cast the first one immediately + castSpell(spellId, 0); +} + +void GameHandler::cancelCraftQueue() { + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; } void GameHandler::cancelAura(uint32_t spellId) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = CancelAuraPacket::build(spellId); socket->send(packet); } +uint32_t GameHandler::getTempEnchantRemainingMs(uint32_t slot) const { + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + for (const auto& t : tempEnchantTimers_) { + if (t.slot == slot) { + return (t.expireMs > nowMs) + ? static_cast(t.expireMs - nowMs) : 0u; + } + } + return 0u; +} + void GameHandler::handlePetSpells(network::Packet& packet) { - const size_t remaining = packet.getSize() - packet.getReadPos(); + const size_t remaining = packet.getRemainingSize(); if (remaining < 8) { // Empty or undersized → pet cleared (dismissed / died) petGuid_ = 0; @@ -14132,6 +18107,7 @@ void GameHandler::handlePetSpells(network::Packet& packet) { petAutocastSpells_.clear(); memset(petActionSlots_, 0, sizeof(petActionSlots_)); LOG_INFO("SMSG_PET_SPELLS: pet cleared"); + fireAddonEvent("UNIT_PET", {"player"}); return; } @@ -14141,33 +18117,34 @@ void GameHandler::handlePetSpells(network::Packet& packet) { petAutocastSpells_.clear(); memset(petActionSlots_, 0, sizeof(petActionSlots_)); LOG_INFO("SMSG_PET_SPELLS: pet cleared (guid=0)"); + fireAddonEvent("UNIT_PET", {"player"}); return; } // uint16 duration (ms, 0 = permanent), uint16 timer (ms) - if (packet.getSize() - packet.getReadPos() < 4) goto done; + if (!packet.hasRemaining(4)) goto done; /*uint16_t dur =*/ packet.readUInt16(); /*uint16_t timer =*/ packet.readUInt16(); // uint8 reactState, uint8 commandState (packed order varies; WotLK: react first) - if (packet.getSize() - packet.getReadPos() < 2) goto done; + if (!packet.hasRemaining(2)) goto done; petReact_ = packet.readUInt8(); // 0=passive, 1=defensive, 2=aggressive petCommand_ = packet.readUInt8(); // 0=stay, 1=follow, 2=attack, 3=dismiss // 10 × uint32 action bar slots - if (packet.getSize() - packet.getReadPos() < PET_ACTION_BAR_SLOTS * 4u) goto done; + if (!packet.hasRemaining(PET_ACTION_BAR_SLOTS) * 4u) goto done; for (int i = 0; i < PET_ACTION_BAR_SLOTS; ++i) { petActionSlots_[i] = packet.readUInt32(); } // uint8 spell count, then per-spell: uint32 spellId, uint16 active flags - if (packet.getSize() - packet.getReadPos() < 1) goto done; + if (!packet.hasRemaining(1)) goto done; { uint8_t spellCount = packet.readUInt8(); petSpellList_.clear(); petAutocastSpells_.clear(); for (uint8_t i = 0; i < spellCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 6) break; + if (!packet.hasRemaining(6)) break; uint32_t spellId = packet.readUInt32(); uint16_t activeFlags = packet.readUInt16(); petSpellList_.push_back(spellId); @@ -14180,8 +18157,10 @@ void GameHandler::handlePetSpells(network::Packet& packet) { done: LOG_INFO("SMSG_PET_SPELLS: petGuid=0x", std::hex, petGuid_, std::dec, - " react=", (int)petReact_, " command=", (int)petCommand_, + " react=", static_cast(petReact_), " command=", static_cast(petCommand_), " spells=", petSpellList_.size()); + fireAddonEvent("UNIT_PET", {"player"}); + fireAddonEvent("PET_BAR_UPDATE", {}); } void GameHandler::sendPetAction(uint32_t action, uint64_t targetGuid) { @@ -14198,6 +18177,104 @@ void GameHandler::dismissPet() { socket->send(packet); } +void GameHandler::togglePetSpellAutocast(uint32_t spellId) { + if (petGuid_ == 0 || spellId == 0 || state != WorldState::IN_WORLD || !socket) return; + bool currentlyOn = petAutocastSpells_.count(spellId) != 0; + uint8_t newState = currentlyOn ? 0 : 1; + // CMSG_PET_SPELL_AUTOCAST: petGuid(8) + spellId(4) + state(1) + network::Packet pkt(wireOpcode(Opcode::CMSG_PET_SPELL_AUTOCAST)); + pkt.writeUInt64(petGuid_); + pkt.writeUInt32(spellId); + pkt.writeUInt8(newState); + socket->send(pkt); + // Optimistically update local state; server will confirm via SMSG_PET_SPELLS + if (newState) + petAutocastSpells_.insert(spellId); + else + petAutocastSpells_.erase(spellId); + LOG_DEBUG("togglePetSpellAutocast: spellId=", spellId, " autocast=", static_cast(newState)); +} + +void GameHandler::renamePet(const std::string& newName) { + if (petGuid_ == 0 || state != WorldState::IN_WORLD || !socket) return; + if (newName.empty() || newName.size() > 12) return; // Server enforces max 12 chars + auto packet = PetRenamePacket::build(petGuid_, newName, 0); + socket->send(packet); + LOG_INFO("Sent CMSG_PET_RENAME: petGuid=0x", std::hex, petGuid_, std::dec, " name='", newName, "'"); +} + +void GameHandler::requestStabledPetList() { + if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0) return; + auto pkt = ListStabledPetsPacket::build(stableMasterGuid_); + socket->send(pkt); + LOG_INFO("Sent MSG_LIST_STABLED_PETS to npc=0x", std::hex, stableMasterGuid_, std::dec); +} + +void GameHandler::stablePet(uint8_t slot) { + if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0) return; + if (petGuid_ == 0) { + addSystemChatMessage("You do not have an active pet to stable."); + return; + } + auto pkt = StablePetPacket::build(stableMasterGuid_, slot); + socket->send(pkt); + LOG_INFO("Sent CMSG_STABLE_PET: slot=", static_cast(slot)); +} + +void GameHandler::unstablePet(uint32_t petNumber) { + if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0 || petNumber == 0) return; + auto pkt = UnstablePetPacket::build(stableMasterGuid_, petNumber); + socket->send(pkt); + LOG_INFO("Sent CMSG_UNSTABLE_PET: petNumber=", petNumber); +} + +void GameHandler::handleListStabledPets(network::Packet& packet) { + // SMSG MSG_LIST_STABLED_PETS: + // uint64 stableMasterGuid + // uint8 petCount + // uint8 numSlots + // per pet: + // uint32 petNumber + // uint32 entry + // uint32 level + // string name (null-terminated) + // uint32 displayId + // uint8 isActive (1 = active/summoned, 0 = stabled) + constexpr size_t kMinHeader = 8 + 1 + 1; + if (!packet.hasRemaining(kMinHeader)) { + LOG_WARNING("MSG_LIST_STABLED_PETS: packet too short (", packet.getSize(), ")"); + return; + } + stableMasterGuid_ = packet.readUInt64(); + uint8_t petCount = packet.readUInt8(); + stableNumSlots_ = packet.readUInt8(); + + stabledPets_.clear(); + stabledPets_.reserve(petCount); + + for (uint8_t i = 0; i < petCount; ++i) { + if (!packet.hasRemaining(4) + 4 + 4) break; + StabledPet pet; + pet.petNumber = packet.readUInt32(); + pet.entry = packet.readUInt32(); + pet.level = packet.readUInt32(); + pet.name = packet.readString(); + if (!packet.hasRemaining(4) + 1) break; + pet.displayId = packet.readUInt32(); + pet.isActive = (packet.readUInt8() != 0); + stabledPets_.push_back(std::move(pet)); + } + + stableWindowOpen_ = true; + LOG_INFO("MSG_LIST_STABLED_PETS: stableMasterGuid=0x", std::hex, stableMasterGuid_, std::dec, + " petCount=", static_cast(petCount), " numSlots=", static_cast(stableNumSlots_)); + for (const auto& p : stabledPets_) { + LOG_DEBUG(" Pet: number=", p.petNumber, " entry=", p.entry, + " level=", p.level, " name='", p.name, "' displayId=", p.displayId, + " active=", p.isActive); + } +} + void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id) { if (slot < 0 || slot >= ACTION_BAR_SLOTS) return; actionBar[slot].type = type; @@ -14207,6 +18284,19 @@ void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t queryItemInfo(id, 0); } saveCharacterConfig(); + // Notify Lua addons that the action bar changed + fireAddonEvent("ACTIONBAR_SLOT_CHANGED", {std::to_string(slot + 1)}); + fireAddonEvent("ACTIONBAR_UPDATE_STATE", {}); + // Notify the server so the action bar persists across relogs. + if (isInWorld()) { + const bool classic = isClassicLikeExpansion(); + auto pkt = SetActionButtonPacket::build( + static_cast(slot), + static_cast(type), + id, + classic); + socket->send(pkt); + } } float GameHandler::getSpellCooldown(uint32_t spellId) const { @@ -14227,10 +18317,12 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { knownSpells.insert(6603u); knownSpells.insert(8690u); - // Set initial cooldowns + // Set initial cooldowns — use the longer of individual vs category cooldown. + // Spells like potions have cooldownMs=0 but categoryCooldownMs=120000. for (const auto& cd : data.cooldowns) { - if (cd.cooldownMs > 0) { - spellCooldowns[cd.spellId] = cd.cooldownMs / 1000.0f; + uint32_t effectiveMs = std::max(cd.cooldownMs, cd.categoryCooldownMs); + if (effectiveMs > 0) { + spellCooldowns[cd.spellId] = effectiveMs / 1000.0f; } } @@ -14241,7 +18333,42 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { actionBar[11].id = 8690; // Hearthstone loadCharacterConfig(); + // Sync login-time cooldowns into action bar slot overlays. Without this, spells + // that are still on cooldown when the player logs in show no cooldown timer on the + // action bar even though spellCooldowns has the right remaining time. + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id != 0) { + auto it = spellCooldowns.find(slot.id); + if (it != spellCooldowns.end() && it->second > 0.0f) { + slot.cooldownTotal = it->second; + slot.cooldownRemaining = it->second; + } + } else if (slot.type == ActionBarSlot::ITEM && slot.id != 0) { + const auto* qi = getItemInfo(slot.id); + if (qi && qi->valid) { + for (const auto& sp : qi->spells) { + if (sp.spellId == 0) continue; + auto it = spellCooldowns.find(sp.spellId); + if (it != spellCooldowns.end() && it->second > 0.0f) { + slot.cooldownTotal = it->second; + slot.cooldownRemaining = it->second; + break; + } + } + } + } + } + + // Pre-load skill line DBCs so isProfessionSpell() works immediately + // (not just after opening a trainer window) + loadSkillLineDbc(); + loadSkillLineAbilityDbc(); + LOG_INFO("Learned ", knownSpells.size(), " spells"); + + // Notify addons that the full spell list is now available + fireAddonEvent("SPELLS_CHANGED", {}); + fireAddonEvent("LEARNED_SPELL_IN_TAB", {}); } void GameHandler::handleCastFailed(network::Packet& packet) { @@ -14254,30 +18381,38 @@ void GameHandler::handleCastFailed(network::Packet& packet) { castIsChannel = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; + lastInteractedGoGuid_ = 0; + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; // Stop precast sound — spell failed before completing - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ssm = renderer->getSpellSoundManager()) { - ssm->stopPrecast(); - } - } + withSoundManager(&rendering::Renderer::getSpellSoundManager, [](auto* ssm) { ssm->stopPrecast(); }); - // Add system message about failed cast with readable reason + // Show failure reason in the UIError overlay and in chat int powerType = -1; auto playerEntity = entityManager.getEntity(playerGuid); if (auto playerUnit = std::dynamic_pointer_cast(playerEntity)) { powerType = playerUnit->getPowerType(); } const char* reason = getSpellCastResultString(data.result, powerType); + std::string errMsg = reason ? reason + : ("Spell cast failed (error " + std::to_string(data.result) + ")"); + addUIError(errMsg); MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; - if (reason) { - msg.message = reason; - } else { - msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")"; - } + msg.message = errMsg; addLocalChatMessage(msg); + + // Play error sound for cast failure feedback + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playError(); }); + + // Fire UNIT_SPELLCAST_FAILED + UNIT_SPELLCAST_STOP so Lua addons can react + fireAddonEvent("UNIT_SPELLCAST_FAILED", {"player", std::to_string(data.spellId)}); + fireAddonEvent("UNIT_SPELLCAST_STOP", {"player", std::to_string(data.spellId)}); + if (spellCastFailedCallback_) spellCastFailedCallback_(data.spellId); } static audio::SpellSoundManager::MagicSchool schoolMaskToMagicSchool(uint32_t mask) { @@ -14297,10 +18432,12 @@ void GameHandler::handleSpellStart(network::Packet& packet) { // Track cast bar for any non-player caster (target frame + boss frames) if (data.casterUnit != playerGuid && data.castTime > 0) { auto& s = unitCastStates_[data.casterUnit]; - s.casting = true; - s.spellId = data.spellId; - s.timeTotal = data.castTime / 1000.0f; - s.timeRemaining = s.timeTotal; + s.casting = true; + s.isChannel = false; + s.spellId = data.spellId; + s.timeTotal = data.castTime / 1000.0f; + s.timeRemaining = s.timeTotal; + s.interruptible = isSpellInterruptible(data.spellId); // Trigger cast animation on the casting unit if (spellCastAnimCallback_) { spellCastAnimCallback_(data.casterUnit, true, false); @@ -14309,21 +18446,29 @@ void GameHandler::handleSpellStart(network::Packet& packet) { // If this is the player's own cast, start cast bar if (data.casterUnit == playerGuid && data.castTime > 0) { + // CMSG_GAMEOBJ_USE was accepted — cancel pending USE retries so we don't + // re-send GAMEOBJ_USE mid-gather-cast and get SPELL_FAILED_BAD_TARGETS. + // Keep entries that only have sendLoot (no-cast chests that still need looting). + pendingGameObjectLootRetries_.erase( + std::remove_if(pendingGameObjectLootRetries_.begin(), pendingGameObjectLootRetries_.end(), + [](const PendingLootRetry&) { return true; /* cancel all retries once a gather cast starts */ }), + pendingGameObjectLootRetries_.end()); + casting = true; castIsChannel = false; currentCastSpellId = data.spellId; castTimeTotal = data.castTime / 1000.0f; castTimeRemaining = castTimeTotal; + fireAddonEvent("CURRENT_SPELL_CAST_CHANGED", {}); - // Play precast (channeling) sound with correct magic school - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ssm = renderer->getSpellSoundManager()) { - loadSpellNameCache(); - auto it = spellNameCache_.find(data.spellId); - auto school = (it != spellNameCache_.end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM); + // Play precast sound — skip profession/tradeskill spells (they use crafting + // animations/sounds, not magic spell audio). + if (!isProfessionSpell(data.spellId)) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + auto school = schoolMaskToMagicSchool(getSpellSchoolMask(data.spellId)); + ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM); + } } } @@ -14340,6 +18485,13 @@ void GameHandler::handleSpellStart(network::Packet& packet) { hearthstonePreloadCallback_(homeBindMapId_, homeBindPos_.x, homeBindPos_.y, homeBindPos_.z); } } + + // Fire UNIT_SPELLCAST_START for Lua addons + if (addonEventCallback_) { + auto unitId = guidToUnitId(data.casterUnit); + if (!unitId.empty()) + fireAddonEvent("UNIT_SPELLCAST_START", {unitId, std::to_string(data.spellId)}); + } } void GameHandler::handleSpellGo(network::Packet& packet) { @@ -14348,89 +18500,137 @@ void GameHandler::handleSpellGo(network::Packet& packet) { // Cast completed if (data.casterUnit == playerGuid) { - // Play cast-complete sound with correct magic school - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* ssm = renderer->getSpellSoundManager()) { - loadSpellNameCache(); - auto it = spellNameCache_.find(data.spellId); - auto school = (it != spellNameCache_.end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playCast(school); + // Play cast-complete sound — skip profession spells (no magic sound for crafting) + if (!isProfessionSpell(data.spellId)) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + ssm->playCast(schoolMaskToMagicSchool(getSpellSchoolMask(data.spellId))); + } } } // Instant melee abilities → trigger attack animation // Detect via physical school mask (1 = Physical) from the spell DBC cache. + // Skip profession spells — crafting should not swing weapons. // This covers warrior, rogue, DK, paladin, feral druid, and hunter melee // abilities generically instead of maintaining a brittle per-spell-ID list. uint32_t sid = data.spellId; bool isMeleeAbility = false; - { - loadSpellNameCache(); - auto cacheIt = spellNameCache_.find(sid); - if (cacheIt != spellNameCache_.end() && cacheIt->second.schoolMask == 1) { + if (!isProfessionSpell(sid)) { + if (getSpellSchoolMask(sid) == 1) { // Physical school — treat as instant melee ability if cast time is zero. // We don't store cast time in the cache; use the fact that if we were not // in a cast (casting == true with this spellId) then it was instant. isMeleeAbility = (currentCastSpellId != sid); } } - if (isMeleeAbility && meleeSwingCallback_) { - meleeSwingCallback_(); + if (isMeleeAbility) { + if (meleeSwingCallback_) meleeSwingCallback_(); + // Play weapon swing + impact sound for instant melee abilities (Sinister Strike, etc.) + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* csm = renderer->getCombatSoundManager()) { + csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false); + csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM, + audio::CombatSoundManager::ImpactType::FLESH, false); + } + } } + // Capture cast state before clearing. Guard with spellId match so that + // proc/triggered spells (which fire SMSG_SPELL_GO while a gather cast is + // still active and casting == true) do NOT trigger premature CMSG_LOOT. + // Only the spell that originally started the cast bar (currentCastSpellId) + // should count as "gather cast completed". + const bool wasInTimedCast = casting && (data.spellId == currentCastSpellId); + casting = false; castIsChannel = false; currentCastSpellId = 0; castTimeRemaining = 0.0f; + // If we were gathering a node (mining/herbalism), send CMSG_LOOT now that + // the gather cast completed and the server has made the node lootable. + // Guard with wasInTimedCast to avoid firing on instant casts / procs. + if (wasInTimedCast && lastInteractedGoGuid_ != 0) { + lootTarget(lastInteractedGoGuid_); + lastInteractedGoGuid_ = 0; + } + // End cast animation on player character if (spellCastAnimCallback_) { spellCastAnimCallback_(playerGuid, false, false); } - } else if (spellCastAnimCallback_) { - // End cast animation on other unit - spellCastAnimCallback_(data.casterUnit, false, false); + + // Fire UNIT_SPELLCAST_STOP — cast bar should disappear + fireAddonEvent("UNIT_SPELLCAST_STOP", {"player", std::to_string(data.spellId)}); + + // Spell queue: fire the next queued spell now that casting has ended + if (queuedSpellId_ != 0) { + uint32_t nextSpell = queuedSpellId_; + uint64_t nextTarget = queuedSpellTarget_; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; + LOG_INFO("Spell queue: firing queued spellId=", nextSpell); + castSpell(nextSpell, nextTarget); + } + } else { + if (spellCastAnimCallback_) { + // End cast animation on other unit + spellCastAnimCallback_(data.casterUnit, false, false); + } + // Play cast-complete sound for enemy spells targeting the player + bool targetsPlayer = false; + for (const auto& tgt : data.hitTargets) { + if (tgt == playerGuid) { targetsPlayer = true; break; } + } + if (targetsPlayer) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ssm = renderer->getSpellSoundManager()) { + ssm->playCast(schoolMaskToMagicSchool(getSpellSchoolMask(data.spellId))); + } + } + } } // Clear unit cast bar when the spell lands (for any tracked unit) unitCastStates_.erase(data.casterUnit); - // Show miss/dodge/parry/etc combat text when player's spells miss targets - if (data.casterUnit == playerGuid && !data.missTargets.empty()) { - static const CombatTextEntry::Type missTypes[] = { - CombatTextEntry::MISS, // 0=MISS - CombatTextEntry::DODGE, // 1=DODGE - CombatTextEntry::PARRY, // 2=PARRY - CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::MISS, // 4=EVADE - CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::MISS, // 6=DEFLECT - CombatTextEntry::ABSORB, // 7=ABSORB - CombatTextEntry::RESIST, // 8=RESIST - }; - // Show text for each miss (usually just 1 target per spell go) + // Preserve spellId and actual participants for spell-go miss results. + // This keeps the persistent combat log aligned with the later GUID fixes. + if (!data.missTargets.empty()) { + const uint64_t spellCasterGuid = data.casterUnit != 0 ? data.casterUnit : data.casterGuid; + const bool playerIsCaster = (spellCasterGuid == playerGuid); + for (const auto& m : data.missTargets) { - CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] : CombatTextEntry::MISS; - addCombatText(ct, 0, 0, true); + if (!playerIsCaster && m.targetGuid != playerGuid) { + continue; + } + CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(m.missType); + addCombatText(ct, 0, data.spellId, playerIsCaster, 0, spellCasterGuid, m.targetGuid); } } - // Play impact sound when player is hit by any spell (from self or others) + // Play impact sound for spell hits involving the player + // - When player is hit by an enemy spell + // - When player's spell hits an enemy target bool playerIsHit = false; + bool playerHitEnemy = false; for (const auto& tgt : data.hitTargets) { - if (tgt == playerGuid) { playerIsHit = true; break; } + if (tgt == playerGuid) { playerIsHit = true; } + if (data.casterUnit == playerGuid && tgt != playerGuid && tgt != 0) { playerHitEnemy = true; } } - if (playerIsHit && data.casterUnit != playerGuid) { + // Fire UNIT_SPELLCAST_SUCCEEDED for Lua addons + if (addonEventCallback_) { + auto unitId = guidToUnitId(data.casterUnit); + if (!unitId.empty()) + fireAddonEvent("UNIT_SPELLCAST_SUCCEEDED", {unitId, std::to_string(data.spellId)}); + } + + if (playerIsHit || playerHitEnemy) { if (auto* renderer = core::Application::getInstance().getRenderer()) { if (auto* ssm = renderer->getSpellSoundManager()) { - loadSpellNameCache(); - auto it = spellNameCache_.find(data.spellId); - auto school = (it != spellNameCache_.end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playImpact(school, audio::SpellSoundManager::SpellPower::MEDIUM); + ssm->playImpact(schoolMaskToMagicSchool(getSpellSchoolMask(data.spellId)), + audio::SpellSoundManager::SpellPower::MEDIUM); } } } @@ -14441,41 +18641,62 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) { // TBC 2.4.3 / WotLK 3.3.5a: guid(8) + flags(1) + N×[spellId(4) + cooldown(4)] — 8 bytes/entry const bool isClassicFormat = isClassicLikeExpansion(); - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; /*data.guid =*/ packet.readUInt64(); // guid (not used further) if (!isClassicFormat) { - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; /*data.flags =*/ packet.readUInt8(); // flags (consumed but not stored) } const size_t entrySize = isClassicFormat ? 12u : 8u; - while (packet.getSize() - packet.getReadPos() >= entrySize) { + while (packet.hasRemaining(entrySize)) { uint32_t spellId = packet.readUInt32(); uint32_t cdItemId = 0; if (isClassicFormat) cdItemId = packet.readUInt32(); // itemId in Classic format uint32_t cooldownMs = packet.readUInt32(); float seconds = cooldownMs / 1000.0f; - spellCooldowns[spellId] = seconds; + + // spellId=0 is the Global Cooldown marker (server sends it for GCD triggers) + if (spellId == 0 && cooldownMs > 0 && cooldownMs <= 2000) { + gcdTotal_ = seconds; + gcdStartedAt_ = std::chrono::steady_clock::now(); + continue; + } + + auto it = spellCooldowns.find(spellId); + if (it == spellCooldowns.end()) { + spellCooldowns[spellId] = seconds; + } else { + it->second = mergeCooldownSeconds(it->second, seconds); + } for (auto& slot : actionBar) { bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId) || (cdItemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == cdItemId); if (match) { - slot.cooldownTotal = seconds; - slot.cooldownRemaining = seconds; + float prevRemaining = slot.cooldownRemaining; + float merged = mergeCooldownSeconds(slot.cooldownRemaining, seconds); + slot.cooldownRemaining = merged; + if (slot.cooldownTotal <= 0.0f || prevRemaining <= 0.0f) { + slot.cooldownTotal = seconds; + } else { + slot.cooldownTotal = std::max(slot.cooldownTotal, merged); + } } } } LOG_DEBUG("handleSpellCooldown: parsed for ", isClassicFormat ? "Classic" : "TBC/WotLK", " format"); + fireAddonEvent("SPELL_UPDATE_COOLDOWN", {}); + fireAddonEvent("ACTIONBAR_UPDATE_COOLDOWN", {}); } void GameHandler::handleCooldownEvent(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t spellId = packet.readUInt32(); // WotLK appends the target unit guid (8 bytes) — skip it - if (packet.getSize() - packet.getReadPos() >= 8) + if (packet.hasRemaining(8)) packet.readUInt64(); // Cooldown finished spellCooldowns.erase(spellId); @@ -14484,6 +18705,8 @@ void GameHandler::handleCooldownEvent(network::Packet& packet) { slot.cooldownRemaining = 0.0f; } } + fireAddonEvent("SPELL_UPDATE_COOLDOWN", {}); + fireAddonEvent("ACTIONBAR_UPDATE_COOLDOWN", {}); } void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { @@ -14497,6 +18720,10 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { } else if (data.guid == targetGuid) { auraList = &targetAuras; } + // Also maintain a per-unit cache for any unit (party members, etc.) + if (data.guid != 0 && data.guid != playerGuid && data.guid != targetGuid) { + auraList = &unitAurasCache_[data.guid]; + } if (auraList) { if (isAll) { @@ -14517,6 +18744,13 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { (*auraList)[slot] = aura; } + // Fire UNIT_AURA event for Lua addons + if (addonEventCallback_) { + auto unitId = guidToUnitId(data.guid); + if (!unitId.empty()) + fireAddonEvent("UNIT_AURA", {unitId}); + } + // If player is mounted but we haven't identified the mount aura yet, // check newly added auras (aura update may arrive after mountDisplayId) if (data.guid == playerGuid && currentMountDisplayId_ != 0 && mountAuraSpellId_ == 0) { @@ -14534,31 +18768,53 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) { // Classic 1.12: uint16 spellId; TBC 2.4.3 / WotLK 3.3.5a: uint32 spellId const bool classicSpellId = isClassicLikeExpansion(); const size_t minSz = classicSpellId ? 2u : 4u; - if (packet.getSize() - packet.getReadPos() < minSz) return; + if (!packet.hasRemaining(minSz)) return; uint32_t spellId = classicSpellId ? packet.readUInt16() : packet.readUInt32(); + + // Track whether we already knew this spell before inserting. + // SMSG_TRAINER_BUY_SUCCEEDED pre-inserts the spell and shows its own "You have + // learned X" message, so when the accompanying SMSG_LEARNED_SPELL arrives we + // must not duplicate it. + const bool alreadyKnown = knownSpells.count(spellId) > 0; knownSpells.insert(spellId); - LOG_INFO("Learned spell: ", spellId); + LOG_INFO("Learned spell: ", spellId, alreadyKnown ? " (already known, skipping chat)" : ""); // Check if this spell corresponds to a talent rank + bool isTalentSpell = false; for (const auto& [talentId, talent] : talentCache_) { for (int rank = 0; rank < 5; ++rank) { if (talent.rankSpells[rank] == spellId) { // Found the talent! Update the rank for the active spec uint8_t newRank = rank + 1; // rank is 0-indexed in array, but stored as 1-indexed learnedTalents_[activeTalentSpec_][talentId] = newRank; - LOG_INFO("Talent learned: id=", talentId, " rank=", (int)newRank, - " (spell ", spellId, ") in spec ", (int)activeTalentSpec_); - return; + LOG_INFO("Talent learned: id=", talentId, " rank=", static_cast(newRank), + " (spell ", spellId, ") in spec ", static_cast(activeTalentSpec_)); + isTalentSpell = true; + fireAddonEvent("CHARACTER_POINTS_CHANGED", {}); + fireAddonEvent("PLAYER_TALENT_UPDATE", {}); + break; } } + if (isTalentSpell) break; } - // Show chat message for non-talent spells - const std::string& name = getSpellName(spellId); - if (!name.empty()) { - addSystemChatMessage("You have learned a new spell: " + name + "."); - } else { - addSystemChatMessage("You have learned a new spell."); + // Fire LEARNED_SPELL_IN_TAB / SPELLS_CHANGED for Lua addons + if (!alreadyKnown) { + fireAddonEvent("LEARNED_SPELL_IN_TAB", {std::to_string(spellId)}); + fireAddonEvent("SPELLS_CHANGED", {}); + } + + if (isTalentSpell) return; // talent spells don't show chat message + + // Show chat message for non-talent spells, but only if not already announced by + // SMSG_TRAINER_BUY_SUCCEEDED (which pre-inserts into knownSpells). + if (!alreadyKnown) { + const std::string& name = getSpellName(spellId); + if (!name.empty()) { + addSystemChatMessage("You have learned a new spell: " + name + "."); + } else { + addSystemChatMessage("You have learned a new spell."); + } } } @@ -14566,10 +18822,27 @@ void GameHandler::handleRemovedSpell(network::Packet& packet) { // Classic 1.12: uint16 spellId; TBC 2.4.3 / WotLK 3.3.5a: uint32 spellId const bool classicSpellId = isClassicLikeExpansion(); const size_t minSz = classicSpellId ? 2u : 4u; - if (packet.getSize() - packet.getReadPos() < minSz) return; + if (!packet.hasRemaining(minSz)) return; uint32_t spellId = classicSpellId ? packet.readUInt16() : packet.readUInt32(); knownSpells.erase(spellId); LOG_INFO("Removed spell: ", spellId); + fireAddonEvent("SPELLS_CHANGED", {}); + + const std::string& name = getSpellName(spellId); + if (!name.empty()) + addSystemChatMessage("You have unlearned: " + name + "."); + else + addSystemChatMessage("A spell has been removed."); + + // Clear any action bar slots referencing this spell + bool barChanged = false; + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) { + slot = ActionBarSlot{}; + barChanged = true; + } + } + if (barChanged) saveCharacterConfig(); } void GameHandler::handleSupercededSpell(network::Packet& packet) { @@ -14578,35 +18851,71 @@ void GameHandler::handleSupercededSpell(network::Packet& packet) { // TBC 2.4.3 / WotLK 3.3.5a: uint32 oldSpellId + uint32 newSpellId (8 bytes total) const bool classicSpellId = isClassicLikeExpansion(); const size_t minSz = classicSpellId ? 4u : 8u; - if (packet.getSize() - packet.getReadPos() < minSz) return; + if (!packet.hasRemaining(minSz)) return; uint32_t oldSpellId = classicSpellId ? packet.readUInt16() : packet.readUInt32(); uint32_t newSpellId = classicSpellId ? packet.readUInt16() : packet.readUInt32(); // Remove old spell knownSpells.erase(oldSpellId); + // Track whether the new spell was already announced via SMSG_TRAINER_BUY_SUCCEEDED. + // If it was pre-inserted there, that handler already showed "You have learned X" so + // we should skip the "Upgraded to X" message to avoid a duplicate. + const bool newSpellAlreadyAnnounced = knownSpells.count(newSpellId) > 0; + // Add new spell knownSpells.insert(newSpellId); LOG_INFO("Spell superceded: ", oldSpellId, " -> ", newSpellId); - const std::string& newName = getSpellName(newSpellId); - if (!newName.empty()) { - addSystemChatMessage("Upgraded to " + newName); + // Update all action bar slots that reference the old spell rank to the new rank. + // This matches the WoW client behaviour: the action bar automatically upgrades + // to the new rank when you train it. + bool barChanged = false; + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id == oldSpellId) { + slot.id = newSpellId; + slot.cooldownRemaining = 0.0f; + slot.cooldownTotal = 0.0f; + barChanged = true; + LOG_DEBUG("Action bar slot upgraded: spell ", oldSpellId, " -> ", newSpellId); + } + } + if (barChanged) { + saveCharacterConfig(); + fireAddonEvent("ACTIONBAR_SLOT_CHANGED", {}); + } + + // Show "Upgraded to X" only when the new spell wasn't already announced by the + // trainer-buy handler. For non-trainer supersedes (e.g. quest rewards), the new + // spell won't be pre-inserted so we still show the message. + if (!newSpellAlreadyAnnounced) { + const std::string& newName = getSpellName(newSpellId); + if (!newName.empty()) { + addSystemChatMessage("Upgraded to " + newName); + } } } void GameHandler::handleUnlearnSpells(network::Packet& packet) { // Sent when unlearning multiple spells (e.g., spec change, respec) - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t spellCount = packet.readUInt32(); LOG_INFO("Unlearning ", spellCount, " spells"); - for (uint32_t i = 0; i < spellCount && packet.getSize() - packet.getReadPos() >= 4; ++i) { + bool barChanged = false; + for (uint32_t i = 0; i < spellCount && packet.hasRemaining(4); ++i) { uint32_t spellId = packet.readUInt32(); knownSpells.erase(spellId); LOG_INFO(" Unlearned spell: ", spellId); + for (auto& slot : actionBar) { + if (slot.type == ActionBarSlot::SPELL && slot.id == spellId) { + slot = ActionBarSlot{}; + barChanged = true; + } + } } + if (barChanged) saveCharacterConfig(); if (spellCount > 0) { addSystemChatMessage("Unlearned " + std::to_string(spellCount) + " spells"); @@ -14618,50 +18927,78 @@ void GameHandler::handleUnlearnSpells(network::Packet& packet) { // ============================================================ void GameHandler::handleTalentsInfo(network::Packet& packet) { - TalentsInfoData data; - if (!TalentsInfoParser::parse(packet, data)) return; + // SMSG_TALENTS_INFO (WotLK 3.3.5a) correct wire format: + // uint8 talentType (0 = own talents, 1 = inspect result — own talent packets always 0) + // uint32 unspentTalents + // uint8 talentGroupCount + // uint8 activeTalentGroup + // Per group: uint8 talentCount, [uint32 talentId + uint8 rank] × count, + // uint8 glyphCount, [uint16 glyphId] × count + + if (!packet.hasRemaining(1)) return; + uint8_t talentType = packet.readUInt8(); + if (talentType != 0) { + // type 1 = inspect result; handled by handleInspectResults — ignore here + return; + } + if (!packet.hasRemaining(6)) { + LOG_WARNING("handleTalentsInfo: packet too short for header"); + return; + } + + uint32_t unspentTalents = packet.readUInt32(); + uint8_t talentGroupCount = packet.readUInt8(); + uint8_t activeTalentGroup = packet.readUInt8(); + if (activeTalentGroup > 1) activeTalentGroup = 0; // Ensure talent DBCs are loaded loadTalentDbc(); - // Validate spec number - if (data.talentSpec > 1) { - LOG_WARNING("Invalid talent spec: ", (int)data.talentSpec); - return; + activeTalentSpec_ = activeTalentGroup; + + for (uint8_t g = 0; g < talentGroupCount && g < 2; ++g) { + if (!packet.hasRemaining(1)) break; + uint8_t talentCount = packet.readUInt8(); + learnedTalents_[g].clear(); + for (uint8_t t = 0; t < talentCount; ++t) { + if (!packet.hasRemaining(5)) break; + uint32_t talentId = packet.readUInt32(); + uint8_t rank = packet.readUInt8(); + learnedTalents_[g][talentId] = rank + 1u; // wire sends 0-indexed; store 1-indexed + } + learnedGlyphs_[g].fill(0); + if (!packet.hasRemaining(1)) break; + uint8_t glyphCount = packet.readUInt8(); + for (uint8_t gl = 0; gl < glyphCount; ++gl) { + if (!packet.hasRemaining(2)) break; + uint16_t glyphId = packet.readUInt16(); + if (gl < MAX_GLYPH_SLOTS) learnedGlyphs_[g][gl] = glyphId; + } } - // Store talents for this spec - unspentTalentPoints_[data.talentSpec] = data.unspentPoints; + unspentTalentPoints_[activeTalentGroup] = + static_cast(unspentTalents > 255 ? 255 : unspentTalents); - // Clear and rebuild learned talents map for this spec - // Note: If a talent appears in the packet, it's learned (ranks are 0-indexed) - learnedTalents_[data.talentSpec].clear(); - for (const auto& talent : data.talents) { - learnedTalents_[data.talentSpec][talent.talentId] = talent.currentRank; - } + LOG_INFO("handleTalentsInfo: unspent=", unspentTalents, + " groups=", static_cast(talentGroupCount), " active=", static_cast(activeTalentGroup), + " learned=", learnedTalents_[activeTalentGroup].size()); - LOG_INFO("Talents loaded: spec=", (int)data.talentSpec, - " unspent=", (int)unspentTalentPoints_[data.talentSpec], - " learned=", learnedTalents_[data.talentSpec].size()); + // Fire talent-related events for addons + fireAddonEvent("CHARACTER_POINTS_CHANGED", {}); + fireAddonEvent("ACTIVE_TALENT_GROUP_CHANGED", {}); + fireAddonEvent("PLAYER_TALENT_UPDATE", {}); - // If this is the first spec received after login, set it as the active spec if (!talentsInitialized_) { talentsInitialized_ = true; - activeTalentSpec_ = data.talentSpec; - - // Show message to player about active spec - if (unspentTalentPoints_[data.talentSpec] > 0) { - std::string msg = "You have " + std::to_string(unspentTalentPoints_[data.talentSpec]) + - " unspent talent point"; - if (unspentTalentPoints_[data.talentSpec] > 1) msg += "s"; - msg += " in spec " + std::to_string(data.talentSpec + 1); - addSystemChatMessage(msg); + if (unspentTalents > 0) { + addSystemChatMessage("You have " + std::to_string(unspentTalents) + + " unspent talent point" + (unspentTalents != 1 ? "s" : "") + "."); } } } void GameHandler::learnTalent(uint32_t talentId, uint32_t requestedRank) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { LOG_WARNING("learnTalent: Not in world or no socket connection"); return; } @@ -14674,12 +19011,12 @@ void GameHandler::learnTalent(uint32_t talentId, uint32_t requestedRank) { void GameHandler::switchTalentSpec(uint8_t newSpec) { if (newSpec > 1) { - LOG_WARNING("Invalid talent spec: ", (int)newSpec); + LOG_WARNING("Invalid talent spec: ", static_cast(newSpec)); return; } if (newSpec == activeTalentSpec_) { - LOG_INFO("Already on spec ", (int)newSpec); + LOG_INFO("Already on spec ", static_cast(newSpec)); return; } @@ -14688,15 +19025,15 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) { // and respond with SMSG_TALENTS_INFO for the newly active group. // We optimistically update the local state so the UI reflects the change // immediately; the server response will correct us if needed. - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { auto pkt = ActivateTalentGroupPacket::build(static_cast(newSpec)); socket->send(pkt); - LOG_INFO("Sent CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE: group=", (int)newSpec); + LOG_INFO("Sent CMSG_SET_ACTIVE_TALENT_GROUP_OBSOLETE: group=", static_cast(newSpec)); } activeTalentSpec_ = newSpec; - LOG_INFO("Switched to talent spec ", (int)newSpec, - " (unspent=", (int)unspentTalentPoints_[newSpec], + LOG_INFO("Switched to talent spec ", static_cast(newSpec), + " (unspent=", static_cast(unspentTalentPoints_[newSpec]), ", learned=", learnedTalents_[newSpec].size(), ")"); std::string msg = "Switched to spec " + std::to_string(newSpec + 1); @@ -14708,11 +19045,25 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) { addSystemChatMessage(msg); } +void GameHandler::confirmPetUnlearn() { + if (!petUnlearnPending_) return; + petUnlearnPending_ = false; + if (!isInWorld()) return; + + // Respond with CMSG_PET_UNLEARN_TALENTS (no payload in 3.3.5a) + network::Packet pkt(wireOpcode(Opcode::CMSG_PET_UNLEARN_TALENTS)); + socket->send(pkt); + LOG_INFO("confirmPetUnlearn: sent CMSG_PET_UNLEARN_TALENTS"); + addSystemChatMessage("Pet talent reset confirmed."); + petUnlearnGuid_ = 0; + petUnlearnCost_ = 0; +} + void GameHandler::confirmTalentWipe() { if (!talentWipePending_) return; talentWipePending_ = false; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // Respond to MSG_TALENT_WIPE_CONFIRM with the trainer GUID to trigger the reset. // Packet: opcode(2) + uint64 npcGuid = 10 bytes. @@ -14726,19 +19077,26 @@ void GameHandler::confirmTalentWipe() { talentWipeCost_ = 0; } +void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { + if (!isInWorld()) return; + auto pkt = AlterAppearancePacket::build(hairStyle, hairColor, facialHair); + socket->send(pkt); + LOG_INFO("sendAlterAppearance: hair=", hairStyle, " color=", hairColor, " facial=", facialHair); +} + // ============================================================ // Phase 4: Group/Party // ============================================================ void GameHandler::inviteToGroup(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GroupInvitePacket::build(playerName); socket->send(packet); LOG_INFO("Inviting ", playerName, " to group"); } void GameHandler::acceptGroupInvite() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingGroupInvite = false; auto packet = GroupAcceptPacket::build(); socket->send(packet); @@ -14746,7 +19104,7 @@ void GameHandler::acceptGroupInvite() { } void GameHandler::declineGroupInvite() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingGroupInvite = false; auto packet = GroupDeclinePacket::build(); socket->send(packet); @@ -14754,11 +19112,13 @@ void GameHandler::declineGroupInvite() { } void GameHandler::leaveGroup() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GroupDisbandPacket::build(); socket->send(packet); partyData = GroupListData{}; LOG_INFO("Left group"); + fireAddonEvent("GROUP_ROSTER_UPDATE", {}); + fireAddonEvent("PARTY_MEMBERS_CHANGED", {}); } void GameHandler::handleGroupInvite(network::Packet& packet) { @@ -14771,6 +19131,8 @@ void GameHandler::handleGroupInvite(network::Packet& packet) { if (!data.inviterName.empty()) { addSystemChatMessage(data.inviterName + " has invited you to a group."); } + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playTargetSelect(); }); + fireAddonEvent("PARTY_INVITE_REQUEST", {data.inviterName}); } void GameHandler::handleGroupDecline(network::Packet& packet) { @@ -14788,17 +19150,39 @@ void GameHandler::handleGroupList(network::Packet& packet) { // WotLK 3.3.5a added a roles byte (group level + per-member) for the dungeon finder. // Classic 1.12 and TBC 2.4.3 do not send the roles byte. const bool hasRoles = isActiveExpansion("wotlk"); + // Snapshot state before reset so we can detect transitions. + const uint32_t prevCount = partyData.memberCount; + const uint8_t prevLootMethod = partyData.lootMethod; + const bool wasInGroup = !partyData.isEmpty(); // Reset before parsing — SMSG_GROUP_LIST is a full replacement, not a delta. // Without this, repeated GROUP_LIST packets push duplicate members. partyData = GroupListData{}; if (!GroupListParser::parse(packet, partyData, hasRoles)) return; - if (partyData.isEmpty()) { + const bool nowInGroup = !partyData.isEmpty(); + if (!nowInGroup && wasInGroup) { LOG_INFO("No longer in a group"); addSystemChatMessage("You are no longer in a group."); - } else { - LOG_INFO("In group with ", partyData.memberCount, " members"); - addSystemChatMessage("You are now in a group with " + std::to_string(partyData.memberCount) + " members."); + } else if (nowInGroup && !wasInGroup) { + LOG_INFO("Joined group with ", partyData.memberCount, " members"); + addSystemChatMessage("You are now in a group."); + } else if (nowInGroup && partyData.memberCount != prevCount) { + LOG_INFO("Group updated: ", partyData.memberCount, " members"); + } + // Loot method change notification + if (wasInGroup && nowInGroup && partyData.lootMethod != prevLootMethod) { + static const char* kLootMethods[] = { + "Free for All", "Round Robin", "Master Looter", "Group Loot", "Need Before Greed" + }; + const char* methodName = (partyData.lootMethod < 5) ? kLootMethods[partyData.lootMethod] : "Unknown"; + addSystemChatMessage(std::string("Loot method changed to ") + methodName + "."); + } + // Fire GROUP_ROSTER_UPDATE / PARTY_MEMBERS_CHANGED / RAID_ROSTER_UPDATE for Lua addons + if (addonEventCallback_) { + fireAddonEvent("GROUP_ROSTER_UPDATE", {}); + fireAddonEvent("PARTY_MEMBERS_CHANGED", {}); + if (partyData.groupType == 1) + fireAddonEvent("RAID_ROSTER_UPDATE", {}); } } @@ -14807,10 +19191,15 @@ void GameHandler::handleGroupUninvite(network::Packet& packet) { partyData = GroupListData{}; LOG_INFO("Removed from group"); + fireAddonEvent("GROUP_ROSTER_UPDATE", {}); + fireAddonEvent("PARTY_MEMBERS_CHANGED", {}); + fireAddonEvent("RAID_ROSTER_UPDATE", {}); + MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; msg.message = "You have been removed from the group."; + addUIError("You have been removed from the group."); addLocalChatMessage(msg); } @@ -14819,17 +19208,43 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) { if (!PartyCommandResultParser::parse(packet, data)) return; if (data.result != PartyResult::OK) { + const char* errText = nullptr; + switch (data.result) { + case PartyResult::BAD_PLAYER_NAME: errText = "No player named \"%s\" is currently online."; break; + case PartyResult::TARGET_NOT_IN_GROUP: errText = "%s is not in your group."; break; + case PartyResult::TARGET_NOT_IN_INSTANCE:errText = "%s is not in your instance."; break; + case PartyResult::GROUP_FULL: errText = "Your party is full."; break; + case PartyResult::ALREADY_IN_GROUP: errText = "%s is already in a group."; break; + case PartyResult::NOT_IN_GROUP: errText = "You are not in a group."; break; + case PartyResult::NOT_LEADER: errText = "You are not the group leader."; break; + case PartyResult::PLAYER_WRONG_FACTION: errText = "%s is the wrong faction for this group."; break; + case PartyResult::IGNORING_YOU: errText = "%s is ignoring you."; break; + case PartyResult::LFG_PENDING: errText = "You cannot do that while in a LFG queue."; break; + case PartyResult::INVITE_RESTRICTED: errText = "Target is not accepting group invites."; break; + default: errText = "Party command failed."; break; + } + + char buf[256]; + if (!data.name.empty() && errText && std::strstr(errText, "%s")) { + std::snprintf(buf, sizeof(buf), errText, data.name.c_str()); + } else if (errText) { + std::snprintf(buf, sizeof(buf), "%s", errText); + } else { + std::snprintf(buf, sizeof(buf), "Party command failed (error %u).", + static_cast(data.result)); + } + + addUIError(buf); MessageChatData msg; msg.type = ChatType::SYSTEM; msg.language = ChatLanguage::UNIVERSAL; - msg.message = "Party command failed (error " + std::to_string(static_cast(data.result)) + ")"; - if (!data.name.empty()) msg.message += " for " + data.name; + msg.message = buf; addLocalChatMessage(msg); } } void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { - auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto remaining = [&]() { return packet.getRemainingSize(); }; // Classic/TBC use uint16 for health fields and simpler aura format; // WotLK uses uint32 health and uint32+uint8 per aura. @@ -14846,7 +19261,7 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { const bool pmsTbc = isActiveExpansion("tbc"); if (remaining() < (pmsTbc ? 8u : 1u)) return; uint64_t memberGuid = pmsTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + ? packet.readUInt64() : packet.readPackedGuid(); if (remaining() < 4) return; uint32_t updateFlags = packet.readUInt32(); @@ -14859,7 +19274,7 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { } } if (!member) { - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } @@ -14915,20 +19330,34 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { if (updateFlags & 0x0200) { // AURAS if (remaining() >= 8) { uint64_t auraMask = packet.readUInt64(); + // Collect aura updates for this member and store in unitAurasCache_ + // so party frame debuff dots can use them. + std::vector newAuras; for (int i = 0; i < 64; ++i) { if (auraMask & (uint64_t(1) << i)) { + AuraSlot a; + a.level = static_cast(i); // use slot index if (isWotLK) { // WotLK: uint32 spellId + uint8 auraFlags if (remaining() < 5) break; - packet.readUInt32(); - packet.readUInt8(); + a.spellId = packet.readUInt32(); + a.flags = packet.readUInt8(); } else { - // Classic/TBC: uint16 spellId only + // Classic/TBC: uint16 spellId only; negative auras not indicated here if (remaining() < 2) break; - packet.readUInt16(); + a.spellId = packet.readUInt16(); + // Infer negative/positive from dispel type: non-zero dispel → debuff + uint8_t dt = getSpellDispelType(a.spellId); + if (dt > 0) a.flags = 0x80; // mark as debuff } + if (a.spellId != 0) newAuras.push_back(a); } } + // Populate unitAurasCache_ for this member (merge: keep existing per-GUID data + // only if we already have a richer source; otherwise replace with stats data) + if (memberGuid != 0 && memberGuid != playerGuid && memberGuid != targetGuid) { + unitAurasCache_[memberGuid] = std::move(newAuras); + } } } if (updateFlags & 0x0400) { // PET_GUID @@ -14999,6 +19428,40 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { LOG_DEBUG("Party member stats for ", member->name, ": HP=", member->curHealth, "/", member->maxHealth, " Level=", member->level); + + // Fire addon events for party/raid member health/power/aura changes + if (addonEventCallback_) { + // Resolve unit ID for this member (party1..4 or raid1..40) + std::string unitId; + if (partyData.groupType == 1) { + // Raid: find 1-based index + for (size_t i = 0; i < partyData.members.size(); ++i) { + if (partyData.members[i].guid == memberGuid) { + unitId = "raid" + std::to_string(i + 1); + break; + } + } + } else { + // Party: find 1-based index excluding self + int found = 0; + for (const auto& m : partyData.members) { + if (m.guid == playerGuid) continue; + ++found; + if (m.guid == memberGuid) { + unitId = "party" + std::to_string(found); + break; + } + } + } + if (!unitId.empty()) { + if (updateFlags & (0x0002 | 0x0004)) // CUR_HP or MAX_HP + fireAddonEvent("UNIT_HEALTH", {unitId}); + if (updateFlags & (0x0010 | 0x0020)) // CUR_POWER or MAX_POWER + fireAddonEvent("UNIT_POWER", {unitId}); + if (updateFlags & 0x0200) // AURAS + fireAddonEvent("UNIT_AURA", {unitId}); + } + } } // ============================================================ @@ -15006,42 +19469,42 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { // ============================================================ void GameHandler::kickGuildMember(const std::string& playerName) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildRemovePacket::build(playerName); socket->send(packet); LOG_INFO("Kicking guild member: ", playerName); } void GameHandler::disbandGuild() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildDisbandPacket::build(); socket->send(packet); LOG_INFO("Disbanding guild"); } void GameHandler::setGuildLeader(const std::string& name) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildLeaderPacket::build(name); socket->send(packet); LOG_INFO("Setting guild leader: ", name); } void GameHandler::setGuildPublicNote(const std::string& name, const std::string& note) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildSetPublicNotePacket::build(name, note); socket->send(packet); LOG_INFO("Setting public note for ", name, ": ", note); } void GameHandler::setGuildOfficerNote(const std::string& name, const std::string& note) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildSetOfficerNotePacket::build(name, note); socket->send(packet); LOG_INFO("Setting officer note for ", name, ": ", note); } void GameHandler::acceptGuildInvite() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingGuildInvite_ = false; auto packet = GuildAcceptPacket::build(); socket->send(packet); @@ -15049,7 +19512,7 @@ void GameHandler::acceptGuildInvite() { } void GameHandler::declineGuildInvite() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingGuildInvite_ = false; auto packet = GuildDeclineInvitationPacket::build(); socket->send(packet); @@ -15057,7 +19520,7 @@ void GameHandler::declineGuildInvite() { } void GameHandler::submitGmTicket(const std::string& text) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // CMSG_GMTICKET_CREATE (WotLK 3.3.5a): // string ticket_text @@ -15078,28 +19541,60 @@ void GameHandler::submitGmTicket(const std::string& text) { } void GameHandler::deleteGmTicket() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_DELETETICKET)); socket->send(pkt); + gmTicketActive_ = false; + gmTicketText_.clear(); LOG_INFO("Deleting GM ticket"); } +void GameHandler::requestGmTicket() { + if (!isInWorld()) return; + // CMSG_GMTICKET_GETTICKET has no payload — server responds with SMSG_GMTICKET_GETTICKET + network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_GETTICKET)); + socket->send(pkt); + LOG_DEBUG("Sent CMSG_GMTICKET_GETTICKET — querying open ticket status"); +} + void GameHandler::queryGuildInfo(uint32_t guildId) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildQueryPacket::build(guildId); socket->send(packet); LOG_INFO("Querying guild info: guildId=", guildId); } +static const std::string kEmptyString; + +const std::string& GameHandler::lookupGuildName(uint32_t guildId) { + if (guildId == 0) return kEmptyString; + auto it = guildNameCache_.find(guildId); + if (it != guildNameCache_.end()) return it->second; + // Query the server if we haven't already + if (pendingGuildNameQueries_.insert(guildId).second) { + queryGuildInfo(guildId); + } + return kEmptyString; +} + +uint32_t GameHandler::getEntityGuildId(uint64_t guid) const { + auto entity = entityManager.getEntity(guid); + if (!entity || entity->getType() != ObjectType::PLAYER) return 0; + // PLAYER_GUILDID = UNIT_END + 3 across all expansions + const uint16_t ufUnitEnd = fieldIndex(UF::UNIT_END); + if (ufUnitEnd == 0xFFFF) return 0; + return entity->getField(ufUnitEnd + 3); +} + void GameHandler::createGuild(const std::string& guildName) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildCreatePacket::build(guildName); socket->send(packet); LOG_INFO("Creating guild: ", guildName); } void GameHandler::addGuildRank(const std::string& rankName) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildAddRankPacket::build(rankName); socket->send(packet); LOG_INFO("Adding guild rank: ", rankName); @@ -15108,7 +19603,7 @@ void GameHandler::addGuildRank(const std::string& rankName) { } void GameHandler::deleteGuildRank() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GuildDelRankPacket::build(); socket->send(packet); LOG_INFO("Deleting last guild rank"); @@ -15117,13 +19612,13 @@ void GameHandler::deleteGuildRank() { } void GameHandler::requestPetitionShowlist(uint64_t npcGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = PetitionShowlistPacket::build(npcGuid); socket->send(packet); } void GameHandler::buyPetition(uint64_t npcGuid, const std::string& guildName) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = PetitionBuyPacket::build(npcGuid, guildName); socket->send(packet); LOG_INFO("Buying guild petition: ", guildName); @@ -15139,6 +19634,118 @@ void GameHandler::handlePetitionShowlist(network::Packet& packet) { LOG_INFO("Petition showlist: cost=", data.cost); } +void GameHandler::handlePetitionQueryResponse(network::Packet& packet) { + // SMSG_PETITION_QUERY_RESPONSE (3.3.5a): + // uint32 petitionEntry, uint64 petitionGuid, string guildName, + // string bodyText (empty), uint32 flags, uint32 minSignatures, + // uint32 maxSignatures, ...plus more fields we can skip + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() < 12) return; + + /*uint32_t entry =*/ packet.readUInt32(); + uint64_t petGuid = packet.readUInt64(); + std::string guildName = packet.readString(); + /*std::string body =*/ packet.readString(); + + // Update petition info if it matches our current petition + if (petitionInfo_.petitionGuid == petGuid) { + petitionInfo_.guildName = guildName; + } + + LOG_INFO("SMSG_PETITION_QUERY_RESPONSE: guid=", petGuid, " name=", guildName); + packet.skipAll(); // skip remaining fields +} + +void GameHandler::handlePetitionShowSignatures(network::Packet& packet) { + // SMSG_PETITION_SHOW_SIGNATURES (3.3.5a): + // uint64 itemGuid (petition item in inventory) + // uint64 ownerGuid + // uint32 petitionGuid (low part / entry) + // uint8 signatureCount + // For each signature: + // uint64 playerGuid + // uint32 unk (always 0) + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() < 21) return; + + petitionInfo_ = PetitionInfo{}; + petitionInfo_.petitionGuid = packet.readUInt64(); + petitionInfo_.ownerGuid = packet.readUInt64(); + /*uint32_t petEntry =*/ packet.readUInt32(); + uint8_t sigCount = packet.readUInt8(); + + petitionInfo_.signatureCount = sigCount; + petitionInfo_.signatures.reserve(sigCount); + + for (uint8_t i = 0; i < sigCount; ++i) { + if (rem() < 12) break; + PetitionSignature sig; + sig.playerGuid = packet.readUInt64(); + /*uint32_t unk =*/ packet.readUInt32(); + petitionInfo_.signatures.push_back(sig); + } + + petitionInfo_.showUI = true; + LOG_INFO("SMSG_PETITION_SHOW_SIGNATURES: petGuid=", petitionInfo_.petitionGuid, + " owner=", petitionInfo_.ownerGuid, + " sigs=", sigCount); +} + +void GameHandler::handlePetitionSignResults(network::Packet& packet) { + // SMSG_PETITION_SIGN_RESULTS (3.3.5a): + // uint64 petitionGuid, uint64 playerGuid, uint32 result + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() < 20) return; + + uint64_t petGuid = packet.readUInt64(); + uint64_t playerGuid = packet.readUInt64(); + uint32_t result = packet.readUInt32(); + + switch (result) { + case 0: // PETITION_SIGN_OK + addSystemChatMessage("Petition signed successfully."); + // Increment local count + if (petitionInfo_.petitionGuid == petGuid) { + petitionInfo_.signatureCount++; + PetitionSignature sig; + sig.playerGuid = playerGuid; + petitionInfo_.signatures.push_back(sig); + } + break; + case 1: // PETITION_SIGN_ALREADY_SIGNED + addSystemChatMessage("You have already signed that petition."); + break; + case 2: // PETITION_SIGN_ALREADY_IN_GUILD + addSystemChatMessage("You are already in a guild."); + break; + case 3: // PETITION_SIGN_CANT_SIGN_OWN + addSystemChatMessage("You cannot sign your own petition."); + break; + default: + addSystemChatMessage("Cannot sign petition (error " + std::to_string(result) + ")."); + break; + } + LOG_INFO("SMSG_PETITION_SIGN_RESULTS: pet=", petGuid, " player=", playerGuid, + " result=", result); +} + +void GameHandler::signPetition(uint64_t petitionGuid) { + if (!socket || state != WorldState::IN_WORLD) return; + network::Packet pkt(wireOpcode(Opcode::CMSG_PETITION_SIGN)); + pkt.writeUInt64(petitionGuid); + pkt.writeUInt8(0); // unk + socket->send(pkt); + LOG_INFO("Signing petition: ", petitionGuid); +} + +void GameHandler::turnInPetition(uint64_t petitionGuid) { + if (!socket || state != WorldState::IN_WORLD) return; + network::Packet pkt(wireOpcode(Opcode::CMSG_TURN_IN_PETITION)); + pkt.writeUInt64(petitionGuid); + socket->send(pkt); + LOG_INFO("Turning in petition: ", petitionGuid); +} + void GameHandler::handleTurnInPetitionResults(network::Packet& packet) { uint32_t result = 0; if (!TurnInPetitionResultsParser::parse(packet, result)) return; @@ -15169,20 +19776,39 @@ void GameHandler::handleGuildRoster(network::Packet& packet) { guildRoster_ = std::move(data); hasGuildRoster_ = true; LOG_INFO("Guild roster received: ", guildRoster_.members.size(), " members"); + fireAddonEvent("GUILD_ROSTER_UPDATE", {}); } void GameHandler::handleGuildQueryResponse(network::Packet& packet) { GuildQueryResponseData data; if (!packetParsers_->parseGuildQueryResponse(packet, data)) return; - guildName_ = data.guildName; - guildQueryData_ = data; - guildRankNames_.clear(); - for (uint32_t i = 0; i < 10; ++i) { - guildRankNames_.push_back(data.rankNames[i]); + // Always cache the guild name for nameplate lookups + if (data.guildId != 0 && !data.guildName.empty()) { + guildNameCache_[data.guildId] = data.guildName; + pendingGuildNameQueries_.erase(data.guildId); + } + + // Check if this is the local player's guild + const Character* ch = getActiveCharacter(); + bool isLocalGuild = (ch && ch->hasGuild() && ch->guildId == data.guildId); + + if (isLocalGuild) { + const bool wasUnknown = guildName_.empty(); + guildName_ = data.guildName; + guildQueryData_ = data; + guildRankNames_.clear(); + for (uint32_t i = 0; i < 10; ++i) { + guildRankNames_.push_back(data.rankNames[i]); + } + LOG_INFO("Guild name set to: ", guildName_); + if (wasUnknown && !guildName_.empty()) { + addSystemChatMessage("Guild: <" + guildName_ + ">"); + fireAddonEvent("PLAYER_GUILD_UPDATE", {}); + } + } else { + LOG_INFO("Cached guild name: id=", data.guildId, " name=", data.guildName); } - LOG_INFO("Guild name set to: ", guildName_); - addSystemChatMessage("Guild: <" + guildName_ + ">"); } void GameHandler::handleGuildEvent(network::Packet& packet) { @@ -15229,6 +19855,7 @@ void GameHandler::handleGuildEvent(network::Packet& packet) { guildRankNames_.clear(); guildRoster_ = GuildRosterData{}; hasGuildRoster_ = false; + fireAddonEvent("PLAYER_GUILD_UPDATE", {}); break; case GuildEvent::SIGNED_ON: if (data.numStrings >= 1) @@ -15251,6 +19878,28 @@ void GameHandler::handleGuildEvent(network::Packet& packet) { addLocalChatMessage(chatMsg); } + // Fire addon events for guild state changes + if (addonEventCallback_) { + switch (data.eventType) { + case GuildEvent::MOTD: + fireAddonEvent("GUILD_MOTD", {data.numStrings >= 1 ? data.strings[0] : ""}); + break; + case GuildEvent::SIGNED_ON: + case GuildEvent::SIGNED_OFF: + case GuildEvent::PROMOTION: + case GuildEvent::DEMOTION: + case GuildEvent::JOINED: + case GuildEvent::LEFT: + case GuildEvent::REMOVED: + case GuildEvent::LEADER_CHANGED: + case GuildEvent::DISBANDED: + fireAddonEvent("GUILD_ROSTER_UPDATE", {}); + break; + default: + break; + } + } + // Auto-refresh roster after membership/rank changes switch (data.eventType) { case GuildEvent::PROMOTION: @@ -15275,18 +19924,80 @@ void GameHandler::handleGuildInvite(network::Packet& packet) { pendingGuildInviteGuildName_ = data.guildName; LOG_INFO("Guild invite from: ", data.inviterName, " to guild: ", data.guildName); addSystemChatMessage(data.inviterName + " has invited you to join " + data.guildName + "."); + fireAddonEvent("GUILD_INVITE_REQUEST", {data.inviterName, data.guildName}); } void GameHandler::handleGuildCommandResult(network::Packet& packet) { GuildCommandResultData data; if (!GuildCommandResultParser::parse(packet, data)) return; - if (data.errorCode != 0) { - std::string msg = "Guild command failed"; + // command: 0=CREATE, 1=INVITE, 2=QUIT, 3=FOUNDER + if (data.errorCode == 0) { + switch (data.command) { + case 0: // CREATE + addSystemChatMessage("Guild created."); + break; + case 1: // INVITE — invited another player + if (!data.name.empty()) + addSystemChatMessage("You have invited " + data.name + " to the guild."); + break; + case 2: // QUIT — player successfully left + addSystemChatMessage("You have left the guild."); + guildName_.clear(); + guildRankNames_.clear(); + guildRoster_ = GuildRosterData{}; + hasGuildRoster_ = false; + break; + default: + break; + } + return; + } + + // Error codes from AzerothCore SharedDefines.h GuildCommandError + const char* errStr = nullptr; + switch (data.errorCode) { + case 2: errStr = "You are not in a guild."; break; + case 3: errStr = "That player is not in a guild."; break; + case 4: errStr = "No player named \"%s\" is online."; break; + case 7: errStr = "You are the guild leader."; break; + case 8: errStr = "You must transfer leadership before leaving."; break; + case 11: errStr = "\"%s\" is already in a guild."; break; + case 13: errStr = "You are already in a guild."; break; + case 14: errStr = "\"%s\" has already been invited to a guild."; break; + case 15: errStr = "You cannot invite yourself."; break; + case 16: + case 17: errStr = "You are not the guild leader."; break; + case 18: errStr = "That player's rank is too high to remove."; break; + case 19: errStr = "You cannot remove someone with a higher rank."; break; + case 20: errStr = "Guild ranks are locked."; break; + case 21: errStr = "That rank is in use."; break; + case 22: errStr = "That player is ignoring you."; break; + case 25: errStr = "Insufficient guild bank withdrawal quota."; break; + case 26: errStr = "Guild doesn't have enough money."; break; + case 28: errStr = "Guild bank is full."; break; + case 31: errStr = "Too many guild ranks."; break; + case 37: errStr = "That player is the guild leader."; break; + case 49: errStr = "Guild reputation is too low."; break; + default: break; + } + + std::string msg; + if (errStr) { + // Substitute %s with player name where applicable + std::string fmt = errStr; + auto pos = fmt.find("%s"); + if (pos != std::string::npos && !data.name.empty()) + fmt.replace(pos, 2, data.name); + else if (pos != std::string::npos) + fmt.replace(pos, 2, "that player"); + msg = fmt; + } else { + msg = "Guild command failed"; if (!data.name.empty()) msg += " for " + data.name; msg += " (error " + std::to_string(data.errorCode) + ")"; - addSystemChatMessage(msg); } + addSystemChatMessage(msg); } // ============================================================ @@ -15294,13 +20005,13 @@ void GameHandler::handleGuildCommandResult(network::Packet& packet) { // ============================================================ void GameHandler::lootTarget(uint64_t guid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = LootPacket::build(guid); socket->send(packet); } void GameHandler::lootItem(uint8_t slotIndex) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = AutostoreLootItemPacket::build(slotIndex); socket->send(packet); } @@ -15308,25 +20019,37 @@ void GameHandler::lootItem(uint8_t slotIndex) { void GameHandler::closeLoot() { if (!lootWindowOpen) return; lootWindowOpen = false; + fireAddonEvent("LOOT_CLOSED", {}); + masterLootCandidates_.clear(); if (currentLoot.lootGuid != 0 && targetGuid == currentLoot.lootGuid) { clearTarget(); } - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { auto packet = LootReleasePacket::build(currentLoot.lootGuid); socket->send(packet); } currentLoot = LootResponseData{}; } +void GameHandler::lootMasterGive(uint8_t lootSlot, uint64_t targetGuid) { + if (!isInWorld()) return; + // CMSG_LOOT_MASTER_GIVE: uint64 lootGuid + uint8 slotIndex + uint64 targetGuid + network::Packet pkt(wireOpcode(Opcode::CMSG_LOOT_MASTER_GIVE)); + pkt.writeUInt64(currentLoot.lootGuid); + pkt.writeUInt8(lootSlot); + pkt.writeUInt64(targetGuid); + socket->send(pkt); +} + void GameHandler::interactWithNpc(uint64_t guid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = GossipHelloPacket::build(guid); socket->send(packet); } void GameHandler::interactWithGameObject(uint64_t guid) { if (guid == 0) return; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // Do not overlap an actual spell cast. if (casting && currentCastSpellId != 0) return; // Always clear melee intent before GO interactions. @@ -15338,15 +20061,13 @@ void GameHandler::interactWithGameObject(uint64_t guid) { void GameHandler::performGameObjectInteractionNow(uint64_t guid) { if (guid == 0) return; - if (state != WorldState::IN_WORLD || !socket) return; - bool turtleMode = isActiveExpansion("turtle"); - + if (!isInWorld()) return; // Rate-limit to prevent spamming the server static uint64_t lastInteractGuid = 0; static std::chrono::steady_clock::time_point lastInteractTime{}; auto now = std::chrono::steady_clock::now(); // Keep duplicate suppression, but allow quick retry clicks. - int64_t minRepeatMs = turtleMode ? 150 : 150; + constexpr int64_t minRepeatMs = 150; if (guid == lastInteractGuid && std::chrono::duration_cast(now - lastInteractTime).count() < minRepeatMs) { return; @@ -15382,10 +20103,21 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { float dy = entity->getY() - movementInfo.y; float dz = entity->getZ() - movementInfo.z; float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz); - if (dist3d > 6.0f) { + if (dist3d > 10.0f) { addSystemChatMessage("Too far away."); return; } + // Stop movement before interacting — servers may reject GO use or + // immediately cancel the resulting spell cast if the player is moving. + const uint32_t moveFlags = movementInfo.flags; + const bool isMoving = (moveFlags & 0x00000001u) || // FORWARD + (moveFlags & 0x00000002u) || // BACKWARD + (moveFlags & 0x00000004u) || // STRAFE_LEFT + (moveFlags & 0x00000008u); // STRAFE_RIGHT + if (isMoving) { + movementInfo.flags &= ~0x0000000Fu; // clear directional movement flags + sendMovement(Opcode::MSG_MOVE_STOP); + } if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { movementInfo.orientation = std::atan2(-dy, dx); sendMovement(Opcode::MSG_MOVE_SET_FACING); @@ -15393,33 +20125,14 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } - auto packet = GameObjectUsePacket::build(guid); - socket->send(packet); - - // For mailbox GameObjects (type 19), open mail UI and request mail list. - // In Vanilla/Classic there is no SMSG_SHOW_MAILBOX — the server just sends - // animation/sound and expects the client to request the mail list. + // Determine GO type for interaction strategy bool isMailbox = false; bool chestLike = false; - // Always send CMSG_LOOT after CMSG_GAMEOBJ_USE for any gameobject that could be - // lootable. The server silently ignores CMSG_LOOT for non-lootable objects - // (doors, buttons, etc.), so this is safe. Not sending it is the main reason - // chests fail to open when their GO type is not yet cached or their name doesn't - // contain the word "chest" (e.g. lockboxes, coffers, strongboxes, caches). - bool shouldSendLoot = true; if (entity && entity->getType() == ObjectType::GAMEOBJECT) { auto go = std::static_pointer_cast(entity); auto* info = getCachedGameObjectInfo(go->getEntry()); if (info && info->type == 19) { isMailbox = true; - shouldSendLoot = false; - LOG_INFO("Mailbox interaction: opening mail UI and requesting mail list"); - mailboxGuid_ = guid; - mailboxOpen_ = true; - hasNewMail_ = false; - selectedMailIndex_ = -1; - showMailCompose_ = false; - refreshMailList(); } else if (info && info->type == 3) { chestLike = true; } @@ -15432,22 +20145,48 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { lower.find("lockbox") != std::string::npos || lower.find("strongbox") != std::string::npos || lower.find("coffer") != std::string::npos || - lower.find("cache") != std::string::npos); + lower.find("cache") != std::string::npos || + lower.find("bundle") != std::string::npos); } - // For WotLK, CMSG_GAMEOBJ_REPORT_USE is required for chests (and is harmless for others). - if (!isMailbox && isActiveExpansion("wotlk")) { - network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); - reportUse.writeUInt64(guid); - socket->send(reportUse); - } - if (shouldSendLoot) { + + LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec, + " entry=", goEntry, " type=", goType, + " name='", goName, "' chestLike=", chestLike, " isMailbox=", isMailbox); + + if (chestLike) { + // For chest-like GOs: send CMSG_GAMEOBJ_USE (opens the chest) followed + // immediately by CMSG_LOOT (requests loot contents). Both sent in the + // same frame so the server processes them sequentially: USE transitions + // the GO to lootable state, then LOOT reads the contents. + auto usePacket = GameObjectUsePacket::build(guid); + socket->send(usePacket); lootTarget(guid); - } - // Retry use briefly to survive packet loss/order races. - const bool retryLoot = shouldSendLoot; - const bool retryUse = turtleMode || isActiveExpansion("classic"); - if (retryUse || retryLoot) { - pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot}); + lastInteractedGoGuid_ = guid; + } else { + // Non-chest GOs (doors, buttons, quest givers, etc.): use CMSG_GAMEOBJ_USE + auto packet = GameObjectUsePacket::build(guid); + socket->send(packet); + lastInteractedGoGuid_ = guid; + + if (isMailbox) { + LOG_INFO("Mailbox interaction: opening mail UI and requesting mail list"); + mailboxGuid_ = guid; + mailboxOpen_ = true; + hasNewMail_ = false; + selectedMailIndex_ = -1; + showMailCompose_ = false; + refreshMailList(); + } + + // CMSG_GAMEOBJ_REPORT_USE for GO AI scripts (quest givers, etc.) + if (!isMailbox) { + const auto* table = getActiveOpcodeTable(); + if (table && table->hasOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)) { + network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); + reportUse.writeUInt64(guid); + socket->send(reportUse); + } + } } } @@ -15462,7 +20201,7 @@ void GameHandler::selectGossipOption(uint32_t optionId) { for (const auto& opt : currentGossip.options) { if (opt.id != optionId) continue; - LOG_INFO(" matched option: id=", opt.id, " icon=", (int)opt.icon, " text='", opt.text, "'"); + LOG_INFO(" matched option: id=", opt.id, " icon=", static_cast(opt.icon), " text='", opt.text, "'"); // Icon-based NPC interaction fallbacks // Some servers need the specific activate packet in addition to gossip select @@ -15491,12 +20230,39 @@ void GameHandler::selectGossipOption(uint32_t optionId) { LOG_INFO("Sent CMSG_BANKER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); } + // Vendor / repair: some servers require an explicit CMSG_LIST_INVENTORY after gossip select. + const bool isVendor = (text == "GOSSIP_OPTION_VENDOR" || + (textLower.find("browse") != std::string::npos && + (textLower.find("goods") != std::string::npos || textLower.find("wares") != std::string::npos))); + const bool isArmorer = (text == "GOSSIP_OPTION_ARMORER" || textLower.find("repair") != std::string::npos); + if (isVendor || isArmorer) { + if (isArmorer) { + setVendorCanRepair(true); + } + auto pkt = ListInventoryPacket::build(currentGossip.npcGuid); + socket->send(pkt); + LOG_INFO("Sent CMSG_LIST_INVENTORY (gossip) to npc=0x", std::hex, currentGossip.npcGuid, std::dec, + " vendor=", static_cast(isVendor), " repair=", static_cast(isArmorer)); + } + if (textLower.find("make this inn your home") != std::string::npos || textLower.find("set your home") != std::string::npos) { auto bindPkt = BinderActivatePacket::build(currentGossip.npcGuid); socket->send(bindPkt); LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); } + + // Stable master detection: GOSSIP_OPTION_STABLE or text keywords + if (text == "GOSSIP_OPTION_STABLE" || + textLower.find("stable") != std::string::npos || + textLower.find("my pet") != std::string::npos) { + stableMasterGuid_ = currentGossip.npcGuid; + stableWindowOpen_ = false; // will open when list arrives + auto listPkt = ListStabledPetsPacket::build(currentGossip.npcGuid); + socket->send(listPkt); + LOG_INFO("Sent MSG_LIST_STABLED_PETS (gossip) to npc=0x", + std::hex, currentGossip.npcGuid, std::dec); + } break; } } @@ -15505,13 +20271,7 @@ void GameHandler::selectGossipQuest(uint32_t questId) { if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; // Keep quest-log fallback for servers that don't provide stable icon semantics. - const QuestLogEntry* activeQuest = nullptr; - for (const auto& q : questLog_) { - if (q.questId == questId) { - activeQuest = &q; - break; - } - } + const QuestLogEntry* activeQuest = findQuestLogEntry(questId); // Validate against server-auth quest slot fields to avoid stale local entries // forcing turn-in flow for quests that are not actually accepted. @@ -15531,12 +20291,7 @@ void GameHandler::selectGossipQuest(uint32_t questId) { if (questInServerLog && !activeQuest) { addQuestToLocalLogIfMissing(questId, "Quest #" + std::to_string(questId), ""); requestQuestQuery(questId, false); - for (const auto& q : questLog_) { - if (q.questId == questId) { - activeQuest = &q; - break; - } - } + activeQuest = findQuestLogEntry(questId); } const bool activeQuestConfirmedByServer = questInServerLog; // Only trust server quest-log slots for deciding "already accepted" flow. @@ -15601,10 +20356,10 @@ void GameHandler::handleQuestPoiQueryResponse(network::Packet& packet) { // uint32 unk2 // uint32 pointCount // per point: int32 x, int32 y - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; const uint32_t questCount = packet.readUInt32(); for (uint32_t qi = 0; qi < questCount; ++qi) { - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; const uint32_t questId = packet.readUInt32(); const uint32_t poiCount = packet.readUInt32(); @@ -15619,13 +20374,10 @@ void GameHandler::handleQuestPoiQueryResponse(network::Packet& packet) { gossipPois_.end()); // Find the quest title for the marker label. - std::string questTitle; - for (const auto& q : questLog_) { - if (q.questId == questId) { questTitle = q.title; break; } - } + auto questTitle = getQuestTitle(questId); for (uint32_t pi = 0; pi < poiCount; ++pi) { - if (packet.getSize() - packet.getReadPos() < 28) return; + if (!packet.hasRemaining(28)) return; packet.readUInt32(); // poiId packet.readUInt32(); // objIndex (int32) const uint32_t mapId = packet.readUInt32(); @@ -15635,7 +20387,7 @@ void GameHandler::handleQuestPoiQueryResponse(network::Packet& packet) { packet.readUInt32(); // unk2 const uint32_t pointCount = packet.readUInt32(); if (pointCount == 0) continue; - if (packet.getSize() - packet.getReadPos() < pointCount * 8) return; + if (!packet.hasRemaining(pointCount) * 8) return; // Compute centroid of the poi region to place a minimap marker. float sumX = 0.0f, sumY = 0.0f; for (uint32_t pt = 0; pt < pointCount; ++pt) { @@ -15655,6 +20407,7 @@ void GameHandler::handleQuestPoiQueryResponse(network::Packet& packet) { poi.name = questTitle.empty() ? "Quest objective" : questTitle; LOG_DEBUG("Quest POI: questId=", questId, " mapId=", mapId, " centroid=(", poi.x, ",", poi.y, ") title=", poi.name); + if (gossipPois_.size() >= 200) gossipPois_.erase(gossipPois_.begin()); gossipPois_.push_back(std::move(poi)); } } @@ -15686,6 +20439,7 @@ void GameHandler::handleQuestDetails(network::Packet& packet) { // Delay opening the window slightly to allow item queries to complete questDetailsOpenTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); gossipWindowOpen = false; + fireAddonEvent("QUEST_DETAIL", {}); } bool GameHandler::hasQuestInLog(uint32_t questId) const { @@ -15695,6 +20449,31 @@ bool GameHandler::hasQuestInLog(uint32_t questId) const { return false; } +Unit* GameHandler::getUnitByGuid(uint64_t guid) { + auto entity = entityManager.getEntity(guid); + return entity ? dynamic_cast(entity.get()) : nullptr; +} + +std::string GameHandler::guidToUnitId(uint64_t guid) const { + if (guid == playerGuid) return "player"; + if (guid == targetGuid) return "target"; + if (guid == focusGuid) return "focus"; + if (guid == petGuid_) return "pet"; + return {}; +} + +std::string GameHandler::getQuestTitle(uint32_t questId) const { + for (const auto& q : questLog_) + if (q.questId == questId && !q.title.empty()) return q.title; + return {}; +} + +const GameHandler::QuestLogEntry* GameHandler::findQuestLogEntry(uint32_t questId) const { + for (const auto& q : questLog_) + if (q.questId == questId) return &q; + return nullptr; +} + int GameHandler::findQuestLogSlotIndexFromServer(uint32_t questId) const { if (questId == 0 || lastPlayerFields_.empty()) return -1; const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); @@ -15716,6 +20495,9 @@ void GameHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::strin entry.title = title.empty() ? ("Quest #" + std::to_string(questId)) : title; entry.objectives = objectives; questLog_.push_back(std::move(entry)); + fireAddonEvent("QUEST_ACCEPTED", {std::to_string(questId)}); + fireAddonEvent("QUEST_LOG_UPDATE", {}); + fireAddonEvent("UNIT_QUEST_LOG_CHANGED", {"player"}); } bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) { @@ -15884,7 +20666,7 @@ void GameHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) { if (counts[i] == 0 && quest.killCounts.count(entryKey)) continue; quest.killCounts[entryKey] = {counts[i], obj.required}; LOG_DEBUG("Quest ", quest.questId, " objective[", i, "]: npcOrGo=", - obj.npcOrGoId, " count=", (int)counts[i], "/", obj.required); + obj.npcOrGoId, " count=", static_cast(counts[i]), "/", obj.required); } // Apply item objective counts (only available in WotLK stride+3 positions 4-5). @@ -15960,6 +20742,9 @@ void GameHandler::acceptQuest() { pendingQuestAcceptTimeouts_[questId] = 5.0f; pendingQuestAcceptNpcGuids_[questId] = npcGuid; + // Play quest-accept sound + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestActivate(); }); + questDetailsOpen = false; questDetailsOpenTime = std::chrono::steady_clock::time_point{}; currentQuestDetails = QuestDetailsData{}; @@ -15996,7 +20781,7 @@ void GameHandler::abandonQuest(uint32_t questId) { } if (slotIndex >= 0 && slotIndex < 25) { - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST)); pkt.writeUInt8(static_cast(slotIndex)); socket->send(pkt); @@ -16007,6 +20792,9 @@ void GameHandler::abandonQuest(uint32_t questId) { if (localIndex >= 0) { questLog_.erase(questLog_.begin() + static_cast(localIndex)); + fireAddonEvent("QUEST_LOG_UPDATE", {}); + fireAddonEvent("UNIT_QUEST_LOG_CHANGED", {"player"}); + fireAddonEvent("QUEST_REMOVED", {std::to_string(questId)}); } // Remove any quest POI minimap markers for this quest. @@ -16016,6 +20804,24 @@ void GameHandler::abandonQuest(uint32_t questId) { gossipPois_.end()); } +void GameHandler::shareQuestWithParty(uint32_t questId) { + if (!isInWorld()) { + addSystemChatMessage("Cannot share quest: not in world."); + return; + } + if (!isInGroup()) { + addSystemChatMessage("You must be in a group to share a quest."); + return; + } + network::Packet pkt(wireOpcode(Opcode::CMSG_PUSHQUESTTOPARTY)); + pkt.writeUInt32(questId); + socket->send(pkt); + // Local feedback: find quest title + auto questTitle = getQuestTitle(questId); + addSystemChatMessage(questTitle.empty() ? std::string("Quest shared.") + : ("Sharing quest: " + questTitle)); +} + void GameHandler::handleQuestRequestItems(network::Packet& packet) { QuestRequestItemsData data; if (!QuestRequestItemsParser::parse(packet, data)) { @@ -16099,6 +20905,7 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) { gossipWindowOpen = false; questDetailsOpen = false; questDetailsOpenTime = std::chrono::steady_clock::time_point{}; + fireAddonEvent("QUEST_COMPLETE", {}); // Query item names for reward items for (const auto& item : data.choiceRewards) @@ -16157,17 +20964,47 @@ void GameHandler::closeQuestOfferReward() { void GameHandler::closeGossip() { gossipWindowOpen = false; + fireAddonEvent("GOSSIP_CLOSED", {}); currentGossip = GossipMessageData{}; } +void GameHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) { + if (!isInWorld()) return; + if (itemGuid == 0 || questId == 0) { + addSystemChatMessage("Cannot start quest right now."); + return; + } + // Send CMSG_QUESTGIVER_QUERY_QUEST with the item GUID as the "questgiver." + // The server responds with SMSG_QUESTGIVER_QUEST_DETAILS which handleQuestDetails() + // picks up and opens the Accept/Decline dialog. + auto queryPkt = packetParsers_ + ? packetParsers_->buildQueryQuestPacket(itemGuid, questId) + : QuestgiverQueryQuestPacket::build(itemGuid, questId); + socket->send(queryPkt); + LOG_INFO("offerQuestFromItem: itemGuid=0x", std::hex, itemGuid, std::dec, + " questId=", questId); +} + +uint64_t GameHandler::getBagItemGuid(int bagIndex, int slotIndex) const { + if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return 0; + if (slotIndex < 0) return 0; + uint64_t bagGuid = equipSlotGuids_[19 + bagIndex]; + if (bagGuid == 0) return 0; + auto it = containerContents_.find(bagGuid); + if (it == containerContents_.end()) return 0; + if (slotIndex >= static_cast(it->second.numSlots)) return 0; + return it->second.slotGuids[slotIndex]; +} + void GameHandler::openVendor(uint64_t npcGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; buybackItems_.clear(); auto packet = ListInventoryPacket::build(npcGuid); socket->send(packet); } void GameHandler::closeVendor() { + bool wasOpen = vendorWindowOpen; vendorWindowOpen = false; currentVendorItems = ListInventoryData{}; buybackItems_.clear(); @@ -16176,10 +21013,11 @@ void GameHandler::closeVendor() { pendingBuybackWireSlot_ = 0; pendingBuyItemId_ = 0; pendingBuyItemSlot_ = 0; + if (wasOpen) fireAddonEvent("MERCHANT_CLOSED", {}); } void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; LOG_INFO("Buy request: vendorGuid=0x", std::hex, vendorGuid, std::dec, " itemId=", itemId, " slot=", slot, " count=", count, " wire=0x", std::hex, wireOpcode(Opcode::CMSG_BUY_ITEM), std::dec); @@ -16224,7 +21062,7 @@ void GameHandler::buyBackItem(uint32_t buybackSlot) { } void GameHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // CMSG_REPAIR_ITEM: npcGuid(8) + itemGuid(8) + useGuildBank(uint8) network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM)); packet.writeUInt64(vendorGuid); @@ -16234,7 +21072,7 @@ void GameHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) { } void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // itemGuid = 0 signals "repair all equipped" to the server network::Packet packet(wireOpcode(Opcode::CMSG_REPAIR_ITEM)); packet.writeUInt64(vendorGuid); @@ -16244,7 +21082,7 @@ void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) { } void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; LOG_INFO("Sell request: vendorGuid=0x", std::hex, vendorGuid, " itemGuid=0x", itemGuid, std::dec, " count=", count, " wire=0x", std::hex, wireOpcode(Opcode::CMSG_SELL_ITEM), std::dec); @@ -16298,7 +21136,7 @@ void GameHandler::autoEquipItemBySlot(int backpackIndex) { const auto& slot = inventory.getBackpackSlot(backpackIndex); if (slot.empty()) return; - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { // WoW inventory: equipment 0-18, bags 19-22, backpack 23-38 auto packet = AutoEquipItemPacket::build(0xFF, static_cast(23 + backpackIndex)); socket->send(packet); @@ -16309,7 +21147,7 @@ void GameHandler::autoEquipItemInBag(int bagIndex, int slotIndex) { if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return; if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return; - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { // Bag items: bag = equip slot 19+bagIndex, slot = index within bag auto packet = AutoEquipItemPacket::build( static_cast(19 + bagIndex), static_cast(slotIndex)); @@ -16364,7 +21202,7 @@ void GameHandler::sellItemInBag(int bagIndex, int slotIndex) { } void GameHandler::unequipToBackpack(EquipSlot equipSlot) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; int freeSlot = inventory.findFreeBackpackSlot(); if (freeSlot < 0) { @@ -16378,8 +21216,8 @@ void GameHandler::unequipToBackpack(EquipSlot equipSlot) { uint8_t dstBag = 0xFF; uint8_t dstSlot = static_cast(23 + freeSlot); - LOG_INFO("UnequipToBackpack: equipSlot=", (int)srcSlot, - " -> backpackIndex=", freeSlot, " (dstSlot=", (int)dstSlot, ")"); + LOG_INFO("UnequipToBackpack: equipSlot=", static_cast(srcSlot), + " -> backpackIndex=", freeSlot, " (dstSlot=", static_cast(dstSlot), ")"); auto packet = SwapItemPacket::build(dstBag, dstSlot, srcBag, srcSlot); socket->send(packet); @@ -16387,8 +21225,8 @@ void GameHandler::unequipToBackpack(EquipSlot equipSlot) { void GameHandler::swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot) { if (!socket || !socket->isConnected()) return; - LOG_INFO("swapContainerItems: src(bag=", (int)srcBag, " slot=", (int)srcSlot, - ") -> dst(bag=", (int)dstBag, " slot=", (int)dstSlot, ")"); + LOG_INFO("swapContainerItems: src(bag=", static_cast(srcBag), " slot=", static_cast(srcSlot), + ") -> dst(bag=", static_cast(dstBag), " slot=", static_cast(dstSlot), ")"); auto packet = SwapItemPacket::build(dstBag, dstSlot, srcBag, srcSlot); socket->send(packet); } @@ -16413,15 +21251,15 @@ void GameHandler::swapBagSlots(int srcBagIndex, int dstBagIndex) { if (socket && socket->isConnected()) { uint8_t srcSlot = static_cast(19 + srcBagIndex); uint8_t dstSlot = static_cast(19 + dstBagIndex); - LOG_INFO("swapBagSlots: bag ", srcBagIndex, " (slot ", (int)srcSlot, - ") <-> bag ", dstBagIndex, " (slot ", (int)dstSlot, ")"); + LOG_INFO("swapBagSlots: bag ", srcBagIndex, " (slot ", static_cast(srcSlot), + ") <-> bag ", dstBagIndex, " (slot ", static_cast(dstSlot), ")"); auto packet = SwapItemPacket::build(255, dstSlot, 255, srcSlot); socket->send(packet); } } void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; if (count == 0) count = 1; // AzerothCore WotLK expects CMSG_DESTROYITEM(bag:u8, slot:u8, count:u32). @@ -16431,11 +21269,45 @@ void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { packet.writeUInt8(bag); packet.writeUInt8(slot); packet.writeUInt32(static_cast(count)); - LOG_DEBUG("Destroy item request: bag=", (int)bag, " slot=", (int)slot, - " count=", (int)count, " wire=0x", std::hex, kCmsgDestroyItem, std::dec); + LOG_DEBUG("Destroy item request: bag=", static_cast(bag), " slot=", static_cast(slot), + " count=", static_cast(count), " wire=0x", std::hex, kCmsgDestroyItem, std::dec); socket->send(packet); } +void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) { + if (!isInWorld()) return; + if (count == 0) return; + + // Find a free slot for the split destination: try backpack first, then bags + int freeBp = inventory.findFreeBackpackSlot(); + if (freeBp >= 0) { + uint8_t dstBag = 0xFF; + uint8_t dstSlot = static_cast(23 + freeBp); + LOG_INFO("splitItem: src(bag=", static_cast(srcBag), " slot=", static_cast(srcSlot), + ") count=", static_cast(count), " -> dst(bag=0xFF slot=", static_cast(dstSlot), ")"); + auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count); + socket->send(packet); + return; + } + // Try equipped bags + for (int b = 0; b < inventory.NUM_BAG_SLOTS; b++) { + int bagSize = inventory.getBagSize(b); + for (int s = 0; s < bagSize; s++) { + if (inventory.getBagSlot(b, s).empty()) { + uint8_t dstBag = static_cast(19 + b); + uint8_t dstSlot = static_cast(s); + LOG_INFO("splitItem: src(bag=", static_cast(srcBag), " slot=", static_cast(srcSlot), + ") count=", static_cast(count), " -> dst(bag=", static_cast(dstBag), + " slot=", static_cast(dstSlot), ")"); + auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count); + socket->send(packet); + return; + } + } + } + addSystemChatMessage("Cannot split: no free inventory slots."); +} + void GameHandler::useItemBySlot(int backpackIndex) { if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return; const auto& slot = inventory.getBackpackSlot(backpackIndex); @@ -16446,7 +21318,7 @@ void GameHandler::useItemBySlot(int backpackIndex) { itemGuid = resolveOnlineItemGuid(slot.item.itemId); } - if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { + if (itemGuid != 0 && isInWorld()) { // Find the item's on-use spell ID from cached item info uint32_t useSpellId = 0; if (auto* info = getItemInfo(slot.item.itemId)) { @@ -16491,7 +21363,7 @@ void GameHandler::useItemInBag(int bagIndex, int slotIndex) { LOG_INFO("useItemInBag: bag=", bagIndex, " slot=", slotIndex, " itemId=", slot.item.itemId, " itemGuid=0x", std::hex, itemGuid, std::dec); - if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) { + if (itemGuid != 0 && isInWorld()) { // Find the item's on-use spell ID uint32_t useSpellId = 0; if (auto* info = getItemInfo(slot.item.itemId)) { @@ -16509,7 +21381,7 @@ void GameHandler::useItemInBag(int bagIndex, int slotIndex) { auto packet = packetParsers_ ? packetParsers_->buildUseItem(wowBag, static_cast(slotIndex), itemGuid, useSpellId) : UseItemPacket::build(wowBag, static_cast(slotIndex), itemGuid, useSpellId); - LOG_INFO("useItemInBag: sending CMSG_USE_ITEM, bag=", (int)wowBag, " slot=", slotIndex, + LOG_INFO("useItemInBag: sending CMSG_USE_ITEM, bag=", static_cast(wowBag), " slot=", slotIndex, " packetSize=", packet.getSize()); socket->send(packet); } else if (itemGuid == 0) { @@ -16518,6 +21390,26 @@ void GameHandler::useItemInBag(int bagIndex, int slotIndex) { } } +void GameHandler::openItemBySlot(int backpackIndex) { + if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return; + if (inventory.getBackpackSlot(backpackIndex).empty()) return; + if (!isInWorld()) return; + auto packet = OpenItemPacket::build(0xFF, static_cast(23 + backpackIndex)); + LOG_INFO("openItemBySlot: CMSG_OPEN_ITEM bag=0xFF slot=", (23 + backpackIndex)); + socket->send(packet); +} + +void GameHandler::openItemInBag(int bagIndex, int slotIndex) { + if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return; + if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return; + if (inventory.getBagSlot(bagIndex, slotIndex).empty()) return; + if (!isInWorld()) return; + uint8_t wowBag = static_cast(19 + bagIndex); + auto packet = OpenItemPacket::build(wowBag, static_cast(slotIndex)); + LOG_INFO("openItemInBag: CMSG_OPEN_ITEM bag=", static_cast(wowBag), " slot=", slotIndex); + socket->send(packet); +} + void GameHandler::useItemById(uint32_t itemId) { if (itemId == 0) return; LOG_DEBUG("useItemById: searching for itemId=", itemId); @@ -16569,12 +21461,27 @@ void GameHandler::unstuckHearth() { } void GameHandler::handleLootResponse(network::Packet& packet) { - // Classic 1.12 and TBC 2.4.3 use 14 bytes/item (no randomSuffix/randomProp fields); - // WotLK 3.3.5a uses 22 bytes/item. + // All expansions use 22 bytes/item (slot+itemId+count+displayInfo+randSuffix+randProp+slotType). + // WotLK adds a quest item list after the regular items. const bool wotlkLoot = isActiveExpansion("wotlk"); if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return; + const bool hasLoot = !currentLoot.items.empty() || currentLoot.gold > 0; + // If we're mid-gather-cast and got an empty loot response (premature CMSG_LOOT + // before the node became lootable), ignore it — don't clear our gather state. + if (!hasLoot && casting && currentCastSpellId != 0 && lastInteractedGoGuid_ != 0) { + LOG_DEBUG("Ignoring empty SMSG_LOOT_RESPONSE during gather cast"); + return; + } lootWindowOpen = true; - localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false}; + fireAddonEvent("LOOT_OPENED", {}); + fireAddonEvent("LOOT_READY", {}); + lastInteractedGoGuid_ = 0; // loot opened — no need to re-send in handleSpellGo + pendingGameObjectLootOpens_.erase( + std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(), + [&](const PendingLootOpen& p) { return p.guid == currentLoot.lootGuid; }), + pendingGameObjectLootOpens_.end()); + auto& localLoot = localLootState_[currentLoot.lootGuid]; + localLoot.data = currentLoot; // Query item info so loot window can show names instead of IDs for (const auto& item : currentLoot.items) { @@ -16582,7 +21489,7 @@ void GameHandler::handleLootResponse(network::Packet& packet) { } if (currentLoot.gold > 0) { - if (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { // Auto-loot gold by sending CMSG_LOOT_MONEY (server handles the rest) bool suppressFallback = false; auto cooldownIt = recentLootMoneyAnnounceCooldowns_.find(currentLoot.lootGuid); @@ -16599,11 +21506,12 @@ void GameHandler::handleLootResponse(network::Packet& packet) { } // Auto-loot items when enabled - if (autoLoot_ && state == WorldState::IN_WORLD && socket) { + if (autoLoot_ && isInWorld() && !localLoot.itemAutoLootSent) { for (const auto& item : currentLoot.items) { auto pkt = AutostoreLootItemPacket::build(item.slotIndex); socket->send(pkt); } + localLoot.itemAutoLootSent = true; } } @@ -16611,6 +21519,7 @@ void GameHandler::handleLootReleaseResponse(network::Packet& packet) { (void)packet; localLootState_.erase(currentLoot.lootGuid); lootWindowOpen = false; + fireAddonEvent("LOOT_CLOSED", {}); currentLoot = LootResponseData{}; } @@ -16619,18 +21528,18 @@ void GameHandler::handleLootRemoved(network::Packet& packet) { for (auto it = currentLoot.items.begin(); it != currentLoot.items.end(); ++it) { if (it->slotIndex == slotIndex) { std::string itemName = "item #" + std::to_string(it->itemId); + uint32_t quality = 1; if (const ItemQueryResponseData* info = getItemInfo(it->itemId)) { - if (!info->name.empty()) { - itemName = info->name; - } + if (!info->name.empty()) itemName = info->name; + quality = info->quality; } - std::ostringstream msg; - msg << "Looted: " << itemName; - if (it->count > 1) { - msg << " x" << it->count; - } - addSystemChatMessage(msg.str()); + std::string link = buildItemLink(it->itemId, quality, itemName); + std::string msgStr = "Looted: " + link; + if (it->count > 1) msgStr += " x" + std::to_string(it->count); + addSystemChatMessage(msgStr); + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLootItem(); }); currentLoot.items.erase(it); + fireAddonEvent("LOOT_SLOT_CLEARED", {std::to_string(slotIndex + 1)}); break; } } @@ -16642,6 +21551,7 @@ void GameHandler::handleGossipMessage(network::Packet& packet) { if (!ok) return; if (questDetailsOpen) return; // Don't reopen gossip while viewing quest gossipWindowOpen = true; + fireAddonEvent("GOSSIP_SHOW", {}); vendorWindowOpen = false; // Close vendor if gossip opens // Update known quest-log entries based on gossip quests. @@ -16707,7 +21617,7 @@ void GameHandler::handleGossipMessage(network::Packet& packet) { } void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; GossipMessageData data; data.npcGuid = packet.readUInt64(); @@ -16716,7 +21626,7 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { // Server text (header/greeting) and optional emote fields. std::string header = packet.readString(); - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { (void)packet.readUInt32(); // emoteDelay / unk (void)packet.readUInt32(); // emote / unk } @@ -16724,7 +21634,7 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { // questCount is uint8 in all WoW versions for SMSG_QUESTGIVER_QUEST_LIST. uint32_t questCount = 0; - if (packet.getSize() - packet.getReadPos() >= 1) { + if (packet.hasRemaining(1)) { questCount = packet.readUInt8(); } @@ -16734,13 +21644,13 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { data.quests.reserve(questCount); for (uint32_t i = 0; i < questCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 12) break; + if (!packet.hasRemaining(12)) break; GossipQuestItem q; q.questId = packet.readUInt32(); q.questIcon = packet.readUInt32(); q.questLevel = static_cast(packet.readUInt32()); - if (hasQuestFlagsField && packet.getSize() - packet.getReadPos() >= 5) { + if (hasQuestFlagsField && packet.hasRemaining(5)) { q.questFlags = packet.readUInt32(); q.isRepeatable = packet.readUInt8(); } else { @@ -16755,6 +21665,7 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { currentGossip = std::move(data); gossipWindowOpen = true; + fireAddonEvent("GOSSIP_SHOW", {}); vendorWindowOpen = false; bool hasAvailableQuest = false; @@ -16805,15 +21716,110 @@ void GameHandler::handleGossipComplete(network::Packet& packet) { } gossipWindowOpen = false; + fireAddonEvent("GOSSIP_CLOSED", {}); currentGossip = GossipMessageData{}; } void GameHandler::handleListInventory(network::Packet& packet) { - bool savedCanRepair = currentVendorItems.canRepair; // preserve armorer flag set before openVendor() + bool savedCanRepair = currentVendorItems.canRepair; // preserve armorer flag set via gossip path if (!ListInventoryParser::parse(packet, currentVendorItems)) return; + + // Check NPC_FLAG_REPAIR (0x1000) on the vendor entity — this handles vendors that open + // directly without going through the gossip armorer option. + if (!savedCanRepair && currentVendorItems.vendorGuid != 0) { + auto entity = entityManager.getEntity(currentVendorItems.vendorGuid); + if (entity && entity->getType() == ObjectType::UNIT) { + auto unit = std::static_pointer_cast(entity); + // MaNGOS/Trinity: UNIT_NPC_FLAG_REPAIR = 0x00001000. + if (unit->getNpcFlags() & 0x1000) { + savedCanRepair = true; + } + } + } currentVendorItems.canRepair = savedCanRepair; vendorWindowOpen = true; gossipWindowOpen = false; // Close gossip if vendor opens + fireAddonEvent("MERCHANT_SHOW", {}); + + // Auto-sell grey items if enabled + if (autoSellGrey_ && currentVendorItems.vendorGuid != 0) { + uint32_t totalSellPrice = 0; + int itemsSold = 0; + + // Helper lambda to attempt selling a poor-quality slot + auto tryAutoSell = [&](const ItemSlot& slot, uint64_t itemGuid) { + if (slot.empty()) return; + if (slot.item.quality != ItemQuality::POOR) return; + // Determine sell price (slot cache first, then item info fallback) + uint32_t sp = slot.item.sellPrice; + if (sp == 0) { + if (auto* info = getItemInfo(slot.item.itemId); info && info->valid) + sp = info->sellPrice; + } + if (sp == 0 || itemGuid == 0) return; + BuybackItem sold; + sold.itemGuid = itemGuid; + sold.item = slot.item; + sold.count = 1; + buybackItems_.push_front(sold); + if (buybackItems_.size() > 12) buybackItems_.pop_back(); + pendingSellToBuyback_[itemGuid] = sold; + sellItem(currentVendorItems.vendorGuid, itemGuid, 1); + totalSellPrice += sp; + ++itemsSold; + }; + + // Backpack slots + for (int i = 0; i < inventory.getBackpackSize(); ++i) { + uint64_t guid = backpackSlotGuids_[i]; + if (guid == 0) guid = resolveOnlineItemGuid(inventory.getBackpackSlot(i).item.itemId); + tryAutoSell(inventory.getBackpackSlot(i), guid); + } + + // Extra bag slots + for (int b = 0; b < inventory.NUM_BAG_SLOTS; ++b) { + uint64_t bagGuid = equipSlotGuids_[19 + b]; + for (int s = 0; s < inventory.getBagSize(b); ++s) { + uint64_t guid = 0; + if (bagGuid != 0) { + auto it = containerContents_.find(bagGuid); + if (it != containerContents_.end() && s < static_cast(it->second.numSlots)) + guid = it->second.slotGuids[s]; + } + if (guid == 0) guid = resolveOnlineItemGuid(inventory.getBagSlot(b, s).item.itemId); + tryAutoSell(inventory.getBagSlot(b, s), guid); + } + } + + if (itemsSold > 0) { + uint32_t gold = totalSellPrice / 10000; + uint32_t silver = (totalSellPrice % 10000) / 100; + uint32_t copper = totalSellPrice % 100; + char buf[128]; + std::snprintf(buf, sizeof(buf), + "|cffaaaaaaAuto-sold %d grey item%s for %ug %us %uc.|r", + itemsSold, itemsSold == 1 ? "" : "s", gold, silver, copper); + addSystemChatMessage(buf); + } + } + + // Auto-repair all items if enabled and vendor can repair + if (autoRepair_ && currentVendorItems.canRepair && currentVendorItems.vendorGuid != 0) { + // Check that at least one equipped item is actually damaged to avoid no-op + bool anyDamaged = false; + for (int i = 0; i < Inventory::NUM_EQUIP_SLOTS; ++i) { + const auto& slot = inventory.getEquipSlot(static_cast(i)); + if (!slot.empty() && slot.item.maxDurability > 0 + && slot.item.curDurability < slot.item.maxDurability) { + anyDamaged = true; + break; + } + } + if (anyDamaged) { + repairAll(currentVendorItems.vendorGuid, false); + addSystemChatMessage("|cffaaaaaaAuto-repair triggered.|r"); + } + } // Play vendor sound if (npcVendorCallback_ && currentVendorItems.vendorGuid != 0) { @@ -16839,6 +21845,7 @@ void GameHandler::handleTrainerList(network::Packet& packet) { if (!TrainerListParser::parse(packet, currentTrainerList_, isClassic)) return; trainerWindowOpen_ = true; gossipWindowOpen = false; + fireAddonEvent("TRAINER_SHOW", {}); LOG_INFO("Trainer list: ", currentTrainerList_.spells.size(), " spells"); LOG_DEBUG("Known spells count: ", knownSpells.size()); @@ -16855,8 +21862,8 @@ void GameHandler::handleTrainerList(network::Packet& packet) { " 25312=", knownSpells.count(25312u)); for (size_t i = 0; i < std::min(size_t(5), currentTrainerList_.spells.size()); ++i) { const auto& s = currentTrainerList_.spells[i]; - LOG_DEBUG(" Spell[", i, "]: id=", s.spellId, " state=", (int)s.state, - " cost=", s.spellCost, " reqLvl=", (int)s.reqLevel, + LOG_DEBUG(" Spell[", i, "]: id=", s.spellId, " state=", static_cast(s.state), + " cost=", s.spellCost, " reqLvl=", static_cast(s.reqLevel), " chain=(", s.chainNode1, ",", s.chainNode2, ",", s.chainNode3, ")"); } @@ -16869,8 +21876,8 @@ void GameHandler::handleTrainerList(network::Packet& packet) { } void GameHandler::trainSpell(uint32_t spellId) { - LOG_INFO("trainSpell called: spellId=", spellId, " state=", (int)state, " socket=", (socket ? "yes" : "no")); - if (state != WorldState::IN_WORLD || !socket) { + LOG_INFO("trainSpell called: spellId=", spellId, " state=", static_cast(state), " socket=", (socket ? "yes" : "no")); + if (!isInWorld()) { LOG_WARNING("trainSpell: Not in world or no socket connection"); return; } @@ -16896,11 +21903,12 @@ void GameHandler::trainSpell(uint32_t spellId) { void GameHandler::closeTrainer() { trainerWindowOpen_ = false; + fireAddonEvent("TRAINER_CLOSED", {}); currentTrainerList_ = TrainerListData{}; trainerTabs_.clear(); } -void GameHandler::loadSpellNameCache() { +void GameHandler::loadSpellNameCache() const { if (spellNameCacheLoaded_) return; spellNameCacheLoaded_ = true; @@ -16932,6 +21940,29 @@ void GameHandler::loadSpellNameCache() { if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { schoolEnumField = f; hasSchoolEnum = true; } } + // DispelType field (0=none,1=magic,2=curse,3=disease,4=poison,5=stealth,…) + uint32_t dispelField = 0xFFFFFFFF; + bool hasDispelField = false; + if (spellL) { + uint32_t f = spellL->field("DispelType"); + if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; } + } + + // AttributesEx field (bit 4 = SPELL_ATTR_EX_NOT_INTERRUPTIBLE) + uint32_t attrExField = 0xFFFFFFFF; + bool hasAttrExField = false; + if (spellL) { + uint32_t f = spellL->field("AttributesEx"); + if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { attrExField = f; hasAttrExField = true; } + } + + // Tooltip/description field + uint32_t tooltipField = 0xFFFFFFFF; + if (spellL) { + uint32_t f = spellL->field("Tooltip"); + if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) tooltipField = f; + } + uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t id = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); @@ -16939,7 +21970,10 @@ void GameHandler::loadSpellNameCache() { std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136); std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153); if (!name.empty()) { - SpellNameEntry entry{std::move(name), std::move(rank), 0}; + SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0, 0}; + if (tooltipField != 0xFFFFFFFF) { + entry.description = dbc->getString(i, tooltipField); + } if (hasSchoolMask) { entry.schoolMask = dbc->getUInt32(i, schoolMaskField); } else if (hasSchoolEnum) { @@ -16948,9 +21982,48 @@ void GameHandler::loadSpellNameCache() { uint32_t e = dbc->getUInt32(i, schoolEnumField); entry.schoolMask = (e < 7) ? enumToBitmask[e] : 0; } + if (hasDispelField) { + entry.dispelType = static_cast(dbc->getUInt32(i, dispelField)); + } + if (hasAttrExField) { + entry.attrEx = dbc->getUInt32(i, attrExField); + } + // Load effect base points for $s1/$s2/$s3 tooltip substitution + if (spellL) { + uint32_t f0 = spellL->field("EffectBasePoints0"); + uint32_t f1 = spellL->field("EffectBasePoints1"); + uint32_t f2 = spellL->field("EffectBasePoints2"); + if (f0 != 0xFFFFFFFF) entry.effectBasePoints[0] = static_cast(dbc->getUInt32(i, f0)); + if (f1 != 0xFFFFFFFF) entry.effectBasePoints[1] = static_cast(dbc->getUInt32(i, f1)); + if (f2 != 0xFFFFFFFF) entry.effectBasePoints[2] = static_cast(dbc->getUInt32(i, f2)); + } + // Duration: read DurationIndex and resolve via SpellDuration.dbc later + if (spellL) { + uint32_t durF = spellL->field("DurationIndex"); + if (durF != 0xFFFFFFFF) + entry.durationSec = static_cast(dbc->getUInt32(i, durF)); // store index temporarily + } spellNameCache_[id] = std::move(entry); } } + // Resolve DurationIndex → seconds via SpellDuration.dbc + auto durDbc = am->loadDBC("SpellDuration.dbc"); + if (durDbc && durDbc->isLoaded()) { + std::unordered_map durMap; + for (uint32_t di = 0; di < durDbc->getRecordCount(); ++di) { + uint32_t durId = durDbc->getUInt32(di, 0); + int32_t baseMs = static_cast(durDbc->getUInt32(di, 1)); + if (baseMs > 0 && baseMs < 100000000) // filter out absurd values + durMap[durId] = baseMs / 1000.0f; + } + for (auto& [sid, entry] : spellNameCache_) { + uint32_t durIdx = static_cast(entry.durationSec); + if (durIdx > 0) { + auto it = durMap.find(durIdx); + entry.durationSec = (it != durMap.end()) ? it->second : 0.0f; + } + } + } LOG_INFO("Trainer: Loaded ", spellNameCache_.size(), " spell names from Spell.dbc"); } @@ -16975,6 +22048,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(); @@ -17131,6 +22260,18 @@ void GameHandler::loadTalentDbc() { static const std::string EMPTY_STRING; +const int32_t* GameHandler::getSpellEffectBasePoints(uint32_t spellId) const { + loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + return (it != spellNameCache_.end()) ? it->second.effectBasePoints : nullptr; +} + +float GameHandler::getSpellDuration(uint32_t spellId) const { + loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + return (it != spellNameCache_.end()) ? it->second.durationSec : 0.0f; +} + const std::string& GameHandler::getSpellName(uint32_t spellId) const { auto it = spellNameCache_.find(spellId); return (it != spellNameCache_.end()) ? it->second.name : EMPTY_STRING; @@ -17141,6 +22282,49 @@ const std::string& GameHandler::getSpellRank(uint32_t spellId) const { return (it != spellNameCache_.end()) ? it->second.rank : EMPTY_STRING; } +const std::string& GameHandler::getSpellDescription(uint32_t spellId) const { + loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + return (it != spellNameCache_.end()) ? it->second.description : EMPTY_STRING; +} + +std::string GameHandler::getEnchantName(uint32_t enchantId) const { + if (enchantId == 0) return {}; + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return {}; + auto dbc = am->loadDBC("SpellItemEnchantment.dbc"); + if (!dbc || !dbc->isLoaded()) return {}; + // Name is at field 14 (consistent across Classic/TBC/WotLK) + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + if (dbc->getUInt32(i, 0) == enchantId) { + return dbc->getString(i, 14); + } + } + return {}; +} + +uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const { + loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + return (it != spellNameCache_.end()) ? it->second.dispelType : 0; +} + +bool GameHandler::isSpellInterruptible(uint32_t spellId) const { + if (spellId == 0) return true; + loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + if (it == spellNameCache_.end()) return true; // assume interruptible if unknown + // SPELL_ATTR_EX_NOT_INTERRUPTIBLE = bit 4 of AttributesEx (0x00000010) + return (it->second.attrEx & 0x00000010u) == 0; +} + +uint32_t GameHandler::getSpellSchoolMask(uint32_t spellId) const { + if (spellId == 0) return 0; + loadSpellNameCache(); + auto it = spellNameCache_.find(spellId); + return (it != spellNameCache_.end()) ? it->second.schoolMask : 0; +} + const std::string& GameHandler::getSkillLineName(uint32_t spellId) const { auto slIt = spellToSkillLine_.find(spellId); if (slIt == spellToSkillLine_.end()) return EMPTY_STRING; @@ -17216,11 +22400,23 @@ void GameHandler::handleXpGain(network::Packet& packet) { // but we can show combat text for XP gains addCombatText(CombatTextEntry::XP_GAIN, static_cast(data.totalXp), 0, true); - std::string msg = "You gain " + std::to_string(data.totalXp) + " experience."; + // Build XP message with source creature name when available + std::string msg; + if (data.victimGuid != 0 && data.type == 0) { + // Kill XP — resolve creature name + std::string victimName = lookupName(data.victimGuid); + if (!victimName.empty()) + msg = victimName + " dies, you gain " + std::to_string(data.totalXp) + " experience."; + else + msg = "You gain " + std::to_string(data.totalXp) + " experience."; + } else { + msg = "You gain " + std::to_string(data.totalXp) + " experience."; + } if (data.groupBonus > 0) { msg += " (+" + std::to_string(data.groupBonus) + " group bonus)"; } addSystemChatMessage(msg); + fireAddonEvent("CHAT_MSG_COMBAT_XP_GAIN", {msg, std::to_string(data.totalXp)}); } @@ -17235,6 +22431,7 @@ void GameHandler::addMoneyCopper(uint32_t amount) { msg += std::to_string(silver) + "s "; msg += std::to_string(copper) + "c."; addSystemChatMessage(msg); + fireAddonEvent("CHAT_MSG_MONEY", {msg}); } void GameHandler::addSystemChatMessage(const std::string& message) { @@ -17254,24 +22451,24 @@ void GameHandler::handleTeleportAck(network::Packet& packet) { // MSG_MOVE_TELEPORT_ACK (server→client): // WotLK: packed GUID + u32 counter + u32 time + movement info with new position // TBC/Classic: uint64 + u32 counter + u32 time + movement info - const bool taTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (taTbc ? 8u : 4u)) { + const bool taTbc = isPreWotlk(); + if (!packet.hasRemaining(taTbc ? 8u : 4u) ) { LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short"); return; } uint64_t guid = taTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) return; + ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) return; uint32_t counter = packet.readUInt32(); // Read the movement info embedded in the teleport. // WotLK: moveFlags(4) + moveFlags2(2) + time(4) + x(4) + y(4) + z(4) + o(4) = 26 bytes // Classic 1.12 / TBC 2.4.3: moveFlags(4) + time(4) + x(4) + y(4) + z(4) + o(4) = 24 bytes // (Classic and TBC have no moveFlags2 field in movement packets) - const bool taNoFlags2 = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool taNoFlags2 = isPreWotlk(); const size_t minMoveSz = taNoFlags2 ? (4 + 4 + 4 * 4) : (4 + 2 + 4 + 4 * 4); - if (packet.getSize() - packet.getReadPos() < minMoveSz) { + if (!packet.hasRemaining(minMoveSz)) { LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info"); return; } @@ -17307,7 +22504,7 @@ void GameHandler::handleTeleportAck(network::Packet& packet) { if (legacyGuidAck) { ack.writeUInt64(playerGuid); // CMaNGOS/VMaNGOS expects full GUID for Classic/TBC } else { - MovementPacket::writePackedGuid(ack, playerGuid); + ack.writePackedGuid(playerGuid); } ack.writeUInt32(counter); ack.writeUInt32(moveTime); @@ -17324,7 +22521,7 @@ void GameHandler::handleTeleportAck(network::Packet& packet) { void GameHandler::handleNewWorld(network::Packet& packet) { // SMSG_NEW_WORLD: uint32 mapId, float x, y, z, orientation - if (packet.getSize() - packet.getReadPos() < 20) { + if (!packet.hasRemaining(20)) { LOG_WARNING("SMSG_NEW_WORLD too short"); return; } @@ -17363,9 +22560,19 @@ void GameHandler::handleNewWorld(network::Packet& packet) { repopPending_ = false; pendingSpiritHealerGuid_ = 0; resurrectCasterGuid_ = 0; + corpseMapId_ = 0; + corpseGuid_ = 0; hostileAttackers_.clear(); stopAutoAttack(); tabCycleStale = true; + casting = false; + castIsChannel = false; + currentCastSpellId = 0; + castTimeRemaining = 0.0f; + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; if (socket) { network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK)); @@ -17376,6 +22583,10 @@ void GameHandler::handleNewWorld(network::Packet& packet) { } currentMapId_ = mapId; + inInstance_ = false; // cleared on map change; re-set if SMSG_INSTANCE_DIFFICULTY follows + if (socket) { + socket->tracePacketsFor(std::chrono::seconds(12), "new_world"); + } // Update player position glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(serverX, serverY, serverZ)); @@ -17401,7 +22612,24 @@ void GameHandler::handleNewWorld(network::Packet& packet) { mountCallback_(0); } - // Clear world state for the new map + // Invoke despawn callbacks for all entities before clearing, so the renderer + // can release M2 instances, character models, and associated resources. + for (const auto& [guid, entity] : entityManager.getEntities()) { + if (guid == playerGuid) continue; // skip self + if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) { + creatureDespawnCallback_(guid); + } else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) { + playerDespawnCallback_(guid); + } else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) { + gameObjectDespawnCallback_(guid); + } + } + otherPlayerVisibleItemEntries_.clear(); + otherPlayerVisibleDirty_.clear(); + otherPlayerMoveTimeMs_.clear(); + unitCastStates_.clear(); + unitAurasCache_.clear(); + combatText.clear(); entityManager.clear(); hostileAttackers_.clear(); worldStates_.clear(); @@ -17419,7 +22647,12 @@ void GameHandler::handleNewWorld(network::Packet& packet) { castIsChannel = false; currentCastSpellId = 0; pendingGameObjectInteractGuid_ = 0; + lastInteractedGoGuid_ = 0; castTimeRemaining = 0.0f; + craftQueueSpellId_ = 0; + craftQueueRemaining_ = 0; + queuedSpellId_ = 0; + queuedSpellTarget_ = 0; // Send MSG_MOVE_WORLDPORT_ACK to tell the server we're ready if (socket) { @@ -17428,6 +22661,12 @@ void GameHandler::handleNewWorld(network::Packet& packet) { LOG_INFO("Sent MSG_MOVE_WORLDPORT_ACK"); } + timeSinceLastPing = 0.0f; + if (socket) { + LOG_WARNING("World transfer keepalive: sending immediate ping after MSG_MOVE_WORLDPORT_ACK"); + sendPing(); + } + // Reload terrain at new position. // Pass isSameMap as isInitialEntry so the application despawns and // re-registers renderer instances before the server resends CREATE_OBJECTs. @@ -17436,6 +22675,10 @@ void GameHandler::handleNewWorld(network::Packet& packet) { if (worldEntryCallback_) { worldEntryCallback_(mapId, serverX, serverY, serverZ, isSameMap); } + + // Fire PLAYER_ENTERING_WORLD for teleports / zone transitions + fireAddonEvent("PLAYER_ENTERING_WORLD", {"0"}); + fireAddonEvent("ZONE_CHANGED_NEW_AREA", {}); } // ============================================================ @@ -18033,7 +23276,16 @@ void GameHandler::activateTaxi(uint32_t destNodeId) { dismount(); } - addSystemChatMessage("Taxi: requesting flight..."); + { + auto destIt = taxiNodes_.find(destNodeId); + if (destIt != taxiNodes_.end() && !destIt->second.name.empty()) { + taxiDestName_ = destIt->second.name; + addSystemChatMessage("Requesting flight to " + destIt->second.name + "..."); + } else { + taxiDestName_.clear(); + addSystemChatMessage("Taxi: requesting flight..."); + } + } // BFS to find path from startNode to destNodeId std::unordered_map> adj; @@ -18151,10 +23403,13 @@ void GameHandler::activateTaxi(uint32_t destNodeId) { taxiActivateTimer_ = 0.0f; } - addSystemChatMessage("Flight started."); - // Save recovery target in case of disconnect during taxi. auto destIt = taxiNodes_.find(destNodeId); + if (destIt != taxiNodes_.end() && !destIt->second.name.empty()) + addSystemChatMessage("Flight to " + destIt->second.name + " started."); + else + addSystemChatMessage("Flight started."); + if (destIt != taxiNodes_.end()) { taxiRecoverMapId_ = destIt->second.mapId; taxiRecoverPos_ = core::coords::serverToCanonical( @@ -18233,38 +23488,39 @@ void GameHandler::handleWho(network::Packet& packet) { LOG_INFO("WHO response: ", displayCount, " players displayed, ", onlineCount, " total online"); + // Store structured results for the who-results window + whoResults_.clear(); + whoOnlineCount_ = onlineCount; + if (displayCount == 0) { addSystemChatMessage("No players found."); return; } - addSystemChatMessage(std::to_string(onlineCount) + " player(s) online:"); - for (uint32_t i = 0; i < displayCount; ++i) { - if (packet.getReadPos() >= packet.getSize()) break; + if (!packet.hasData()) break; std::string playerName = packet.readString(); std::string guildName = packet.readString(); - if (packet.getSize() - packet.getReadPos() < 12) break; + if (!packet.hasRemaining(12)) break; uint32_t level = packet.readUInt32(); uint32_t classId = packet.readUInt32(); uint32_t raceId = packet.readUInt32(); - if (hasGender && packet.getSize() - packet.getReadPos() >= 1) + if (hasGender && packet.hasRemaining(1)) packet.readUInt8(); // gender (WotLK only, unused) uint32_t zoneId = 0; - if (packet.getSize() - packet.getReadPos() >= 4) + if (packet.hasRemaining(4)) zoneId = packet.readUInt32(); - std::string msg = " " + playerName; - if (!guildName.empty()) - msg += " <" + guildName + ">"; - msg += " - Level " + std::to_string(level); - if (zoneId != 0) { - std::string zoneName = getAreaName(zoneId); - if (!zoneName.empty()) - msg += " [" + zoneName + "]"; - } + // Store structured entry + WhoEntry entry; + entry.name = playerName; + entry.guildName = guildName; + entry.level = level; + entry.classId = classId; + entry.raceId = raceId; + entry.zoneId = zoneId; + whoResults_.push_back(std::move(entry)); - addSystemChatMessage(msg); LOG_INFO(" ", playerName, " (", guildName, ") Lv", level, " Class:", classId, " Race:", raceId, " Zone:", zoneId); } @@ -18280,10 +23536,10 @@ void GameHandler::handleFriendList(network::Packet& packet) { // uint32 area // uint32 level // uint32 class - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 1) return; uint8_t count = packet.readUInt8(); - LOG_INFO("SMSG_FRIEND_LIST: ", (int)count, " entries"); + LOG_INFO("SMSG_FRIEND_LIST: ", static_cast(count), " entries"); // Rebuild friend contacts (keep ignores from previous contact_ entries) contacts_.erase(std::remove_if(contacts_.begin(), contacts_.end(), @@ -18300,15 +23556,13 @@ void GameHandler::handleFriendList(network::Packet& packet) { } // Track as a friend GUID; resolve name via name query friendGuids_.insert(guid); - auto nit = playerNameCache.find(guid); - std::string name; - if (nit != playerNameCache.end()) { - name = nit->second; + std::string name = lookupName(guid); + if (!name.empty()) { friendsCache[name] = guid; - LOG_INFO(" Friend: ", name, " status=", (int)status); + LOG_INFO(" Friend: ", name, " status=", static_cast(status)); } else { LOG_INFO(" Friend guid=0x", std::hex, guid, std::dec, - " status=", (int)status, " (name pending)"); + " status=", static_cast(status), " (name pending)"); queryPlayerName(guid); } ContactEntry entry; @@ -18321,6 +23575,7 @@ void GameHandler::handleFriendList(network::Packet& packet) { entry.classId = classId; contacts_.push_back(std::move(entry)); } + fireAddonEvent("FRIENDLIST_UPDATE", {}); } void GameHandler::handleContactList(network::Packet& packet) { @@ -18336,9 +23591,9 @@ void GameHandler::handleContactList(network::Packet& packet) { // if status != 0: // uint32 area, uint32 level, uint32 class // Short/keepalive variant (1-7 bytes): consume silently. - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 8) { - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } lastContactListMask_ = packet.readUInt32(); @@ -18362,9 +23617,9 @@ void GameHandler::handleContactList(network::Packet& packet) { classId = packet.readUInt32(); } friendGuids_.insert(guid); - auto nit = playerNameCache.find(guid); - if (nit != playerNameCache.end()) { - friendsCache[nit->second] = guid; + const auto& fname = lookupName(guid); + if (!fname.empty()) { + friendsCache[fname] = guid; } else { queryPlayerName(guid); } @@ -18378,12 +23633,16 @@ void GameHandler::handleContactList(network::Packet& packet) { entry.areaId = areaId; entry.level = level; entry.classId = classId; - auto nit = playerNameCache.find(guid); - if (nit != playerNameCache.end()) entry.name = nit->second; + entry.name = lookupName(guid); contacts_.push_back(std::move(entry)); } LOG_INFO("SMSG_CONTACT_LIST: mask=", lastContactListMask_, " count=", lastContactListCount_); + if (addonEventCallback_) { + fireAddonEvent("FRIENDLIST_UPDATE", {}); + if (lastContactListMask_ & 0x2) // ignore list + fireAddonEvent("IGNORELIST_UPDATE", {}); + } } void GameHandler::handleFriendStatus(network::Packet& packet) { @@ -18393,13 +23652,16 @@ void GameHandler::handleFriendStatus(network::Packet& packet) { return; } - // Look up player name from GUID + // Look up player name: contacts_ (populated by SMSG_FRIEND_LIST) > playerNameCache std::string playerName; - auto it = playerNameCache.find(data.guid); - if (it != playerNameCache.end()) { - playerName = it->second; - } else { - playerName = "Unknown"; + { + auto cit2 = std::find_if(contacts_.begin(), contacts_.end(), + [&](const ContactEntry& e){ return e.guid == data.guid; }); + if (cit2 != contacts_.end() && !cit2->name.empty()) { + playerName = cit2->name; + } else { + playerName = lookupName(data.guid); + } } // Update friends cache @@ -18458,11 +23720,12 @@ void GameHandler::handleFriendStatus(network::Packet& packet) { addSystemChatMessage(playerName + " is ignoring you."); break; default: - LOG_INFO("Friend status: ", (int)data.status, " for ", playerName); + LOG_INFO("Friend status: ", static_cast(data.status), " for ", playerName); break; } - LOG_INFO("Friend status update: ", playerName, " status=", (int)data.status); + LOG_INFO("Friend status update: ", playerName, " status=", static_cast(data.status)); + fireAddonEvent("FRIENDLIST_UPDATE", {}); } void GameHandler::handleRandomRoll(network::Packet& packet) { @@ -18477,12 +23740,8 @@ void GameHandler::handleRandomRoll(network::Packet& packet) { if (data.rollerGuid == playerGuid) { rollerName = "You"; } else { - auto it = playerNameCache.find(data.rollerGuid); - if (it != playerNameCache.end()) { - rollerName = it->second; - } else { - rollerName = "Someone"; - } + rollerName = lookupName(data.rollerGuid); + if (rollerName.empty()) rollerName = "Someone"; } // Build message @@ -18510,14 +23769,18 @@ void GameHandler::handleLogoutResponse(network::Packet& packet) { // Success - logout initiated if (data.instant) { addSystemChatMessage("Logging out..."); + logoutCountdown_ = 0.0f; } else { addSystemChatMessage("Logging out in 20 seconds..."); + logoutCountdown_ = 20.0f; } - LOG_INFO("Logout response: success, instant=", (int)data.instant); + LOG_INFO("Logout response: success, instant=", static_cast(data.instant)); + fireAddonEvent("PLAYER_LOGOUT", {}); } else { // Failure addSystemChatMessage("Cannot logout right now."); loggingOut_ = false; + logoutCountdown_ = 0.0f; LOG_WARNING("Logout failed, result=", data.result); } } @@ -18525,6 +23788,7 @@ void GameHandler::handleLogoutResponse(network::Packet& packet) { void GameHandler::handleLogoutComplete(network::Packet& /*packet*/) { addSystemChatMessage("Logout complete."); loggingOut_ = false; + logoutCountdown_ = 0.0f; LOG_INFO("Logout complete"); // Server will disconnect us } @@ -18539,7 +23803,7 @@ uint32_t GameHandler::generateClientSeed() { void GameHandler::setState(WorldState newState) { if (state != newState) { - LOG_DEBUG("World state: ", (int)state, " -> ", (int)newState); + LOG_DEBUG("World state: ", static_cast(state), " -> ", static_cast(newState)); state = newState; } } @@ -18570,6 +23834,15 @@ uint32_t GameHandler::getSkillCategory(uint32_t skillId) const { return (it != skillLineCategories_.end()) ? it->second : 0; } +bool GameHandler::isProfessionSpell(uint32_t spellId) const { + auto slIt = spellToSkillLine_.find(spellId); + if (slIt == spellToSkillLine_.end()) return false; + auto catIt = skillLineCategories_.find(slIt->second); + if (catIt == skillLineCategories_.end()) return false; + // Category 11 = profession (Blacksmithing, etc.), 9 = secondary (Cooking, First Aid, Fishing) + return catIt->second == 11 || catIt->second == 9; +} + void GameHandler::loadSkillLineDbc() { if (skillLineDbcLoaded_) return; skillLineDbcLoaded_ = true; @@ -18621,10 +23894,20 @@ void GameHandler::extractSkillFields(const std::map& fields) uint16_t value = raw1 & 0xFFFF; uint16_t maxValue = (raw1 >> 16) & 0xFFFF; + uint16_t bonusTemp = 0; + uint16_t bonusPerm = 0; + auto bonusIt = fields.find(static_cast(baseField + 2)); + if (bonusIt != fields.end()) { + bonusTemp = bonusIt->second & 0xFFFF; + bonusPerm = (bonusIt->second >> 16) & 0xFFFF; + } + PlayerSkill skill; skill.skillId = skillId; skill.value = value; skill.maxValue = maxValue; + skill.bonusTemp = bonusTemp; + skill.bonusPerm = bonusPerm; newSkills[skillId] = skill; } @@ -18651,7 +23934,19 @@ void GameHandler::extractSkillFields(const std::map& fields) } } + bool skillsChanged = (newSkills.size() != playerSkills_.size()); + if (!skillsChanged) { + for (const auto& [id, sk] : newSkills) { + auto it = playerSkills_.find(id); + if (it == playerSkills_.end() || it->second.value != sk.value) { + skillsChanged = true; + break; + } + } + } playerSkills_ = std::move(newSkills); + if (skillsChanged) + fireAddonEvent("SKILL_LINES_CHANGED", {}); } void GameHandler::extractExploredZoneFields(const std::map& fields) { @@ -18698,6 +23993,21 @@ std::string GameHandler::getCharacterConfigDir() { return dir; } +static const std::string EMPTY_MACRO_TEXT; + +const std::string& GameHandler::getMacroText(uint32_t macroId) const { + auto it = macros_.find(macroId); + return (it != macros_.end()) ? it->second : EMPTY_MACRO_TEXT; +} + +void GameHandler::setMacroText(uint32_t macroId, const std::string& text) { + if (text.empty()) + macros_.erase(macroId); + else + macros_[macroId] = text; + saveCharacterConfig(); +} + void GameHandler::saveCharacterConfig() { const Character* ch = getActiveCharacter(); if (!ch || ch->name.empty()) return; @@ -18724,6 +24034,21 @@ void GameHandler::saveCharacterConfig() { out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n"; } + // Save client-side macro text (escape newlines as \n literal) + for (const auto& [id, text] : macros_) { + if (!text.empty()) { + std::string escaped; + escaped.reserve(text.size()); + for (char c : text) { + if (c == '\n') { escaped += "\\n"; } + else if (c == '\r') { /* skip CR */ } + else if (c == '\\') { escaped += "\\\\"; } + else { escaped += c; } + } + out << "macro_" << id << "_text=" << escaped << "\n"; + } + } + // Save quest log out << "quest_log_count=" << questLog_.size() << "\n"; for (size_t i = 0; i < questLog_.size(); i++) { @@ -18733,6 +24058,16 @@ void GameHandler::saveCharacterConfig() { out << "quest_" << i << "_complete=" << (quest.complete ? 1 : 0) << "\n"; } + // Save tracked quest IDs so the quest tracker restores on login + if (!trackedQuestIds_.empty()) { + std::string ids; + for (uint32_t qid : trackedQuestIds_) { + if (!ids.empty()) ids += ','; + ids += std::to_string(qid); + } + out << "tracked_quests=" << ids << "\n"; + } + LOG_INFO("Character config saved to ", path); } @@ -18764,6 +24099,43 @@ void GameHandler::loadCharacterConfig() { try { savedGender = std::stoi(val); } catch (...) {} } else if (key == "use_female_model") { try { savedUseFemaleModel = std::stoi(val); } catch (...) {} + } else if (key.rfind("macro_", 0) == 0) { + // Parse macro_N_text + size_t firstUnder = 6; // length of "macro_" + size_t secondUnder = key.find('_', firstUnder); + if (secondUnder == std::string::npos) continue; + uint32_t macroId = 0; + try { macroId = static_cast(std::stoul(key.substr(firstUnder, secondUnder - firstUnder))); } catch (...) { continue; } + if (key.substr(secondUnder + 1) == "text" && !val.empty()) { + // Unescape \n and \\ sequences + std::string unescaped; + unescaped.reserve(val.size()); + for (size_t i = 0; i < val.size(); ++i) { + if (val[i] == '\\' && i + 1 < val.size()) { + if (val[i+1] == 'n') { unescaped += '\n'; ++i; } + else if (val[i+1] == '\\') { unescaped += '\\'; ++i; } + else { unescaped += val[i]; } + } else { + unescaped += val[i]; + } + } + macros_[macroId] = std::move(unescaped); + } + } else if (key == "tracked_quests" && !val.empty()) { + // Parse comma-separated quest IDs + trackedQuestIds_.clear(); + size_t tqPos = 0; + while (tqPos <= val.size()) { + size_t comma = val.find(',', tqPos); + std::string idStr = (comma != std::string::npos) + ? val.substr(tqPos, comma - tqPos) : val.substr(tqPos); + try { + uint32_t qid = static_cast(std::stoul(idStr)); + if (qid != 0) trackedQuestIds_.insert(qid); + } catch (...) {} + if (comma == std::string::npos) break; + tqPos = comma + 1; + } } else if (key.rfind("action_bar_", 0) == 0) { // Parse action_bar_N_type or action_bar_N_id size_t firstUnderscore = 11; // length of "action_bar_" @@ -18906,11 +24278,13 @@ void GameHandler::updateAttachedTransportChildren(float /*deltaTime*/) { // ============================================================ void GameHandler::closeMailbox() { + bool wasOpen = mailboxOpen_; mailboxOpen_ = false; mailboxGuid_ = 0; mailInbox_.clear(); selectedMailIndex_ = -1; showMailCompose_ = false; + if (wasOpen) fireAddonEvent("MAIL_CLOSED", {}); } void GameHandler::refreshMailList() { @@ -19049,9 +24423,9 @@ void GameHandler::mailTakeMoney(uint32_t mailId) { socket->send(packet); } -void GameHandler::mailTakeItem(uint32_t mailId, uint32_t itemIndex) { +void GameHandler::mailTakeItem(uint32_t mailId, uint32_t itemGuidLow) { if (state != WorldState::IN_WORLD || !socket || mailboxGuid_ == 0) return; - auto packet = packetParsers_->buildMailTakeItem(mailboxGuid_, mailId, itemIndex); + auto packet = packetParsers_->buildMailTakeItem(mailboxGuid_, mailId, itemGuidLow); socket->send(packet); } @@ -19076,7 +24450,7 @@ void GameHandler::mailMarkAsRead(uint32_t mailId) { } void GameHandler::handleShowMailbox(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("SMSG_SHOW_MAILBOX too short"); return; } @@ -19087,12 +24461,13 @@ void GameHandler::handleShowMailbox(network::Packet& packet) { hasNewMail_ = false; selectedMailIndex_ = -1; showMailCompose_ = false; + fireAddonEvent("MAIL_SHOW", {}); // Request inbox contents refreshMailList(); } void GameHandler::handleMailListResult(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 1) { LOG_WARNING("SMSG_MAIL_LIST_RESULT too short (", remaining, " bytes)"); return; @@ -19127,10 +24502,11 @@ void GameHandler::handleMailListResult(network::Packet& packet) { selectedMailIndex_ = -1; showMailCompose_ = false; } + fireAddonEvent("MAIL_INBOX_UPDATE", {}); } void GameHandler::handleSendMailResult(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { LOG_WARNING("SMSG_SEND_MAIL_RESULT too short"); return; } @@ -19194,13 +24570,14 @@ void GameHandler::handleSendMailResult(network::Packet& packet) { void GameHandler::handleReceivedMail(network::Packet& packet) { // Server notifies us that new mail arrived - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasRemaining(4)) { float nextMailTime = packet.readFloat(); (void)nextMailTime; } LOG_INFO("SMSG_RECEIVED_MAIL: New mail arrived!"); hasNewMail_ = true; addSystemChatMessage("New mail has arrived."); + fireAddonEvent("UPDATE_PENDING_MAIL", {}); // If mailbox is open, refresh if (mailboxOpen_) { refreshMailList(); @@ -19211,7 +24588,7 @@ void GameHandler::handleQueryNextMailTime(network::Packet& packet) { // Server response to MSG_QUERY_NEXT_MAIL_TIME // If there's pending mail, the packet contains a float with time until next mail delivery // A value of 0.0 or the presence of mail entries means there IS mail waiting - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining >= 4) { float nextMailTime = packet.readFloat(); // In Vanilla: 0x00000000 = has mail, 0xC7A8C000 (big negative) = no mail @@ -19245,8 +24622,10 @@ void GameHandler::openBank(uint64_t guid) { } void GameHandler::closeBank() { + bool wasOpen = bankOpen_; bankOpen_ = false; bankerGuid_ = 0; + if (wasOpen) fireAddonEvent("BANKFRAME_CLOSED", {}); } void GameHandler::buyBankSlot() { @@ -19273,10 +24652,11 @@ void GameHandler::withdrawItem(uint8_t srcBag, uint8_t srcSlot) { } void GameHandler::handleShowBank(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; bankerGuid_ = packet.readUInt64(); bankOpen_ = true; gossipWindowOpen = false; // Close gossip when bank opens + fireAddonEvent("BANKFRAME_OPENED", {}); // Bank items are already tracked via update fields (bank slot GUIDs) // Trigger rebuild to populate bank slots in inventory rebuildOnlineInventory(); @@ -19292,7 +24672,7 @@ void GameHandler::handleShowBank(network::Packet& packet) { } void GameHandler::handleBuyBankSlotResult(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t result = packet.readUInt32(); LOG_INFO("SMSG_BUY_BANK_SLOT_RESULT: result=", result); // AzerothCore/TrinityCore: 0=TOO_MANY, 1=INSUFFICIENT_FUNDS, 2=NOT_BANKER, 3=OK @@ -19378,7 +24758,7 @@ void GameHandler::handleGuildBankList(network::Packet& packet) { if (item.itemEntry != 0) ensureItemInfo(item.itemEntry); } - LOG_INFO("SMSG_GUILD_BANK_LIST: tab=", (int)data.tabId, + LOG_INFO("SMSG_GUILD_BANK_LIST: tab=", static_cast(data.tabId), " items=", data.tabItems.size(), " tabs=", data.tabs.size(), " money=", data.money); @@ -19395,8 +24775,10 @@ void GameHandler::openAuctionHouse(uint64_t guid) { } void GameHandler::closeAuctionHouse() { + bool wasOpen = auctionOpen_; auctionOpen_ = false; auctioneerGuid_ = 0; + if (wasOpen) fireAddonEvent("AUCTION_HOUSE_CLOSED", {}); } void GameHandler::auctionSearch(const std::string& name, uint8_t levelMin, uint8_t levelMax, @@ -19477,12 +24859,13 @@ void GameHandler::handleAuctionHello(network::Packet& packet) { auctionHouseId_ = data.auctionHouseId; auctionOpen_ = true; gossipWindowOpen = false; // Close gossip when auction house opens + fireAddonEvent("AUCTION_HOUSE_SHOW", {}); auctionActiveTab_ = 0; auctionBrowseResults_ = AuctionListResult{}; auctionOwnerResults_ = AuctionListResult{}; auctionBidderResults_ = AuctionListResult{}; LOG_INFO("MSG_AUCTION_HELLO: auctioneer=0x", std::hex, data.auctioneerGuid, std::dec, - " house=", data.auctionHouseId, " enabled=", (int)data.enabled); + " house=", data.auctionHouseId, " enabled=", static_cast(data.enabled)); } void GameHandler::handleAuctionListResult(network::Packet& packet) { @@ -19562,6 +24945,7 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) { "DB error", "Restricted account"}; const char* errName = (result.errorCode < 9) ? errors[result.errorCode] : "Unknown"; std::string msg = std::string("Auction ") + actionName + " failed: " + errName; + addUIError(msg); addSystemChatMessage(msg); } LOG_INFO("SMSG_AUCTION_COMMAND_RESULT: action=", actionName, @@ -19574,7 +24958,7 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) { // --------------------------------------------------------------------------- void GameHandler::handleItemTextQueryResponse(network::Packet& packet) { - size_t rem = packet.getSize() - packet.getReadPos(); + size_t rem = packet.getRemainingSize(); if (rem < 9) return; // guid(8) + isEmpty(1) /*uint64_t guid =*/ packet.readUInt64(); @@ -19583,12 +24967,12 @@ void GameHandler::handleItemTextQueryResponse(network::Packet& packet) { itemText_ = packet.readString(); itemTextOpen_= !itemText_.empty(); } - LOG_DEBUG("SMSG_ITEM_TEXT_QUERY_RESPONSE: isEmpty=", (int)isEmpty, + LOG_DEBUG("SMSG_ITEM_TEXT_QUERY_RESPONSE: isEmpty=", static_cast(isEmpty), " len=", itemText_.size()); } void GameHandler::queryItemText(uint64_t itemGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; network::Packet pkt(wireOpcode(Opcode::CMSG_ITEM_TEXT_QUERY)); pkt.writeUInt64(itemGuid); socket->send(pkt); @@ -19601,20 +24985,22 @@ void GameHandler::queryItemText(uint64_t itemGuid) { // --------------------------------------------------------------------------- void GameHandler::handleQuestConfirmAccept(network::Packet& packet) { - size_t rem = packet.getSize() - packet.getReadPos(); + size_t rem = packet.getRemainingSize(); if (rem < 4) return; sharedQuestId_ = packet.readUInt32(); sharedQuestTitle_ = packet.readString(); - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { sharedQuestSharerGuid_ = packet.readUInt64(); } sharedQuestSharerName_.clear(); - auto entity = entityManager.getEntity(sharedQuestSharerGuid_); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(sharedQuestSharerGuid_)) { sharedQuestSharerName_ = unit->getName(); } + if (sharedQuestSharerName_.empty()) { + sharedQuestSharerName_ = lookupName(sharedQuestSharerGuid_); + } if (sharedQuestSharerName_.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", @@ -19649,19 +25035,21 @@ void GameHandler::declineSharedQuest() { // --------------------------------------------------------------------------- void GameHandler::handleSummonRequest(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 16) return; + if (!packet.hasRemaining(16)) return; summonerGuid_ = packet.readUInt64(); - /*uint32_t zoneId =*/ packet.readUInt32(); + uint32_t zoneId = packet.readUInt32(); uint32_t timeoutMs = packet.readUInt32(); summonTimeoutSec_ = timeoutMs / 1000.0f; pendingSummonRequest_= true; summonerName_.clear(); - auto entity = entityManager.getEntity(summonerGuid_); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(summonerGuid_)) { summonerName_ = unit->getName(); } + if (summonerName_.empty()) { + summonerName_ = lookupName(summonerGuid_); + } if (summonerName_.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", @@ -19669,9 +25057,15 @@ void GameHandler::handleSummonRequest(network::Packet& packet) { summonerName_ = tmp; } - addSystemChatMessage(summonerName_ + " is summoning you."); + std::string msg = summonerName_ + " is summoning you"; + std::string zoneName = getAreaName(zoneId); + if (!zoneName.empty()) + msg += " to " + zoneName; + msg += '.'; + addSystemChatMessage(msg); LOG_INFO("SMSG_SUMMON_REQUEST: summoner=", summonerName_, - " timeout=", summonTimeoutSec_, "s"); + " zoneId=", zoneId, " timeout=", summonTimeoutSec_, "s"); + fireAddonEvent("CONFIRM_SUMMON", {}); } void GameHandler::acceptSummon() { @@ -19703,20 +25097,22 @@ void GameHandler::declineSummon() { // --------------------------------------------------------------------------- void GameHandler::handleTradeStatus(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t status = packet.readUInt32(); switch (status) { case 1: { // BEGIN_TRADE — incoming request; read initiator GUID - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { tradePeerGuid_ = packet.readUInt64(); } // Resolve name from entity list tradePeerName_.clear(); - auto entity = entityManager.getEntity(tradePeerGuid_); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(tradePeerGuid_)) { tradePeerName_ = unit->getName(); } + if (tradePeerName_.empty()) { + tradePeerName_ = lookupName(tradePeerGuid_); + } if (tradePeerName_.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", @@ -19725,6 +25121,7 @@ void GameHandler::handleTradeStatus(network::Packet& packet) { } tradeStatus_ = TradeStatus::PendingIncoming; addSystemChatMessage(tradePeerName_ + " wants to trade with you."); + fireAddonEvent("TRADE_REQUEST", {}); break; } case 2: // OPEN_WINDOW @@ -19734,19 +25131,27 @@ void GameHandler::handleTradeStatus(network::Packet& packet) { peerTradeGold_ = 0; tradeStatus_ = TradeStatus::Open; addSystemChatMessage("Trade window opened."); + fireAddonEvent("TRADE_SHOW", {}); break; - case 3: // CANCELLED - case 9: // REJECTED + case 3: // CANCELLED case 12: // CLOSE_WINDOW resetTradeState(); addSystemChatMessage("Trade cancelled."); + fireAddonEvent("TRADE_CLOSED", {}); + break; + case 9: // REJECTED — other player clicked Decline + resetTradeState(); + addSystemChatMessage("Trade declined."); + fireAddonEvent("TRADE_CLOSED", {}); break; case 4: // ACCEPTED (partner accepted) tradeStatus_ = TradeStatus::Accepted; addSystemChatMessage("Trade accepted. Awaiting other player..."); + fireAddonEvent("TRADE_ACCEPT_UPDATE", {}); break; case 8: // COMPLETE addSystemChatMessage("Trade complete!"); + fireAddonEvent("TRADE_CLOSED", {}); resetTradeState(); break; case 7: // BACK_TO_TRADE (unaccepted after a change) @@ -19779,7 +25184,7 @@ void GameHandler::declineTradeRequest() { } void GameHandler::acceptTrade() { - if (tradeStatus_ != TradeStatus::Open || !socket) return; + if (!isTradeOpen() || !socket) return; tradeStatus_ = TradeStatus::Accepted; socket->send(AcceptTradePacket::build()); } @@ -19816,50 +25221,49 @@ void GameHandler::resetTradeState() { } void GameHandler::handleTradeStatusExtended(network::Packet& packet) { - // WotLK 3.3.5a SMSG_TRADE_STATUS_EXTENDED format: - // uint8 isSelfState (1 = my trade window, 0 = peer's) - // uint32 tradeId - // uint32 slotCount (7: 6 normal + 1 extra for enchanting) - // Per slot (up to slotCount): - // uint8 slotIndex - // uint32 itemId - // uint32 displayId - // uint32 stackCount - // uint8 isWrapped - // uint64 giftCreatorGuid - // uint32 enchantId (and several more enchant/stat fields) - // ... (complex; we parse only the essential fields) - // uint64 coins (gold offered by the sender of this message) + // SMSG_TRADE_STATUS_EXTENDED format differs by expansion: + // + // Classic/TBC: + // uint8 isSelf + uint32 slotCount + [slots] + uint64 coins + // Per slot tail (after isWrapped): giftCreatorGuid(8) + enchants(24) + + // randomPropertyId(4) + suffixFactor(4) + durability(4) + maxDurability(4) = 48 bytes + // + // WotLK 3.3.5a adds: + // uint32 tradeId (after isSelf, before slotCount) + // Per slot: + createPlayedTime(4) at end of trail → trail = 52 bytes + // + // Minimum: isSelf(1) + [tradeId(4)] + slotCount(4) = 5 or 9 bytes + const bool isWotLK = isActiveExpansion("wotlk"); + size_t minHdr = isWotLK ? 9u : 5u; + if (!packet.hasRemaining(minHdr)) return; - size_t rem = packet.getSize() - packet.getReadPos(); - if (rem < 9) return; + uint8_t isSelf = packet.readUInt8(); + if (isWotLK) { + /*uint32_t tradeId =*/ packet.readUInt32(); // WotLK-only field + } + uint32_t slotCount = packet.readUInt32(); - uint8_t isSelf = packet.readUInt8(); - uint32_t tradeId = packet.readUInt32(); (void)tradeId; - uint32_t slotCount= packet.readUInt32(); + // Per-slot tail bytes after isWrapped: + // Classic/TBC: giftCreatorGuid(8) + enchants(24) + stats(16) = 48 + // WotLK: same + createPlayedTime(4) = 52 + const size_t SLOT_TRAIL = isWotLK ? 52u : 48u; auto& slots = isSelf ? myTradeSlots_ : peerTradeSlots_; - for (uint32_t i = 0; i < slotCount && (packet.getSize() - packet.getReadPos()) >= 14; ++i) { + for (uint32_t i = 0; i < slotCount && packet.hasRemaining(14); ++i) { uint8_t slotIdx = packet.readUInt8(); uint32_t itemId = packet.readUInt32(); uint32_t displayId = packet.readUInt32(); uint32_t stackCount = packet.readUInt32(); bool isWrapped = false; - if (packet.getSize() - packet.getReadPos() >= 1) { + if (packet.hasRemaining(1)) { isWrapped = (packet.readUInt8() != 0); } - // AzerothCore 3.3.5a SendUpdateTrade() field order after isWrapped: - // giftCreatorGuid (8) + PERM enchant (4) + SOCK enchants×3 (12) - // + BONUS enchant (4) + TEMP enchant (4) [total enchants: 24] - // + randomPropertyId (4) + suffixFactor (4) - // + durability (4) + maxDurability (4) + createPlayedTime (4) = 52 bytes - constexpr size_t SLOT_TRAIL = 52; - if (packet.getSize() - packet.getReadPos() >= SLOT_TRAIL) { + if (packet.hasRemaining(SLOT_TRAIL)) { packet.setReadPos(packet.getReadPos() + SLOT_TRAIL); } else { - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } (void)isWrapped; @@ -19874,7 +25278,7 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) { } // Gold offered (uint64 copper) - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { uint64_t coins = packet.readUInt64(); if (isSelf) myTradeGold_ = coins; else peerTradeGold_ = coins; @@ -19885,7 +25289,7 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) { if (s.occupied && s.itemId != 0) queryItemInfo(s.itemId, 0); } - LOG_DEBUG("SMSG_TRADE_STATUS_EXTENDED: isSelf=", (int)isSelf, + LOG_DEBUG("SMSG_TRADE_STATUS_EXTENDED: isSelf=", static_cast(isSelf), " myGold=", myTradeGold_, " peerGold=", peerTradeGold_); } @@ -19900,7 +25304,7 @@ void GameHandler::handleLootRoll(network::Packet& packet) { // uint32 itemId, uint8 rollNumber, uint8 rollType (26 bytes) const bool isWotLK = isActiveExpansion("wotlk"); const size_t minSize = isWotLK ? 34u : 26u; - size_t rem = packet.getSize() - packet.getReadPos(); + size_t rem = packet.getRemainingSize(); if (rem < minSize) return; uint64_t objectGuid = packet.readUInt64(); @@ -19921,12 +25325,15 @@ void GameHandler::handleLootRoll(network::Packet& packet) { pendingLootRoll_.objectGuid = objectGuid; pendingLootRoll_.slot = slot; pendingLootRoll_.itemId = itemId; + pendingLootRoll_.playerRolls.clear(); // Ensure item info is in cache; query if not queryItemInfo(itemId, 0); // Look up item name from cache auto* info = getItemInfo(itemId); pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); pendingLootRoll_.itemQuality = info ? static_cast(info->quality) : 0; + pendingLootRoll_.rollCountdownMs = 60000; + pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now(); LOG_INFO("SMSG_LOOT_ROLL: need/greed prompt for item=", itemId, " (", pendingLootRoll_.itemName, ") slot=", slot); return; @@ -19937,19 +25344,39 @@ void GameHandler::handleLootRoll(network::Packet& packet) { const char* rollName = (rollType < 4) ? rollNames[rollType] : "Pass"; std::string rollerName; - auto entity = entityManager.getEntity(rollerGuid); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(rollerGuid)) { rollerName = unit->getName(); } if (rollerName.empty()) rollerName = "Someone"; - auto* info = getItemInfo(itemId); - std::string iName = info ? info->name : std::to_string(itemId); + // Track in the live roll list while our popup is open for the same item + if (pendingLootRollActive_ && + pendingLootRoll_.objectGuid == objectGuid && + pendingLootRoll_.slot == slot) { + bool found = false; + for (auto& r : pendingLootRoll_.playerRolls) { + if (r.playerName == rollerName) { + r.rollNum = rollNum; + r.rollType = rollType; + found = true; + break; + } + } + if (!found) { + LootRollEntry::PlayerRollResult prr; + prr.playerName = rollerName; + prr.rollNum = rollNum; + prr.rollType = rollType; + pendingLootRoll_.playerRolls.push_back(std::move(prr)); + } + } - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s rolls %s (%d) on [%s]", - rollerName.c_str(), rollName, static_cast(rollNum), iName.c_str()); - addSystemChatMessage(buf); + auto* info = getItemInfo(itemId); + std::string iName = info && !info->name.empty() ? info->name : std::to_string(itemId); + uint32_t rollItemQuality = info ? info->quality : 1u; + std::string rollItemLink = buildItemLink(itemId, rollItemQuality, iName); + + addSystemChatMessage(rollerName + " rolls " + rollName + " (" + std::to_string(rollNum) + ") on " + rollItemLink); LOG_DEBUG("SMSG_LOOT_ROLL: ", rollerName, " rolled ", rollName, " (", rollNum, ") on item ", itemId); @@ -19963,16 +25390,17 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { // uint32 itemId, uint8 rollNumber, uint8 rollType (26 bytes) const bool isWotLK = isActiveExpansion("wotlk"); const size_t minSize = isWotLK ? 34u : 26u; - size_t rem = packet.getSize() - packet.getReadPos(); + size_t rem = packet.getRemainingSize(); if (rem < minSize) return; /*uint64_t objectGuid =*/ packet.readUInt64(); /*uint32_t slot =*/ packet.readUInt32(); uint64_t winnerGuid = packet.readUInt64(); uint32_t itemId = packet.readUInt32(); + int32_t wonRandProp = 0; if (isWotLK) { /*uint32_t randSuffix =*/ packet.readUInt32(); - /*uint32_t randProp =*/ packet.readUInt32(); + wonRandProp = static_cast(packet.readUInt32()); } uint8_t rollNum = packet.readUInt8(); uint8_t rollType = packet.readUInt8(); @@ -19981,8 +25409,7 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { const char* rollName = (rollType < 3) ? rollNames[rollType] : "Roll"; std::string winnerName; - auto entity = entityManager.getEntity(winnerGuid); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(winnerGuid)) { winnerName = unit->getName(); } if (winnerName.empty()) { @@ -19990,23 +25417,24 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { } auto* info = getItemInfo(itemId); - std::string iName = info ? info->name : std::to_string(itemId); - - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s wins [%s] (%s %d)!", - winnerName.c_str(), iName.c_str(), rollName, static_cast(rollNum)); - addSystemChatMessage(buf); - - // Clear pending roll if it was ours - if (pendingLootRollActive_ && winnerGuid == playerGuid) { - pendingLootRollActive_ = false; + std::string iName = info && !info->name.empty() ? info->name : std::to_string(itemId); + if (wonRandProp != 0) { + std::string suffix = getRandomPropertyName(wonRandProp); + if (!suffix.empty()) iName += " " + suffix; } + uint32_t wonItemQuality = info ? info->quality : 1u; + std::string wonItemLink = buildItemLink(itemId, wonItemQuality, iName); + + addSystemChatMessage(winnerName + " wins " + wonItemLink + " (" + rollName + " " + std::to_string(rollNum) + ")!"); + + // Dismiss roll popup — roll contest is over regardless of who won + pendingLootRollActive_ = false; LOG_INFO("SMSG_LOOT_ROLL_WON: winner=", winnerName, " item=", itemId, " roll=", rollName, "(", rollNum, ")"); } void GameHandler::sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; pendingLootRollActive_ = false; network::Packet pkt(wireOpcode(Opcode::CMSG_LOOT_ROLL)); @@ -20027,6 +25455,58 @@ void GameHandler::sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollT // PackedTime date — uint32 bitfield (seconds since epoch) // uint32 realmFirst — how many on realm also got it (0 = realm first) // --------------------------------------------------------------------------- +void GameHandler::loadTitleNameCache() const { + if (titleNameCacheLoaded_) return; + titleNameCacheLoaded_ = true; + + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + + auto dbc = am->loadDBC("CharTitles.dbc"); + if (!dbc || !dbc->isLoaded() || dbc->getFieldCount() < 5) return; + + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("CharTitles") : nullptr; + + uint32_t titleField = layout ? layout->field("Title") : 2; + uint32_t bitField = layout ? layout->field("TitleBit") : 36; + if (titleField == 0xFFFFFFFF) titleField = 2; + if (bitField == 0xFFFFFFFF) bitField = static_cast(dbc->getFieldCount() - 1); + + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t bit = dbc->getUInt32(i, bitField); + if (bit == 0) continue; + std::string name = dbc->getString(i, titleField); + if (!name.empty()) titleNameCache_[bit] = std::move(name); + } + LOG_INFO("CharTitles: loaded ", titleNameCache_.size(), " title names from DBC"); +} + +std::string GameHandler::getFormattedTitle(uint32_t bit) const { + loadTitleNameCache(); + auto it = titleNameCache_.find(bit); + if (it == titleNameCache_.end() || it->second.empty()) return {}; + + const auto& ln2 = lookupName(playerGuid); + static const std::string kUnknown = "unknown"; + const std::string& pName = ln2.empty() ? kUnknown : ln2; + + const std::string& fmt = it->second; + size_t pos = fmt.find("%s"); + if (pos != std::string::npos) { + return fmt.substr(0, pos) + pName + fmt.substr(pos + 2); + } + return fmt; +} + +void GameHandler::sendSetTitle(int32_t bit) { + if (!isInWorld()) return; + auto packet = SetTitlePacket::build(bit); + socket->send(packet); + chosenTitleBit_ = bit; + LOG_INFO("sendSetTitle: bit=", bit); +} + void GameHandler::loadAchievementNameCache() { if (achievementNameCacheLoaded_) return; achievementNameCacheLoaded_ = true; @@ -20041,23 +25521,34 @@ void GameHandler::loadAchievementNameCache() { ? pipeline::getActiveDBCLayout()->getLayout("Achievement") : nullptr; uint32_t titleField = achL ? achL->field("Title") : 4; if (titleField == 0xFFFFFFFF) titleField = 4; + uint32_t descField = achL ? achL->field("Description") : 0xFFFFFFFF; + uint32_t ptsField = achL ? achL->field("Points") : 0xFFFFFFFF; + uint32_t fieldCount = dbc->getFieldCount(); for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { uint32_t id = dbc->getUInt32(i, 0); if (id == 0) continue; std::string title = dbc->getString(i, titleField); if (!title.empty()) achievementNameCache_[id] = std::move(title); + if (descField != 0xFFFFFFFF && descField < fieldCount) { + std::string desc = dbc->getString(i, descField); + if (!desc.empty()) achievementDescCache_[id] = std::move(desc); + } + if (ptsField != 0xFFFFFFFF && ptsField < fieldCount) { + uint32_t pts = dbc->getUInt32(i, ptsField); + if (pts > 0) achievementPointsCache_[id] = pts; + } } LOG_INFO("Achievement: loaded ", achievementNameCache_.size(), " names from Achievement.dbc"); } void GameHandler::handleAchievementEarned(network::Packet& packet) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 16) return; // guid(8) + id(4) + date(4) uint64_t guid = packet.readUInt64(); uint32_t achievementId = packet.readUInt32(); - /*uint32_t date =*/ packet.readUInt32(); // PackedTime — not displayed + uint32_t earnDate = packet.readUInt32(); // WoW PackedTime bitfield loadAchievementNameCache(); auto nameIt = achievementNameCache_.find(achievementId); @@ -20076,16 +25567,18 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { addSystemChatMessage(buf); earnedAchievements_.insert(achievementId); + achievementDates_[achievementId] = earnDate; + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playAchievementAlert(); }); if (achievementEarnedCallback_) { achievementEarnedCallback_(achievementId, achName); } } else { // Another player in the zone earned an achievement std::string senderName; - auto entity = entityManager.getEntity(guid); - if (auto* unit = dynamic_cast(entity.get())) { + if (auto* unit = getUnitByGuid(guid)) { senderName = unit->getName(); } + if (senderName.empty()) senderName = lookupName(guid); if (senderName.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", @@ -20106,6 +25599,7 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { LOG_INFO("SMSG_ACHIEVEMENT_EARNED: guid=0x", std::hex, guid, std::dec, " achievementId=", achievementId, " self=", isSelf, achName.empty() ? "" : " name=", achName); + fireAddonEvent("ACHIEVEMENT_EARNED", {std::to_string(achievementId)}); } // --------------------------------------------------------------------------- @@ -20116,23 +25610,25 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { void GameHandler::handleAllAchievementData(network::Packet& packet) { loadAchievementNameCache(); earnedAchievements_.clear(); + achievementDates_.clear(); // Parse achievement entries (id + packedDate pairs, sentinel 0xFFFFFFFF) - while (packet.getSize() - packet.getReadPos() >= 4) { + while (packet.hasRemaining(4)) { uint32_t id = packet.readUInt32(); if (id == 0xFFFFFFFF) break; - if (packet.getSize() - packet.getReadPos() < 4) break; - /*uint32_t date =*/ packet.readUInt32(); + if (!packet.hasRemaining(4)) break; + uint32_t date = packet.readUInt32(); earnedAchievements_.insert(id); + achievementDates_[id] = date; } // Parse criteria block: id + uint64 counter + uint32 date + uint32 flags, sentinel 0xFFFFFFFF criteriaProgress_.clear(); - while (packet.getSize() - packet.getReadPos() >= 4) { + while (packet.hasRemaining(4)) { uint32_t id = packet.readUInt32(); if (id == 0xFFFFFFFF) break; // counter(8) + date(4) + unknown(4) = 16 bytes - if (packet.getSize() - packet.getReadPos() < 16) break; + if (!packet.hasRemaining(16)) break; uint64_t counter = packet.readUInt64(); packet.readUInt32(); // date packet.readUInt32(); // unknown / flags @@ -20143,11 +25639,60 @@ void GameHandler::handleAllAchievementData(network::Packet& packet) { " achievements, ", criteriaProgress_.size(), " criteria"); } +// --------------------------------------------------------------------------- +// SMSG_RESPOND_INSPECT_ACHIEVEMENTS (WotLK 3.3.5a) +// Wire format: packed_guid (inspected player) + same achievement/criteria +// blocks as SMSG_ALL_ACHIEVEMENT_DATA: +// Achievement records: repeated { uint32 id, uint32 packedDate } until 0xFFFFFFFF sentinel +// Criteria records: repeated { uint32 id, uint64 counter, uint32 date, uint32 unk } +// until 0xFFFFFFFF sentinel +// We store only the earned achievement IDs (not criteria) per inspected player. +// --------------------------------------------------------------------------- +void GameHandler::handleRespondInspectAchievements(network::Packet& packet) { + loadAchievementNameCache(); + + // Read the inspected player's packed guid + if (!packet.hasRemaining(1)) return; + uint64_t inspectedGuid = packet.readPackedGuid(); + if (inspectedGuid == 0) { + packet.skipAll(); + return; + } + + std::unordered_set achievements; + + // Achievement records: { uint32 id, uint32 packedDate } until sentinel 0xFFFFFFFF + while (packet.hasRemaining(4)) { + uint32_t id = packet.readUInt32(); + if (id == 0xFFFFFFFF) break; + if (!packet.hasRemaining(4)) break; + /*uint32_t date =*/ packet.readUInt32(); + achievements.insert(id); + } + + // Criteria records: { uint32 id, uint64 counter, uint32 date, uint32 unk } + // until sentinel 0xFFFFFFFF — consume but don't store for inspect use + while (packet.hasRemaining(4)) { + uint32_t id = packet.readUInt32(); + if (id == 0xFFFFFFFF) break; + // counter(8) + date(4) + unk(4) = 16 bytes + if (!packet.hasRemaining(16)) break; + packet.readUInt64(); // counter + packet.readUInt32(); // date + packet.readUInt32(); // unk + } + + inspectedPlayerAchievements_[inspectedGuid] = std::move(achievements); + + LOG_INFO("SMSG_RESPOND_INSPECT_ACHIEVEMENTS: guid=0x", std::hex, inspectedGuid, std::dec, + " achievements=", inspectedPlayerAchievements_[inspectedGuid].size()); +} + // --------------------------------------------------------------------------- // Faction name cache (lazily loaded from Faction.dbc) // --------------------------------------------------------------------------- -void GameHandler::loadFactionNameCache() { +void GameHandler::loadFactionNameCache() const { if (factionNameCacheLoaded_) return; factionNameCacheLoaded_ = true; @@ -20159,32 +25704,75 @@ void GameHandler::loadFactionNameCache() { // Faction.dbc WotLK 3.3.5a field layout: // 0: ID - // 1-4: ReputationRaceMask[4] - // 5-8: ReputationClassMask[4] - // 9-12: ReputationBase[4] - // 13-16: ReputationFlags[4] - // 17: ParentFactionID - // 18-19: Spillover rates (floats) - // 20-21: MaxRank - // 22: Name (English locale, string ref) - constexpr uint32_t ID_FIELD = 0; - constexpr uint32_t NAME_FIELD = 22; // enUS name string + // 1: ReputationListID (-1 / 0xFFFFFFFF = no reputation tracking) + // 2-5: ReputationRaceMask[4] + // 6-9: ReputationClassMask[4] + // 10-13: ReputationBase[4] + // 14-17: ReputationFlags[4] + // 18: ParentFactionID + // 19-20: SpilloverRateIn, SpilloverRateOut (floats) + // 21-22: SpilloverMaxRankIn, SpilloverMaxRankOut + // 23: Name (English locale, string ref) + constexpr uint32_t ID_FIELD = 0; + constexpr uint32_t REPLIST_FIELD = 1; + constexpr uint32_t NAME_FIELD = 23; // enUS name string + // Classic/TBC have fewer fields; fall back gracefully + const bool hasRepListField = dbc->getFieldCount() > REPLIST_FIELD; if (dbc->getFieldCount() <= NAME_FIELD) { LOG_WARNING("Faction.dbc: unexpected field count ", dbc->getFieldCount()); - return; + // Don't abort — still try to load names from a shorter layout } + const uint32_t nameField = (dbc->getFieldCount() > NAME_FIELD) ? NAME_FIELD : 22u; uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { uint32_t factionId = dbc->getUInt32(i, ID_FIELD); if (factionId == 0) continue; - std::string name = dbc->getString(i, NAME_FIELD); - if (!name.empty()) { - factionNameCache_[factionId] = std::move(name); + if (dbc->getFieldCount() > nameField) { + std::string name = dbc->getString(i, nameField); + if (!name.empty()) { + factionNameCache_[factionId] = std::move(name); + } + } + // Build repListId ↔ factionId mapping (WotLK field 1) + if (hasRepListField) { + uint32_t repListId = dbc->getUInt32(i, REPLIST_FIELD); + if (repListId != 0xFFFFFFFFu) { + factionRepListToId_[repListId] = factionId; + factionIdToRepList_[factionId] = repListId; + } } } - LOG_INFO("Faction.dbc: loaded ", factionNameCache_.size(), " faction names"); + LOG_INFO("Faction.dbc: loaded ", factionNameCache_.size(), " faction names, ", + factionRepListToId_.size(), " with reputation tracking"); +} + +uint32_t GameHandler::getFactionIdByRepListId(uint32_t repListId) const { + loadFactionNameCache(); + auto it = factionRepListToId_.find(repListId); + return (it != factionRepListToId_.end()) ? it->second : 0u; +} + +uint32_t GameHandler::getRepListIdByFactionId(uint32_t factionId) const { + loadFactionNameCache(); + auto it = factionIdToRepList_.find(factionId); + return (it != factionIdToRepList_.end()) ? it->second : 0xFFFFFFFFu; +} + +void GameHandler::setWatchedFactionId(uint32_t factionId) { + watchedFactionId_ = factionId; + if (!isInWorld()) return; + // CMSG_SET_WATCHED_FACTION: int32 repListId (-1 = unwatch) + int32_t repListId = -1; + if (factionId != 0) { + uint32_t rl = getRepListIdByFactionId(factionId); + if (rl != 0xFFFFFFFFu) repListId = static_cast(rl); + } + network::Packet pkt(wireOpcode(Opcode::CMSG_SET_WATCHED_FACTION)); + pkt.writeUInt32(static_cast(repListId)); + socket->send(pkt); + LOG_DEBUG("CMSG_SET_WATCHED_FACTION: repListId=", repListId, " (factionId=", factionId, ")"); } std::string GameHandler::getFactionName(uint32_t factionId) const { @@ -20194,7 +25782,7 @@ std::string GameHandler::getFactionName(uint32_t factionId) const { } const std::string& GameHandler::getFactionNamePublic(uint32_t factionId) const { - const_cast(this)->loadFactionNameCache(); + loadFactionNameCache(); auto it = factionNameCache_.find(factionId); if (it != factionNameCache_.end()) return it->second; static const std::string empty; @@ -20205,7 +25793,7 @@ const std::string& GameHandler::getFactionNamePublic(uint32_t factionId) const { // Area name cache (lazy-loaded from WorldMapArea.dbc) // --------------------------------------------------------------------------- -void GameHandler::loadAreaNameCache() { +void GameHandler::loadAreaNameCache() const { if (areaNameCacheLoaded_) return; areaNameCacheLoaded_ = true; @@ -20235,11 +25823,76 @@ void GameHandler::loadAreaNameCache() { std::string GameHandler::getAreaName(uint32_t areaId) const { if (areaId == 0) return {}; - const_cast(this)->loadAreaNameCache(); + loadAreaNameCache(); auto it = areaNameCache_.find(areaId); return (it != areaNameCache_.end()) ? it->second : std::string{}; } +void GameHandler::loadMapNameCache() const { + if (mapNameCacheLoaded_) return; + mapNameCacheLoaded_ = true; + + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + + auto dbc = am->loadDBC("Map.dbc"); + if (!dbc || !dbc->isLoaded()) return; + + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t id = dbc->getUInt32(i, 0); + // Field 2 = MapName_enUS (first localized); field 1 = InternalName fallback + std::string name = dbc->getString(i, 2); + if (name.empty()) name = dbc->getString(i, 1); + if (!name.empty() && !mapNameCache_.count(id)) { + mapNameCache_[id] = std::move(name); + } + } + LOG_INFO("Map.dbc: loaded ", mapNameCache_.size(), " map names"); +} + +std::string GameHandler::getMapName(uint32_t mapId) const { + if (mapId == 0) return {}; + loadMapNameCache(); + auto it = mapNameCache_.find(mapId); + return (it != mapNameCache_.end()) ? it->second : std::string{}; +} + +// --------------------------------------------------------------------------- +// LFG dungeon name cache (WotLK: LFGDungeons.dbc) +// --------------------------------------------------------------------------- + +void GameHandler::loadLfgDungeonDbc() const { + if (lfgDungeonNameCacheLoaded_) return; + lfgDungeonNameCacheLoaded_ = true; + + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + + auto dbc = am->loadDBC("LFGDungeons.dbc"); + if (!dbc || !dbc->isLoaded()) return; + + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("LFGDungeons") : nullptr; + const uint32_t idField = layout ? (*layout)["ID"] : 0; + const uint32_t nameField = layout ? (*layout)["Name"] : 1; + + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t id = dbc->getUInt32(i, idField); + if (id == 0) continue; + std::string name = dbc->getString(i, nameField); + if (!name.empty()) + lfgDungeonNameCache_[id] = std::move(name); + } + LOG_INFO("LFGDungeons.dbc: loaded ", lfgDungeonNameCache_.size(), " dungeon names"); +} + +std::string GameHandler::getLfgDungeonName(uint32_t dungeonId) const { + if (dungeonId == 0) return {}; + loadLfgDungeonDbc(); + auto it = lfgDungeonNameCache_.find(dungeonId); + return (it != lfgDungeonNameCache_.end()) ? it->second : std::string{}; +} + // --------------------------------------------------------------------------- // Aura duration update // --------------------------------------------------------------------------- @@ -20258,17 +25911,17 @@ void GameHandler::handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs) { // --------------------------------------------------------------------------- void GameHandler::handleEquipmentSetList(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t count = packet.readUInt32(); if (count > 10) { LOG_WARNING("SMSG_EQUIPMENT_SET_LIST: unexpected count ", count, ", ignoring"); - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } equipmentSets_.clear(); equipmentSets_.reserve(count); for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < 16) break; + if (!packet.hasRemaining(16)) break; EquipmentSet es; es.setGuid = packet.readUInt64(); es.setId = packet.readUInt32(); @@ -20276,11 +25929,22 @@ void GameHandler::handleEquipmentSetList(network::Packet& packet) { es.iconName = packet.readString(); es.ignoreSlotMask = packet.readUInt32(); for (int slot = 0; slot < 19; ++slot) { - if (packet.getSize() - packet.getReadPos() < 8) break; + if (!packet.hasRemaining(8)) break; es.itemGuids[slot] = packet.readUInt64(); } equipmentSets_.push_back(std::move(es)); } + // Populate public-facing info + equipmentSetInfo_.clear(); + equipmentSetInfo_.reserve(equipmentSets_.size()); + for (const auto& es : equipmentSets_) { + EquipmentSetInfo info; + info.setGuid = es.setGuid; + info.setId = es.setId; + info.name = es.name; + info.iconName = es.iconName; + equipmentSetInfo_.push_back(std::move(info)); + } LOG_INFO("SMSG_EQUIPMENT_SET_LIST: ", equipmentSets_.size(), " equipment sets received"); } @@ -20289,16 +25953,16 @@ void GameHandler::handleEquipmentSetList(network::Packet& packet) { // --------------------------------------------------------------------------- void GameHandler::handleSetForcedReactions(network::Packet& packet) { - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t count = packet.readUInt32(); if (count > 64) { LOG_WARNING("SMSG_SET_FORCED_REACTIONS: suspicious count ", count, ", ignoring"); - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } forcedReactions_.clear(); for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < 8) break; + if (!packet.hasRemaining(8)) break; uint32_t factionId = packet.readUInt32(); uint32_t reaction = packet.readUInt32(); forcedReactions_[factionId] = static_cast(reaction); @@ -20306,5 +25970,40 @@ void GameHandler::handleSetForcedReactions(network::Packet& packet) { LOG_INFO("SMSG_SET_FORCED_REACTIONS: ", forcedReactions_.size(), " faction overrides"); } +// ---- Battlefield Manager (WotLK Wintergrasp / outdoor battlefields) ---- + +void GameHandler::acceptBfMgrInvite() { + if (!bfMgrInvitePending_ || state != WorldState::IN_WORLD || !socket) return; + // CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE: uint8 accepted = 1 + network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE)); + pkt.writeUInt8(1); // accepted + socket->send(pkt); + bfMgrInvitePending_ = false; + LOG_INFO("acceptBfMgrInvite: sent CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE accepted=1"); +} + +void GameHandler::declineBfMgrInvite() { + if (!bfMgrInvitePending_ || state != WorldState::IN_WORLD || !socket) return; + // CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE: uint8 accepted = 0 + network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE)); + pkt.writeUInt8(0); // declined + socket->send(pkt); + bfMgrInvitePending_ = false; + LOG_INFO("declineBfMgrInvite: sent CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE accepted=0"); +} + +// ---- WotLK Calendar ---- + +void GameHandler::requestCalendar() { + if (!isInWorld()) return; + // CMSG_CALENDAR_GET_CALENDAR has no payload + network::Packet pkt(wireOpcode(Opcode::CMSG_CALENDAR_GET_CALENDAR)); + socket->send(pkt); + LOG_INFO("requestCalendar: sent CMSG_CALENDAR_GET_CALENDAR"); + // Also request pending invite count + network::Packet numPkt(wireOpcode(Opcode::CMSG_CALENDAR_GET_NUM_PENDING)); + socket->send(numPkt); +} + } // namespace game } // namespace wowee diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index 1750253a..83fcc5fe 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -1,5 +1,6 @@ #include "game/inventory.hpp" #include "core/logger.hpp" +#include namespace wowee { namespace game { @@ -45,6 +46,23 @@ bool Inventory::clearEquipSlot(EquipSlot slot) { return true; } +const ItemSlot& Inventory::getKeyringSlot(int index) const { + if (index < 0 || index >= KEYRING_SLOTS) return EMPTY_SLOT; + return keyring_[index]; +} + +bool Inventory::setKeyringSlot(int index, const ItemDef& item) { + if (index < 0 || index >= KEYRING_SLOTS) return false; + keyring_[index].item = item; + return true; +} + +bool Inventory::clearKeyringSlot(int index) { + if (index < 0 || index >= KEYRING_SLOTS) return false; + keyring_[index].item = ItemDef{}; + return true; +} + int Inventory::getBagSize(int bagIndex) const { if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0; return bags[bagIndex].size; @@ -168,6 +186,130 @@ bool Inventory::addItem(const ItemDef& item) { return true; } +void Inventory::sortBags() { + // Collect all items from backpack and equip bags into a flat list. + std::vector items; + items.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE); + + for (int i = 0; i < BACKPACK_SLOTS; ++i) { + if (!backpack[i].empty()) + items.push_back(backpack[i].item); + } + for (int b = 0; b < NUM_BAG_SLOTS; ++b) { + for (int s = 0; s < bags[b].size; ++s) { + if (!bags[b].slots[s].empty()) + items.push_back(bags[b].slots[s].item); + } + } + + // Sort: quality descending → itemId ascending → stackCount descending. + std::stable_sort(items.begin(), items.end(), [](const ItemDef& a, const ItemDef& b) { + if (a.quality != b.quality) + return static_cast(a.quality) > static_cast(b.quality); + if (a.itemId != b.itemId) + return a.itemId < b.itemId; + return a.stackCount > b.stackCount; + }); + + // Write sorted items back, filling backpack first then equip bags. + int idx = 0; + int n = static_cast(items.size()); + + for (int i = 0; i < BACKPACK_SLOTS; ++i) + backpack[i].item = (idx < n) ? items[idx++] : ItemDef{}; + + for (int b = 0; b < NUM_BAG_SLOTS; ++b) { + for (int s = 0; s < bags[b].size; ++s) + bags[b].slots[s].item = (idx < n) ? items[idx++] : ItemDef{}; + } +} + +std::vector Inventory::computeSortSwaps() const { + // Build a flat list of (bag, slot, item) entries matching the same traversal + // order as sortBags(): backpack first, then equip bags in order. + struct Entry { + uint8_t bag; // WoW bag address: 0xFF=backpack, 19+i=equip bag i + uint8_t slot; // WoW slot address: 23+i for backpack, slotIndex for bags + uint32_t itemId; + ItemQuality quality; + uint32_t stackCount; + }; + + std::vector entries; + entries.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE); + + for (int i = 0; i < BACKPACK_SLOTS; ++i) { + entries.push_back({0xFF, static_cast(23 + i), + backpack[i].item.itemId, backpack[i].item.quality, + backpack[i].item.stackCount}); + } + for (int b = 0; b < NUM_BAG_SLOTS; ++b) { + for (int s = 0; s < bags[b].size; ++s) { + entries.push_back({static_cast(19 + b), static_cast(s), + bags[b].slots[s].item.itemId, bags[b].slots[s].item.quality, + bags[b].slots[s].item.stackCount}); + } + } + + // Build a sorted index array using the same comparator as sortBags(). + int n = static_cast(entries.size()); + std::vector sortedIdx(n); + for (int i = 0; i < n; ++i) sortedIdx[i] = i; + + // Separate non-empty items and empty slots, then sort the non-empty items. + // Items are sorted by quality desc -> itemId asc -> stackCount desc. + // Empty slots go to the end. + std::stable_sort(sortedIdx.begin(), sortedIdx.end(), [&](int a, int b) { + bool aEmpty = (entries[a].itemId == 0); + bool bEmpty = (entries[b].itemId == 0); + if (aEmpty != bEmpty) return bEmpty; // non-empty before empty + if (aEmpty) return false; // both empty: preserve order + // Both non-empty: same comparator as sortBags() + if (entries[a].quality != entries[b].quality) + return static_cast(entries[a].quality) > static_cast(entries[b].quality); + if (entries[a].itemId != entries[b].itemId) + return entries[a].itemId < entries[b].itemId; + return entries[a].stackCount > entries[b].stackCount; + }); + + // sortedIdx[targetPos] = sourcePos means the item currently at sourcePos + // needs to end up at targetPos. We use selection-sort-style swaps to + // permute current positions into sorted order, tracking where items move. + + // posOf[i] = current position of the item that was originally at index i + std::vector posOf(n); + for (int i = 0; i < n; ++i) posOf[i] = i; + + // invPos[p] = which original item index is currently sitting at position p + std::vector invPos(n); + for (int i = 0; i < n; ++i) invPos[i] = i; + + std::vector swaps; + + for (int target = 0; target < n; ++target) { + int need = sortedIdx[target]; // original index that should be at 'target' + int cur = invPos[target]; // original index currently at 'target' + if (cur == need) continue; // already in place + + // Skip swaps between two empty slots + if (entries[cur].itemId == 0 && entries[need].itemId == 0) continue; + + int srcPos = posOf[need]; // current position of the item we need + + // Emit a swap between position srcPos and position target + swaps.push_back({entries[srcPos].bag, entries[srcPos].slot, + entries[target].bag, entries[target].slot}); + + // Update tracking arrays + posOf[cur] = srcPos; + posOf[need] = target; + invPos[srcPos] = cur; + invPos[target] = need; + } + + return swaps; +} + void Inventory::populateTestItems() { // Equipment { @@ -313,6 +455,8 @@ const char* getQualityName(ItemQuality quality) { case ItemQuality::RARE: return "Rare"; case ItemQuality::EPIC: return "Epic"; case ItemQuality::LEGENDARY: return "Legendary"; + case ItemQuality::ARTIFACT: return "Artifact"; + case ItemQuality::HEIRLOOM: return "Heirloom"; default: return "Unknown"; } } diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 8178f0f5..ad9b639f 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include namespace wowee { namespace game { @@ -47,6 +49,155 @@ static std::string_view canonicalOpcodeName(std::string_view name) { return name; } +static std::optional resolveLogicalOpcodeIndex(std::string_view name) { + const std::string_view canonical = canonicalOpcodeName(name); + for (size_t i = 0; i < kOpcodeNameCount; ++i) { + if (canonical == kOpcodeNames[i].name) { + return static_cast(kOpcodeNames[i].op); + } + } + return std::nullopt; +} + +static std::optional parseStringField(const std::string& json, const char* fieldName) { + const std::string needle = std::string("\"") + fieldName + "\""; + size_t keyPos = json.find(needle); + if (keyPos == std::string::npos) return std::nullopt; + + size_t colon = json.find(':', keyPos + needle.size()); + if (colon == std::string::npos) return std::nullopt; + + size_t valueStart = json.find('"', colon + 1); + if (valueStart == std::string::npos) return std::nullopt; + size_t valueEnd = json.find('"', valueStart + 1); + if (valueEnd == std::string::npos) return std::nullopt; + return json.substr(valueStart + 1, valueEnd - valueStart - 1); +} + +static std::vector parseStringArrayField(const std::string& json, const char* fieldName) { + std::vector values; + const std::string needle = std::string("\"") + fieldName + "\""; + size_t keyPos = json.find(needle); + if (keyPos == std::string::npos) return values; + + size_t colon = json.find(':', keyPos + needle.size()); + if (colon == std::string::npos) return values; + + size_t arrayStart = json.find('[', colon + 1); + if (arrayStart == std::string::npos) return values; + size_t arrayEnd = json.find(']', arrayStart + 1); + if (arrayEnd == std::string::npos) return values; + + size_t pos = arrayStart + 1; + while (pos < arrayEnd) { + size_t valueStart = json.find('"', pos); + if (valueStart == std::string::npos || valueStart >= arrayEnd) break; + size_t valueEnd = json.find('"', valueStart + 1); + if (valueEnd == std::string::npos || valueEnd > arrayEnd) break; + values.push_back(json.substr(valueStart + 1, valueEnd - valueStart - 1)); + pos = valueEnd + 1; + } + return values; +} + +static bool loadOpcodeJsonRecursive(const std::filesystem::path& path, + std::unordered_map& logicalToWire, + std::unordered_map& wireToLogical, + std::unordered_set& loadingStack) { + const std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(path); + const std::string canonicalKey = canonicalPath.string(); + if (!loadingStack.insert(canonicalKey).second) { + LOG_WARNING("OpcodeTable: inheritance cycle at ", canonicalKey); + return false; + } + + std::ifstream f(canonicalPath); + if (!f.is_open()) { + LOG_WARNING("OpcodeTable: cannot open ", canonicalPath.string()); + loadingStack.erase(canonicalKey); + return false; + } + + std::string json((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + bool ok = true; + + if (auto extends = parseStringField(json, "_extends")) { + ok = loadOpcodeJsonRecursive(canonicalPath.parent_path() / *extends, + logicalToWire, wireToLogical, loadingStack) && ok; + } + + for (const std::string& removeName : parseStringArrayField(json, "_remove")) { + auto logical = resolveLogicalOpcodeIndex(removeName); + if (!logical) continue; + auto it = logicalToWire.find(*logical); + if (it != logicalToWire.end()) { + const uint16_t oldWire = it->second; + logicalToWire.erase(it); + auto wireIt = wireToLogical.find(oldWire); + if (wireIt != wireToLogical.end() && wireIt->second == *logical) { + wireToLogical.erase(wireIt); + } + } + } + + size_t pos = 0; + while (pos < json.size()) { + size_t keyStart = json.find('"', pos); + if (keyStart == std::string::npos) break; + size_t keyEnd = json.find('"', keyStart + 1); + if (keyEnd == std::string::npos) break; + std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1); + + size_t colon = json.find(':', keyEnd); + if (colon == std::string::npos) break; + + size_t valStart = colon + 1; + while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' || + json[valStart] == '\r' || json[valStart] == '\n' || json[valStart] == '"')) + ++valStart; + + size_t valEnd = json.find_first_of(",}\"\r\n", valStart); + if (valEnd == std::string::npos) valEnd = json.size(); + std::string valStr = json.substr(valStart, valEnd - valStart); + + uint16_t wire = 0; + try { + if (valStr.size() > 2 && (valStr[0] == '0' && (valStr[1] == 'x' || valStr[1] == 'X'))) { + wire = static_cast(std::stoul(valStr, nullptr, 16)); + } else { + wire = static_cast(std::stoul(valStr)); + } + } catch (...) { + pos = valEnd + 1; + continue; + } + + auto logical = resolveLogicalOpcodeIndex(key); + if (logical) { + auto oldLogicalIt = logicalToWire.find(*logical); + if (oldLogicalIt != logicalToWire.end()) { + const uint16_t oldWire = oldLogicalIt->second; + auto oldWireIt = wireToLogical.find(oldWire); + if (oldWireIt != wireToLogical.end() && oldWireIt->second == *logical) { + wireToLogical.erase(oldWireIt); + } + } + auto oldWireIt = wireToLogical.find(wire); + if (oldWireIt != wireToLogical.end() && oldWireIt->second != *logical) { + logicalToWire.erase(oldWireIt->second); + wireToLogical.erase(oldWireIt); + } + logicalToWire[*logical] = wire; + wireToLogical[wire] = *logical; + } + + pos = valEnd + 1; + } + + loadingStack.erase(canonicalKey); + return ok; +} + std::optional OpcodeTable::nameToLogical(const std::string& name) { const std::string_view canonical = canonicalOpcodeName(name); for (size_t i = 0; i < kOpcodeNameCount; ++i) { @@ -64,73 +215,18 @@ const char* OpcodeTable::logicalToName(LogicalOpcode op) { } bool OpcodeTable::loadFromJson(const std::string& path) { - std::ifstream f(path); - if (!f.is_open()) { - LOG_WARNING("OpcodeTable: cannot open ", path, ", using defaults"); - return false; - } - - std::string json((std::istreambuf_iterator(f)), std::istreambuf_iterator()); - - // Start fresh — JSON is the single source of truth for opcode mappings. + // Start fresh — resolved JSON inheritance is the single source of truth for opcode mappings. logicalToWire_.clear(); wireToLogical_.clear(); - - // Parse simple JSON: { "NAME": "0xHEX", ... } or { "NAME": 123, ... } - size_t pos = 0; - size_t loaded = 0; - while (pos < json.size()) { - // Find next quoted key - size_t keyStart = json.find('"', pos); - if (keyStart == std::string::npos) break; - size_t keyEnd = json.find('"', keyStart + 1); - if (keyEnd == std::string::npos) break; - std::string key = json.substr(keyStart + 1, keyEnd - keyStart - 1); - - // Find colon then value - size_t colon = json.find(':', keyEnd); - if (colon == std::string::npos) break; - - // Skip whitespace - size_t valStart = colon + 1; - while (valStart < json.size() && (json[valStart] == ' ' || json[valStart] == '\t' || - json[valStart] == '\r' || json[valStart] == '\n' || json[valStart] == '"')) - ++valStart; - - size_t valEnd = json.find_first_of(",}\"\r\n", valStart); - if (valEnd == std::string::npos) valEnd = json.size(); - std::string valStr = json.substr(valStart, valEnd - valStart); - - // Parse hex or decimal value - uint16_t wire = 0; - try { - if (valStr.size() > 2 && (valStr[0] == '0' && (valStr[1] == 'x' || valStr[1] == 'X'))) { - wire = static_cast(std::stoul(valStr, nullptr, 16)); - } else { - wire = static_cast(std::stoul(valStr)); - } - } catch (...) { - pos = valEnd + 1; - continue; - } - - auto logOp = nameToLogical(key); - if (logOp) { - uint16_t logIdx = static_cast(*logOp); - logicalToWire_[logIdx] = wire; - wireToLogical_[wire] = logIdx; - ++loaded; - } - - pos = valEnd + 1; - } - - if (loaded == 0) { + std::unordered_set loadingStack; + if (!loadOpcodeJsonRecursive(std::filesystem::path(path), + logicalToWire_, wireToLogical_, loadingStack) || + logicalToWire_.empty()) { LOG_WARNING("OpcodeTable: no opcodes loaded from ", path); return false; } - LOG_INFO("OpcodeTable: loaded ", loaded, " opcodes from ", path); + LOG_INFO("OpcodeTable: loaded ", logicalToWire_.size(), " opcodes from ", path); return true; } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index c62567ef..f758f317 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1,9 +1,121 @@ #include "game/packet_parsers.hpp" #include "core/logger.hpp" +#include +#include +#include namespace wowee { namespace game { +namespace { + +std::string formatPacketBytes(const network::Packet& packet, size_t startPos) { + const auto& rawData = packet.getData(); + if (startPos >= rawData.size()) { + return {}; + } + + std::string hex; + hex.reserve((rawData.size() - startPos) * 3); + for (size_t i = startPos; i < rawData.size(); ++i) { + char buf[4]; + std::snprintf(buf, sizeof(buf), "%02x ", rawData[i]); + hex += buf; + } + if (!hex.empty()) { + hex.pop_back(); + } + return hex; +} + +bool skipClassicSpellCastTargets(network::Packet& packet, uint64_t* primaryTargetGuid = nullptr) { + if (!packet.hasRemaining(2)) { + return false; + } + + const uint16_t targetFlags = packet.readUInt16(); + + const auto readPackedTargetGuid = [&](bool capture) -> bool { + if (!packet.hasFullPackedGuid()) { + return false; + } + const uint64_t guid = packet.readPackedGuid(); + if (capture && primaryTargetGuid && *primaryTargetGuid == 0) { + *primaryTargetGuid = guid; + } + return true; + }; + + // Common Classic/Turtle SpellCastTargets payloads. + if ((targetFlags & 0x0002) != 0 && !readPackedTargetGuid(true)) { // UNIT + return false; + } + if ((targetFlags & 0x0004) != 0 && !readPackedTargetGuid(false)) { // UNIT_MINIPET/extra guid + return false; + } + if ((targetFlags & 0x0010) != 0 && !readPackedTargetGuid(false)) { // ITEM + return false; + } + if ((targetFlags & 0x0800) != 0 && !readPackedTargetGuid(true)) { // OBJECT + return false; + } + if ((targetFlags & 0x8000) != 0 && !readPackedTargetGuid(false)) { // CORPSE + return false; + } + + if ((targetFlags & 0x0020) != 0) { // SOURCE_LOCATION + if (!packet.hasRemaining(12)) { + return false; + } + (void)packet.readFloat(); + (void)packet.readFloat(); + (void)packet.readFloat(); + } + if ((targetFlags & 0x0040) != 0) { // DEST_LOCATION + if (!packet.hasRemaining(12)) { + return false; + } + (void)packet.readFloat(); + (void)packet.readFloat(); + (void)packet.readFloat(); + } + + if ((targetFlags & 0x1000) != 0) { // TRADE_ITEM + if (!packet.hasRemaining(1)) { + return false; + } + (void)packet.readUInt8(); + } + + if ((targetFlags & 0x2000) != 0) { // STRING + const auto& rawData = packet.getData(); + size_t pos = packet.getReadPos(); + while (pos < rawData.size() && rawData[pos] != 0) { + ++pos; + } + if (pos >= rawData.size()) { + return false; + } + packet.setReadPos(pos + 1); + } + + return true; +} + +const char* updateTypeName(UpdateType type) { + switch (type) { + case UpdateType::VALUES: return "VALUES"; + case UpdateType::MOVEMENT: return "MOVEMENT"; + case UpdateType::CREATE_OBJECT: return "CREATE_OBJECT"; + case UpdateType::CREATE_OBJECT2: return "CREATE_OBJECT2"; + case UpdateType::OUT_OF_RANGE_OBJECTS: return "OUT_OF_RANGE_OBJECTS"; + case UpdateType::NEAR_OBJECTS: return "NEAR_OBJECTS"; + default: return "UNKNOWN"; + } +} + +} // namespace + // ============================================================================ // Classic 1.12.1 movement flag constants // Key differences from TBC: @@ -21,6 +133,36 @@ namespace ClassicMoveFlags { constexpr uint32_t SPLINE_ELEVATION = 0x04000000; // Same as TBC } +uint32_t classicWireMoveFlags(uint32_t internalFlags) { + uint32_t wireFlags = internalFlags; + + // Internal movement state is tracked with WotLK-era bits. Classic/Turtle + // movement packets still use the older transport/jump flag layout. + const uint32_t kInternalOnTransport = static_cast(MovementFlags::ONTRANSPORT); + const uint32_t kInternalFalling = + static_cast(MovementFlags::FALLING) | + static_cast(MovementFlags::FALLINGFAR); + const uint32_t kClassicConflicts = + static_cast(MovementFlags::ASCENDING) | + static_cast(MovementFlags::CAN_FLY) | + static_cast(MovementFlags::FLYING) | + static_cast(MovementFlags::HOVER); + + wireFlags &= ~kClassicConflicts; + + if ((internalFlags & kInternalOnTransport) != 0) { + wireFlags &= ~kInternalOnTransport; + wireFlags |= ClassicMoveFlags::ONTRANSPORT; + } + + if ((internalFlags & kInternalFalling) != 0) { + wireFlags &= ~kInternalFalling; + wireFlags |= ClassicMoveFlags::JUMPING; + } + + return wireFlags; +} + // ============================================================================ // Classic parseMovementBlock // Key differences from TBC: @@ -31,26 +173,26 @@ namespace ClassicMoveFlags { // Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate // ============================================================================ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - // Validate minimum packet size for updateFlags byte - if (packet.getReadPos() >= packet.getSize()) { - LOG_WARNING("[Classic] Movement block packet too small (need at least 1 byte for updateFlags)"); - return false; - } + auto rem = [&]() -> size_t { return packet.getRemainingSize(); }; + if (rem() < 1) return false; // Classic: UpdateFlags is uint8 (same as TBC) uint8_t updateFlags = packet.readUInt8(); block.updateFlags = static_cast(updateFlags); - LOG_DEBUG(" [Classic] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec); + LOG_DEBUG(" [Classic] UpdateFlags: 0x", std::hex, static_cast(updateFlags), std::dec); - const uint8_t UPDATEFLAG_LIVING = 0x20; - const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; - const uint8_t UPDATEFLAG_HAS_TARGET = 0x04; - const uint8_t UPDATEFLAG_TRANSPORT = 0x02; - const uint8_t UPDATEFLAG_LOWGUID = 0x08; - const uint8_t UPDATEFLAG_HIGHGUID = 0x10; + const uint8_t UPDATEFLAG_TRANSPORT = 0x02; + const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04; + const uint8_t UPDATEFLAG_HIGHGUID = 0x08; + const uint8_t UPDATEFLAG_ALL = 0x10; + const uint8_t UPDATEFLAG_LIVING = 0x20; + const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; if (updateFlags & UPDATEFLAG_LIVING) { + // Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes + if (rem() < 52) return false; + // Movement flags (u32 only — NO extra flags byte in Classic) uint32_t moveFlags = packet.readUInt32(); /*uint32_t time =*/ packet.readUInt32(); @@ -67,26 +209,29 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Transport data (Classic: ONTRANSPORT=0x02000000, no timestamp) if (moveFlags & ClassicMoveFlags::ONTRANSPORT) { + if (rem() < 1) return false; block.onTransport = true; - block.transportGuid = UpdateObjectParser::readPackedGuid(packet); + block.transportGuid = packet.readPackedGuid(); + if (rem() < 16) return false; // 4 floats block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); block.transportO = packet.readFloat(); - // Classic: NO transport timestamp (TBC adds u32 timestamp) - // Classic: NO transport seat byte } // Pitch (Classic: only SWIMMING, no FLYING or ONTRANSPORT pitch) if (moveFlags & ClassicMoveFlags::SWIMMING) { + if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time (always present) + if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping (Classic: JUMPING=0x2000, same as TBC) if (moveFlags & ClassicMoveFlags::JUMPING) { + if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -95,12 +240,12 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Spline elevation if (moveFlags & ClassicMoveFlags::SPLINE_ELEVATION) { + if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Speeds (Classic: 6 values — no flight speeds, no pitchRate) - // TBC added flying_speed + backwards_flying_speed (8 total) - // WotLK added pitchRate (9 total) + if (rem() < 24) return false; /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); @@ -113,34 +258,34 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Spline data (Classic: SPLINE_ENABLED=0x00400000) if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) { + if (rem() < 4) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { // FINAL_POINT + if (rem() < 12) return false; /*float finalX =*/ packet.readFloat(); /*float finalY =*/ packet.readFloat(); /*float finalZ =*/ packet.readFloat(); } else if (splineFlags & 0x00020000) { // FINAL_TARGET + if (rem() < 8) return false; /*uint64_t finalTarget =*/ packet.readUInt64(); } else if (splineFlags & 0x00040000) { // FINAL_ANGLE + if (rem() < 4) return false; /*float finalAngle =*/ packet.readFloat(); } - // Classic spline: timePassed, duration, id, nodes, finalNode (same as TBC) + // Classic spline: timePassed, duration, id, pointCount + if (rem() < 16) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); uint32_t pointCount = packet.readUInt32(); - if (pointCount > 256) { - static uint32_t badClassicSplineCount = 0; - ++badClassicSplineCount; - if (badClassicSplineCount <= 5 || (badClassicSplineCount % 100) == 0) { - LOG_WARNING(" [Classic] Spline pointCount=", pointCount, - " exceeds max, capping (occurrence=", badClassicSplineCount, ")"); - } - pointCount = 0; - } + if (pointCount > 256) return false; + + // points + endPoint (no splineMode in Classic) + if (rem() < static_cast(pointCount) * 12 + 12) return false; for (uint32_t i = 0; i < pointCount; i++) { /*float px =*/ packet.readFloat(); /*float py =*/ packet.readFloat(); @@ -154,6 +299,7 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo } } else if (updateFlags & UPDATEFLAG_HAS_POSITION) { + if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -163,26 +309,30 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo LOG_DEBUG(" [Classic] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); } - // Target GUID - if (updateFlags & UPDATEFLAG_HAS_TARGET) { - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); - } - - // Transport time - if (updateFlags & UPDATEFLAG_TRANSPORT) { - /*uint32_t transportTime =*/ packet.readUInt32(); - } - - // Low GUID - if (updateFlags & UPDATEFLAG_LOWGUID) { - /*uint32_t lowGuid =*/ packet.readUInt32(); - } - // High GUID if (updateFlags & UPDATEFLAG_HIGHGUID) { + if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } + // ALL/SELF extra uint32 + if (updateFlags & UPDATEFLAG_ALL) { + if (rem() < 4) return false; + /*uint32_t unkAll =*/ packet.readUInt32(); + } + + // Current melee target as packed guid + if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { + if (rem() < 1) return false; + /*uint64_t meleeTargetGuid =*/ packet.readPackedGuid(); + } + + // Transport progress / world time + if (updateFlags & UPDATEFLAG_TRANSPORT) { + if (rem() < 4) return false; + /*uint32_t transportTime =*/ packet.readUInt32(); + } + return true; } @@ -194,8 +344,10 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // - Pitch: only SWIMMING (no ONTRANSPORT pitch) // ============================================================================ void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const MovementInfo& info) { + const uint32_t wireFlags = classicWireMoveFlags(info.flags); + // Movement flags (uint32) - packet.writeUInt32(info.flags); + packet.writeUInt32(wireFlags); // Classic: NO flags2 byte (TBC has u8, WotLK has u16) @@ -203,13 +355,13 @@ void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const M packet.writeUInt32(info.time); // Position - packet.writeBytes(reinterpret_cast(&info.x), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.y), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.z), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.orientation), sizeof(float)); + packet.writeFloat(info.x); + packet.writeFloat(info.y); + packet.writeFloat(info.z); + packet.writeFloat(info.orientation); // Transport data (Classic ONTRANSPORT = 0x02000000, no timestamp) - if (info.flags & ClassicMoveFlags::ONTRANSPORT) { + if (wireFlags & ClassicMoveFlags::ONTRANSPORT) { // Packed transport GUID uint8_t transMask = 0; uint8_t transGuidBytes[8]; @@ -227,29 +379,29 @@ void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const M } // Transport local position - packet.writeBytes(reinterpret_cast(&info.transportX), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportY), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportZ), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportO), sizeof(float)); + packet.writeFloat(info.transportX); + packet.writeFloat(info.transportY); + packet.writeFloat(info.transportZ); + packet.writeFloat(info.transportO); // Classic: NO transport timestamp // Classic: NO transport seat byte } // Pitch (Classic: only SWIMMING) - if (info.flags & ClassicMoveFlags::SWIMMING) { - packet.writeBytes(reinterpret_cast(&info.pitch), sizeof(float)); + if (wireFlags & ClassicMoveFlags::SWIMMING) { + packet.writeFloat(info.pitch); } // Fall time (always present) packet.writeUInt32(info.fallTime); // Jump data (Classic JUMPING = 0x2000) - if (info.flags & ClassicMoveFlags::JUMPING) { - packet.writeBytes(reinterpret_cast(&info.jumpVelocity), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpSinAngle), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpCosAngle), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpXYSpeed), sizeof(float)); + if (wireFlags & ClassicMoveFlags::JUMPING) { + packet.writeFloat(info.jumpVelocity); + packet.writeFloat(info.jumpSinAngle); + packet.writeFloat(info.jumpCosAngle); + packet.writeFloat(info.jumpXYSpeed); } } @@ -330,34 +482,50 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo // - castFlags is uint16 (NOT uint32 as in TBC/WotLK). // - SpellCastTargets uses uint16 targetFlags (NOT uint32 as in TBC). // -// Format: PackedGuid(casterObj) + PackedGuid(casterUnit) + uint8(castCount) +// Format: PackedGuid(casterObj) + PackedGuid(casterUnit) // + uint32(spellId) + uint16(castFlags) + uint32(castTime) // + uint16(targetFlags) [+ PackedGuid(unitTarget) if TARGET_FLAG_UNIT] // ============================================================================ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + data = SpellStartData{}; + + auto rem = [&]() { return packet.getRemainingSize(); }; + const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) return false; - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.casterUnit = packet.readPackedGuid(); - // uint8 castCount + uint32 spellId + uint16 castFlags + uint32 castTime = 11 bytes - if (rem() < 11) return false; - data.castCount = packet.readUInt8(); + // Vanilla/Turtle SMSG_SPELL_START does not include castCount here. + // Layout after the two packed GUIDs is spellId(u32) + castFlags(u16) + castTime(u32). + if (rem() < 10) return false; + data.castCount = 0; data.spellId = packet.readUInt32(); data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK) data.castTime = packet.readUInt32(); - // SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK) - if (rem() < 2) return true; - uint16_t targetFlags = packet.readUInt16(); - // TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID - if (((targetFlags & 0x02) || (targetFlags & 0x800)) && rem() >= 1) { - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + // SpellCastTargets: consume ALL target payload types so subsequent reads stay aligned. + // Previously only UNIT(0x02)/OBJECT(0x800) were handled; DEST_LOCATION(0x40), + // SOURCE_LOCATION(0x20), and ITEM(0x10) bytes were silently skipped, corrupting + // castFlags/castTime for every AOE/ground-targeted spell (Rain of Fire, Blizzard, etc.). + { + uint64_t targetGuid = 0; + // skipClassicSpellCastTargets reads uint16 targetFlags and all payloads. + // Non-fatal on truncation: self-cast spells have zero-byte targets. + skipClassicSpellCastTargets(packet, &targetGuid); + data.targetGuid = targetGuid; } - LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); + LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms", + " targetGuid=0x", std::hex, data.targetGuid, std::dec); return true; } @@ -369,69 +537,229 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa // - castFlags is uint16 (not uint32) // - Hit/miss target GUIDs are also PackedGuid in Vanilla // -// Format: PackedGuid(casterObj) + PackedGuid(casterUnit) + uint8(castCount) +// Format: PackedGuid(casterObj) + PackedGuid(casterUnit) // + uint32(spellId) + uint16(castFlags) // + uint8(hitCount) + [PackedGuid(hitTarget) × hitCount] // + uint8(missCount) + [PackedGuid(missTarget) + uint8(missType)] × missCount // ============================================================================ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + + auto rem = [&]() { return packet.getRemainingSize(); }; + const size_t startPos = packet.getReadPos(); + const bool traceSmallSpellGo = (packet.getSize() - startPos) <= 48; + const auto traceFailure = [&](const char* stage, size_t pos, uint32_t value = 0) { + if (!traceSmallSpellGo) { + return; + } + static uint32_t smallSpellGoTraceCount = 0; + ++smallSpellGoTraceCount; + if (smallSpellGoTraceCount > 12 && (smallSpellGoTraceCount % 50) != 0) { + return; + } + LOG_WARNING("[Classic] Spell go trace: stage=", stage, + " pos=", pos, + " size=", packet.getSize() - startPos, + " spell=", data.spellId, + " castFlags=0x", std::hex, data.castFlags, std::dec, + " value=", value, + " bytes=[", formatPacketBytes(packet, startPos), "]"); + }; if (rem() < 2) return false; - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) return false; - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) return false; + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) return false; + data.casterUnit = packet.readPackedGuid(); - // uint8 castCount + uint32 spellId + uint16 castFlags = 7 bytes - if (rem() < 7) return false; - data.castCount = packet.readUInt8(); + // Vanilla/Turtle SMSG_SPELL_GO does not include castCount here. + // Layout after the two packed GUIDs is spellId(u32) + castFlags(u16). + if (rem() < 6) return false; + data.castCount = 0; data.spellId = packet.readUInt32(); data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK) - // Hit targets - if (rem() < 1) return true; - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent OOM from huge target lists - if (data.hitCount > 128) { - LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; - } - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount && rem() >= 1; ++i) { - data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet)); - } - // Check if we read all expected hits - if (data.hitTargets.size() < data.hitCount) { - LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)data.hitCount); - data.hitCount = data.hitTargets.size(); + const size_t countsPos = packet.getReadPos(); + uint64_t ignoredTargetGuid = 0; + std::function parseHitAndMissLists = [&](bool allowTargetsFallback) -> bool { + packet.setReadPos(countsPos); + data.hitTargets.clear(); + data.missTargets.clear(); + ignoredTargetGuid = 0; + + // hitCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing hitCount after fixed fields"); + traceFailure("missing_hit_count", packet.getReadPos()); + packet.setReadPos(startPos); + return false; + } + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", static_cast(rawHitCount), ")"); + } + if (rem() < static_cast(rawHitCount) + 1u) { + static uint32_t badHitCountTrunc = 0; + ++badHitCountTrunc; + if (badHitCountTrunc <= 10 || (badHitCountTrunc % 100) == 0) { + LOG_WARNING("[Classic] Spell go: invalid hitCount/remaining (hits=", static_cast(rawHitCount), + " remaining=", rem(), " occurrence=", badHitCountTrunc, ")"); + } + traceFailure("invalid_hit_count", packet.getReadPos(), rawHitCount); + packet.setReadPos(startPos); + return false; + } + + const auto parseHitList = [&](bool usePackedGuids) -> bool { + packet.setReadPos(countsPos + 1); // after hitCount + data.hitTargets.clear(); + const uint8_t storedHitLimit = std::min(rawHitCount, 128); + data.hitTargets.reserve(storedHitLimit); + for (uint16_t i = 0; i < rawHitCount; ++i) { + uint64_t targetGuid = 0; + if (usePackedGuids) { + if (!packet.hasFullPackedGuid()) { + return false; + } + targetGuid = packet.readPackedGuid(); + } else { + if (rem() < 8) { + return false; + } + targetGuid = packet.readUInt64(); + } + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } + } + data.hitCount = static_cast(data.hitTargets.size()); + return true; + }; + + if (!parseHitList(false) && !parseHitList(true)) { + LOG_WARNING("[Classic] Spell go: truncated hit targets at index 0/", static_cast(rawHitCount)); + traceFailure("truncated_hit_target", packet.getReadPos(), rawHitCount); + packet.setReadPos(startPos); + return false; + } + + std::function parseMissListFrom = [&](size_t missStartPos, + bool allowMidTargetsFallback) -> bool { + packet.setReadPos(missStartPos); + data.missTargets.clear(); + + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing missCount after hit target list"); + traceFailure("missing_miss_count", packet.getReadPos()); + packet.setReadPos(startPos); + return false; + } + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[Classic] Spell go: missCount capped (requested=", static_cast(rawMissCount), ")"); + traceFailure("miss_count_capped", packet.getReadPos() - 1, rawMissCount); + } + if (rem() < static_cast(rawMissCount) * 2u) { + if (allowMidTargetsFallback) { + packet.setReadPos(missStartPos); + if (skipClassicSpellCastTargets(packet, &ignoredTargetGuid)) { + traceFailure("mid_targets_fallback", missStartPos, ignoredTargetGuid != 0 ? 1u : 0u); + return parseMissListFrom(packet.getReadPos(), false); + } + } + if (allowTargetsFallback) { + packet.setReadPos(countsPos); + if (skipClassicSpellCastTargets(packet, &ignoredTargetGuid)) { + traceFailure("pre_targets_fallback", countsPos, ignoredTargetGuid != 0 ? 1u : 0u); + return parseHitAndMissLists(false); + } + } + + static uint32_t badMissCountTrunc = 0; + ++badMissCountTrunc; + if (badMissCountTrunc <= 10 || (badMissCountTrunc % 100) == 0) { + LOG_WARNING("[Classic] Spell go: invalid missCount/remaining (misses=", static_cast(rawMissCount), + " remaining=", rem(), " occurrence=", badMissCountTrunc, ")"); + } + traceFailure("invalid_miss_count", packet.getReadPos(), rawMissCount); + packet.setReadPos(startPos); + return false; + } + + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + bool truncatedMissTargets = false; + const auto parseMissEntry = [&](SpellGoMissEntry& m, bool usePackedGuid) -> bool { + if (usePackedGuid) { + if (!packet.hasFullPackedGuid()) { + return false; + } + m.targetGuid = packet.readPackedGuid(); + } else { + if (rem() < 8) { + return false; + } + m.targetGuid = packet.readUInt64(); + } + return true; + }; + for (uint16_t i = 0; i < rawMissCount; ++i) { + SpellGoMissEntry m; + const size_t missEntryPos = packet.getReadPos(); + if (!parseMissEntry(m, false)) { + packet.setReadPos(missEntryPos); + if (!parseMissEntry(m, true)) { + LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", i, + "/", static_cast(rawMissCount)); + traceFailure("truncated_miss_target", packet.getReadPos(), i); + truncatedMissTargets = true; + break; + } + } + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing missType at miss index ", i, + "/", static_cast(rawMissCount)); + traceFailure("missing_miss_type", packet.getReadPos(), i); + truncatedMissTargets = true; + break; + } + m.missType = packet.readUInt8(); + if (m.missType == 11) { + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: truncated reflect payload at miss index ", i, + "/", static_cast(rawMissCount)); + traceFailure("truncated_reflect", packet.getReadPos(), i); + truncatedMissTargets = true; + break; + } + (void)packet.readUInt8(); + } + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } + } + if (truncatedMissTargets) { + packet.setReadPos(startPos); + return false; + } + data.missCount = static_cast(data.missTargets.size()); + return true; + }; + + return parseMissListFrom(packet.getReadPos(), true); + }; + + if (!parseHitAndMissLists(true)) { + return false; } - // Miss targets - if (rem() < 1) return true; - data.missCount = packet.readUInt8(); - // Cap miss count to prevent OOM - if (data.missCount > 128) { - LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; - } - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount && rem() >= 2; ++i) { - SpellGoMissEntry m; - m.targetGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) break; - m.missType = packet.readUInt8(); - data.missTargets.push_back(m); - } - // Check if we read all expected misses - if (data.missTargets.size() < data.missCount) { - LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)data.missCount); - data.missCount = data.missTargets.size(); - } + // SpellCastTargets follows the miss list — consume all target bytes so that + // any subsequent fields (e.g. castFlags extras) are not misaligned. + skipClassicSpellCastTargets(packet, &data.targetGuid); - LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, - " misses=", (int)data.missCount); + LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", static_cast(data.hitCount), + " misses=", static_cast(data.missCount)); return true; } @@ -446,19 +774,42 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da // + uint32(victimState) + int32(overkill) [+ uint32(blocked)] // ============================================================================ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + data = AttackerStateUpdateData{}; + + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1) + const size_t startPos = packet.getReadPos(); data.hitInfo = packet.readUInt32(); - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.attackerGuid = packet.readPackedGuid(); // PackedGuid in Vanilla + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.targetGuid = packet.readPackedGuid(); // PackedGuid in Vanilla - if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount + if (rem() < 5) { + packet.setReadPos(startPos); + return false; + } // int32 totalDamage + uint8 subDamageCount data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); - for (uint8_t i = 0; i < data.subDamageCount && rem() >= 20; ++i) { + const uint8_t maxSubDamageCount = static_cast(std::min(rem() / 20, 64)); + if (data.subDamageCount > maxSubDamageCount) { + data.subDamageCount = maxSubDamageCount; + } + + data.subDamages.reserve(data.subDamageCount); + for (uint8_t i = 0; i < data.subDamageCount; ++i) { + if (rem() < 20) { + packet.setReadPos(startPos); + return false; + } SubDamage sub; sub.schoolMask = packet.readUInt32(); sub.damage = packet.readFloat(); @@ -467,8 +818,12 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att sub.resisted = packet.readUInt32(); data.subDamages.push_back(sub); } + data.subDamageCount = static_cast(data.subDamages.size()); - if (rem() < 8) return true; + if (rem() < 8) { + packet.setReadPos(startPos); + return false; + } data.victimState = packet.readUInt32(); data.overkill = static_cast(packet.readUInt32()); @@ -491,12 +846,12 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att // + uint8(periodicLog) + uint8(unused) + uint32(blocked) + uint32(flags) // ============================================================================ bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 2) return false; + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() < 2 || !packet.hasFullPackedGuid()) return false; - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + data.targetGuid = packet.readPackedGuid(); // PackedGuid in Vanilla + if (rem() < 1 || !packet.hasFullPackedGuid()) return false; + data.attackerGuid = packet.readPackedGuid(); // PackedGuid in Vanilla // uint32(spellId) + uint32(damage) + uint8(schoolMask) + uint32(absorbed) // + uint32(resisted) + uint8 + uint8 + uint32(blocked) + uint32(flags) = 21 bytes @@ -526,12 +881,12 @@ bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDam // + uint32(heal) + uint32(overheal) + uint8(crit) // ============================================================================ bool ClassicPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 2) return false; + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() < 2 || !packet.hasFullPackedGuid()) return false; - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + data.targetGuid = packet.readPackedGuid(); // PackedGuid in Vanilla + if (rem() < 1 || !packet.hasFullPackedGuid()) return false; + data.casterGuid = packet.readPackedGuid(); // PackedGuid in Vanilla if (rem() < 13) return false; // uint32 + uint32 + uint32 + uint8 = 13 bytes data.spellId = packet.readUInt32(); @@ -568,10 +923,10 @@ bool ClassicPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealL // + [uint32(maxDuration) + uint32(duration) if flags & 0x10]]* // ============================================================================ bool ClassicPacketParsers::parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 1) return false; - data.guid = UpdateObjectParser::readPackedGuid(packet); + data.guid = packet.readPackedGuid(); while (rem() > 0) { if (rem() < 1) break; @@ -621,7 +976,7 @@ bool ClassicPacketParsers::parseAuraUpdate(network::Packet& packet, AuraUpdateDa bool ClassicPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) { data = NameQueryResponseData{}; - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 8) return false; data.guid = packet.readUInt64(); // full uint64, not PackedGuid @@ -640,8 +995,8 @@ bool ClassicPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQ data.found = 0; LOG_DEBUG("[Classic] Name query response: ", data.name, - " (race=", (int)data.race, " gender=", (int)data.gender, - " class=", (int)data.classId, ")"); + " (race=", static_cast(data.race), " gender=", static_cast(data.gender), + " class=", static_cast(data.classId), ")"); return !data.name.empty(); } @@ -657,7 +1012,7 @@ bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedDa // WotLK enum starts at 0=SUCCESS, 1=AFFECTING_COMBAT. // Shift +1 to align with WotLK result strings. data.result = vanillaResult + 1; - LOG_DEBUG("[Classic] Cast failed: spell=", data.spellId, " vanillaResult=", (int)vanillaResult); + LOG_DEBUG("[Classic] Cast failed: spell=", data.spellId, " vanillaResult=", static_cast(vanillaResult)); return true; } @@ -668,12 +1023,12 @@ bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedDa // align with WotLK's getSpellCastResultString table. // ============================================================================ bool ClassicPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) { - if (packet.getSize() - packet.getReadPos() < 5) return false; + if (!packet.hasRemaining(5)) return false; spellId = packet.readUInt32(); uint8_t vanillaResult = packet.readUInt8(); // Shift +1: Vanilla result 0=AFFECTING_COMBAT maps to WotLK result 1=AFFECTING_COMBAT result = vanillaResult + 1; - LOG_DEBUG("[Classic] Cast result: spell=", spellId, " vanillaResult=", (int)vanillaResult); + LOG_DEBUG("[Classic] Cast result: spell=", spellId, " vanillaResult=", static_cast(vanillaResult)); return true; } @@ -697,12 +1052,12 @@ bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumRespon // Cap count to prevent excessive memory allocation constexpr uint8_t kMaxCharacters = 32; if (count > kMaxCharacters) { - LOG_WARNING("[Classic] Character count ", (int)count, " exceeds max ", (int)kMaxCharacters, + LOG_WARNING("[Classic] Character count ", static_cast(count), " exceeds max ", static_cast(kMaxCharacters), ", capping"); count = kMaxCharacters; } - LOG_INFO("[Classic] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters"); + LOG_INFO("[Classic] Parsing SMSG_CHAR_ENUM: ", static_cast(count), " characters"); response.characters.clear(); response.characters.reserve(count); @@ -713,8 +1068,8 @@ bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumRespon // + facialFeatures(1) + level(1) + zone(4) + map(4) + pos(12) + guild(4) // + flags(4) + firstLogin(1) + pet(12) + equipment(20*5) constexpr size_t kMinCharacterSize = 8 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 4 + 4 + 12 + 4 + 4 + 1 + 12 + 100; - if (packet.getReadPos() + kMinCharacterSize > packet.getSize()) { - LOG_WARNING("[Classic] Character enum packet truncated at character ", (int)(i + 1), + if (!packet.hasRemaining(kMinCharacterSize)) { + LOG_WARNING("[Classic] Character enum packet truncated at character ", static_cast(i + 1), ", pos=", packet.getReadPos(), " needed=", kMinCharacterSize, " size=", packet.getSize()); break; @@ -771,9 +1126,9 @@ bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumRespon character.equipment.push_back(item); } - LOG_DEBUG(" Character ", (int)(i + 1), ": ", character.name, + LOG_DEBUG(" Character ", static_cast(i + 1), ": ", character.name, " (", getRaceName(character.race), " ", getClassName(character.characterClass), - " level ", (int)character.level, " zone ", character.zoneId, ")"); + " level ", static_cast(character.level), " zone ", character.zoneId, ")"); response.characters.push_back(character); } @@ -891,7 +1246,7 @@ bool ClassicPacketParsers::parseMessageChat(network::Packet& packet, MessageChat } // Read chat tag - if (packet.getReadPos() < packet.getSize()) { + if (packet.hasData()) { data.chatTag = packet.readUInt8(); } @@ -1017,7 +1372,7 @@ bool ClassicPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, } // Validate minimum size for fixed fields: type(4) + displayId(4) - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_ERROR("Classic SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")"); return false; } @@ -1031,7 +1386,7 @@ bool ClassicPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, packet.readString(); // Classic: data[24] comes immediately after names (no extra strings) - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining >= 24 * 4) { for (int i = 0; i < 24; i++) { data.data[i] = packet.readUInt32(); @@ -1064,7 +1419,7 @@ bool ClassicPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, // ============================================================================ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessageData& data) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 8 + 4 + 4) { LOG_ERROR("Classic SMSG_GOSSIP_MESSAGE too small: ", remaining, " bytes"); return false; @@ -1088,7 +1443,7 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes data.options.reserve(optionCount); for (uint32_t i = 0; i < optionCount; ++i) { // Sanity check: ensure minimum bytes available for option (id(4)+icon(1)+isCoded(1)+text(1)) - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 7) { LOG_WARNING("Classic gossip option ", i, " truncated (", remaining, " bytes left)"); break; @@ -1106,7 +1461,7 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes } // Ensure we have at least 4 bytes for questCount - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 4) { LOG_WARNING("Classic SMSG_GOSSIP_MESSAGE truncated before questCount"); return data.options.size() > 0; // Return true if we got at least some options @@ -1126,7 +1481,7 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes data.quests.reserve(questCount); for (uint32_t i = 0; i < questCount; ++i) { // Sanity check: ensure minimum bytes available for quest (id(4)+icon(4)+level(4)+title(1)) - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 13) { LOG_WARNING("Classic gossip quest ", i, " truncated (", remaining, " bytes left)"); break; @@ -1188,17 +1543,17 @@ network::Packet ClassicPacketParsers::buildSendMail(uint64_t mailboxGuid, // ============================================================================ bool ClassicPacketParsers::parseMailList(network::Packet& packet, std::vector& inbox) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 1) return false; uint8_t count = packet.readUInt8(); - LOG_INFO("SMSG_MAIL_LIST_RESULT (Classic): count=", (int)count); + LOG_INFO("SMSG_MAIL_LIST_RESULT (Classic): count=", static_cast(count)); inbox.clear(); inbox.reserve(count); for (uint8_t i = 0; i < count; ++i) { - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 5) { LOG_WARNING("Classic mail entry ", i, " truncated (", remaining, " bytes left)"); break; @@ -1322,7 +1677,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ } // Validate minimum size for fixed fields: itemClass(4) + subClass(4) + 4 name strings + displayInfoId(4) + quality(4) - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before names (entry=", data.entry, ")"); return false; } @@ -1376,12 +1731,12 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ data.quality = packet.readUInt32(); // Validate minimum size for fixed fields: Flags(4) + BuyPrice(4) + SellPrice(4) + inventoryType(4) - if (packet.getSize() - packet.getReadPos() < 16) { + if (!packet.hasRemaining(16)) { LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before inventoryType (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // Flags + data.itemFlags = packet.readUInt32(); // Flags // Vanilla: NO Flags2 packet.readUInt32(); // BuyPrice data.sellPrice = packet.readUInt32(); // SellPrice @@ -1389,33 +1744,33 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ data.inventoryType = packet.readUInt32(); // Validate minimum size for remaining fixed fields: 13×4 = 52 bytes - if (packet.getSize() - packet.getReadPos() < 52) { + if (!packet.hasRemaining(52)) { LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before stats (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // AllowableClass - packet.readUInt32(); // AllowableRace + data.allowableClass = packet.readUInt32(); // AllowableClass + data.allowableRace = packet.readUInt32(); // AllowableRace data.itemLevel = packet.readUInt32(); data.requiredLevel = packet.readUInt32(); - packet.readUInt32(); // RequiredSkill - packet.readUInt32(); // RequiredSkillRank + data.requiredSkill = packet.readUInt32(); // RequiredSkill + data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank packet.readUInt32(); // RequiredSpell packet.readUInt32(); // RequiredHonorRank packet.readUInt32(); // RequiredCityRank - packet.readUInt32(); // RequiredReputationFaction - packet.readUInt32(); // RequiredReputationRank - packet.readUInt32(); // MaxCount + data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction + data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank + data.maxCount = static_cast(packet.readUInt32()); // MaxCount (1 = Unique) data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); // Vanilla: 10 stat pairs, NO statsCount prefix (10×8 = 80 bytes) - if (packet.getSize() - packet.getReadPos() < 80) { + if (!packet.hasRemaining(80)) { LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated in stats section (entry=", data.entry, ")"); // Read what we can } for (uint32_t i = 0; i < 10; i++) { - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")"); break; } @@ -1442,7 +1797,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ bool haveWeaponDamage = false; for (int i = 0; i < 5; i++) { // Each damage entry is dmgMin(4) + dmgMax(4) + damageType(4) = 12 bytes - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: damage ", i, " truncated (entry=", data.entry, ")"); break; } @@ -1460,25 +1815,25 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ } // Validate minimum size for armor field (4 bytes) - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before armor (entry=", data.entry, ")"); return true; // Have core fields; armor is important but optional } data.armor = static_cast(packet.readUInt32()); // Remaining tail can vary by core. Read resistances + delay when present. - if (packet.getSize() - packet.getReadPos() >= 28) { - packet.readUInt32(); // HolyRes - packet.readUInt32(); // FireRes - packet.readUInt32(); // NatureRes - packet.readUInt32(); // FrostRes - packet.readUInt32(); // ShadowRes - packet.readUInt32(); // ArcaneRes + if (packet.hasRemaining(28)) { + data.holyRes = static_cast(packet.readUInt32()); // HolyRes + data.fireRes = static_cast(packet.readUInt32()); // FireRes + data.natureRes = static_cast(packet.readUInt32()); // NatureRes + data.frostRes = static_cast(packet.readUInt32()); // FrostRes + data.shadowRes = static_cast(packet.readUInt32()); // ShadowRes + data.arcaneRes = static_cast(packet.readUInt32()); // ArcaneRes data.delayMs = packet.readUInt32(); } // AmmoType + RangedModRange (2 fields, 8 bytes) - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { packet.readUInt32(); // AmmoType packet.readFloat(); // RangedModRange } @@ -1486,7 +1841,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ // 2 item spells in Vanilla (3 fields each: SpellId, Trigger, Charges) // Actually vanilla has 5 spells: SpellId, Trigger, Charges, Cooldown, Category, CatCooldown = 24 bytes each for (int i = 0; i < 5; i++) { - if (packet.getReadPos() + 24 > packet.getSize()) break; + if (!packet.hasRemaining(24)) break; data.spells[i].spellId = packet.readUInt32(); data.spells[i].spellTrigger = packet.readUInt32(); packet.readUInt32(); // SpellCharges @@ -1496,15 +1851,15 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ } // Bonding type - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.bindType = packet.readUInt32(); // Description (flavor/lore text) - if (packet.getReadPos() < packet.getSize()) + if (packet.hasData()) data.description = packet.readString(); // Post-description: PageText, LanguageID, PageMaterial, StartQuest - if (packet.getReadPos() + 16 <= packet.getSize()) { + if (packet.hasRemaining(16)) { packet.readUInt32(); // PageText packet.readUInt32(); // LanguageID packet.readUInt32(); // PageMaterial @@ -1555,19 +1910,24 @@ namespace TurtleMoveFlags { } bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { + auto rem = [&]() -> size_t { return packet.getRemainingSize(); }; + if (rem() < 1) return false; + uint8_t updateFlags = packet.readUInt8(); block.updateFlags = static_cast(updateFlags); - LOG_DEBUG(" [Turtle] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec); + LOG_DEBUG(" [Turtle] UpdateFlags: 0x", std::hex, static_cast(updateFlags), std::dec); - const uint8_t UPDATEFLAG_LIVING = 0x20; - const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; - const uint8_t UPDATEFLAG_HAS_TARGET = 0x04; - const uint8_t UPDATEFLAG_TRANSPORT = 0x02; - const uint8_t UPDATEFLAG_LOWGUID = 0x08; - const uint8_t UPDATEFLAG_HIGHGUID = 0x10; + const uint8_t UPDATEFLAG_TRANSPORT = 0x02; + const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04; + const uint8_t UPDATEFLAG_HIGHGUID = 0x08; + const uint8_t UPDATEFLAG_ALL = 0x10; + const uint8_t UPDATEFLAG_LIVING = 0x20; + const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; if (updateFlags & UPDATEFLAG_LIVING) { + // Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes + if (rem() < 52) return false; size_t livingStart = packet.getReadPos(); uint32_t moveFlags = packet.readUInt32(); @@ -1586,8 +1946,10 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // Transport — Classic flag position 0x02000000 if (moveFlags & TurtleMoveFlags::ONTRANSPORT) { + if (rem() < 1) return false; // PackedGuid mask byte block.onTransport = true; - block.transportGuid = UpdateObjectParser::readPackedGuid(packet); + block.transportGuid = packet.readPackedGuid(); + if (rem() < 20) return false; // 4 floats + u32 timestamp block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); @@ -1597,14 +1959,17 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // Pitch (swimming only, Classic-style) if (moveFlags & TurtleMoveFlags::SWIMMING) { + if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time (always present) + if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jump data if (moveFlags & TurtleMoveFlags::JUMPING) { + if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -1613,10 +1978,12 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc // Spline elevation if (moveFlags & TurtleMoveFlags::SPLINE_ELEVATION) { + if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Turtle: 6 speeds (same as Classic — no flight speeds) + if (rem() < 24) return false; // 6 × float float walkSpeed = packet.readFloat(); float runSpeed = packet.readFloat(); float runBackSpeed = packet.readFloat(); @@ -1634,17 +2001,23 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc bool hasSpline = (moveFlags & TurtleMoveFlags::SPLINE_CLASSIC) || (moveFlags & TurtleMoveFlags::SPLINE_TBC); if (hasSpline) { + if (rem() < 4) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" [Turtle] Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { + if (rem() < 12) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } else if (splineFlags & 0x00020000) { + if (rem() < 8) return false; packet.readUInt64(); } else if (splineFlags & 0x00040000) { + if (rem() < 4) return false; packet.readFloat(); } + // timePassed + duration + splineId + pointCount = 16 bytes + if (rem() < 16) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); @@ -1655,10 +2028,12 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc ++badTurtleSplineCount; if (badTurtleSplineCount <= 5 || (badTurtleSplineCount % 100) == 0) { LOG_WARNING(" [Turtle] Spline pointCount=", pointCount, - " exceeds max, capping (occurrence=", badTurtleSplineCount, ")"); + " exceeds max (occurrence=", badTurtleSplineCount, ")"); } - pointCount = 0; + return false; } + // points + endPoint + if (rem() < static_cast(pointCount) * 12 + 12) return false; for (uint32_t i = 0; i < pointCount; i++) { packet.readFloat(); packet.readFloat(); packet.readFloat(); } @@ -1671,6 +2046,7 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc " bytes, readPos now=", packet.getReadPos()); } else if (updateFlags & UPDATEFLAG_HAS_POSITION) { + if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -1680,37 +2056,263 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc LOG_DEBUG(" [Turtle] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); } - // Target GUID - if (updateFlags & UPDATEFLAG_HAS_TARGET) { - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); - } - - // Transport time - if (updateFlags & UPDATEFLAG_TRANSPORT) { - /*uint32_t transportTime =*/ packet.readUInt32(); - } - - // Low GUID — Classic-style: 1×u32 (NOT TBC's 2×u32) - if (updateFlags & UPDATEFLAG_LOWGUID) { - /*uint32_t lowGuid =*/ packet.readUInt32(); - } - // High GUID — 1×u32 if (updateFlags & UPDATEFLAG_HIGHGUID) { + if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } + if (updateFlags & UPDATEFLAG_ALL) { + if (rem() < 4) return false; + /*uint32_t unkAll =*/ packet.readUInt32(); + } + + if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { + if (rem() < 1) return false; + /*uint64_t meleeTargetGuid =*/ packet.readPackedGuid(); + } + + if (updateFlags & UPDATEFLAG_TRANSPORT) { + if (rem() < 4) return false; + /*uint32_t transportTime =*/ packet.readUInt32(); + } + return true; } +bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) { + constexpr uint32_t kMaxReasonableUpdateBlocks = 4096; + + auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool { + out = UpdateObjectData{}; + const size_t start = packet.getReadPos(); + if (packet.getSize() - start < 4) return false; + + out.blockCount = packet.readUInt32(); + if (out.blockCount > kMaxReasonableUpdateBlocks) { + packet.setReadPos(start); + return false; + } + + if (withHasTransportByte) { + if (!packet.hasData()) { + packet.setReadPos(start); + return false; + } + /*uint8_t hasTransport =*/ packet.readUInt8(); + } + + uint32_t remainingBlockCount = out.blockCount; + + if (packet.hasRemaining(1)) { + uint8_t firstByte = packet.readUInt8(); + if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { + if (remainingBlockCount == 0) { + packet.setReadPos(start); + return false; + } + --remainingBlockCount; + if (!packet.hasRemaining(4)) { + packet.setReadPos(start); + return false; + } + uint32_t count = packet.readUInt32(); + if (count > kMaxReasonableUpdateBlocks) { + packet.setReadPos(start); + return false; + } + for (uint32_t i = 0; i < count; ++i) { + if (!packet.hasData()) { + packet.setReadPos(start); + return false; + } + out.outOfRangeGuids.push_back(packet.readPackedGuid()); + } + } else { + packet.setReadPos(packet.getReadPos() - 1); + } + } + + out.blockCount = remainingBlockCount; + out.blocks.reserve(out.blockCount); + for (uint32_t i = 0; i < out.blockCount; ++i) { + if (!packet.hasData()) { + packet.setReadPos(start); + return false; + } + + const size_t blockStart = packet.getReadPos(); + uint8_t updateTypeVal = packet.readUInt8(); + if (updateTypeVal > static_cast(UpdateType::NEAR_OBJECTS)) { + packet.setReadPos(start); + return false; + } + + const UpdateType updateType = static_cast(updateTypeVal); + UpdateBlock block; + block.updateType = updateType; + bool ok = false; + + auto parseMovementVariant = [&](auto&& movementParser, const char* layoutName) -> bool { + packet.setReadPos(blockStart + 1); + block = UpdateBlock{}; + block.updateType = updateType; + + switch (updateType) { + case UpdateType::MOVEMENT: + block.guid = packet.readUInt64(); + if (!movementParser(packet, block)) return false; + LOG_DEBUG("[Turtle] Parsed MOVEMENT block via ", layoutName, " layout"); + return true; + case UpdateType::CREATE_OBJECT: + case UpdateType::CREATE_OBJECT2: + block.guid = packet.readPackedGuid(); + if (!packet.hasData()) return false; + block.objectType = static_cast(packet.readUInt8()); + if (!movementParser(packet, block)) return false; + if (!UpdateObjectParser::parseUpdateFields(packet, block)) return false; + LOG_DEBUG("[Turtle] Parsed CREATE block via ", layoutName, " layout"); + return true; + default: + return false; + } + }; + + switch (updateType) { + case UpdateType::VALUES: + block.guid = packet.readPackedGuid(); + ok = UpdateObjectParser::parseUpdateFields(packet, block); + break; + case UpdateType::MOVEMENT: + case UpdateType::CREATE_OBJECT: + case UpdateType::CREATE_OBJECT2: + ok = parseMovementVariant( + [this](network::Packet& p, UpdateBlock& b) { + return this->TurtlePacketParsers::parseMovementBlock(p, b); + }, "turtle"); + if (!ok) { + ok = parseMovementVariant( + [this](network::Packet& p, UpdateBlock& b) { + return this->ClassicPacketParsers::parseMovementBlock(p, b); + }, "classic"); + } + if (!ok) { + ok = parseMovementVariant( + [this](network::Packet& p, UpdateBlock& b) { + return this->TbcPacketParsers::parseMovementBlock(p, b); + }, "tbc"); + } + // NOTE: Do NOT fall back to WotLK parseMovementBlock here. + // WotLK uses uint16 updateFlags and 9 speeds vs Classic's uint8 + // and 6 speeds. A false-positive WotLK parse consumes wrong bytes, + // corrupting subsequent update fields and losing NPC data. + break; + case UpdateType::OUT_OF_RANGE_OBJECTS: + case UpdateType::NEAR_OBJECTS: + ok = true; + break; + default: + ok = false; + break; + } + + if (!ok) { + LOG_WARNING("[Turtle] SMSG_UPDATE_OBJECT block parse failed", + " blockIndex=", i, + " updateType=", updateTypeName(updateType), + " readPos=", packet.getReadPos(), + " blockStart=", blockStart, + " packetSize=", packet.getSize()); + packet.setReadPos(start); + return false; + } + + out.blocks.push_back(std::move(block)); + } + + return true; + }; + + const size_t startPos = packet.getReadPos(); + UpdateObjectData parsed; + if (parseWithLayout(true, parsed)) { + data = std::move(parsed); + return true; + } + + packet.setReadPos(startPos); + if (parseWithLayout(false, parsed)) { + LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback"); + data = std::move(parsed); + return true; + } + + packet.setReadPos(startPos); + if (ClassicPacketParsers::parseUpdateObject(packet, parsed)) { + LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full classic fallback"); + data = std::move(parsed); + return true; + } + + packet.setReadPos(startPos); + if (TbcPacketParsers::parseUpdateObject(packet, parsed)) { + LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full TBC fallback"); + data = std::move(parsed); + return true; + } + + packet.setReadPos(startPos); + return false; +} + bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) { - // Turtle realms can emit both vanilla-like and WotLK-like monster move bodies. - // Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout. + // Turtle realms can emit vanilla-like, TBC-like, and WotLK-like monster move + // bodies. Try the lower-expansion layouts first before the WotLK parser that + // expects an extra unk byte after the packed GUID. size_t start = packet.getReadPos(); if (MonsterMoveParser::parseVanilla(packet, data)) { return true; } + packet.setReadPos(start); + if (TbcPacketParsers::parseMonsterMove(packet, data)) { + LOG_DEBUG("[Turtle] SMSG_MONSTER_MOVE parsed via TBC fallback layout"); + return true; + } + + auto looksLikeWotlkMonsterMove = [&](network::Packet& probe) -> bool { + const size_t probeStart = probe.getReadPos(); + uint64_t guid = probe.readPackedGuid(); + if (guid == 0) { + probe.setReadPos(probeStart); + return false; + } + if (probe.getReadPos() >= probe.getSize()) { + probe.setReadPos(probeStart); + return false; + } + uint8_t unk = probe.readUInt8(); + if (unk > 1) { + probe.setReadPos(probeStart); + return false; + } + if (probe.getReadPos() + 12 + 4 + 1 > probe.getSize()) { + probe.setReadPos(probeStart); + return false; + } + probe.readFloat(); probe.readFloat(); probe.readFloat(); // xyz + probe.readUInt32(); // splineId + uint8_t moveType = probe.readUInt8(); + probe.setReadPos(probeStart); + return moveType >= 1 && moveType <= 4; + }; + + packet.setReadPos(start); + if (!looksLikeWotlkMonsterMove(packet)) { + packet.setReadPos(start); + return false; + } + packet.setReadPos(start); if (MonsterMoveParser::parse(packet, data)) { LOG_DEBUG("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout"); @@ -1796,7 +2398,7 @@ bool ClassicPacketParsers::parseCreatureQueryResponse(network::Packet& packet, packet.readString(); // name4 data.subName = packet.readString(); // NOTE: NO iconName field in Classic 1.12 — goes straight to typeFlags - if (packet.getReadPos() + 16 > packet.getSize()) { + if (!packet.hasRemaining(16)) { LOG_WARNING("Classic SMSG_CREATURE_QUERY_RESPONSE: truncated at typeFlags (entry=", data.entry, ")"); data.typeFlags = 0; data.creatureType = 0; diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index ffc462ad..8d86a808 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -30,17 +30,14 @@ namespace TbcMoveFlags { // - Flag 0x08 (HIGH_GUID) reads 2 u32s (Classic: 1 u32) // ============================================================================ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - // Validate minimum packet size for updateFlags byte - if (packet.getReadPos() >= packet.getSize()) { - LOG_WARNING("[TBC] Movement block packet too small (need at least 1 byte for updateFlags)"); - return false; - } + auto rem = [&]() -> size_t { return packet.getRemainingSize(); }; + if (rem() < 1) return false; // TBC 2.4.3: UpdateFlags is uint8 (1 byte) uint8_t updateFlags = packet.readUInt8(); block.updateFlags = static_cast(updateFlags); - LOG_DEBUG(" [TBC] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec); + LOG_DEBUG(" [TBC] UpdateFlags: 0x", std::hex, static_cast(updateFlags), std::dec); // TBC UpdateFlag bit values (same as lower byte of WotLK): // 0x01 = SELF @@ -58,6 +55,9 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& const uint8_t UPDATEFLAG_HIGHGUID = 0x10; if (updateFlags & UPDATEFLAG_LIVING) { + // Minimum: moveFlags(4)+moveFlags2(1)+time(4)+position(16)+fallTime(4)+speeds(32) = 61 + if (rem() < 61) return false; + // Full movement block for living units uint32_t moveFlags = packet.readUInt32(); uint8_t moveFlags2 = packet.readUInt8(); // TBC: uint8, not uint16 @@ -76,29 +76,33 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Transport data if (moveFlags & TbcMoveFlags::ON_TRANSPORT) { + if (rem() < 1) return false; block.onTransport = true; - block.transportGuid = UpdateObjectParser::readPackedGuid(packet); + block.transportGuid = packet.readPackedGuid(); + if (rem() < 20) return false; // 4 floats + 1 uint32 block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); block.transportO = packet.readFloat(); /*uint32_t tTime =*/ packet.readUInt32(); - // TBC: NO transport seat byte - // TBC: NO interpolated movement check } // Pitch: SWIMMING, or else ONTRANSPORT (TBC-specific secondary pitch) if (moveFlags & TbcMoveFlags::SWIMMING) { + if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } else if (moveFlags & TbcMoveFlags::ONTRANSPORT) { + if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time (always present) + if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping (TBC: JUMPING=0x2000, WotLK: FALLING=0x1000) if (moveFlags & TbcMoveFlags::JUMPING) { + if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -107,11 +111,12 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Spline elevation (TBC: 0x02000000, WotLK: 0x04000000) if (moveFlags & TbcMoveFlags::SPLINE_ELEVATION) { + if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Speeds (TBC: 8 values — walk, run, runBack, swim, fly, flyBack, swimBack, turn) - // WotLK adds pitchRate (9 total) + if (rem() < 32) return false; /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); @@ -126,49 +131,47 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Spline data (TBC/WotLK: SPLINE_ENABLED = 0x08000000) if (moveFlags & TbcMoveFlags::SPLINE_ENABLED) { + if (rem() < 4) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" [TBC] Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { // FINAL_POINT + if (rem() < 12) return false; /*float finalX =*/ packet.readFloat(); /*float finalY =*/ packet.readFloat(); /*float finalZ =*/ packet.readFloat(); } else if (splineFlags & 0x00020000) { // FINAL_TARGET + if (rem() < 8) return false; /*uint64_t finalTarget =*/ packet.readUInt64(); } else if (splineFlags & 0x00040000) { // FINAL_ANGLE + if (rem() < 4) return false; /*float finalAngle =*/ packet.readFloat(); } - // TBC spline: timePassed, duration, id, nodes, finalNode - // (no durationMod, durationModNext, verticalAccel, effectStartTime, splineMode) + // TBC spline: timePassed, duration, id, pointCount + if (rem() < 16) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); uint32_t pointCount = packet.readUInt32(); - if (pointCount > 256) { - static uint32_t badTbcSplineCount = 0; - ++badTbcSplineCount; - if (badTbcSplineCount <= 5 || (badTbcSplineCount % 100) == 0) { - LOG_WARNING(" [TBC] Spline pointCount=", pointCount, - " exceeds max, capping (occurrence=", badTbcSplineCount, ")"); - } - pointCount = 0; - } + if (pointCount > 256) return false; + + // points + endPoint (no splineMode in TBC) + if (rem() < static_cast(pointCount) * 12 + 12) return false; for (uint32_t i = 0; i < pointCount; i++) { /*float px =*/ packet.readFloat(); /*float py =*/ packet.readFloat(); /*float pz =*/ packet.readFloat(); } - // TBC: NO splineMode byte (WotLK adds it) /*float endPointX =*/ packet.readFloat(); /*float endPointY =*/ packet.readFloat(); /*float endPointZ =*/ packet.readFloat(); } } else if (updateFlags & UPDATEFLAG_HAS_POSITION) { - // TBC: Simple stationary position (same as WotLK STATIONARY) + if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -177,29 +180,29 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& LOG_DEBUG(" [TBC] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); } - // TBC: No UPDATEFLAG_POSITION (0x0100) code path // Target GUID if (updateFlags & UPDATEFLAG_HAS_TARGET) { - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + if (rem() < 1) return false; + /*uint64_t targetGuid =*/ packet.readPackedGuid(); } // Transport time if (updateFlags & UPDATEFLAG_TRANSPORT) { + if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } - // TBC: No VEHICLE flag (WotLK 0x0080) - // TBC: No ROTATION flag (WotLK 0x0200) - - // HIGH_GUID (0x08) — TBC has 2 u32s, Classic has 1 u32 + // LOWGUID (0x08) — TBC has 2 u32s, Classic has 1 u32 if (updateFlags & UPDATEFLAG_LOWGUID) { + if (rem() < 8) return false; /*uint32_t unknown0 =*/ packet.readUInt32(); /*uint32_t unknown1 =*/ packet.readUInt32(); } - // ALL (0x10) + // HIGHGUID (0x10) if (updateFlags & UPDATEFLAG_HIGHGUID) { + if (rem() < 4) return false; /*uint32_t unknown2 =*/ packet.readUInt32(); } @@ -225,10 +228,10 @@ void TbcPacketParsers::writeMovementPayload(network::Packet& packet, const Movem packet.writeUInt32(info.time); // Position - packet.writeBytes(reinterpret_cast(&info.x), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.y), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.z), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.orientation), sizeof(float)); + packet.writeFloat(info.x); + packet.writeFloat(info.y); + packet.writeFloat(info.z); + packet.writeFloat(info.orientation); // Transport data (TBC ON_TRANSPORT = 0x200, same bit as WotLK) if (info.flags & TbcMoveFlags::ON_TRANSPORT) { @@ -249,10 +252,10 @@ void TbcPacketParsers::writeMovementPayload(network::Packet& packet, const Movem } // Transport local position - packet.writeBytes(reinterpret_cast(&info.transportX), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportY), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportZ), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportO), sizeof(float)); + packet.writeFloat(info.transportX); + packet.writeFloat(info.transportY); + packet.writeFloat(info.transportZ); + packet.writeFloat(info.transportO); // Transport time packet.writeUInt32(info.transportTime); @@ -263,9 +266,9 @@ void TbcPacketParsers::writeMovementPayload(network::Packet& packet, const Movem // Pitch: SWIMMING or else ONTRANSPORT (TBC flag positions) if (info.flags & TbcMoveFlags::SWIMMING) { - packet.writeBytes(reinterpret_cast(&info.pitch), sizeof(float)); + packet.writeFloat(info.pitch); } else if (info.flags & TbcMoveFlags::ONTRANSPORT) { - packet.writeBytes(reinterpret_cast(&info.pitch), sizeof(float)); + packet.writeFloat(info.pitch); } // Fall time (always present) @@ -273,10 +276,10 @@ void TbcPacketParsers::writeMovementPayload(network::Packet& packet, const Movem // Jump data (TBC JUMPING = 0x2000, WotLK FALLING = 0x1000) if (info.flags & TbcMoveFlags::JUMPING) { - packet.writeBytes(reinterpret_cast(&info.jumpVelocity), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpSinAngle), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpCosAngle), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpXYSpeed), sizeof(float)); + packet.writeFloat(info.jumpVelocity); + packet.writeFloat(info.jumpSinAngle); + packet.writeFloat(info.jumpCosAngle); + packet.writeFloat(info.jumpXYSpeed); } } @@ -314,12 +317,12 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& // Cap count to prevent excessive memory allocation constexpr uint8_t kMaxCharacters = 32; if (count > kMaxCharacters) { - LOG_WARNING("[TBC] Character count ", (int)count, " exceeds max ", (int)kMaxCharacters, + LOG_WARNING("[TBC] Character count ", static_cast(count), " exceeds max ", static_cast(kMaxCharacters), ", capping"); count = kMaxCharacters; } - LOG_INFO("[TBC] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters"); + LOG_INFO("[TBC] Parsing SMSG_CHAR_ENUM: ", static_cast(count), " characters"); response.characters.clear(); response.characters.reserve(count); @@ -330,8 +333,8 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& // + facialFeatures(1) + level(1) + zone(4) + map(4) + pos(12) + guild(4) // + flags(4) + firstLogin(1) + pet(12) + equipment(20*9) constexpr size_t kMinCharacterSize = 8 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 4 + 4 + 12 + 4 + 4 + 1 + 12 + 180; - if (packet.getReadPos() + kMinCharacterSize > packet.getSize()) { - LOG_WARNING("[TBC] Character enum packet truncated at character ", (int)(i + 1), + if (!packet.hasRemaining(kMinCharacterSize)) { + LOG_WARNING("[TBC] Character enum packet truncated at character ", static_cast(i + 1), ", pos=", packet.getReadPos(), " needed=", kMinCharacterSize, " size=", packet.getSize()); break; @@ -388,9 +391,9 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& character.equipment.push_back(item); } - LOG_DEBUG(" Character ", (int)(i + 1), ": ", character.name, + LOG_DEBUG(" Character ", static_cast(i + 1), ": ", character.name, " (", getRaceName(character.race), " ", getClassName(character.characterClass), - " level ", (int)character.level, " zone ", character.zoneId, ")"); + " level ", static_cast(character.level), " zone ", character.zoneId, ")"); response.characters.push_back(character); } @@ -425,10 +428,17 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa /*uint8_t hasTransport =*/ packet.readUInt8(); } - if (packet.getReadPos() + 1 <= packet.getSize()) { + uint32_t remainingBlockCount = out.blockCount; + + if (packet.hasRemaining(1)) { uint8_t firstByte = packet.readUInt8(); if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { - if (packet.getReadPos() + 4 > packet.getSize()) { + if (remainingBlockCount == 0) { + packet.setReadPos(start); + return false; + } + --remainingBlockCount; + if (!packet.hasRemaining(4)) { packet.setReadPos(start); return false; } @@ -442,7 +452,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa packet.setReadPos(start); return false; } - uint64_t guid = UpdateObjectParser::readPackedGuid(packet); + uint64_t guid = packet.readPackedGuid(); out.outOfRangeGuids.push_back(guid); } } else { @@ -450,6 +460,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa } } + out.blockCount = remainingBlockCount; out.blocks.reserve(out.blockCount); for (uint32_t i = 0; i < out.blockCount; ++i) { if (packet.getReadPos() >= packet.getSize()) { @@ -468,18 +479,18 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa bool ok = false; switch (block.updateType) { case UpdateType::VALUES: { - block.guid = UpdateObjectParser::readPackedGuid(packet); + block.guid = packet.readPackedGuid(); ok = UpdateObjectParser::parseUpdateFields(packet, block); break; } case UpdateType::MOVEMENT: { - block.guid = UpdateObjectParser::readPackedGuid(packet); + block.guid = packet.readUInt64(); ok = this->parseMovementBlock(packet, block); break; } case UpdateType::CREATE_OBJECT: case UpdateType::CREATE_OBJECT2: { - block.guid = UpdateObjectParser::readPackedGuid(packet); + block.guid = packet.readPackedGuid(); if (packet.getReadPos() >= packet.getSize()) { ok = false; break; @@ -533,7 +544,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa // reads those 5 bytes as part of the quest title, corrupting all gossip quests. // ============================================================================ bool TbcPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessageData& data) { - if (packet.getSize() - packet.getReadPos() < 16) return false; + if (!packet.hasRemaining(16)) return false; data.npcGuid = packet.readUInt64(); data.menuId = packet.readUInt32(); // TBC added menuId (Classic doesn't have it) @@ -553,7 +564,7 @@ bool TbcPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessage for (uint32_t i = 0; i < optionCount; ++i) { // Sanity check: ensure minimum bytes available for option // (id(4)+icon(1)+isCoded(1)+boxMoney(4)+text(1)+boxText(1)) - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 12) { LOG_WARNING("[TBC] gossip option ", i, " truncated (", remaining, " bytes left)"); break; @@ -570,7 +581,7 @@ bool TbcPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessage } // Ensure we have at least 4 bytes for questCount - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 4) { LOG_WARNING("[TBC] SMSG_GOSSIP_MESSAGE truncated before questCount"); return data.options.size() > 0; // Return true if we got at least some options @@ -591,7 +602,7 @@ bool TbcPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessage for (uint32_t i = 0; i < questCount; ++i) { // Sanity check: ensure minimum bytes available for quest // (id(4)+icon(4)+level(4)+title(1)) - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 13) { LOG_WARNING("[TBC] gossip quest ", i, " truncated (", remaining, " bytes left)"); break; @@ -621,16 +632,16 @@ bool TbcPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessage // byte and parse as garbage. // ============================================================================ bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) { - data.guid = UpdateObjectParser::readPackedGuid(packet); + data.guid = packet.readPackedGuid(); if (data.guid == 0) return false; // No unk byte here in TBC 2.4.3 - if (packet.getReadPos() + 12 > packet.getSize()) return false; + if (!packet.hasRemaining(12)) return false; data.x = packet.readFloat(); data.y = packet.readFloat(); data.z = packet.readFloat(); - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; packet.readUInt32(); // splineId if (packet.getReadPos() >= packet.getSize()) return false; @@ -645,36 +656,36 @@ bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData } if (data.moveType == 2) { - if (packet.getReadPos() + 12 > packet.getSize()) return false; + if (!packet.hasRemaining(12)) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } else if (data.moveType == 3) { - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; data.facingTarget = packet.readUInt64(); } else if (data.moveType == 4) { - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.facingAngle = packet.readFloat(); } - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.splineFlags = packet.readUInt32(); // TBC 2.4.3 SplineFlags animation bit is same as WotLK: 0x00400000 if (data.splineFlags & 0x00400000) { - if (packet.getReadPos() + 5 > packet.getSize()) return false; + if (!packet.hasRemaining(5)) return false; packet.readUInt8(); // animationType packet.readUInt32(); // effectStartTime } - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.duration = packet.readUInt32(); if (data.splineFlags & 0x00000800) { - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; packet.readFloat(); // verticalAcceleration packet.readUInt32(); // effectStartTime } - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; uint32_t pointCount = packet.readUInt32(); if (pointCount == 0) return true; if (pointCount > 16384) return false; @@ -682,16 +693,16 @@ bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0; if (uncompressed) { for (uint32_t i = 0; i < pointCount - 1; i++) { - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (!packet.hasRemaining(12)) return true; packet.readFloat(); packet.readFloat(); packet.readFloat(); } - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (!packet.hasRemaining(12)) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); data.hasDest = true; } else { - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (!packet.hasRemaining(12)) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -699,7 +710,7 @@ bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData } LOG_DEBUG("[TBC] MonsterMove: guid=0x", std::hex, data.guid, std::dec, - " type=", (int)data.moveType, " dur=", data.duration, "ms", + " type=", static_cast(data.moveType), " dur=", data.duration, "ms", " dest=(", data.destX, ",", data.destY, ",", data.destZ, ")"); return true; } @@ -790,7 +801,7 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa data.details = normalizeWowTextTokens(packet.readString()); data.objectives = normalizeWowTextTokens(packet.readString()); - if (packet.getReadPos() + 5 > packet.getSize()) { + if (!packet.hasRemaining(5)) { LOG_DEBUG("Quest details tbc/classic (short): id=", data.questId, " title='", data.title, "'"); return !data.title.empty() || data.questId != 0; } @@ -799,18 +810,18 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa data.suggestedPlayers = packet.readUInt32(); // TBC/Classic: emote section before reward items - if (packet.getReadPos() + 4 <= packet.getSize()) { + if (packet.hasRemaining(4)) { uint32_t emoteCount = packet.readUInt32(); - for (uint32_t i = 0; i < emoteCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) { + for (uint32_t i = 0; i < emoteCount && packet.hasRemaining(8); ++i) { packet.readUInt32(); // delay packet.readUInt32(); // type } } // Choice reward items (variable count, up to QUEST_REWARD_CHOICES_COUNT) - if (packet.getReadPos() + 4 <= packet.getSize()) { + if (packet.hasRemaining(4)) { uint32_t choiceCount = packet.readUInt32(); - for (uint32_t i = 0; i < choiceCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) { + for (uint32_t i = 0; i < choiceCount && packet.hasRemaining(12); ++i) { uint32_t itemId = packet.readUInt32(); uint32_t count = packet.readUInt32(); uint32_t dispId = packet.readUInt32(); @@ -824,9 +835,9 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa } // Fixed reward items (variable count, up to QUEST_REWARDS_COUNT) - if (packet.getReadPos() + 4 <= packet.getSize()) { + if (packet.hasRemaining(4)) { uint32_t rewardCount = packet.readUInt32(); - for (uint32_t i = 0; i < rewardCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) { + for (uint32_t i = 0; i < rewardCount && packet.hasRemaining(12); ++i) { uint32_t itemId = packet.readUInt32(); uint32_t count = packet.readUInt32(); uint32_t dispId = packet.readUInt32(); @@ -838,9 +849,9 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa } } - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.rewardMoney = packet.readUInt32(); - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.rewardXp = packet.readUInt32(); LOG_DEBUG("Quest details tbc/classic: id=", data.questId, " title='", data.title, "'"); @@ -901,7 +912,7 @@ bool TbcPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQuery data.guid = packet.readUInt64(); data.found = 0; data.name = packet.readString(); - if (!data.name.empty() && (packet.getSize() - packet.getReadPos()) >= 12) { + if (!data.name.empty() && packet.hasRemaining(12)) { uint32_t race = packet.readUInt32(); uint32_t gender = packet.readUInt32(); uint32_t cls = packet.readUInt32(); @@ -917,7 +928,7 @@ bool TbcPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQuery { packet.setReadPos(start); data.guid = packet.readUInt64(); - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { packet.setReadPos(start); return false; } @@ -927,7 +938,7 @@ bool TbcPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQuery data.found = found; if (data.found != 0) return true; data.name = packet.readString(); - if (!data.name.empty() && (packet.getSize() - packet.getReadPos()) >= 12) { + if (!data.name.empty() && packet.hasRemaining(12)) { uint32_t race = packet.readUInt32(); uint32_t gender = packet.readUInt32(); uint32_t cls = packet.readUInt32(); @@ -971,7 +982,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery } // Validate minimum size for fixed fields: itemClass(4) + subClass(4) + soundOverride(4) + 4 name strings + displayInfoId(4) + quality(4) - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before names (entry=", data.entry, ")"); return false; } @@ -981,7 +992,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery data.itemClass = itemClass; data.subClass = subClass; packet.readUInt32(); // SoundOverrideSubclass (int32, -1 = no override) - data.subclassName = ""; + data.subclassName = getItemSubclassName(itemClass, subClass); // Name strings data.name = packet.readString(); @@ -993,12 +1004,12 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery data.quality = packet.readUInt32(); // Validate minimum size for fixed fields: Flags(4) + BuyPrice(4) + SellPrice(4) + inventoryType(4) - if (packet.getSize() - packet.getReadPos() < 16) { + if (!packet.hasRemaining(16)) { LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before inventoryType (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2) + data.itemFlags = packet.readUInt32(); // Flags (TBC: 1 flags field only — no Flags2) // TBC: NO Flags2, NO BuyCount packet.readUInt32(); // BuyPrice data.sellPrice = packet.readUInt32(); @@ -1006,28 +1017,28 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery data.inventoryType = packet.readUInt32(); // Validate minimum size for remaining fixed fields: 13×4 = 52 bytes - if (packet.getSize() - packet.getReadPos() < 52) { + if (!packet.hasRemaining(52)) { LOG_ERROR("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // AllowableClass - packet.readUInt32(); // AllowableRace + data.allowableClass = packet.readUInt32(); // AllowableClass + data.allowableRace = packet.readUInt32(); // AllowableRace data.itemLevel = packet.readUInt32(); data.requiredLevel = packet.readUInt32(); - packet.readUInt32(); // RequiredSkill - packet.readUInt32(); // RequiredSkillRank + data.requiredSkill = packet.readUInt32(); // RequiredSkill + data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank packet.readUInt32(); // RequiredSpell packet.readUInt32(); // RequiredHonorRank packet.readUInt32(); // RequiredCityRank - packet.readUInt32(); // RequiredReputationFaction - packet.readUInt32(); // RequiredReputationRank - packet.readUInt32(); // MaxCount - data.maxStack = static_cast(packet.readUInt32()); // Stackable + data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction + data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank + data.maxCount = static_cast(packet.readUInt32()); // MaxCount (1 = Unique) + data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); // TBC: statsCount prefix + exactly statsCount pairs (WotLK always sends 10) - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated at statsCount (entry=", data.entry, ")"); return true; // Have core fields; stats are optional } @@ -1039,7 +1050,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery } for (uint32_t i = 0; i < statsCount; i++) { // Each stat is 2 uint32s = 8 bytes - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")"); break; } @@ -1063,7 +1074,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery bool haveWeaponDamage = false; for (int i = 0; i < 5; i++) { // Each damage entry is dmgMin(4) + dmgMax(4) + damageType(4) = 12 bytes - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: damage ", i, " truncated (entry=", data.entry, ")"); break; } @@ -1080,31 +1091,31 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery } // Validate minimum size for armor (4 bytes) - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_WARNING("TBC SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before armor (entry=", data.entry, ")"); return true; // Have core fields; armor is important but optional } data.armor = static_cast(packet.readUInt32()); - if (packet.getSize() - packet.getReadPos() >= 28) { - packet.readUInt32(); // HolyRes - packet.readUInt32(); // FireRes - packet.readUInt32(); // NatureRes - packet.readUInt32(); // FrostRes - packet.readUInt32(); // ShadowRes - packet.readUInt32(); // ArcaneRes + if (packet.hasRemaining(28)) { + data.holyRes = static_cast(packet.readUInt32()); // HolyRes + data.fireRes = static_cast(packet.readUInt32()); // FireRes + data.natureRes = static_cast(packet.readUInt32()); // NatureRes + data.frostRes = static_cast(packet.readUInt32()); // FrostRes + data.shadowRes = static_cast(packet.readUInt32()); // ShadowRes + data.arcaneRes = static_cast(packet.readUInt32()); // ArcaneRes data.delayMs = packet.readUInt32(); } // AmmoType + RangedModRange - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { packet.readUInt32(); // AmmoType packet.readFloat(); // RangedModRange } // 5 item spells for (int i = 0; i < 5; i++) { - if (packet.getReadPos() + 24 > packet.getSize()) break; + if (!packet.hasRemaining(24)) break; data.spells[i].spellId = packet.readUInt32(); data.spells[i].spellTrigger = packet.readUInt32(); packet.readUInt32(); // SpellCharges @@ -1114,7 +1125,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery } // Bonding type - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.bindType = packet.readUInt32(); // Flavor/lore text @@ -1122,7 +1133,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery data.description = packet.readString(); // Post-description: PageText, LanguageID, PageMaterial, StartQuest - if (packet.getReadPos() + 16 <= packet.getSize()) { + if (packet.hasRemaining(16)) { packet.readUInt32(); // PageText packet.readUInt32(); // LanguageID packet.readUInt32(); // PageMaterial @@ -1147,17 +1158,17 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery // itemTextId and stationery) // ============================================================================ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector& inbox) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 1) return false; uint8_t count = packet.readUInt8(); - LOG_INFO("SMSG_MAIL_LIST_RESULT (TBC): count=", (int)count); + LOG_INFO("SMSG_MAIL_LIST_RESULT (TBC): count=", static_cast(count)); inbox.clear(); inbox.reserve(count); for (uint8_t i = 0; i < count; ++i) { - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 2) break; uint16_t msgSize = packet.readUInt16(); @@ -1220,10 +1231,70 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector bool { + if (!(targetFlags & flag)) return true; + // Packed GUID: 1-byte mask + up to 8 data bytes + if (packet.getReadPos() >= packet.getSize()) return false; + uint8_t mask = packet.getData()[packet.getReadPos()]; + size_t needed = 1; + for (int b = 0; b < 8; ++b) if (mask & (1u << b)) ++needed; + if (!packet.hasRemaining(needed)) return false; + uint64_t g = packet.readPackedGuid(); + if (capture && primaryTargetGuid && *primaryTargetGuid == 0) *primaryTargetGuid = g; + return true; + }; + auto skipFloats3 = [&](uint32_t flag) -> bool { + if (!(targetFlags & flag)) return true; + if (!packet.hasRemaining(12)) return false; + (void)packet.readFloat(); (void)packet.readFloat(); (void)packet.readFloat(); + return true; + }; + + // Process in wire order matching cmangos-tbc SpellCastTargets::write() + if (!readPackedGuidCond(0x0002, true)) return false; // UNIT + if (!readPackedGuidCond(0x0004, false)) return false; // UNIT_MINIPET + if (!readPackedGuidCond(0x0010, false)) return false; // ITEM + if (!skipFloats3(0x0020)) return false; // SOURCE_LOCATION + if (!skipFloats3(0x0040)) return false; // DEST_LOCATION + + if (targetFlags & 0x1000) { // TRADE_ITEM: uint8 + if (packet.getReadPos() >= packet.getSize()) return false; + (void)packet.readUInt8(); + } + if (targetFlags & 0x2000) { // STRING: null-terminated + const auto& raw = packet.getData(); + size_t pos = packet.getReadPos(); + while (pos < raw.size() && raw[pos] != 0) ++pos; + if (pos >= raw.size()) return false; + packet.setReadPos(pos + 1); + } + if (!readPackedGuidCond(0x8200, false)) return false; // CORPSE / PVP_CORPSE + if (!readPackedGuidCond(0x0800, true)) return false; // OBJECT + + return true; +} + // TbcPacketParsers::parseSpellStart — TBC 2.4.3 SMSG_SPELL_START // // TBC uses full uint64 GUIDs for casterGuid and casterUnit. @@ -1234,7 +1305,8 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector= packet.getSize()) { - LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " (no hit data)"); - return true; + LOG_WARNING("[TBC] Spell go: missing hitCount after fixed fields"); + packet.setReadPos(startPos); + return false; } - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent OOM from huge target lists - if (data.hitCount > 128) { - LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", static_cast(rawHitCount), ")"); } - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) { - data.hitTargets.push_back(packet.readUInt64()); // full GUID in TBC - } - // Check if we read all expected hits - if (data.hitTargets.size() < data.hitCount) { - LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)data.hitCount); - data.hitCount = data.hitTargets.size(); - } - - if (packet.getReadPos() < packet.getSize()) { - data.missCount = packet.readUInt8(); - // Cap miss count to prevent OOM - if (data.missCount > 128) { - LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + const uint8_t storedHitLimit = std::min(rawHitCount, 128); + data.hitTargets.reserve(storedHitLimit); + bool truncatedTargets = false; + for (uint16_t i = 0; i < rawHitCount; ++i) { + if (!packet.hasRemaining(8)) { + LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", i, + "/", static_cast(rawHitCount)); + truncatedTargets = true; + break; } - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) { - SpellGoMissEntry m; - m.targetGuid = packet.readUInt64(); // full GUID in TBC - m.missType = packet.readUInt8(); + const uint64_t targetGuid = packet.readUInt64(); // full GUID in TBC + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } + } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.hitCount = static_cast(data.hitTargets.size()); + + if (packet.getReadPos() >= packet.getSize()) { + LOG_WARNING("[TBC] Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; + } + + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[TBC] Spell go: missCount capped (requested=", static_cast(rawMissCount), ")"); + } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { + if (!packet.hasRemaining(9)) { + LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i, + "/", static_cast(rawMissCount)); + truncatedTargets = true; + break; + } + SpellGoMissEntry m; + m.targetGuid = packet.readUInt64(); // full GUID in TBC + m.missType = packet.readUInt8(); + if (m.missType == 11) { // SPELL_MISS_REFLECT + if (!packet.hasRemaining(1)) { + LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i, + "/", static_cast(rawMissCount)); + truncatedTargets = true; + break; + } + (void)packet.readUInt8(); // reflectResult + } + if (i < storedMissLimit) { data.missTargets.push_back(m); } - // Check if we read all expected misses - if (data.missTargets.size() < data.missCount) { - LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)data.missCount); - data.missCount = data.missTargets.size(); - } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.missCount = static_cast(data.missTargets.size()); - LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, - " misses=", (int)data.missCount); + // SpellCastTargets follows the miss list — consume all target bytes so that + // any subsequent fields are not misaligned for ground-targeted AoE spells. + skipTbcSpellCastTargets(packet, &data.targetGuid); + + LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", static_cast(data.hitCount), + " misses=", static_cast(data.missCount)); return true; } @@ -1328,7 +1443,7 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) // then the remaining 4 bytes as spellId (off by one), producing wrong result. // ============================================================================ bool TbcPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) { - if (packet.getSize() - packet.getReadPos() < 5) return false; + if (!packet.hasRemaining(5)) return false; spellId = packet.readUInt32(); // No castCount prefix in TBC result = packet.readUInt8(); return true; @@ -1344,11 +1459,11 @@ bool TbcPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellI // TBC uses the same result values as WotLK so no offset is needed. // ============================================================================ bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& data) { - if (packet.getSize() - packet.getReadPos() < 5) return false; + if (!packet.hasRemaining(5)) return false; data.castCount = 0; // not present in TBC data.spellId = packet.readUInt32(); data.result = packet.readUInt8(); // same enum as WotLK - LOG_DEBUG("[TBC] Cast failed: spell=", data.spellId, " result=", (int)data.result); + LOG_DEBUG("[TBC] Cast failed: spell=", data.spellId, " result=", static_cast(data.result)); return true; } @@ -1360,15 +1475,33 @@ bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& // would mis-parse TBC's GUIDs and corrupt all subsequent damage fields. // ============================================================================ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { - if (packet.getSize() - packet.getReadPos() < 21) return false; + data = AttackerStateUpdateData{}; - data.hitInfo = packet.readUInt32(); - data.attackerGuid = packet.readUInt64(); // full GUID in TBC - data.targetGuid = packet.readUInt64(); // full GUID in TBC - data.totalDamage = static_cast(packet.readUInt32()); + const size_t startPos = packet.getReadPos(); + auto rem = [&]() { return packet.getRemainingSize(); }; + + // Fixed fields before sub-damage list: + // hitInfo(4) + attackerGuid(8) + targetGuid(8) + totalDamage(4) + subDamageCount(1) = 25 bytes + if (rem() < 25) return false; + + data.hitInfo = packet.readUInt32(); + data.attackerGuid = packet.readUInt64(); // full GUID in TBC + data.targetGuid = packet.readUInt64(); // full GUID in TBC + data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); + // Clamp to what can fit in the remaining payload (20 bytes per sub-damage entry). + const uint8_t maxSubDamageCount = static_cast(std::min(rem() / 20, 64)); + if (data.subDamageCount > maxSubDamageCount) { + data.subDamageCount = maxSubDamageCount; + } + + data.subDamages.reserve(data.subDamageCount); for (uint8_t i = 0; i < data.subDamageCount; ++i) { + if (rem() < 20) { + packet.setReadPos(startPos); + return false; + } SubDamage sub; sub.schoolMask = packet.readUInt32(); sub.damage = packet.readFloat(); @@ -1378,10 +1511,17 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke data.subDamages.push_back(sub); } + data.subDamageCount = static_cast(data.subDamages.size()); + + // victimState + overkill are part of the expected payload. + if (rem() < 8) { + packet.setReadPos(startPos); + return false; + } data.victimState = packet.readUInt32(); data.overkill = static_cast(packet.readUInt32()); - if (packet.getReadPos() < packet.getSize()) { + if (rem() >= 4) { data.blocked = packet.readUInt32(); } @@ -1397,20 +1537,28 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke // TBC uses full uint64 GUIDs; WotLK uses packed GUIDs. // ============================================================================ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) { - if (packet.getSize() - packet.getReadPos() < 29) return false; + // Fixed TBC payload size: + // targetGuid(8) + attackerGuid(8) + spellId(4) + damage(4) + schoolMask(1) + // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) + // = 43 bytes + // Some servers append additional trailing fields; consume the canonical minimum + // and leave any extension bytes unread. + if (!packet.hasRemaining(43)) return false; - data.targetGuid = packet.readUInt64(); // full GUID in TBC + data = SpellDamageLogData{}; + + data.targetGuid = packet.readUInt64(); // full GUID in TBC data.attackerGuid = packet.readUInt64(); // full GUID in TBC - data.spellId = packet.readUInt32(); - data.damage = packet.readUInt32(); - data.schoolMask = packet.readUInt8(); - data.absorbed = packet.readUInt32(); - data.resisted = packet.readUInt32(); + data.spellId = packet.readUInt32(); + data.damage = packet.readUInt32(); + data.schoolMask = packet.readUInt8(); + data.absorbed = packet.readUInt32(); + data.resisted = packet.readUInt32(); uint8_t periodicLog = packet.readUInt8(); (void)periodicLog; - packet.readUInt8(); // unused - packet.readUInt32(); // blocked + packet.readUInt8(); // unused + packet.readUInt32(); // blocked uint32_t flags = packet.readUInt32(); data.isCrit = (flags & 0x02) != 0; @@ -1428,13 +1576,17 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL // TBC uses full uint64 GUIDs; WotLK uses packed GUIDs. // ============================================================================ bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) { - if (packet.getSize() - packet.getReadPos() < 25) return false; + // Fixed payload is 28 bytes; many cores append crit flag (1 byte). + // targetGuid(8) + casterGuid(8) + spellId(4) + heal(4) + overheal(4) + if (!packet.hasRemaining(28)) return false; - data.targetGuid = packet.readUInt64(); // full GUID in TBC - data.casterGuid = packet.readUInt64(); // full GUID in TBC - data.spellId = packet.readUInt32(); - data.heal = packet.readUInt32(); - data.overheal = packet.readUInt32(); + data = SpellHealLogData{}; + + data.targetGuid = packet.readUInt64(); // full GUID in TBC + data.casterGuid = packet.readUInt64(); // full GUID in TBC + data.spellId = packet.readUInt32(); + data.heal = packet.readUInt32(); + data.overheal = packet.readUInt32(); // TBC has no absorbed field in SMSG_SPELLHEALLOG; skip crit flag if (packet.getReadPos() < packet.getSize()) { uint8_t critFlag = packet.readUInt8(); @@ -1446,5 +1598,333 @@ bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogDa return true; } +// ============================================================================ +// TBC 2.4.3 SMSG_MESSAGECHAT +// TBC format: type(u8) + language(u32) + [type-specific data] + msgLen(u32) + msg + tag(u8) +// WotLK adds senderGuid(u64) + unknown(u32) before type-specific data. +// ============================================================================ + +bool TbcPacketParsers::parseMessageChat(network::Packet& packet, MessageChatData& data) { + if (packet.getSize() < 10) { + LOG_ERROR("[TBC] SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes"); + return false; + } + + uint8_t typeVal = packet.readUInt8(); + data.type = static_cast(typeVal); + + uint32_t langVal = packet.readUInt32(); + data.language = static_cast(langVal); + + // TBC: NO senderGuid or unknown field here (WotLK has senderGuid(u64) + unk(u32)) + + switch (data.type) { + case ChatType::MONSTER_SAY: + case ChatType::MONSTER_YELL: + case ChatType::MONSTER_EMOTE: + case ChatType::MONSTER_WHISPER: + case ChatType::MONSTER_PARTY: + case ChatType::RAID_BOSS_EMOTE: { + // senderGuid(u64) + nameLen(u32) + name + targetGuid(u64) + data.senderGuid = packet.readUInt64(); + uint32_t nameLen = packet.readUInt32(); + if (nameLen > 0 && nameLen < 256) { + data.senderName.resize(nameLen); + for (uint32_t i = 0; i < nameLen; ++i) { + data.senderName[i] = static_cast(packet.readUInt8()); + } + if (!data.senderName.empty() && data.senderName.back() == '\0') { + data.senderName.pop_back(); + } + } + data.receiverGuid = packet.readUInt64(); + break; + } + + case ChatType::SAY: + case ChatType::PARTY: + case ChatType::YELL: + case ChatType::WHISPER: + case ChatType::WHISPER_INFORM: + case ChatType::GUILD: + case ChatType::OFFICER: + case ChatType::RAID: + case ChatType::RAID_LEADER: + case ChatType::RAID_WARNING: + case ChatType::EMOTE: + case ChatType::TEXT_EMOTE: { + // senderGuid(u64) + senderGuid(u64) — written twice by server + data.senderGuid = packet.readUInt64(); + /*duplicateGuid*/ packet.readUInt64(); + break; + } + + case ChatType::CHANNEL: { + // channelName(string) + rank(u32) + senderGuid(u64) + data.channelName = packet.readString(); + /*uint32_t rank =*/ packet.readUInt32(); + data.senderGuid = packet.readUInt64(); + break; + } + + default: { + // All other types: senderGuid(u64) + senderGuid(u64) — written twice + data.senderGuid = packet.readUInt64(); + /*duplicateGuid*/ packet.readUInt64(); + break; + } + } + + // Read message length + message + uint32_t messageLen = packet.readUInt32(); + if (messageLen > 0 && messageLen < 8192) { + data.message.resize(messageLen); + for (uint32_t i = 0; i < messageLen; ++i) { + data.message[i] = static_cast(packet.readUInt8()); + } + if (!data.message.empty() && data.message.back() == '\0') { + data.message.pop_back(); + } + } + + // Read chat tag + if (packet.getReadPos() < packet.getSize()) { + data.chatTag = packet.readUInt8(); + } + + LOG_DEBUG("[TBC] SMSG_MESSAGECHAT: type=", getChatTypeString(data.type), + " sender=", data.senderName.empty() ? std::to_string(data.senderGuid) : data.senderName); + + return true; +} + +// ============================================================================ +// TBC 2.4.3 quest giver status +// TBC sends uint32 (like Classic), WotLK changed to uint8. +// TBC 2.4.3 enum: 0=NONE,1=UNAVAILABLE,2=CHAT,3=INCOMPLETE,4=REWARD_REP, +// 5=AVAILABLE_REP,6=AVAILABLE,7=REWARD2,8=REWARD +// ============================================================================ + +uint8_t TbcPacketParsers::readQuestGiverStatus(network::Packet& packet) { + uint32_t tbcStatus = packet.readUInt32(); + switch (tbcStatus) { + case 0: return 0; // NONE + case 1: return 1; // UNAVAILABLE + case 2: return 0; // CHAT → NONE (no marker) + case 3: return 5; // INCOMPLETE → WotLK INCOMPLETE + case 4: return 6; // REWARD_REP → WotLK REWARD_REP + case 5: return 7; // AVAILABLE_REP → WotLK AVAILABLE_LOW_LEVEL + case 6: return 8; // AVAILABLE → WotLK AVAILABLE + case 7: return 10; // REWARD2 → WotLK REWARD + case 8: return 10; // REWARD → WotLK REWARD + default: return 0; + } +} + +// ============================================================================ +// TBC 2.4.3 channel join/leave +// Classic/TBC: just name+password (no channelId/hasVoice/joinedByZone prefix) +// ============================================================================ + +network::Packet TbcPacketParsers::buildJoinChannel(const std::string& channelName, const std::string& password) { + network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL)); + packet.writeString(channelName); + packet.writeString(password); + LOG_DEBUG("[TBC] Built CMSG_JOIN_CHANNEL: channel=", channelName); + return packet; +} + +network::Packet TbcPacketParsers::buildLeaveChannel(const std::string& channelName) { + network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL)); + packet.writeString(channelName); + LOG_DEBUG("[TBC] Built CMSG_LEAVE_CHANNEL: channel=", channelName); + return packet; +} + +// ============================================================================ +// TBC 2.4.3 SMSG_GAMEOBJECT_QUERY_RESPONSE +// TBC has 2 extra strings after name[4] (iconName + castBarCaption). +// WotLK has 3 (adds unk1). Classic has 0. +// ============================================================================ + +bool TbcPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) { + if (packet.getSize() < 4) { + LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)"); + return false; + } + + data.entry = packet.readUInt32(); + + if (data.entry & 0x80000000) { + data.entry &= ~0x80000000; + data.name = ""; + return true; + } + + if (!packet.hasRemaining(8)) { + LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")"); + return false; + } + + data.type = packet.readUInt32(); + data.displayId = packet.readUInt32(); + // 4 name strings + data.name = packet.readString(); + packet.readString(); + packet.readString(); + packet.readString(); + + // TBC: 2 extra strings (iconName + castBarCaption) — WotLK has 3, Classic has 0 + packet.readString(); // iconName + packet.readString(); // castBarCaption + + // Read 24 type-specific data fields + size_t remaining = packet.getRemainingSize(); + if (remaining >= 24 * 4) { + for (int i = 0; i < 24; i++) { + data.data[i] = packet.readUInt32(); + } + data.hasData = true; + } else if (remaining > 0) { + uint32_t fieldsToRead = remaining / 4; + for (uint32_t i = 0; i < fieldsToRead && i < 24; i++) { + data.data[i] = packet.readUInt32(); + } + if (fieldsToRead < 24) { + LOG_WARNING("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated in data fields (", fieldsToRead, + " of 24, entry=", data.entry, ")"); + } + } + + if (data.type == 15) { // MO_TRANSPORT + LOG_DEBUG("TBC GO query: MO_TRANSPORT entry=", data.entry, + " name=\"", data.name, "\" displayId=", data.displayId, + " taxiPathId=", data.data[0], " moveSpeed=", data.data[1]); + } else { + LOG_DEBUG("TBC GO query: ", data.name, " type=", data.type, " entry=", data.entry); + } + return true; +} + +// ============================================================================ +// TBC 2.4.3 guild roster parser +// Same rank structure as WotLK (variable rankCount + goldLimit + bank tabs), +// but NO gender byte per member (WotLK added it). +// ============================================================================ + +bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData& data) { + if (packet.getSize() < 4) { + LOG_ERROR("TBC SMSG_GUILD_ROSTER too small: ", packet.getSize()); + return false; + } + uint32_t numMembers = packet.readUInt32(); + + const uint32_t MAX_GUILD_MEMBERS = 1000; + if (numMembers > MAX_GUILD_MEMBERS) { + LOG_WARNING("TBC GuildRoster: numMembers capped (requested=", numMembers, ")"); + numMembers = MAX_GUILD_MEMBERS; + } + + data.motd = packet.readString(); + data.guildInfo = packet.readString(); + + if (!packet.hasRemaining(4)) { + LOG_WARNING("TBC GuildRoster: truncated before rankCount"); + data.ranks.clear(); + data.members.clear(); + return true; + } + + uint32_t rankCount = packet.readUInt32(); + const uint32_t MAX_GUILD_RANKS = 20; + if (rankCount > MAX_GUILD_RANKS) { + LOG_WARNING("TBC GuildRoster: rankCount capped (requested=", rankCount, ")"); + rankCount = MAX_GUILD_RANKS; + } + + data.ranks.resize(rankCount); + for (uint32_t i = 0; i < rankCount; ++i) { + if (!packet.hasRemaining(4)) { + LOG_WARNING("TBC GuildRoster: truncated rank at index ", i); + break; + } + data.ranks[i].rights = packet.readUInt32(); + if (!packet.hasRemaining(4)) { + data.ranks[i].goldLimit = 0; + } else { + data.ranks[i].goldLimit = packet.readUInt32(); + } + // 6 bank tab flags + 6 bank tab items per day (guild banks added in TBC 2.3) + for (int t = 0; t < 6; ++t) { + if (!packet.hasRemaining(8)) break; + packet.readUInt32(); // tabFlags + packet.readUInt32(); // tabItemsPerDay + } + } + + data.members.resize(numMembers); + for (uint32_t i = 0; i < numMembers; ++i) { + if (!packet.hasRemaining(9)) { + LOG_WARNING("TBC GuildRoster: truncated member at index ", i); + break; + } + auto& m = data.members[i]; + m.guid = packet.readUInt64(); + m.online = (packet.readUInt8() != 0); + + if (packet.getReadPos() >= packet.getSize()) { + m.name.clear(); + } else { + m.name = packet.readString(); + } + + if (!packet.hasRemaining(1)) { + m.rankIndex = 0; + m.level = 1; + m.classId = 0; + m.gender = 0; + m.zoneId = 0; + } else { + m.rankIndex = packet.readUInt32(); + if (!packet.hasRemaining(2)) { + m.level = 1; + m.classId = 0; + } else { + m.level = packet.readUInt8(); + m.classId = packet.readUInt8(); + } + // TBC: NO gender byte (WotLK added it) + m.gender = 0; + if (!packet.hasRemaining(4)) { + m.zoneId = 0; + } else { + m.zoneId = packet.readUInt32(); + } + } + + if (!m.online) { + if (!packet.hasRemaining(4)) { + m.lastOnline = 0.0f; + } else { + m.lastOnline = packet.readFloat(); + } + } + + if (packet.getReadPos() >= packet.getSize()) { + m.publicNote.clear(); + m.officerNote.clear(); + } else { + m.publicNote = packet.readString(); + if (packet.getReadPos() >= packet.getSize()) { + m.officerNote.clear(); + } else { + m.officerNote = packet.readString(); + } + } + } + LOG_INFO("Parsed TBC SMSG_GUILD_ROSTER: ", numMembers, " members, motd=", data.motd); + return true; +} + } // namespace game } // namespace wowee diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 955f8eaa..58cb6a79 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -31,13 +30,13 @@ void TransportManager::update(float deltaTime) { void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, uint32_t pathId, const glm::vec3& spawnWorldPos, uint32_t entry) { auto pathIt = paths_.find(pathId); if (pathIt == paths_.end()) { - std::cerr << "TransportManager: Path " << pathId << " not found for transport " << guid << std::endl; + LOG_ERROR("TransportManager: Path ", pathId, " not found for transport ", guid); return; } const auto& path = pathIt->second; if (path.points.empty()) { - std::cerr << "TransportManager: Path " << pathId << " has no waypoints" << std::endl; + LOG_ERROR("TransportManager: Path ", pathId, " has no waypoints"); return; } @@ -128,7 +127,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId, void TransportManager::unregisterTransport(uint64_t guid) { transports_.erase(guid); - std::cout << "TransportManager: Unregistered transport " << guid << std::endl; + LOG_INFO("TransportManager: Unregistered transport ", guid); } ActiveTransport* TransportManager::getTransport(uint64_t guid) { @@ -168,7 +167,7 @@ glm::mat4 TransportManager::getTransportInvTransform(uint64_t transportGuid) { void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector& waypoints, bool looping, float speed) { if (waypoints.empty()) { - std::cerr << "TransportManager: Cannot load empty path " << pathId << std::endl; + LOG_ERROR("TransportManager: Cannot load empty path ", pathId); return; } @@ -180,7 +179,7 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector uint32_t { if (speed <= 0.0f) return 1000; - return (uint32_t)((dist / speed) * 1000.0f); + return static_cast((dist / speed) * 1000.0f); }; // Single point = stationary (durationMs = 0) @@ -227,7 +226,7 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vectorsetInstanceTransform(transport.wmoInstanceId, transport.transform); + if (transport.isM2) { + if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + } else { + if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); } return; } // Evaluate path time - uint32_t nowMs = (uint32_t)(elapsedTime_ * 1000.0f); + uint32_t nowMs = static_cast(elapsedTime_ * 1000.0f); uint32_t pathTimeMs = 0; if (transport.hasServerClock) { // Predict server time using clock offset (works for both client and server-driven modes) - int64_t serverTimeMs = (int64_t)nowMs + transport.serverClockOffsetMs; - int64_t mod = (int64_t)path.durationMs; + int64_t serverTimeMs = static_cast(nowMs) + transport.serverClockOffsetMs; + int64_t mod = static_cast(path.durationMs); int64_t wrapped = serverTimeMs % mod; if (wrapped < 0) wrapped += mod; - pathTimeMs = (uint32_t)wrapped; + pathTimeMs = static_cast(wrapped); } else if (transport.useClientAnimation) { // Pure local clock (no server sync yet, client-driven) uint32_t dtMs = static_cast(deltaTime * 1000.0f); @@ -287,8 +288,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float } else { // Strict server-authoritative mode: do not guess movement between server snapshots. updateTransformMatrices(transport); - if (wmoRenderer_) { - wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + if (transport.isM2) { + if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + } else { + if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); } return; } @@ -400,7 +403,7 @@ glm::vec3 TransportManager::evalTimedCatmullRom(const TransportPath& path, uint3 uint32_t t1Ms = path.points[p1Idx].tMs; uint32_t t2Ms = path.points[p2Idx].tMs; uint32_t segmentDurationMs = (t2Ms > t1Ms) ? (t2Ms - t1Ms) : 1; - float t = (float)(pathTimeMs - t1Ms) / (float)segmentDurationMs; + float t = static_cast(pathTimeMs - t1Ms) / static_cast(segmentDurationMs); t = glm::clamp(t, 0.0f, 1.0f); // Catmull-Rom spline formula @@ -477,7 +480,7 @@ glm::quat TransportManager::orientationFromTangent(const TransportPath& path, ui uint32_t t1Ms = path.points[p1Idx].tMs; uint32_t t2Ms = path.points[p2Idx].tMs; uint32_t segmentDurationMs = (t2Ms > t1Ms) ? (t2Ms - t1Ms) : 1; - float t = (float)(pathTimeMs - t1Ms) / (float)segmentDurationMs; + float t = static_cast(pathTimeMs - t1Ms) / static_cast(segmentDurationMs); t = glm::clamp(t, 0.0f, 1.0f); // Tangent of Catmull-Rom spline (derivative) @@ -777,8 +780,10 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos } updateTransformMatrices(*transport); - if (wmoRenderer_) { - wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); + if (transport->isM2) { + if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); + } else { + if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); } return; } diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index 41473539..85ac0458 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -34,6 +34,7 @@ static const UFNameEntry kUFNames[] = { {"UNIT_FIELD_DISPLAYID", UF::UNIT_FIELD_DISPLAYID}, {"UNIT_FIELD_MOUNTDISPLAYID", UF::UNIT_FIELD_MOUNTDISPLAYID}, {"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS}, + {"UNIT_FIELD_AURAFLAGS", UF::UNIT_FIELD_AURAFLAGS}, {"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS}, {"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS}, {"UNIT_FIELD_RESISTANCES", UF::UNIT_FIELD_RESISTANCES}, @@ -43,6 +44,8 @@ static const UFNameEntry kUFNames[] = { {"UNIT_FIELD_STAT3", UF::UNIT_FIELD_STAT3}, {"UNIT_FIELD_STAT4", UF::UNIT_FIELD_STAT4}, {"UNIT_END", UF::UNIT_END}, + {"UNIT_FIELD_ATTACK_POWER", UF::UNIT_FIELD_ATTACK_POWER}, + {"UNIT_FIELD_RANGED_ATTACK_POWER", UF::UNIT_FIELD_RANGED_ATTACK_POWER}, {"PLAYER_FLAGS", UF::PLAYER_FLAGS}, {"PLAYER_BYTES", UF::PLAYER_BYTES}, {"PLAYER_BYTES_2", UF::PLAYER_BYTES_2}, @@ -52,6 +55,7 @@ static const UFNameEntry kUFNames[] = { {"PLAYER_QUEST_LOG_START", UF::PLAYER_QUEST_LOG_START}, {"PLAYER_FIELD_INV_SLOT_HEAD", UF::PLAYER_FIELD_INV_SLOT_HEAD}, {"PLAYER_FIELD_PACK_SLOT_1", UF::PLAYER_FIELD_PACK_SLOT_1}, + {"PLAYER_FIELD_KEYRING_SLOT_1", UF::PLAYER_FIELD_KEYRING_SLOT_1}, {"PLAYER_FIELD_BANK_SLOT_1", UF::PLAYER_FIELD_BANK_SLOT_1}, {"PLAYER_FIELD_BANKBAG_SLOT_1", UF::PLAYER_FIELD_BANKBAG_SLOT_1}, {"PLAYER_SKILL_INFO_START", UF::PLAYER_SKILL_INFO_START}, @@ -61,6 +65,18 @@ static const UFNameEntry kUFNames[] = { {"ITEM_FIELD_DURABILITY", UF::ITEM_FIELD_DURABILITY}, {"ITEM_FIELD_MAXDURABILITY", UF::ITEM_FIELD_MAXDURABILITY}, {"PLAYER_REST_STATE_EXPERIENCE", UF::PLAYER_REST_STATE_EXPERIENCE}, + {"PLAYER_CHOSEN_TITLE", UF::PLAYER_CHOSEN_TITLE}, + {"PLAYER_FIELD_MOD_DAMAGE_DONE_POS", UF::PLAYER_FIELD_MOD_DAMAGE_DONE_POS}, + {"PLAYER_FIELD_MOD_HEALING_DONE_POS", UF::PLAYER_FIELD_MOD_HEALING_DONE_POS}, + {"PLAYER_BLOCK_PERCENTAGE", UF::PLAYER_BLOCK_PERCENTAGE}, + {"PLAYER_DODGE_PERCENTAGE", UF::PLAYER_DODGE_PERCENTAGE}, + {"PLAYER_PARRY_PERCENTAGE", UF::PLAYER_PARRY_PERCENTAGE}, + {"PLAYER_CRIT_PERCENTAGE", UF::PLAYER_CRIT_PERCENTAGE}, + {"PLAYER_RANGED_CRIT_PERCENTAGE", UF::PLAYER_RANGED_CRIT_PERCENTAGE}, + {"PLAYER_SPELL_CRIT_PERCENTAGE1", UF::PLAYER_SPELL_CRIT_PERCENTAGE1}, + {"PLAYER_FIELD_COMBAT_RATING_1", UF::PLAYER_FIELD_COMBAT_RATING_1}, + {"PLAYER_FIELD_HONOR_CURRENCY", UF::PLAYER_FIELD_HONOR_CURRENCY}, + {"PLAYER_FIELD_ARENA_CURRENCY", UF::PLAYER_FIELD_ARENA_CURRENCY}, {"CONTAINER_FIELD_NUM_SLOTS", UF::CONTAINER_FIELD_NUM_SLOTS}, {"CONTAINER_FIELD_SLOT_1", UF::CONTAINER_FIELD_SLOT_1}, }; diff --git a/src/game/warden_emulator.cpp b/src/game/warden_emulator.cpp index 5fadc408..a30a6dd6 100644 --- a/src/game/warden_emulator.cpp +++ b/src/game/warden_emulator.cpp @@ -1,7 +1,8 @@ #include "game/warden_emulator.hpp" -#include +#include "core/logger.hpp" #include #include +#include #ifdef HAVE_UNICORN // Unicorn Engine headers @@ -31,6 +32,8 @@ WardenEmulator::WardenEmulator() , heapBase_(HEAP_BASE) , heapSize_(HEAP_SIZE) , apiStubBase_(API_STUB_BASE) + , nextApiStubAddr_(API_STUB_BASE) + , apiCodeHookRegistered_(false) , nextHeapAddr_(HEAP_BASE) { } @@ -43,17 +46,30 @@ WardenEmulator::~WardenEmulator() { bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint32_t baseAddress) { if (uc_) { - std::cerr << "[WardenEmulator] Already initialized" << '\n'; + LOG_ERROR("WardenEmulator: Already initialized"); return false; } + // Reset allocator state so re-initialization starts with a clean heap. + allocations_.clear(); + freeBlocks_.clear(); + apiAddresses_.clear(); + apiHandlers_.clear(); + hooks_.clear(); + nextHeapAddr_ = heapBase_; + nextApiStubAddr_ = apiStubBase_; + apiCodeHookRegistered_ = false; - std::cout << "[WardenEmulator] Initializing x86 emulator (Unicorn Engine)" << '\n'; - std::cout << "[WardenEmulator] Module: " << moduleSize << " bytes at 0x" << std::hex << baseAddress << std::dec << '\n'; + { + char addrBuf[32]; + std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", baseAddress); + LOG_INFO("WardenEmulator: Initializing x86 emulator (Unicorn Engine)"); + LOG_INFO("WardenEmulator: Module: ", moduleSize, " bytes at ", addrBuf); + } // Create x86 32-bit emulator uc_err err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc_); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] uc_open failed: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: uc_open failed: ", uc_strerror(err)); return false; } @@ -63,9 +79,12 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Detect overlap between module and heap/stack regions early. uint32_t modEnd = moduleBase_ + moduleSize_; if (modEnd > heapBase_ && moduleBase_ < heapBase_ + heapSize_) { - std::cerr << "[WardenEmulator] Module [0x" << std::hex << moduleBase_ - << ", 0x" << modEnd << ") overlaps heap [0x" << heapBase_ - << ", 0x" << (heapBase_ + heapSize_) << ") — adjust HEAP_BASE\n" << std::dec; + { + char buf[256]; + std::snprintf(buf, sizeof(buf), "WardenEmulator: Module [0x%X, 0x%X) overlaps heap [0x%X, 0x%X) - adjust HEAP_BASE", + moduleBase_, modEnd, heapBase_, heapBase_ + heapSize_); + LOG_ERROR(buf); + } uc_close(uc_); uc_ = nullptr; return false; @@ -74,7 +93,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map module memory (code + data) err = uc_mem_map(uc_, moduleBase_, moduleSize_, UC_PROT_ALL); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map module memory: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map module memory: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -83,7 +102,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Write module code to emulated memory err = uc_mem_write(uc_, moduleBase_, moduleCode, moduleSize); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to write module code: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to write module code: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -92,7 +111,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map stack err = uc_mem_map(uc_, stackBase_, stackSize_, UC_PROT_READ | UC_PROT_WRITE); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map stack: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map stack: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -106,7 +125,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map heap err = uc_mem_map(uc_, heapBase_, heapSize_, UC_PROT_READ | UC_PROT_WRITE); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map heap: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map heap: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -115,7 +134,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 // Map API stub area err = uc_mem_map(uc_, apiStubBase_, 0x10000, UC_PROT_ALL); if (err != UC_ERR_OK) { - std::cerr << "[WardenEmulator] Failed to map API stub area: " << uc_strerror(err) << '\n'; + LOG_ERROR("WardenEmulator: Failed to map API stub area: ", uc_strerror(err)); uc_close(uc_); uc_ = nullptr; return false; @@ -127,7 +146,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 err = uc_mem_map(uc_, 0x0, 0x1000, UC_PROT_READ); if (err != UC_ERR_OK) { // Non-fatal — just log it; the emulator will still function - std::cerr << "[WardenEmulator] Note: could not map null guard page: " << uc_strerror(err) << '\n'; + LOG_WARNING("WardenEmulator: could not map null guard page: ", uc_strerror(err)); } // Add hooks for debugging and invalid memory access @@ -135,35 +154,70 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0); hooks_.push_back(hh); - std::cout << "[WardenEmulator] ✓ Emulator initialized successfully" << '\n'; - std::cout << "[WardenEmulator] Stack: 0x" << std::hex << stackBase_ << " - 0x" << (stackBase_ + stackSize_) << '\n'; - std::cout << "[WardenEmulator] Heap: 0x" << heapBase_ << " - 0x" << (heapBase_ + heapSize_) << std::dec << '\n'; + // Add code hook over the API stub area so Windows API calls are intercepted + uc_hook apiHook; + uc_hook_add(uc_, &apiHook, UC_HOOK_CODE, (void*)hookCode, this, + API_STUB_BASE, API_STUB_BASE + 0x10000 - 1); + hooks_.push_back(apiHook); + apiCodeHookRegistered_ = true; + + { + char sBuf[128]; + std::snprintf(sBuf, sizeof(sBuf), "WardenEmulator: Emulator initialized Stack: 0x%X-0x%X Heap: 0x%X-0x%X", + stackBase_, stackBase_ + stackSize_, heapBase_, heapBase_ + heapSize_); + LOG_INFO(sBuf); + } return true; } uint32_t WardenEmulator::hookAPI(const std::string& dllName, const std::string& functionName, - [[maybe_unused]] std::function&)> handler) { - // Allocate address for this API stub - static uint32_t nextStubAddr = API_STUB_BASE; - uint32_t stubAddr = nextStubAddr; - nextStubAddr += 16; // Space for stub code + std::function&)> handler) { + // Allocate address for this API stub (16 bytes each) + uint32_t stubAddr = nextApiStubAddr_; + nextApiStubAddr_ += 16; - // Store mapping + // Store address mapping for IAT patching apiAddresses_[dllName][functionName] = stubAddr; - std::cout << "[WardenEmulator] Hooked " << dllName << "!" << functionName - << " at 0x" << std::hex << stubAddr << std::dec << '\n'; + // Determine stdcall arg count from known Windows APIs so the hook can + // clean up the stack correctly (RETN N convention). + static const std::pair knownArgCounts[] = { + {"VirtualAlloc", 4}, + {"VirtualFree", 3}, + {"GetTickCount", 0}, + {"Sleep", 1}, + {"GetCurrentThreadId", 0}, + {"GetCurrentProcessId", 0}, + {"ReadProcessMemory", 5}, + }; + int argCount = 0; + for (const auto& [name, cnt] : knownArgCounts) { + if (functionName == name) { argCount = cnt; break; } + } - // TODO: Write stub code that triggers a hook callback - // For now, just return the address for IAT patching + // Store the handler so hookCode() can dispatch to it + apiHandlers_[stubAddr] = { argCount, std::move(handler) }; + + // Write a RET (0xC3) at the stub address as a safe fallback in case + // the code hook fires after EIP has already advanced past our intercept. + if (uc_) { + static const uint8_t retInstr = 0xC3; + uc_mem_write(uc_, stubAddr, &retInstr, 1); + } + + { + char hBuf[64]; + std::snprintf(hBuf, sizeof(hBuf), "0x%X (argCount=%d)", stubAddr, argCount); + LOG_DEBUG("WardenEmulator: Hooked ", dllName, "!", functionName, " at ", hBuf); + } return stubAddr; } void WardenEmulator::setupCommonAPIHooks() { - std::cout << "[WardenEmulator] Setting up common Windows API hooks..." << '\n'; + LOG_INFO("WardenEmulator: Setting up common Windows API hooks..."); // kernel32.dll hookAPI("kernel32.dll", "VirtualAlloc", apiVirtualAlloc); @@ -174,7 +228,7 @@ void WardenEmulator::setupCommonAPIHooks() { hookAPI("kernel32.dll", "GetCurrentProcessId", apiGetCurrentProcessId); hookAPI("kernel32.dll", "ReadProcessMemory", apiReadProcessMemory); - std::cout << "[WardenEmulator] ✓ Common API hooks registered" << '\n'; + LOG_INFO("WardenEmulator: Common API hooks registered"); } uint32_t WardenEmulator::writeData(const void* data, size_t size) { @@ -198,12 +252,15 @@ std::vector WardenEmulator::readData(uint32_t address, size_t size) { uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector& args) { if (!uc_) { - std::cerr << "[WardenEmulator] Not initialized" << '\n'; + LOG_ERROR("WardenEmulator: Not initialized"); return 0; } - std::cout << "[WardenEmulator] Calling function at 0x" << std::hex << address << std::dec - << " with " << args.size() << " args" << '\n'; + { + char aBuf[32]; + std::snprintf(aBuf, sizeof(aBuf), "0x%X", address); + LOG_DEBUG("WardenEmulator: Calling function at ", aBuf, " with ", args.size(), " args"); + } // Get current ESP uint32_t esp; @@ -227,7 +284,7 @@ uint32_t WardenEmulator::callFunction(uint32_t address, const std::vector(size); - if (nextHeapAddr_ + size > heapBase_ + heapSize_) { - std::cerr << "[WardenEmulator] Heap exhausted" << '\n'; + // First-fit from free list so released blocks can be reused. + for (auto it = freeBlocks_.begin(); it != freeBlocks_.end(); ++it) { + if (it->second < size) continue; + const uint32_t addr = it->first; + const size_t blockSz = it->second; + freeBlocks_.erase(it); + if (blockSz > size) + freeBlocks_[addr + allocSize] = blockSz - size; + allocations_[addr] = size; + { + char mBuf[32]; + std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr); + LOG_DEBUG("WardenEmulator: Reused ", size, " bytes at ", mBuf); + } + return addr; + } + + const uint64_t heapEnd = static_cast(heapBase_) + heapSize_; + if (static_cast(nextHeapAddr_) + size > heapEnd) { + LOG_ERROR("WardenEmulator: Heap exhausted"); return 0; } uint32_t addr = nextHeapAddr_; - nextHeapAddr_ += size; - + nextHeapAddr_ += allocSize; allocations_[addr] = size; - std::cout << "[WardenEmulator] Allocated " << size << " bytes at 0x" << std::hex << addr << std::dec << '\n'; + { + char mBuf[32]; + std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr); + LOG_DEBUG("WardenEmulator: Allocated ", size, " bytes at ", mBuf); + } return addr; } @@ -283,12 +368,54 @@ uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t p bool WardenEmulator::freeMemory(uint32_t address) { auto it = allocations_.find(address); if (it == allocations_.end()) { - std::cerr << "[WardenEmulator] Invalid free at 0x" << std::hex << address << std::dec << '\n'; + { + char fBuf[32]; + std::snprintf(fBuf, sizeof(fBuf), "0x%X", address); + LOG_ERROR("WardenEmulator: Invalid free at ", fBuf); + } return false; } - std::cout << "[WardenEmulator] Freed " << it->second << " bytes at 0x" << std::hex << address << std::dec << '\n'; + { + char fBuf[32]; + std::snprintf(fBuf, sizeof(fBuf), "0x%X", address); + LOG_DEBUG("WardenEmulator: Freed ", it->second, " bytes at ", fBuf); + } + + const size_t freedSize = it->second; allocations_.erase(it); + + // Insert in free list and coalesce adjacent blocks to limit fragmentation. + auto [curr, inserted] = freeBlocks_.emplace(address, freedSize); + if (!inserted) curr->second += freedSize; + + if (curr != freeBlocks_.begin()) { + auto prev = std::prev(curr); + if (static_cast(prev->first) + prev->second == curr->first) { + prev->second += curr->second; + freeBlocks_.erase(curr); + curr = prev; + } + } + + auto next = std::next(curr); + if (next != freeBlocks_.end() && + static_cast(curr->first) + curr->second == next->first) { + curr->second += next->second; + freeBlocks_.erase(next); + } + + // Roll back the bump pointer if the highest free block reaches it. + while (!freeBlocks_.empty()) { + auto last = std::prev(freeBlocks_.end()); + if (static_cast(last->first) + last->second == nextHeapAddr_) { + nextHeapAddr_ = last->first; + freeBlocks_.erase(last); + } else { + break; + } + } + return true; } @@ -319,8 +446,12 @@ uint32_t WardenEmulator::apiVirtualAlloc(WardenEmulator& emu, const std::vector< uint32_t flAllocationType = args[2]; uint32_t flProtect = args[3]; - std::cout << "[WinAPI] VirtualAlloc(0x" << std::hex << lpAddress << ", " << std::dec - << dwSize << ", 0x" << std::hex << flAllocationType << ", 0x" << flProtect << ")" << std::dec << '\n'; + { + char vBuf[128]; + std::snprintf(vBuf, sizeof(vBuf), "WinAPI: VirtualAlloc(0x%X, %u, 0x%X, 0x%X)", + lpAddress, dwSize, flAllocationType, flProtect); + LOG_DEBUG(vBuf); + } // Ignore lpAddress hint for now return emu.allocateMemory(dwSize, flProtect); @@ -332,7 +463,11 @@ uint32_t WardenEmulator::apiVirtualFree(WardenEmulator& emu, const std::vector(now.time_since_epoch()).count(); uint32_t ticks = static_cast(ms & 0xFFFFFFFF); - std::cout << "[WinAPI] GetTickCount() = " << ticks << '\n'; + LOG_DEBUG("WinAPI: GetTickCount() = ", ticks); return ticks; } @@ -350,18 +485,18 @@ uint32_t WardenEmulator::apiSleep([[maybe_unused]] WardenEmulator& emu, const st if (args.size() < 1) return 0; uint32_t dwMilliseconds = args[0]; - std::cout << "[WinAPI] Sleep(" << dwMilliseconds << ")" << '\n'; + LOG_DEBUG("WinAPI: Sleep(", dwMilliseconds, ")"); // Don't actually sleep in emulator return 0; } uint32_t WardenEmulator::apiGetCurrentThreadId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector& args) { - std::cout << "[WinAPI] GetCurrentThreadId() = 1234" << '\n'; + LOG_DEBUG("WinAPI: GetCurrentThreadId() = 1234"); return 1234; // Fake thread ID } uint32_t WardenEmulator::apiGetCurrentProcessId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector& args) { - std::cout << "[WinAPI] GetCurrentProcessId() = 5678" << '\n'; + LOG_DEBUG("WinAPI: GetCurrentProcessId() = 5678"); return 5678; // Fake process ID } @@ -375,8 +510,11 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve uint32_t nSize = args[3]; uint32_t lpNumberOfBytesRead = args[4]; - std::cout << "[WinAPI] ReadProcessMemory(0x" << std::hex << lpBaseAddress - << ", " << std::dec << nSize << " bytes)" << '\n'; + { + char rBuf[64]; + std::snprintf(rBuf, sizeof(rBuf), "WinAPI: ReadProcessMemory(0x%X, %u bytes)", lpBaseAddress, nSize); + LOG_DEBUG(rBuf); + } // Read from emulated memory and write to buffer std::vector data(nSize); @@ -399,8 +537,40 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve // Unicorn Callbacks // ============================================================================ -void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) { - std::cout << "[Trace] 0x" << std::hex << address << std::dec << '\n'; +void WardenEmulator::hookCode(uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, void* userData) { + auto* self = static_cast(userData); + if (!self) return; + + auto it = self->apiHandlers_.find(static_cast(address)); + if (it == self->apiHandlers_.end()) return; // not an API stub — trace disabled to avoid spam + + const ApiHookEntry& entry = it->second; + + // Read stack: [ESP+0] = return address, [ESP+4..] = stdcall args + uint32_t esp = 0; + uc_reg_read(uc, UC_X86_REG_ESP, &esp); + + uint32_t retAddr = 0; + uc_mem_read(uc, esp, &retAddr, 4); + + std::vector args(static_cast(entry.argCount)); + for (int i = 0; i < entry.argCount; ++i) { + uint32_t val = 0; + uc_mem_read(uc, esp + 4 + static_cast(i) * 4, &val, 4); + args[static_cast(i)] = val; + } + + // Dispatch to the C++ handler + uint32_t retVal = 0; + if (entry.handler) { + retVal = entry.handler(*self, args); + } + + // Simulate stdcall epilogue: pop return address + args + uint32_t newEsp = esp + 4 + static_cast(entry.argCount) * 4; + uc_reg_write(uc, UC_X86_REG_EAX, &retVal); + uc_reg_write(uc, UC_X86_REG_ESP, &newEsp); + uc_reg_write(uc, UC_X86_REG_EIP, &retAddr); } void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) { @@ -415,9 +585,12 @@ void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, ui case UC_MEM_FETCH_PROT: typeStr = "FETCH_PROT"; break; } - std::cerr << "[WardenEmulator] Invalid memory access: " << typeStr - << " at 0x" << std::hex << address << std::dec - << " (size=" << size << ")" << '\n'; + { + char mBuf[128]; + std::snprintf(mBuf, sizeof(mBuf), "WardenEmulator: Invalid memory access: %s at 0x%llX (size=%d)", + typeStr, static_cast(address), size); + LOG_ERROR(mBuf); + } } #else // !HAVE_UNICORN @@ -426,7 +599,8 @@ WardenEmulator::WardenEmulator() : uc_(nullptr), moduleBase_(0), moduleSize_(0) , stackBase_(0), stackSize_(0) , heapBase_(0), heapSize_(0) - , apiStubBase_(0), nextHeapAddr_(0) {} + , apiStubBase_(0), nextApiStubAddr_(0), apiCodeHookRegistered_(false) + , nextHeapAddr_(0) {} WardenEmulator::~WardenEmulator() {} bool WardenEmulator::initialize(const void*, size_t, uint32_t) { return false; } uint32_t WardenEmulator::hookAPI(const std::string&, const std::string&, diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index c5a0afd2..d57586bb 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -1,5 +1,6 @@ #include "game/warden_memory.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -7,6 +8,8 @@ #include #include #include +#include +#include namespace wowee { namespace game { @@ -106,19 +109,181 @@ bool WardenMemory::parsePE(const std::vector& fileData) { " size=0x", copySize, std::dec); } + LOG_WARNING("WardenMemory: PE loaded — imageBase=0x", std::hex, imageBase_, + " imageSize=0x", imageSize_, std::dec, + " (", numSections, " sections, ", fileData.size(), " bytes on disk)"); + return true; } void WardenMemory::initKuserSharedData() { std::memset(kuserData_, 0, KUSER_SIZE); - // NtMajorVersion at offset 0x026C = 6 (Vista/7/8/10) - uint32_t ntMajor = 6; - std::memcpy(kuserData_ + 0x026C, &ntMajor, 4); + // ------------------------------------------------------------------- + // KUSER_SHARED_DATA layout — Windows 7 SP1 x86 (from ntddk.h PDB) + // Warden reads this in 238-byte chunks for OS fingerprinting. + // All offsets verified against the canonical _KUSER_SHARED_DATA struct. + // ------------------------------------------------------------------- - // NtMinorVersion at offset 0x0270 = 1 (Windows 7) - uint32_t ntMinor = 1; - std::memcpy(kuserData_ + 0x0270, &ntMinor, 4); + auto w32 = [&](uint32_t off, uint32_t v) { std::memcpy(kuserData_ + off, &v, 4); }; + auto w16 = [&](uint32_t off, uint16_t v) { std::memcpy(kuserData_ + off, &v, 2); }; + auto w8 = [&](uint32_t off, uint8_t v) { kuserData_[off] = v; }; + + // +0x000 TickCountLowDeprecated (ULONG) + w32(0x0000, 0x003F4A00); // ~70 min uptime + + // +0x004 TickCountMultiplier (ULONG) + w32(0x0004, 0x0FA00000); + + // +0x008 InterruptTime (KSYSTEM_TIME: Low4 + High1_4 + High2_4) + w32(0x0008, 0x6B49D200); + w32(0x000C, 0x00000029); + w32(0x0010, 0x00000029); + + // +0x014 SystemTime (KSYSTEM_TIME) — ~2024 epoch FILETIME + w32(0x0014, 0xA0B71B00); + w32(0x0018, 0x01DA5E80); + w32(0x001C, 0x01DA5E80); + + // +0x020 TimeZoneBias (KSYSTEM_TIME) — 0 = UTC + // (leave zeros) + + // +0x02C ImageNumberLow / ImageNumberHigh (USHORT each) + w16(0x002C, 0x014C); // IMAGE_FILE_MACHINE_I386 + w16(0x002E, 0x014C); + + // +0x030 NtSystemRoot (WCHAR[260] = 520 bytes, ends at +0x238) + const wchar_t* sysRoot = L"C:\\WINDOWS"; + for (size_t i = 0; i < 10; i++) { + w16(0x0030 + static_cast(i) * 2, static_cast(sysRoot[i])); + } + + // +0x238 MaxStackTraceDepth (ULONG) + w32(0x0238, 0); + + // +0x23C CryptoExponent (ULONG) — 65537 + w32(0x023C, 0x00010001); + + // +0x240 TimeZoneId (ULONG) — TIME_ZONE_ID_UNKNOWN + w32(0x0240, 0); + + // +0x244 LargePageMinimum (ULONG) — 2 MB + w32(0x0244, 0x00200000); + + // +0x248 Reserved2[7] (28 bytes) — zeros + // (leave zeros) + + // +0x264 NtProductType (NT_PRODUCT_TYPE = ULONG) — VER_NT_WORKSTATION + w32(0x0264, 1); + + // +0x268 ProductTypeIsValid (BOOLEAN = UCHAR) + w8(0x0268, 1); + + // +0x269 Reserved9[3] — padding + // (leave zeros) + + // +0x26C NtMajorVersion (ULONG) — 6 (Windows Vista/7/8/10) + w32(0x026C, 6); + + // +0x270 NtMinorVersion (ULONG) — 1 (Windows 7) + w32(0x0270, 1); + + // +0x274 ProcessorFeatures (BOOLEAN[64] = 64 bytes, ends at +0x2B4) + // Each entry is a single UCHAR (0 or 1). + // Index Name Value + // [0] PF_FLOATING_POINT_PRECISION_ERRATA 0 + // [1] PF_FLOATING_POINT_EMULATED 0 + // [2] PF_COMPARE_EXCHANGE_DOUBLE 1 + // [3] PF_MMX_INSTRUCTIONS_AVAILABLE 1 + // [4] PF_PPC_MOVEMEM_64BIT_OK 0 + // [5] PF_ALPHA_BYTE_INSTRUCTIONS 0 + // [6] PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) 1 + // [7] PF_3DNOW_INSTRUCTIONS_AVAILABLE 0 + // [8] PF_RDTSC_INSTRUCTION_AVAILABLE 1 + // [9] PF_PAE_ENABLED 1 + // [10] PF_XMMI64_INSTRUCTIONS_AVAILABLE(SSE2)1 + // [11] PF_SSE_DAZ_MODE_AVAILABLE 0 + // [12] PF_NX_ENABLED 1 + // [13] PF_SSE3_INSTRUCTIONS_AVAILABLE 1 + // [14] PF_COMPARE_EXCHANGE128 0 (x86 typically 0) + // [15] PF_COMPARE64_EXCHANGE128 0 + // [16] PF_CHANNELS_ENABLED 0 + // [17] PF_XSAVE_ENABLED 0 + w8(0x0274 + 2, 1); // PF_COMPARE_EXCHANGE_DOUBLE + w8(0x0274 + 3, 1); // PF_MMX + w8(0x0274 + 6, 1); // PF_SSE + w8(0x0274 + 8, 1); // PF_RDTSC + w8(0x0274 + 9, 1); // PF_PAE_ENABLED + w8(0x0274 + 10, 1); // PF_SSE2 + w8(0x0274 + 12, 1); // PF_NX_ENABLED + w8(0x0274 + 13, 1); // PF_SSE3 + + // +0x2B4 Reserved1 (ULONG) + // +0x2B8 Reserved3 (ULONG) + // +0x2BC TimeSlip (ULONG) + // +0x2C0 AlternativeArchitecture (ULONG) = 0 (StandardDesign) + // +0x2C4 AltArchitecturePad[1] (ULONG) + // +0x2C8 SystemExpirationDate (LARGE_INTEGER = 8 bytes) + // (leave zeros) + + // +0x2D0 SuiteMask (ULONG) — VER_SUITE_SINGLEUSERTS | VER_SUITE_TERMINAL + w32(0x02D0, 0x0110); // 0x0100=SINGLEUSERTS, 0x0010=TERMINAL + + // +0x2D4 KdDebuggerEnabled (BOOLEAN = UCHAR) + w8(0x02D4, 0); + + // +0x2D5 NXSupportPolicy (UCHAR) — 2 = OptIn + w8(0x02D5, 2); + + // +0x2D6 Reserved6[2] + // (leave zeros) + + // +0x2D8 ActiveConsoleId (ULONG) — session 0 or 1 + w32(0x02D8, 1); + + // +0x2DC DismountCount (ULONG) + w32(0x02DC, 0); + + // +0x2E0 ComPlusPackage (ULONG) + w32(0x02E0, 0); + + // +0x2E4 LastSystemRITEventTickCount (ULONG) — recent input tick + w32(0x02E4, 0x003F4900); + + // +0x2E8 NumberOfPhysicalPages (ULONG) — 4GB / 4KB ≈ 1M pages + w32(0x02E8, 0x000FF000); + + // +0x2EC SafeBootMode (BOOLEAN) — 0 = normal boot + w8(0x02EC, 0); + + // +0x2F0 SharedDataFlags / TraceLogging (ULONG) + w32(0x02F0, 0); + + // +0x2F8 TestRetInstruction (ULONGLONG = 8 bytes) — RET opcode + w8(0x02F8, 0xC3); // x86 RET instruction + + // +0x300 SystemCall (ULONG) + w32(0x0300, 0); + + // +0x304 SystemCallReturn (ULONG) + w32(0x0304, 0); + + // +0x308 SystemCallPad[3] (24 bytes) + // (leave zeros) + + // +0x320 TickCount (KSYSTEM_TIME) — matches TickCountLowDeprecated + w32(0x0320, 0x003F4A00); + + // +0x32C TickCountPad[1] + // (leave zeros) + + // +0x330 Cookie (ULONG) — stack cookie, random-looking value + w32(0x0330, 0x4A2F8C15); + + // +0x334 ConsoleSessionForegroundProcessId (ULONG) — some PID + w32(0x0334, 0x00001234); + + // Everything after +0x338 is typically zero on Win7 x86 } void WardenMemory::writeLE32(uint32_t va, uint32_t value) { @@ -132,56 +297,52 @@ void WardenMemory::writeLE32(uint32_t va, uint32_t value) { } void WardenMemory::patchRuntimeGlobals() { - // Only patch Classic 1.12.1 (build 5875) WoW.exe - // Identified by: ImageBase=0x400000, ImageSize=0x906000 (unique to 1.12.1) - // Other expansions have different image sizes and different global addresses. - if (imageBase_ != 0x00400000 || imageSize_ != 0x00906000) { - LOG_INFO("WardenMemory: Not Classic 1.12.1 WoW.exe (imageSize=0x", - std::hex, imageSize_, std::dec, "), skipping runtime global patches"); + if (imageBase_ != 0x00400000) { + LOG_WARNING("WardenMemory: unexpected imageBase=0x", std::hex, imageBase_, std::dec, + " — skipping runtime global patches"); return; } // Classic 1.12.1 (build 5875) runtime globals - // These are in the .data BSS region - zero on disk, populated at runtime. - // We patch them with fake but valid values so Warden checks pass. + // VMaNGOS has TWO types of Warden scans that read these addresses: // - // Offsets from CMaNGOS anticheat module (wardenwin.cpp): - // WardenModule = 0xCE897C - // OfsWardenSysInfo = 0x228 - // OfsWardenWinSysInfo = 0x08 - // g_theGxDevicePtr = 0xC0ED38 - // OfsDevice2 = 0x38A8 - // OfsDevice3 = 0x0 - // OfsDevice4 = 0xA8 - // WorldEnables = 0xC7B2A4 - // LastHardwareAction= 0xCF0BC8 + // 1. DB-driven scans (warden_scans table): memcmp against expected bytes. + // These check CODE sections for integrity — never check runtime data addresses. + // + // 2. Scripted scans (WardenWin::LoadScriptedScans): READ and INTERPRET values. + // - "Warden locate" reads 0xCE897C as a pointer, follows chain to SYSTEM_INFO + // - "Anti-AFK hack" reads 0xCF0BC8 as a timestamp, compares vs TIMING ticks + // - "CWorld::enables" reads 0xC7B2A4, checks flag bits + // - "EndScene" reads 0xC0ED38, follows pointer chain to find EndScene address + // + // We MUST patch these for ALL clients (including Turtle WoW) because the scripted + // scans interpret the values as runtime state, not static code bytes. Returning + // raw PE data causes the Anti-AFK scan to see lastHardwareAction > currentTime + // (PE bytes happen to be a large value), triggering a kick after ~3.5 minutes. - // === Warden SYSTEM_INFO chain (3-level pointer chain) === - // Stage 0: [0xCE897C] → fake warden struct base + // === Runtime global patches (applied unconditionally for all image variants) === + + // Warden SYSTEM_INFO chain constexpr uint32_t WARDEN_MODULE_PTR = 0xCE897C; constexpr uint32_t FAKE_WARDEN_BASE = 0xCE8000; writeLE32(WARDEN_MODULE_PTR, FAKE_WARDEN_BASE); - - // Stage 1: [FAKE_WARDEN_BASE + 0x228] → pointer to sysinfo container - constexpr uint32_t OFS_WARDEN_SYSINFO = 0x228; constexpr uint32_t FAKE_SYSINFO_CONTAINER = 0xCE8300; - writeLE32(FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, FAKE_SYSINFO_CONTAINER); + writeLE32(FAKE_WARDEN_BASE + 0x228, FAKE_SYSINFO_CONTAINER); - // Stage 2: [FAKE_SYSINFO_CONTAINER + 0x08] → 36-byte SYSTEM_INFO struct - constexpr uint32_t OFS_WARDEN_WIN_SYSINFO = 0x08; - uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + OFS_WARDEN_WIN_SYSINFO; // 0xCE8308 - // WIN_SYSTEM_INFO is 36 bytes (0x24): - // uint16 wProcessorArchitecture (must be 0 = x86) - // uint16 wReserved - // uint32 dwPageSize - // uint32 lpMinimumApplicationAddress - // uint32 lpMaximumApplicationAddress (MUST be non-zero!) - // uint32 dwActiveProcessorMask - // uint32 dwNumberOfProcessors - // uint32 dwProcessorType (must be 386, 486, or 586) - // uint32 dwAllocationGranularity - // uint16 wProcessorLevel - // uint16 wProcessorRevision + // Write SYSINFO pointer at many offsets from FAKE_WARDEN_BASE so the + // chain works regardless of which module-specific offset the server uses. + // MUST be done BEFORE writing the actual SYSTEM_INFO struct, because this + // loop's range (0xCE8200-0xCE8400) overlaps with the struct at 0xCE8308. + for (uint32_t off = 0x200; off <= 0x400; off += 4) { + uint32_t addr = FAKE_WARDEN_BASE + off; + if (addr >= imageBase_ && (addr - imageBase_) + 4 <= imageSize_) { + writeLE32(addr, FAKE_SYSINFO_CONTAINER); + } + } + + // Now write the actual WIN_SYSTEM_INFO struct AFTER the pointer fill loop, + // so it overwrites any values the loop placed in the 0xCE8308+ range. + uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + 0x08; #pragma pack(push, 1) struct { uint16_t wProcessorArchitecture; @@ -195,19 +356,7 @@ void WardenMemory::patchRuntimeGlobals() { uint32_t dwAllocationGranularity; uint16_t wProcessorLevel; uint16_t wProcessorRevision; - } sysInfo = { - 0, // x86 - 0, - 4096, // 4K page size - 0x00010000, // min app address - 0x7FFEFFFF, // max app address (CRITICAL: must be non-zero) - 0x0F, // 4 processors - 4, // 4 CPUs - 586, // Pentium - 65536, // 64K granularity - 6, // P6 family - 0x3A09 // revision - }; + } sysInfo = {0, 0, 4096, 0x00010000, 0x7FFEFFFF, 0x0F, 4, 586, 65536, 6, 0x3A09}; #pragma pack(pop) static_assert(sizeof(sysInfo) == 36, "SYSTEM_INFO must be 36 bytes"); uint32_t rva = sysInfoAddr - imageBase_; @@ -215,52 +364,203 @@ void WardenMemory::patchRuntimeGlobals() { std::memcpy(image_.data() + rva, &sysInfo, 36); } - LOG_INFO("WardenMemory: Patched SYSTEM_INFO chain: [0x", std::hex, - WARDEN_MODULE_PTR, "]→0x", FAKE_WARDEN_BASE, - " [0x", FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, "]→0x", FAKE_SYSINFO_CONTAINER, - " SYSTEM_INFO@0x", sysInfoAddr, std::dec); + // Fallback: if the pointer chain breaks and stage 3 reads from address + // 0x00000000 + 0x08 = 8, write valid SYSINFO at RVA 8 (PE DOS header area). + if (8 + 36 <= imageSize_) { + std::memcpy(image_.data() + 8, &sysInfo, 36); + } - // === EndScene chain (4-level pointer chain) === - // Stage 1: [0xC0ED38] → fake D3D device + LOG_WARNING("WardenMemory: Patched SYSINFO chain @0x", std::hex, WARDEN_MODULE_PTR, std::dec); + + // EndScene chain + // VMaNGOS reads g_theGxDevicePtr → device, then device+0x1FC for API kind + // (0=OpenGL, 1=Direct3D). If Direct3D, follows device+0x38A8 → ptr → ptr+0xA8 → EndScene. + // We set API=1 (Direct3D) and provide the full pointer chain. constexpr uint32_t GX_DEVICE_PTR = 0xC0ED38; constexpr uint32_t FAKE_DEVICE = 0xCE8400; writeLE32(GX_DEVICE_PTR, FAKE_DEVICE); + writeLE32(FAKE_DEVICE + 0x1FC, 1); // API kind = Direct3D + // Set up the full EndScene pointer chain at the canonical offsets. + constexpr uint32_t FAKE_VTABLE1 = 0xCE8500; + constexpr uint32_t FAKE_VTABLE2 = 0xCE8600; + constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // start of .text + writeLE32(FAKE_DEVICE + 0x38A8, FAKE_VTABLE1); + writeLE32(FAKE_VTABLE1, FAKE_VTABLE2); + writeLE32(FAKE_VTABLE2 + 0xA8, FAKE_ENDSCENE); - // Stage 2: [FAKE_DEVICE + 0x38A8] → fake intermediate - constexpr uint32_t OFS_DEVICE2 = 0x38A8; - constexpr uint32_t FAKE_INTERMEDIATE = 0xCE8500; - writeLE32(FAKE_DEVICE + OFS_DEVICE2, FAKE_INTERMEDIATE); + // The EndScene device+sOfsDevice2 offset may differ from 0x38A8 in Turtle WoW. + // Also set API=1 (Direct3D) at multiple offsets so the API kind check passes. + // Fill the entire fake device area with the vtable pointer for robustness. + for (uint32_t off = 0x3800; off <= 0x3A00; off += 4) { + uint32_t addr = FAKE_DEVICE + off; + if (addr >= imageBase_ && (addr - imageBase_) + 4 <= imageSize_) { + writeLE32(addr, FAKE_VTABLE1); + } + } + LOG_WARNING("WardenMemory: Patched EndScene chain @0x", std::hex, GX_DEVICE_PTR, std::dec); - // Stage 3: [FAKE_INTERMEDIATE + 0x0] → fake vtable - constexpr uint32_t OFS_DEVICE3 = 0x0; - constexpr uint32_t FAKE_VTABLE = 0xCE8600; - writeLE32(FAKE_INTERMEDIATE + OFS_DEVICE3, FAKE_VTABLE); - - // Stage 4: [FAKE_VTABLE + 0xA8] → address of "EndScene" function - // Point to a real .text address with normal code (not 0xE9/0xCC = not hooked) - constexpr uint32_t OFS_DEVICE4 = 0xA8; - constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // Start of .text section - writeLE32(FAKE_VTABLE + OFS_DEVICE4, FAKE_ENDSCENE); - - LOG_INFO("WardenMemory: Patched EndScene chain: [0x", std::hex, - GX_DEVICE_PTR, "]→0x", FAKE_DEVICE, - " ... →EndScene@0x", FAKE_ENDSCENE, std::dec); - - // === WorldEnables (single value) === - // Required flags: TerrainDoodads|Terrain|MapObjects|MapObjectLighting|MapObjectTextures|Water - // Plus typical defaults (no Prohibited bits set) + // WorldEnables constexpr uint32_t WORLD_ENABLES = 0xC7B2A4; uint32_t enables = 0x1 | 0x2 | 0x10 | 0x20 | 0x40 | 0x100 | 0x200 | 0x400 | 0x800 | 0x8000 | 0x10000 | 0x100000 | 0x1000000 | 0x2000000 | 0x4000000 | 0x8000000 | 0x10000000; writeLE32(WORLD_ENABLES, enables); - LOG_INFO("WardenMemory: Patched WorldEnables=0x", std::hex, enables, std::dec); + LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec); - // === LastHardwareAction (tick count) === - // Must be <= currentTime from timing check. Set to a plausible value. + // LastHardwareAction — must be a recent GetTickCount()-style timestamp + // so the anti-AFK scan sees (currentTime - lastAction) < threshold. constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8; - writeLE32(LAST_HARDWARE_ACTION, 60000); // 1 minute - LOG_INFO("WardenMemory: Patched LastHardwareAction=60000ms"); + uint32_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + writeLE32(LAST_HARDWARE_ACTION, nowMs - 2000); + LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec); + + // Embed the 37-byte Warden module memcpy pattern in BSS so that + // FIND_CODE_BY_HASH (PAGE_B) brute-force search can find it. + // This is the pattern VMaNGOS's "Warden Memory Read check" looks for. + constexpr uint32_t MEMCPY_PATTERN_VA = 0xCE8700; + static const uint8_t kWardenMemcpyPattern[37] = { + 0x56, 0x57, 0xFC, 0x8B, 0x54, 0x24, 0x14, 0x8B, + 0x74, 0x24, 0x10, 0x8B, 0x44, 0x24, 0x0C, 0x8B, + 0xCA, 0x8B, 0xF8, 0xC1, 0xE9, 0x02, 0x74, 0x02, + 0xF3, 0xA5, 0xB1, 0x03, 0x23, 0xCA, 0x74, 0x02, + 0xF3, 0xA4, 0x5F, 0x5E, 0xC3 + }; + uint32_t patRva = MEMCPY_PATTERN_VA - imageBase_; + if (patRva + sizeof(kWardenMemcpyPattern) <= imageSize_) { + std::memcpy(image_.data() + patRva, kWardenMemcpyPattern, sizeof(kWardenMemcpyPattern)); + LOG_WARNING("WardenMemory: Embedded Warden memcpy pattern at 0x", std::hex, MEMCPY_PATTERN_VA, std::dec); + } +} + +void WardenMemory::patchTurtleWowBinary() { + // Apply TurtlePatcher byte patches to make our PE image match a real Turtle WoW client. + // These patches are applied at file offsets which equal RVAs for this PE. + // Source: TurtlePatcher/Main.cpp PatchBinary() + PatchVersion() + + auto patchBytes = [&](uint32_t fileOffset, const std::vector& bytes) { + if (fileOffset + bytes.size() > imageSize_) { + LOG_WARNING("WardenMemory: Turtle patch at 0x", std::hex, fileOffset, + " exceeds image size, skipping"); + return; + } + std::memcpy(image_.data() + fileOffset, bytes.data(), bytes.size()); + }; + + auto patchString = [&](uint32_t fileOffset, const char* str) { + size_t len = std::strlen(str) + 1; // include null terminator + if (fileOffset + len > imageSize_) return; + std::memcpy(image_.data() + fileOffset, str, len); + }; + + // --- PatchBinary() patches --- + + // Patches 1-4: Unknown purpose code patches in .text + patchBytes(0x2F113A, {0xEB, 0x19}); + patchBytes(0x2F1158, {0x03}); + patchBytes(0x2F11A7, {0x03}); + patchBytes(0x2F11F0, {0xEB, 0xB2}); + + // PvP rank check removal (6x NOP) + patchBytes(0x2093B0, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90}); + + // Dwarf mage hackfix removal + patchBytes(0x0706E5, {0xFE}); + patchBytes(0x0706EB, {0xFE}); + patchBytes(0x07075D, {0xFE}); + patchBytes(0x070763, {0xFE}); + + // Emote sound race ID checks (High Elf support) + patchBytes(0x059289, {0x40}); + patchBytes(0x057C81, {0x40}); + + // Nameplate distance (41 yards) + patchBytes(0x40C448, {0x00, 0x00, 0x24, 0x42}); + + // Large address aware flag in PE header + patchBytes(0x000126, {0x2F, 0x01}); + + // Sound channel patches + patchBytes(0x05728C, {0x38, 0x5D, 0x83, 0x00}); // software channels + patchBytes(0x057250, {0x38, 0x5D, 0x83, 0x00}); // hardware channels + patchBytes(0x0572C8, {0x6C, 0x5C, 0x83, 0x00}); // memory cache + + // Sound in background (non-FoV build) + patchBytes(0x3A4869, {0x14}); + + // Hardcore chat patches + patchBytes(0x09B0B8, {0x5F}); + patchBytes(0x09B193, {0xE9, 0xA8, 0xAE, 0x86}); + patchBytes(0x09F7A5, {0x70, 0x53, 0x56, 0x33, 0xF6, 0xE9, 0x71, 0x68, 0x86, 0x00}); + patchBytes(0x09F864, {0x94}); + patchBytes(0x09F878, {0x0E}); + patchBytes(0x09F887, {0x90}); + patchBytes(0x11BAE1, {0x0C, 0x60, 0xD0}); + + // Hardcore chat code cave at 0x48E000 (85 bytes) + patchBytes(0x48E000, { + 0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00, 0x00, 0x43, 0x48, 0x41, 0x54, + 0x5F, 0x4D, 0x53, 0x47, 0x5F, 0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00, + 0x57, 0x8B, 0xDA, 0x8B, 0xF9, 0xC7, 0x45, 0x94, 0x00, 0x60, 0xD0, 0x00, 0xC7, 0x45, 0x90, 0x5E, + 0x00, 0x00, 0x00, 0xE9, 0x77, 0x97, 0x79, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x68, 0x08, 0x46, 0x84, 0x00, 0x83, 0x7D, 0xF0, 0x5E, 0x75, 0x05, 0xB9, 0x1F, 0x02, 0x00, 0x00, + 0xE9, 0x43, 0x51, 0x79, 0xFF + }); + + // Blue child moon patch + patchBytes(0x3E5B83, { + 0xC7, 0x05, 0xA4, 0x98, 0xCE, 0x00, 0xD4, 0xE2, 0xE7, 0xFF, 0xC2, 0x04, 0x00 + }); + + // Blue child moon timer + patchBytes(0x2D2095, {0x00, 0x00, 0x80, 0x3F}); + + // SetUnit codecave jump + patchBytes(0x105E19, {0xE9, 0x02, 0x03, 0x80, 0x00}); + + // SetUnit main code cave at 0x48E060 (291 bytes) + patchBytes(0x48E060, { + 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0x85, 0xD2, 0x53, 0x56, 0x57, 0x89, 0xCF, 0x0F, 0x84, 0xA2, + 0x00, 0x00, 0x00, 0x89, 0xD0, 0x85, 0xC0, 0x0F, 0x8C, 0x98, 0x00, 0x00, 0x00, 0x3B, 0x05, 0x94, + 0xDE, 0xC0, 0x00, 0x0F, 0x8F, 0x8C, 0x00, 0x00, 0x00, 0x8B, 0x0D, 0x90, 0xDE, 0xC0, 0x00, 0x8B, + 0x04, 0x81, 0x85, 0xC0, 0x89, 0x45, 0xF0, 0x74, 0x7C, 0x8B, 0x40, 0x04, 0x85, 0xC0, 0x7C, 0x75, + 0x3B, 0x05, 0x6C, 0xDE, 0xC0, 0x00, 0x7F, 0x6D, 0x8B, 0x15, 0x68, 0xDE, 0xC0, 0x00, 0x8B, 0x1C, + 0x82, 0x85, 0xDB, 0x74, 0x60, 0x8B, 0x43, 0x08, 0x6A, 0x00, 0x50, 0x89, 0xF9, 0xE8, 0xFE, 0x6E, + 0xA6, 0xFF, 0x89, 0xC1, 0xE8, 0x87, 0x12, 0xA0, 0xFF, 0x89, 0xC6, 0x85, 0xF6, 0x74, 0x46, 0x8B, + 0x55, 0xF0, 0x53, 0x89, 0xF1, 0xE8, 0xD6, 0x36, 0x77, 0xFF, 0x8B, 0x17, 0x56, 0x89, 0xF9, 0xFF, + 0x92, 0x90, 0x00, 0x00, 0x00, 0x89, 0xF8, 0x99, 0x52, 0x50, 0x68, 0xA0, 0x62, 0x50, 0x00, 0x89, + 0xF1, 0xE8, 0xBA, 0xBA, 0xA0, 0xFF, 0x6A, 0x01, 0x6A, 0x01, 0x68, 0x00, 0x00, 0x80, 0x3F, 0x6A, + 0x00, 0x6A, 0xFF, 0x6A, 0x00, 0x6A, 0xFF, 0x89, 0xF1, 0xE8, 0x92, 0xC0, 0xA0, 0xFF, 0x89, 0xF1, + 0xE8, 0x8B, 0xA2, 0xA0, 0xFF, 0x5F, 0x5E, 0x5B, 0x89, 0xEC, 0x5D, 0xC3, 0x90, 0x90, 0x90, 0x90, + 0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xD4, 0xD2, 0x9E, 0xFF, 0x83, 0xF8, 0x03, 0x75, + 0x43, 0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xE3, 0xD4, 0x9E, 0xFF, 0xE8, 0x6E, 0x41, + 0x70, 0xFF, 0x56, 0x8B, 0xB7, 0xD4, 0x00, 0x00, 0x00, 0x31, 0xD2, 0x39, 0xD6, 0x89, 0x97, 0xE0, + 0x03, 0x00, 0x00, 0x89, 0x97, 0xE4, 0x03, 0x00, 0x00, 0x89, 0x97, 0xF0, 0x03, 0x00, 0x00, 0x5E, + 0x0F, 0x84, 0xD3, 0xFC, 0x7F, 0xFF, 0x89, 0xC2, 0x89, 0xF9, 0xE8, 0xF1, 0xFE, 0xFF, 0xFF, 0xE9, + 0xC5, 0xFC, 0x7F, 0xFF, 0xBA, 0x02, 0x00, 0x00, 0x00, 0xE9, 0xA0, 0xFC, 0x7F, 0xFF, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 + }); + + // --- PatchVersion() patches --- + + // Net version: build 7199 (0x1C1F LE) + patchBytes(0x1B2122, {0x1F, 0x1C}); + + // Visual version string + patchString(0x437C04, "1.17.2"); + + // Visual build string + patchString(0x437BFC, "7199"); + + // Build date string + patchString(0x434798, "May 20 2024"); + + // Website filters + patchString(0x45CCD8, "*.turtle-wow.org"); + patchString(0x45CC9C, "*.discord.gg"); + + LOG_WARNING("WardenMemory: Applied TurtlePatcher binary patches (build 7199)"); } bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) const { @@ -272,18 +572,40 @@ bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) cons return true; } - // PE image range - if (!loaded_ || va < imageBase_) return false; - uint32_t offset = va - imageBase_; + if (!loaded_) return false; + + // Warden MEM_CHECK offsets are seen in multiple forms: + // 1) Absolute VA (e.g. 0x00401337) + // 2) RVA (e.g. 0x000139A9) + // 3) Tiny module-relative offsets (e.g. 0x00000229, 0x00000008) + // Accept all three to avoid fallback-to-zeros on Classic/Turtle. + uint32_t offset = 0; + if (va >= imageBase_) { + // Absolute VA. + offset = va - imageBase_; + } else if (va < imageSize_) { + // RVA into WoW.exe image. + offset = va; + } else { + // Tiny relative offsets frequently target fake Warden runtime globals. + constexpr uint32_t kFakeWardenBase = 0xCE8000; + const uint32_t remappedVa = kFakeWardenBase + va; + if (remappedVa < imageBase_) return false; + offset = remappedVa - imageBase_; + } + if (static_cast(offset) + length > imageSize_) return false; std::memcpy(outBuf, image_.data() + offset, length); return true; } -uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build) { +uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build, bool isTurtle) { switch (build) { - case 5875: return 0x00906000; // Classic 1.12.1 + case 5875: + // Turtle WoW uses a custom WoW.exe with different code bytes. + // Their warden_scans DB expects bytes from this custom exe. + return isTurtle ? 0x00906000 : 0x009FD000; default: return 0; // Unknown — accept any } } @@ -301,6 +623,7 @@ std::string WardenMemory::findWowExe(uint16_t build) const { } } candidateDirs.push_back("Data/misc"); + candidateDirs.push_back("Data/expansions/turtle/overlay/misc"); const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" }; @@ -318,7 +641,7 @@ std::string WardenMemory::findWowExe(uint16_t build) const { } // If we know the expected imageSize for this build, try to find a matching PE - uint32_t expectedSize = expectedImageSizeForBuild(build); + uint32_t expectedSize = expectedImageSizeForBuild(build, isTurtle_); if (expectedSize != 0 && allPaths.size() > 1) { for (const auto& path : allPaths) { std::ifstream f(path, std::ios::binary); @@ -342,17 +665,29 @@ std::string WardenMemory::findWowExe(uint16_t build) const { } } - // Fallback: return first available - return allPaths.empty() ? "" : allPaths[0]; + // Fallback: prefer the largest PE file (modified clients like Turtle WoW are + // larger than vanilla, and Warden checks target the actual running client). + std::string bestPath; + uintmax_t bestSize = 0; + for (const auto& path : allPaths) { + std::error_code ec; + auto sz = std::filesystem::file_size(path, ec); + if (!ec && sz > bestSize) { + bestSize = sz; + bestPath = path; + } + } + return bestPath.empty() && !allPaths.empty() ? allPaths[0] : bestPath; } -bool WardenMemory::load(uint16_t build) { +bool WardenMemory::load(uint16_t build, bool isTurtle) { + isTurtle_ = isTurtle; std::string path = findWowExe(build); if (path.empty()) { LOG_WARNING("WardenMemory: WoW.exe not found in any candidate directory"); return false; } - LOG_INFO("WardenMemory: Found ", path); + LOG_WARNING("WardenMemory: Loading PE image: ", path, " (build=", build, ")"); return loadFromFile(path); } @@ -377,11 +712,274 @@ bool WardenMemory::loadFromFile(const std::string& exePath) { initKuserSharedData(); patchRuntimeGlobals(); + if (isTurtle_ && imageSize_ != 0x00906000) { + // Only apply TurtlePatcher patches if we loaded the vanilla exe. + // The real Turtle WoW.exe (imageSize=0x906000) already has these bytes. + patchTurtleWowBinary(); + LOG_WARNING("WardenMemory: Applied Turtle patches to vanilla PE (imageSize=0x", std::hex, imageSize_, std::dec, ")"); + } else if (isTurtle_) { + LOG_WARNING("WardenMemory: Loaded native Turtle PE — skipping patches"); + } loaded_ = true; LOG_INFO("WardenMemory: Loaded PE image (", fileData.size(), " bytes on disk, ", imageSize_, " bytes virtual)"); + + // Verify all known warden_scans MEM_CHECK entries against our PE image. + // This checks the exact bytes the server will memcmp against. + verifyWardenScanEntries(); + return true; } +void WardenMemory::verifyWardenScanEntries() { + struct ScanEntry { int id; uint32_t address; uint8_t length; const char* expectedHex; const char* comment; }; + static const ScanEntry entries[] = { + { 1, 8679268, 6, "686561646572", "Packet internal sign - header"}, + { 3, 8530960, 6, "53595354454D", "Packet internal sign - SYSTEM"}, + { 8, 8151666, 4, "D893FEC0", "Jump gravity"}, + { 9, 8151646, 2, "3075", "Jump gravity water"}, + {10, 6382555, 2, "8A47", "Anti root"}, + {11, 6380789, 1, "F8", "Anti move"}, + {12, 8151647, 1, "75", "Anti jump"}, + {13, 8152026, 4, "8B4F7889", "No fall damage"}, + {14, 6504892, 2, "7425", "Super fly"}, + {15, 6383433, 2, "780F", "Heartbeat interval speedhack"}, + {16, 6284623, 1, "F4", "Anti slow hack"}, + {17, 6504931, 2, "85D2", "No fall damage"}, + {18, 8151565, 2, "2000", "Fly hack"}, + {19, 7153475, 6, "890D509CCE00", "General hacks"}, + {20, 7138894, 6, "A3D89BCE00EB", "Wall climb"}, + {21, 7138907, 6, "890DD89BCE00", "Wall climb"}, + {22, 6993044, 1, "74", "Zero gravity"}, + {23, 6502300, 1, "FC", "Air walk"}, + {24, 6340512, 2, "7F7D", "Wall climb"}, + {25, 6380455, 4, "F4010000", "Wall climb"}, + {26, 8151657, 4, "488C11C1", "Wall climb"}, + {27, 6992319, 3, "894704", "Wall climb"}, + {28, 6340529, 2, "746C", "No water hack"}, + {29, 6356016, 10, "C70588D8C4000C000000", "No water hack"}, + {30, 4730584, 6, "0F8CE1000000", "WMO collision"}, + {31, 4803152, 7, "A1C0EACE0085C0", "noclip hack"}, + {32, 5946704, 6, "8BD18B0D80E0", "M2 collision"}, + {33, 6340543, 2, "7546", "M2 collision"}, + {34, 5341282, 1, "7F", "Warden disable"}, + {35, 4989376, 1, "72", "No fog hack"}, + {36, 8145237, 1, "8B", "No fog hack"}, + {37, 6392083, 8, "8B450850E824DA1A", "No fog hack"}, + {38, 8146241, 10, "D9818C0000008BE55DC2", "tp2plane hack"}, + {39, 6995731, 1, "74", "Air swim hack"}, + {40, 6964859, 1, "75", "Infinite jump hack"}, + {41, 6382558, 10, "84C074178B86A4000000", "Gravity water hack"}, + {42, 8151997, 3, "895108", "Gravity hack"}, + {43, 8152025, 1, "34", "Plane teleport"}, + {44, 6516436, 1, "FC", "Zero fall time"}, + {45, 6501616, 1, "FC", "No fall damage"}, + {46, 6511674, 1, "FC", "Fall time hack"}, + {47, 6513048, 1, "FC", "Death bug hack"}, + {48, 6514072, 1, "FC", "Anti slow hack"}, + {49, 8152029, 3, "894E38", "Anti slow hack"}, + {50, 4847346, 3, "8B45D4", "Max camera distance hack"}, + {51, 4847069, 1, "74", "Wall climb"}, + {52, 8155231, 3, "000000", "Signature check"}, + {53, 6356849, 1, "74", "Signature check"}, + {54, 6354889, 6, "0F8A71FFFFFF", "Signature check"}, + {55, 4657642, 1, "74", "Max interact distance hack"}, + {56, 6211360, 8, "558BEC83EC0C8B45", "Hover speed hack"}, + {57, 8153504, 3, "558BEC", "Flight speed hack"}, + {58, 6214285, 6, "8B82500E0000", "Track all units hack"}, + {59, 8151558, 11, "25FFFFDFFB0D0020000089", "No fall damage"}, + {60, 8155228, 6, "89868C000000", "Run speed hack"}, + {61, 6356837, 2, "7474", "Follow anything hack"}, + {62, 6751806, 1, "74", "No water hack"}, + {63, 4657632, 2, "740A", "Any name hack"}, + {64, 8151976, 4, "84E5FFFF", "Plane teleport"}, + {65, 6214371, 6, "8BB1540E0000", "Object tracking hack"}, + {66, 6818689, 5, "A388F2C700", "No water hack"}, + {67, 6186028, 5, "C705ACD2C4", "No fog hack"}, + {68, 5473808, 4, "30855300", "Warden disable hack"}, + {69, 4208171, 3, "6B2C00", "Warden disable hack"}, + {70, 7119285, 1, "74", "Warden disable hack"}, + {71, 4729827, 1, "5E", "Daylight hack"}, + {72, 6354512, 6, "0F84EA000000", "Ranged attack stop hack"}, + {73, 5053463, 2, "7415", "Officer note hack"}, + {79, 8139737, 5, "D84E14DEC1", "UNKNOWN movement hack"}, + {80, 8902804, 4, "8E977042", "Wall climb hack"}, + {81, 8902808, 4, "0000E040", "Run speed hack"}, + {82, 8154755, 7, "8166403FFFDFFF", "Moveflag hack"}, + {83, 8445948, 4, "BB8D243F", "Wall climb hack"}, + {84, 6493717, 2, "741D", "Speed hack"}, + }; + + auto hexToByte = [](char hi, char lo) -> uint8_t { + auto nibble = [](char c) -> uint8_t { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return 10 + c - 'A'; + if (c >= 'a' && c <= 'f') return 10 + c - 'a'; + return 0; + }; + return (nibble(hi) << 4) | nibble(lo); + }; + + int mismatches = 0; + int patched = 0; + for (const auto& e : entries) { + std::string hexStr(e.expectedHex); + std::vector expected; + for (size_t i = 0; i + 1 < hexStr.size(); i += 2) + expected.push_back(hexToByte(hexStr[i], hexStr[i+1])); + + std::vector actual(e.length, 0); + bool ok = readMemory(e.address, e.length, actual.data()); + + if (!ok || actual != expected) { + mismatches++; + + // In Turtle mode, write the expected bytes into the PE image so + // MEM_CHECK responses return what the server expects. + if (isTurtle_ && e.address >= imageBase_) { + uint32_t offset = e.address - imageBase_; + if (offset + expected.size() <= imageSize_) { + std::memcpy(image_.data() + offset, expected.data(), expected.size()); + patched++; + } + } + } + } + + if (mismatches == 0) { + LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]), + " DB scan entries MATCH PE image"); + } else if (patched > 0) { + LOG_WARNING("WardenScan: Patched ", patched, "/", mismatches, + " mismatched scan entries into PE image"); + } else { + LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]), + " DB scan entries MISMATCH"); + } +} + +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) return false; + + // Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1) + std::string cacheKey(26, '\0'); + std::memcpy(&cacheKey[0], seed, 4); + std::memcpy(&cacheKey[4], expectedHash, 20); + cacheKey[24] = patternLen; + cacheKey[25] = imageOnly ? 1 : 0; + + auto cacheIt = codePatternCache_.find(cacheKey); + if (cacheIt != codePatternCache_.end()) { + return cacheIt->second; + } + + // --- Fast path: check the hint offset directly (single HMAC) --- + // The PAGE_A offset field is the RVA where the server expects the pattern. + if (hintOffset > 0 && hintOffset + patternLen <= imageSize_) { + uint8_t hmacOut[20]; + unsigned int hmacLen = 0; + HMAC(EVP_sha1(), seed, 4, + image_.data() + hintOffset, patternLen, + hmacOut, &hmacLen); + if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + LOG_WARNING("WardenMemory: Code pattern found at hint RVA 0x", std::hex, + hintOffset, std::dec, " (direct hit)"); + codePatternCache_[cacheKey] = true; + return true; + } + } + + // --- Wider hint window: search ±4096 bytes around hint offset --- + if (hintOffset > 0) { + size_t winStart = (hintOffset > 4096) ? hintOffset - 4096 : 0; + size_t winEnd = std::min(static_cast(hintOffset) + 4096 + patternLen, + static_cast(imageSize_)); + if (winEnd > winStart + patternLen) { + for (size_t i = winStart; i + patternLen <= winEnd; i++) { + if (i == hintOffset) continue; // already checked + uint8_t hmacOut[20]; + unsigned int hmacLen = 0; + HMAC(EVP_sha1(), seed, 4, + image_.data() + i, patternLen, + hmacOut, &hmacLen); + if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, i, + std::dec, " (hint window, delta=", static_cast(i) - static_cast(hintOffset), ")"); + codePatternCache_[cacheKey] = true; + return true; + } + } + } + } + + // If hint-only mode, skip the expensive brute-force search. + if (hintOnly) return false; + + // --- Brute-force fallback: search all PE sections --- + struct Range { size_t start; size_t end; }; + std::vector ranges; + + if (imageOnly && image_.size() >= 64) { + uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8) + | (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24); + if (peOffset + 4 + 20 <= image_.size()) { + uint16_t numSections = image_[peOffset+4+2] | (uint16_t(image_[peOffset+4+3]) << 8); + uint16_t optHeaderSize = image_[peOffset+4+16] | (uint16_t(image_[peOffset+4+17]) << 8); + size_t secTable = peOffset + 4 + 20 + optHeaderSize; + for (uint16_t i = 0; i < numSections; i++) { + size_t secOfs = secTable + i * 40; + if (secOfs + 40 > image_.size()) break; + uint32_t va = image_[secOfs+12] | (uint32_t(image_[secOfs+13]) << 8) + | (uint32_t(image_[secOfs+14]) << 16) | (uint32_t(image_[secOfs+15]) << 24); + uint32_t vsize = image_[secOfs+8] | (uint32_t(image_[secOfs+9]) << 8) + | (uint32_t(image_[secOfs+10]) << 16) | (uint32_t(image_[secOfs+11]) << 24); + size_t rEnd = std::min(static_cast(va + vsize), static_cast(imageSize_)); + if (va + patternLen <= rEnd) + ranges.push_back({va, rEnd}); + } + } + } + + if (ranges.empty()) { + if (patternLen <= imageSize_) + ranges.push_back({0, imageSize_}); + } + + auto bruteStart = std::chrono::steady_clock::now(); + LOG_WARNING("WardenMemory: Brute-force searching ", ranges.size(), " section(s), hint=0x", + std::hex, hintOffset, std::dec, " patLen=", static_cast(patternLen)); + + size_t totalPositions = 0; + for (const auto& r : ranges) { + size_t positions = r.end - r.start - patternLen + 1; + for (size_t i = 0; i < positions; i++) { + uint8_t hmacOut[20]; + unsigned int hmacLen = 0; + HMAC(EVP_sha1(), seed, 4, + image_.data() + r.start + i, patternLen, + hmacOut, &hmacLen); + if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - bruteStart).count(); + LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, + r.start + i, std::dec, " (searched ", totalPositions + i + 1, + " positions in ", elapsed, "s)"); + codePatternCache_[cacheKey] = true; + return true; + } + } + totalPositions += positions; + } + + auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - bruteStart).count(); + LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ", + ranges.size(), " section(s), took ", elapsed, "s"); + codePatternCache_[cacheKey] = false; + return false; +} + } // namespace game } // namespace wowee diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index 1c253459..bf44c26e 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -1,9 +1,9 @@ #include "game/warden_module.hpp" #include "auth/crypto.hpp" +#include "core/logger.hpp" #include #include #include -#include #include #include #include @@ -51,30 +51,30 @@ bool WardenModule::load(const std::vector& moduleData, moduleData_ = moduleData; md5Hash_ = md5Hash; - std::cout << "[WardenModule] Loading module (MD5: "; - for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) { - printf("%02X", md5Hash[i]); + { + char hexBuf[17] = {}; + for (size_t i = 0; i < std::min(md5Hash.size(), size_t(8)); ++i) { + snprintf(hexBuf + i * 2, 3, "%02X", md5Hash[i]); + } + LOG_INFO("WardenModule: Loading module (MD5: ", hexBuf, "...)"); } - std::cout << "...)" << '\n'; // Step 1: Verify MD5 hash if (!verifyMD5(moduleData, md5Hash)) { - std::cerr << "[WardenModule] MD5 verification failed!" << '\n'; - return false; + LOG_ERROR("WardenModule: MD5 verification failed; continuing in compatibility mode"); } - std::cout << "[WardenModule] ✓ MD5 verified" << '\n'; + LOG_INFO("WardenModule: MD5 verified"); // Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed) if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm] - std::cerr << "[WardenModule] RC4 decryption failed!" << '\n'; - return false; + LOG_ERROR("WardenModule: RC4 decryption failed; using raw module bytes fallback"); + decryptedData_ = moduleData; } - std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n'; + LOG_INFO("WardenModule: RC4 decrypted (", decryptedData_.size(), " bytes)"); // Step 3: Verify RSA signature if (!verifyRSASignature(decryptedData_)) { - std::cerr << "[WardenModule] RSA signature verification failed!" << '\n'; - // 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 @@ -85,71 +85,68 @@ bool WardenModule::load(const std::vector& moduleData, dataWithoutSig = decryptedData_; } if (!decompressZlib(dataWithoutSig, decompressedData_)) { - std::cerr << "[WardenModule] zlib decompression failed!" << '\n'; - return false; + LOG_ERROR("WardenModule: zlib decompression failed; using decrypted bytes fallback"); + decompressedData_ = decryptedData_; } // Step 5: Parse custom executable format if (!parseExecutableFormat(decompressedData_)) { - std::cerr << "[WardenModule] Executable format parsing failed!" << '\n'; - return false; + LOG_ERROR("WardenModule: Executable format parsing failed; continuing with minimal module image"); } // Step 6: Apply relocations if (!applyRelocations()) { - std::cerr << "[WardenModule] Address relocations failed!" << '\n'; - return false; + LOG_ERROR("WardenModule: Address relocations failed; continuing with unrelocated image"); } // Step 7: Bind APIs if (!bindAPIs()) { - std::cerr << "[WardenModule] API binding failed!" << '\n'; + LOG_ERROR("WardenModule: API binding failed!"); // Note: Currently returns true (stub) on both Windows and Linux } // Step 8: Initialize module if (!initializeModule()) { - std::cerr << "[WardenModule] Module initialization failed!" << '\n'; - return false; + LOG_ERROR("WardenModule: Module initialization failed; continuing with stub callbacks"); } // Module loading pipeline complete! // Note: Steps 6-8 are stubs/platform-limited, but infrastructure is ready loaded_ = true; // Mark as loaded (infrastructure complete) - std::cout << "[WardenModule] ✓ Module loading pipeline COMPLETE" << '\n'; - std::cout << "[WardenModule] Status: Infrastructure ready, execution stubs in place" << '\n'; - std::cout << "[WardenModule] Limitations:" << '\n'; - std::cout << "[WardenModule] - Relocations: needs real module data" << '\n'; - std::cout << "[WardenModule] - API Binding: Windows only (or Wine on Linux)" << '\n'; - std::cout << "[WardenModule] - Execution: disabled (unsafe without validation)" << '\n'; - std::cout << "[WardenModule] For strict servers: Would need to enable actual x86 execution" << '\n'; + LOG_INFO("WardenModule: Module loading pipeline COMPLETE"); + LOG_INFO("WardenModule: Status: Infrastructure ready, execution stubs in place"); + LOG_INFO("WardenModule: Limitations:"); + LOG_INFO("WardenModule: - Relocations: needs real module data"); + LOG_INFO("WardenModule: - API Binding: Windows only (or Wine on Linux)"); + LOG_INFO("WardenModule: - Execution: disabled (unsafe without validation)"); + LOG_INFO("WardenModule: For strict servers: Would need to enable actual x86 execution"); 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_) { - std::cerr << "[WardenModule] Module not loaded, cannot process checks" << '\n'; + LOG_ERROR("WardenModule: Module not loaded, cannot process checks"); return false; } #ifdef HAVE_UNICORN if (emulator_ && emulator_->isInitialized() && funcList_.packetHandler) { - std::cout << "[WardenModule] Processing check request via emulator..." << '\n'; - std::cout << "[WardenModule] Check data: " << checkData.size() << " bytes" << '\n'; + LOG_INFO("WardenModule: Processing check request via emulator..."); + LOG_INFO("WardenModule: Check data: ", checkData.size(), " bytes"); // Allocate memory for check data in emulated space uint32_t checkDataAddr = emulator_->allocateMemory(checkData.size(), 0x04); if (checkDataAddr == 0) { - std::cerr << "[WardenModule] Failed to allocate memory for check data" << '\n'; + LOG_ERROR("WardenModule: Failed to allocate memory for check data"); return false; } // Write check data to emulated memory if (!emulator_->writeMemory(checkDataAddr, checkData.data(), checkData.size())) { - std::cerr << "[WardenModule] Failed to write check data" << '\n'; + LOG_ERROR("WardenModule: Failed to write check data"); emulator_->freeMemory(checkDataAddr); return false; } @@ -157,33 +154,62 @@ bool WardenModule::processCheckRequest(const std::vector& checkData, // Allocate response buffer in emulated space (assume max 1KB response) uint32_t responseAddr = emulator_->allocateMemory(1024, 0x04); if (responseAddr == 0) { - std::cerr << "[WardenModule] Failed to allocate response buffer" << '\n'; + LOG_ERROR("WardenModule: Failed to allocate response buffer"); emulator_->freeMemory(checkDataAddr); return false; } try { - // Call module's PacketHandler - // void PacketHandler(uint8_t* checkData, size_t checkSize, - // uint8_t* responseOut, size_t* responseSizeOut) - std::cout << "[WardenModule] Calling PacketHandler..." << '\n'; + if (emulatedPacketHandlerAddr_ == 0) { + LOG_ERROR("WardenModule: PacketHandler address not set (module not fully initialized)"); + emulator_->freeMemory(checkDataAddr); + emulator_->freeMemory(responseAddr); + return false; + } - // For now, this is a placeholder - actual calling would depend on - // the module's exact function signature - std::cout << "[WardenModule] ⚠ PacketHandler execution stubbed" << '\n'; - std::cout << "[WardenModule] Would call emulated function to process checks" << '\n'; - std::cout << "[WardenModule] This would generate REAL responses (not fakes!)" << '\n'; + // Allocate uint32_t for responseSizeOut in emulated memory + uint32_t initialSize = 1024; + uint32_t responseSizeAddr = emulator_->writeData(&initialSize, sizeof(uint32_t)); + if (responseSizeAddr == 0) { + LOG_ERROR("WardenModule: Failed to allocate responseSizeAddr"); + emulator_->freeMemory(checkDataAddr); + emulator_->freeMemory(responseAddr); + return false; + } + + // Call: void PacketHandler(uint8_t* data, uint32_t size, + // uint8_t* responseOut, uint32_t* responseSizeOut) + LOG_INFO("WardenModule: Calling emulated PacketHandler..."); + emulator_->callFunction(emulatedPacketHandlerAddr_, { + checkDataAddr, + static_cast(checkData.size()), + responseAddr, + responseSizeAddr + }); + + // Read back response size and data + uint32_t responseSize = 0; + emulator_->readMemory(responseSizeAddr, &responseSize, sizeof(uint32_t)); + emulator_->freeMemory(responseSizeAddr); + + if (responseSize > 0 && responseSize <= 1024) { + responseOut.resize(responseSize); + if (!emulator_->readMemory(responseAddr, responseOut.data(), responseSize)) { + LOG_ERROR("WardenModule: Failed to read response data"); + responseOut.clear(); + } else { + LOG_INFO("WardenModule: PacketHandler wrote ", responseSize, " byte response"); + } + } else { + LOG_WARNING("WardenModule: PacketHandler returned invalid responseSize=", responseSize); + } - // Clean up emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(responseAddr); - - // For now, return false to use fake responses - // Once we have a real module, we'd read the response from responseAddr - return false; + return !responseOut.empty(); } catch (const std::exception& e) { - std::cerr << "[WardenModule] Exception during PacketHandler: " << e.what() << '\n'; + LOG_ERROR("WardenModule: Exception during PacketHandler: ", e.what()); emulator_->freeMemory(checkDataAddr); emulator_->freeMemory(responseAddr); return false; @@ -191,45 +217,37 @@ bool WardenModule::processCheckRequest(const std::vector& checkData, } #endif - std::cout << "[WardenModule] ⚠ processCheckRequest NOT IMPLEMENTED" << '\n'; - std::cout << "[WardenModule] Would call module->PacketHandler() here" << '\n'; + LOG_WARNING("WardenModule: processCheckRequest NOT IMPLEMENTED"); + LOG_INFO("WardenModule: Would call module->PacketHandler() here"); // For now, return false to fall back to fake responses in GameHandler return false; } -uint32_t WardenModule::tick([[maybe_unused]] uint32_t deltaMs) { +uint32_t WardenModule::tick(uint32_t deltaMs) { if (!loaded_ || !funcList_.tick) { - return 0; // No tick needed + return 0; } - - // TODO: Call module's Tick function - // return funcList_.tick(deltaMs); - - return 0; + return funcList_.tick(deltaMs); } -void WardenModule::generateRC4Keys([[maybe_unused]] uint8_t* packet) { +void WardenModule::generateRC4Keys(uint8_t* packet) { if (!loaded_ || !funcList_.generateRC4Keys) { return; } - - // TODO: Call module's GenerateRC4Keys function - // This re-keys the Warden crypto stream - // funcList_.generateRC4Keys(packet); + funcList_.generateRC4Keys(packet); } void WardenModule::unload() { if (moduleMemory_) { // Call module's Unload() function if loaded if (loaded_ && funcList_.unload) { - std::cout << "[WardenModule] Calling module unload callback..." << '\n'; - // TODO: Implement callback when execution layer is complete - // funcList_.unload(nullptr); + LOG_INFO("WardenModule: Calling module unload callback..."); + funcList_.unload(nullptr); } // Free executable memory region - std::cout << "[WardenModule] Freeing " << moduleSize_ << " bytes of executable memory" << '\n'; + LOG_INFO("WardenModule: Freeing ", moduleSize_, " bytes of executable memory"); #ifdef _WIN32 VirtualFree(moduleMemory_, 0, MEM_RELEASE); #else @@ -242,6 +260,7 @@ void WardenModule::unload() { // Clear function pointers funcList_ = {}; + emulatedPacketHandlerAddr_ = 0; loaded_ = false; moduleData_.clear(); @@ -268,7 +287,7 @@ bool WardenModule::decryptRC4(const std::vector& encrypted, const std::vector& key, std::vector& decryptedOut) { if (key.size() != 16) { - std::cerr << "[WardenModule] Invalid RC4 key size: " << key.size() << " (expected 16)" << '\n'; + LOG_ERROR("WardenModule: Invalid RC4 key size: ", key.size(), " (expected 16)"); return false; } @@ -303,7 +322,7 @@ bool WardenModule::decryptRC4(const std::vector& encrypted, bool WardenModule::verifyRSASignature(const std::vector& data) { // RSA-2048 signature is last 256 bytes if (data.size() < 256) { - std::cerr << "[WardenModule] Data too small for RSA signature (need at least 256 bytes)" << '\n'; + LOG_ERROR("WardenModule: Data too small for RSA signature (need at least 256 bytes)"); return false; } @@ -389,7 +408,7 @@ bool WardenModule::verifyRSASignature(const std::vector& data) { if (pkey) EVP_PKEY_free(pkey); if (decryptedLen < 0) { - std::cerr << "[WardenModule] RSA public decrypt failed" << '\n'; + LOG_ERROR("WardenModule: RSA public decrypt failed"); return false; } @@ -402,24 +421,23 @@ bool WardenModule::verifyRSASignature(const std::vector& data) { std::vector actualHash(decryptedSig.end() - 20, decryptedSig.end()); if (std::memcmp(actualHash.data(), expectedHash.data(), 20) == 0) { - std::cout << "[WardenModule] ✓ RSA signature verified" << '\n'; + LOG_INFO("WardenModule: RSA signature verified"); return true; } } - std::cerr << "[WardenModule] RSA signature verification FAILED (hash mismatch)" << '\n'; - std::cerr << "[WardenModule] NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification" << '\n'; + 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 - std::cout << "[WardenModule] ⚠ Skipping RSA verification (placeholder modulus)" << '\n'; return true; // TEMPORARY - change to false for production } bool WardenModule::decompressZlib(const std::vector& compressed, std::vector& decompressedOut) { if (compressed.size() < 4) { - std::cerr << "[WardenModule] Compressed data too small (need at least 4 bytes for size header)" << '\n'; + LOG_ERROR("WardenModule: Compressed data too small (need at least 4 bytes for size header)"); return false; } @@ -430,11 +448,11 @@ bool WardenModule::decompressZlib(const std::vector& compressed, (compressed[2] << 16) | (compressed[3] << 24); - std::cout << "[WardenModule] Uncompressed size: " << uncompressedSize << " bytes" << '\n'; + LOG_INFO("WardenModule: Uncompressed size: ", uncompressedSize, " bytes"); // Sanity check (modules shouldn't be larger than 10MB) if (uncompressedSize > 10 * 1024 * 1024) { - std::cerr << "[WardenModule] Uncompressed size suspiciously large: " << uncompressedSize << " bytes" << '\n'; + LOG_ERROR("WardenModule: Uncompressed size suspiciously large: ", uncompressedSize, " bytes"); return false; } @@ -451,7 +469,7 @@ bool WardenModule::decompressZlib(const std::vector& compressed, // Initialize inflater int ret = inflateInit(&stream); if (ret != Z_OK) { - std::cerr << "[WardenModule] inflateInit failed: " << ret << '\n'; + LOG_ERROR("WardenModule: inflateInit failed: ", ret); return false; } @@ -462,19 +480,18 @@ bool WardenModule::decompressZlib(const std::vector& compressed, inflateEnd(&stream); if (ret != Z_STREAM_END) { - std::cerr << "[WardenModule] inflate failed: " << ret << '\n'; + LOG_ERROR("WardenModule: inflate failed: ", ret); return false; } - std::cout << "[WardenModule] ✓ zlib decompression successful (" - << stream.total_out << " bytes decompressed)" << '\n'; + LOG_INFO("WardenModule: zlib decompression successful (", stream.total_out, " bytes decompressed)"); return true; } bool WardenModule::parseExecutableFormat(const std::vector& exeData) { if (exeData.size() < 4) { - std::cerr << "[WardenModule] Executable data too small for header" << '\n'; + LOG_ERROR("WardenModule: Executable data too small for header"); return false; } @@ -485,11 +502,11 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { (exeData[2] << 16) | (exeData[3] << 24); - std::cout << "[WardenModule] Final code size: " << finalCodeSize << " bytes" << '\n'; + LOG_INFO("WardenModule: Final code size: ", finalCodeSize, " bytes"); // Sanity check (executable shouldn't be larger than 5MB) if (finalCodeSize > 5 * 1024 * 1024 || finalCodeSize == 0) { - std::cerr << "[WardenModule] Invalid final code size: " << finalCodeSize << '\n'; + LOG_ERROR("WardenModule: Invalid final code size: ", finalCodeSize); return false; } @@ -504,7 +521,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { PAGE_EXECUTE_READWRITE ); if (!moduleMemory_) { - std::cerr << "[WardenModule] VirtualAlloc failed" << '\n'; + LOG_ERROR("WardenModule: VirtualAlloc failed"); return false; } #else @@ -517,7 +534,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { 0 ); if (moduleMemory_ == MAP_FAILED) { - std::cerr << "[WardenModule] mmap failed: " << strerror(errno) << '\n'; + LOG_ERROR("WardenModule: mmap failed: ", strerror(errno)); moduleMemory_ = nullptr; return false; } @@ -526,8 +543,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { moduleSize_ = finalCodeSize; std::memset(moduleMemory_, 0, moduleSize_); // Zero-initialize - std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at " - << moduleMemory_ << '\n'; + LOG_INFO("WardenModule: Allocated ", moduleSize_, " bytes of executable memory"); auto readU16LE = [&](size_t at) -> uint16_t { return static_cast(exeData[at] | (exeData[at + 1] << 8)); @@ -673,10 +689,10 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { if (usedFormat == PairFormat::SkipCopyData) formatName = "skip/copy/data"; if (usedFormat == PairFormat::CopySkipData) formatName = "copy/skip/data"; - std::cout << "[WardenModule] Parsed " << parsedPairCount << " pairs using format " - << formatName << ", final offset: " << parsedFinalOffset << "/" << finalCodeSize << '\n'; - std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_ - << " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n'; + LOG_INFO("WardenModule: Parsed ", parsedPairCount, " pairs using format ", + formatName, ", final offset: ", parsedFinalOffset, "/", finalCodeSize); + LOG_INFO("WardenModule: Relocation data starts at decompressed offset ", relocDataOffset_, + " (", (exeData.size() - relocDataOffset_), " bytes remaining)"); return true; } @@ -687,13 +703,13 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize); } relocDataOffset_ = 0; - std::cerr << "[WardenModule] Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback" << '\n'; + LOG_WARNING("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback"); return true; } bool WardenModule::applyRelocations() { if (!moduleMemory_ || moduleSize_ == 0) { - std::cerr << "[WardenModule] No module memory allocated for relocations" << '\n'; + LOG_ERROR("WardenModule: No module memory allocated for relocations"); return false; } @@ -702,7 +718,7 @@ bool WardenModule::applyRelocations() { // Each offset in the module image has moduleBase_ added to the 32-bit value there if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) { - std::cout << "[WardenModule] No relocation data available" << '\n'; + LOG_INFO("WardenModule: No relocation data available"); return true; } @@ -726,24 +742,27 @@ bool WardenModule::applyRelocations() { std::memcpy(addr, &val, sizeof(uint32_t)); relocCount++; } else { - std::cerr << "[WardenModule] Relocation offset " << currentOffset - << " out of bounds (moduleSize=" << moduleSize_ << ")" << '\n'; + LOG_ERROR("WardenModule: Relocation offset ", currentOffset, + " out of bounds (moduleSize=", moduleSize_, ")"); } } - std::cout << "[WardenModule] Applied " << relocCount << " relocations (base=0x" - << std::hex << moduleBase_ << std::dec << ")" << '\n'; + { + char baseBuf[32]; + std::snprintf(baseBuf, sizeof(baseBuf), "0x%X", moduleBase_); + LOG_INFO("WardenModule: Applied ", relocCount, " relocations (base=", baseBuf, ")"); + } return true; } bool WardenModule::bindAPIs() { if (!moduleMemory_ || moduleSize_ == 0) { - std::cerr << "[WardenModule] No module memory allocated for API binding" << '\n'; + LOG_ERROR("WardenModule: No module memory allocated for API binding"); return false; } - std::cout << "[WardenModule] Binding Windows APIs for module..." << '\n'; + LOG_INFO("WardenModule: Binding Windows APIs for module..."); // Common Windows APIs used by Warden modules: // @@ -763,14 +782,14 @@ bool WardenModule::bindAPIs() { #ifdef _WIN32 // On Windows: Use GetProcAddress to resolve imports - std::cout << "[WardenModule] Platform: Windows - using GetProcAddress" << '\n'; + LOG_INFO("WardenModule: Platform: Windows - using GetProcAddress"); HMODULE kernel32 = GetModuleHandleA("kernel32.dll"); HMODULE user32 = GetModuleHandleA("user32.dll"); HMODULE ntdll = GetModuleHandleA("ntdll.dll"); if (!kernel32 || !user32 || !ntdll) { - std::cerr << "[WardenModule] Failed to get module handles" << '\n'; + LOG_ERROR("WardenModule: Failed to get module handles"); return false; } @@ -781,8 +800,8 @@ bool WardenModule::bindAPIs() { // - Resolve address using GetProcAddress // - Write address to Import Address Table (IAT) - std::cout << "[WardenModule] ⚠ Windows API binding is STUB (needs PE import table parsing)" << '\n'; - std::cout << "[WardenModule] Would parse PE headers and patch IAT with resolved addresses" << '\n'; + LOG_WARNING("WardenModule: Windows API binding is STUB (needs PE import table parsing)"); + LOG_INFO("WardenModule: Would parse PE headers and patch IAT with resolved addresses"); #else // On Linux: Cannot directly execute Windows code @@ -791,15 +810,15 @@ bool WardenModule::bindAPIs() { // 2. Implement Windows API stubs (limited functionality) // 3. Use binfmt_misc + Wine (transparent Windows executable support) - std::cout << "[WardenModule] Platform: Linux - Windows module execution NOT supported" << '\n'; - std::cout << "[WardenModule] Options:" << '\n'; - std::cout << "[WardenModule] 1. Run wowee under Wine (provides Windows API layer)" << '\n'; - std::cout << "[WardenModule] 2. Use a Windows VM" << '\n'; - std::cout << "[WardenModule] 3. Implement Windows API stubs (limited, complex)" << '\n'; + LOG_WARNING("WardenModule: Platform: Linux - Windows module execution NOT supported"); + LOG_INFO("WardenModule: Options:"); + LOG_INFO("WardenModule: 1. Run wowee under Wine (provides Windows API layer)"); + LOG_INFO("WardenModule: 2. Use a Windows VM"); + LOG_INFO("WardenModule: 3. Implement Windows API stubs (limited, complex)"); // For now, we'll return true to continue the loading pipeline // Real execution would fail, but this allows testing the infrastructure - std::cout << "[WardenModule] ⚠ Skipping API binding (Linux platform limitation)" << '\n'; + LOG_WARNING("WardenModule: Skipping API binding (Linux platform limitation)"); #endif return true; // Return true to continue (stub implementation) @@ -807,11 +826,11 @@ bool WardenModule::bindAPIs() { bool WardenModule::initializeModule() { if (!moduleMemory_ || moduleSize_ == 0) { - std::cerr << "[WardenModule] No module memory allocated for initialization" << '\n'; + LOG_ERROR("WardenModule: No module memory allocated for initialization"); return false; } - std::cout << "[WardenModule] Initializing Warden module..." << '\n'; + LOG_INFO("WardenModule: Initializing Warden module..."); // Module initialization protocol: // @@ -848,27 +867,27 @@ bool WardenModule::initializeModule() { // Stub callbacks (would need real implementations) callbacks.sendPacket = []([[maybe_unused]] uint8_t* data, size_t len) { - std::cout << "[WardenModule Callback] sendPacket(" << len << " bytes)" << '\n'; + LOG_DEBUG("WardenModule Callback: sendPacket(", len, " bytes)"); // TODO: Send CMSG_WARDEN_DATA packet }; callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) { - std::cout << "[WardenModule Callback] validateModule()" << '\n'; + LOG_DEBUG("WardenModule Callback: validateModule()"); // TODO: Validate module hash }; callbacks.allocMemory = [](size_t size) -> void* { - std::cout << "[WardenModule Callback] allocMemory(" << size << ")" << '\n'; + LOG_DEBUG("WardenModule Callback: allocMemory(", size, ")"); return malloc(size); }; callbacks.freeMemory = [](void* ptr) { - std::cout << "[WardenModule Callback] freeMemory()" << '\n'; + LOG_DEBUG("WardenModule Callback: freeMemory()"); free(ptr); }; callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) { - std::cout << "[WardenModule Callback] generateRC4()" << '\n'; + LOG_DEBUG("WardenModule Callback: generateRC4()"); // TODO: Re-key RC4 cipher }; @@ -877,7 +896,7 @@ bool WardenModule::initializeModule() { }; callbacks.logMessage = [](const char* msg) { - std::cout << "[WardenModule Log] " << msg << '\n'; + LOG_INFO("WardenModule Log: ", msg); }; // Module entry point is typically at offset 0 (first bytes of loaded code) @@ -885,24 +904,28 @@ bool WardenModule::initializeModule() { #ifdef HAVE_UNICORN // Use Unicorn emulator for cross-platform execution - std::cout << "[WardenModule] Initializing Unicorn emulator..." << '\n'; + LOG_INFO("WardenModule: Initializing Unicorn emulator..."); emulator_ = std::make_unique(); if (!emulator_->initialize(moduleMemory_, moduleSize_, moduleBase_)) { - std::cerr << "[WardenModule] Failed to initialize emulator" << '\n'; + LOG_ERROR("WardenModule: Failed to initialize emulator"); return false; } // Setup Windows API hooks emulator_->setupCommonAPIHooks(); - std::cout << "[WardenModule] ✓ Emulator initialized successfully" << '\n'; - std::cout << "[WardenModule] Ready to execute module at 0x" << std::hex << moduleBase_ << std::dec << '\n'; + { + char addrBuf[32]; + std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", moduleBase_); + LOG_INFO("WardenModule: Emulator initialized successfully"); + LOG_INFO("WardenModule: Ready to execute module at ", addrBuf); + } // Allocate memory for ClientCallbacks structure in emulated space uint32_t callbackStructAddr = emulator_->allocateMemory(sizeof(ClientCallbacks), 0x04); if (callbackStructAddr == 0) { - std::cerr << "[WardenModule] Failed to allocate memory for callbacks" << '\n'; + LOG_ERROR("WardenModule: Failed to allocate memory for callbacks"); return false; } @@ -925,13 +948,21 @@ bool WardenModule::initializeModule() { emulator_->writeMemory(callbackStructAddr + (i * 4), &addr, 4); } - std::cout << "[WardenModule] Prepared ClientCallbacks at 0x" << std::hex << callbackStructAddr << std::dec << '\n'; + { + char cbBuf[32]; + std::snprintf(cbBuf, sizeof(cbBuf), "0x%X", callbackStructAddr); + LOG_INFO("WardenModule: Prepared ClientCallbacks at ", cbBuf); + } // Call module entry point // Entry point is typically at module base (offset 0) uint32_t entryPoint = moduleBase_; - std::cout << "[WardenModule] Calling module entry point at 0x" << std::hex << entryPoint << std::dec << '\n'; + { + char epBuf[32]; + std::snprintf(epBuf, sizeof(epBuf), "0x%X", entryPoint); + LOG_INFO("WardenModule: Calling module entry point at ", epBuf); + } try { // Call: WardenFuncList* InitModule(ClientCallbacks* callbacks) @@ -939,33 +970,82 @@ bool WardenModule::initializeModule() { uint32_t result = emulator_->callFunction(entryPoint, args); if (result == 0) { - std::cerr << "[WardenModule] Module entry returned NULL" << '\n'; + LOG_ERROR("WardenModule: Module entry returned NULL"); return false; } - std::cout << "[WardenModule] ✓ Module initialized, WardenFuncList at 0x" << std::hex << result << std::dec << '\n'; - - // Read WardenFuncList structure from emulated memory - // Structure has 4 function pointers (16 bytes) - uint32_t funcAddrs[4] = {}; - if (emulator_->readMemory(result, funcAddrs, 16)) { - std::cout << "[WardenModule] Module exported functions:" << '\n'; - std::cout << "[WardenModule] generateRC4Keys: 0x" << std::hex << funcAddrs[0] << std::dec << '\n'; - std::cout << "[WardenModule] unload: 0x" << std::hex << funcAddrs[1] << std::dec << '\n'; - std::cout << "[WardenModule] packetHandler: 0x" << std::hex << funcAddrs[2] << std::dec << '\n'; - std::cout << "[WardenModule] tick: 0x" << std::hex << funcAddrs[3] << std::dec << '\n'; - - // Store function addresses for later use - // funcList_.generateRC4Keys = ... (would wrap emulator calls) - // funcList_.unload = ... - // funcList_.packetHandler = ... - // funcList_.tick = ... + { + char resBuf[32]; + std::snprintf(resBuf, sizeof(resBuf), "0x%X", result); + LOG_INFO("WardenModule: Module initialized, WardenFuncList at ", resBuf); } - std::cout << "[WardenModule] ✓ Module fully initialized and ready!" << '\n'; + // Read WardenFuncList structure from emulated memory + // Structure has 4 function pointers (16 bytes): + // [0] generateRC4Keys(uint8_t* seed) + // [1] unload(uint8_t* rc4Keys) + // [2] packetHandler(uint8_t* data, uint32_t size, + // uint8_t* responseOut, uint32_t* responseSizeOut) + // [3] tick(uint32_t deltaMs) -> uint32_t + uint32_t funcAddrs[4] = {}; + if (emulator_->readMemory(result, funcAddrs, 16)) { + char fb[4][32]; + for (int fi = 0; fi < 4; ++fi) + std::snprintf(fb[fi], sizeof(fb[fi]), "0x%X", funcAddrs[fi]); + LOG_INFO("WardenModule: Module exported functions:"); + LOG_INFO("WardenModule: generateRC4Keys: ", fb[0]); + LOG_INFO("WardenModule: unload: ", fb[1]); + LOG_INFO("WardenModule: packetHandler: ", fb[2]); + LOG_INFO("WardenModule: tick: ", fb[3]); + + // Wrap emulated function addresses into std::function dispatchers + WardenEmulator* emu = emulator_.get(); + + if (funcAddrs[0]) { + uint32_t addr = funcAddrs[0]; + funcList_.generateRC4Keys = [emu, addr](uint8_t* seed) { + // Warden RC4 seed is a fixed 4-byte value + uint32_t seedAddr = emu->writeData(seed, 4); + if (seedAddr) { + emu->callFunction(addr, {seedAddr}); + emu->freeMemory(seedAddr); + } + }; + } + + if (funcAddrs[1]) { + uint32_t addr = funcAddrs[1]; + funcList_.unload = [emu, addr]([[maybe_unused]] uint8_t* rc4Keys) { + emu->callFunction(addr, {0u}); // pass NULL; module saves its own state + }; + } + + if (funcAddrs[2]) { + // Store raw address for the 4-arg call in processCheckRequest + emulatedPacketHandlerAddr_ = funcAddrs[2]; + uint32_t addr = funcAddrs[2]; + // Simple 2-arg variant for generic callers (no response extraction) + funcList_.packetHandler = [emu, addr](uint8_t* data, size_t length) { + uint32_t dataAddr = emu->writeData(data, length); + if (dataAddr) { + emu->callFunction(addr, {dataAddr, static_cast(length)}); + emu->freeMemory(dataAddr); + } + }; + } + + if (funcAddrs[3]) { + uint32_t addr = funcAddrs[3]; + funcList_.tick = [emu, addr](uint32_t deltaMs) -> uint32_t { + return emu->callFunction(addr, {deltaMs}); + }; + } + } + + LOG_INFO("WardenModule: Module fully initialized and ready!"); } catch (const std::exception& e) { - std::cerr << "[WardenModule] Exception during module initialization: " << e.what() << '\n'; + LOG_ERROR("WardenModule: Exception during module initialization: ", e.what()); return false; } @@ -974,14 +1054,14 @@ bool WardenModule::initializeModule() { typedef void* (*ModuleEntryPoint)(ClientCallbacks*); ModuleEntryPoint entryPoint = reinterpret_cast(moduleMemory_); - std::cout << "[WardenModule] Calling module entry point at " << moduleMemory_ << '\n'; + LOG_INFO("WardenModule: Calling module entry point at ", moduleMemory_); // NOTE: This would execute native x86 code // Extremely dangerous without proper validation! // void* result = entryPoint(&callbacks); - std::cout << "[WardenModule] ⚠ Module entry point call is DISABLED (unsafe without validation)" << '\n'; - std::cout << "[WardenModule] Would execute x86 code at " << moduleMemory_ << '\n'; + LOG_WARNING("WardenModule: Module entry point call is DISABLED (unsafe without validation)"); + LOG_INFO("WardenModule: Would execute x86 code at ", moduleMemory_); // TODO: Extract WardenFuncList from result // funcList_.packetHandler = ... @@ -990,9 +1070,9 @@ bool WardenModule::initializeModule() { // funcList_.unload = ... #else - std::cout << "[WardenModule] ⚠ Cannot execute Windows x86 code on Linux" << '\n'; - std::cout << "[WardenModule] Module entry point: " << moduleMemory_ << '\n'; - std::cout << "[WardenModule] Would call entry point with ClientCallbacks struct" << '\n'; + LOG_WARNING("WardenModule: Cannot execute Windows x86 code on Linux"); + LOG_INFO("WardenModule: Module entry point: ", moduleMemory_); + LOG_INFO("WardenModule: Would call entry point with ClientCallbacks struct"); #endif // For now, return true to mark module as "loaded" at infrastructure level @@ -1002,7 +1082,7 @@ bool WardenModule::initializeModule() { // 3. Exception handling for crashes // 4. Sandboxing for security - std::cout << "[WardenModule] ⚠ Module initialization is STUB" << '\n'; + LOG_WARNING("WardenModule: Module initialization is STUB"); return true; // Stub implementation } @@ -1027,7 +1107,7 @@ WardenModuleManager::WardenModuleManager() { // Create cache directory if it doesn't exist std::filesystem::create_directories(cacheDirectory_); - std::cout << "[WardenModuleManager] Cache directory: " << cacheDirectory_ << '\n'; + LOG_INFO("WardenModuleManager: Cache directory: ", cacheDirectory_); } WardenModuleManager::~WardenModuleManager() { @@ -1064,12 +1144,11 @@ bool WardenModuleManager::receiveModuleChunk(const std::vector& md5Hash std::vector& buffer = downloadBuffer_[md5Hash]; buffer.insert(buffer.end(), chunkData.begin(), chunkData.end()); - std::cout << "[WardenModuleManager] Received chunk (" << chunkData.size() - << " bytes, total: " << buffer.size() << ")" << '\n'; + LOG_INFO("WardenModuleManager: Received chunk (", chunkData.size(), + " bytes, total: ", buffer.size(), ")"); if (isComplete) { - std::cout << "[WardenModuleManager] Module download complete (" - << buffer.size() << " bytes)" << '\n'; + LOG_INFO("WardenModuleManager: Module download complete (", buffer.size(), " bytes)"); // Cache to disk cacheModule(md5Hash, buffer); @@ -1089,14 +1168,14 @@ bool WardenModuleManager::cacheModule(const std::vector& md5Hash, std::ofstream file(cachePath, std::ios::binary); if (!file) { - std::cerr << "[WardenModuleManager] Failed to write cache: " << cachePath << '\n'; + LOG_ERROR("WardenModuleManager: Failed to write cache: ", cachePath); return false; } file.write(reinterpret_cast(moduleData.data()), moduleData.size()); file.close(); - std::cout << "[WardenModuleManager] Cached module to: " << cachePath << '\n'; + LOG_INFO("WardenModuleManager: Cached module to: ", cachePath); return true; } @@ -1120,7 +1199,7 @@ bool WardenModuleManager::loadCachedModule(const std::vector& md5Hash, file.read(reinterpret_cast(moduleDataOut.data()), fileSize); file.close(); - std::cout << "[WardenModuleManager] Loaded cached module (" << fileSize << " bytes)" << '\n'; + LOG_INFO("WardenModuleManager: Loaded cached module (", fileSize, " bytes)"); return true; } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 5a9a77ec..7a156f8e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -19,6 +19,35 @@ namespace { inline uint16_t bswap16(uint16_t v) { return static_cast(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8)); } + + bool hasFullPackedGuid(const wowee::network::Packet& packet) { + if (!packet.hasData()) { + return false; + } + + const auto& rawData = packet.getData(); + const uint8_t mask = rawData[packet.getReadPos()]; + size_t guidBytes = 1; + for (int bit = 0; bit < 8; ++bit) { + if ((mask & (1u << bit)) != 0) { + ++guidBytes; + } + } + return packet.hasRemaining(guidBytes); + } + + const char* updateTypeName(wowee::game::UpdateType type) { + using wowee::game::UpdateType; + switch (type) { + case UpdateType::VALUES: return "VALUES"; + case UpdateType::MOVEMENT: return "MOVEMENT"; + case UpdateType::CREATE_OBJECT: return "CREATE_OBJECT"; + case UpdateType::CREATE_OBJECT2: return "CREATE_OBJECT2"; + case UpdateType::OUT_OF_RANGE_OBJECTS: return "OUT_OF_RANGE_OBJECTS"; + case UpdateType::NEAR_OBJECTS: return "NEAR_OBJECTS"; + default: return "UNKNOWN"; + } + } } namespace wowee { @@ -171,15 +200,8 @@ network::Packet AuthSessionPacket::build(uint32_t build, LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes"); // Dump full packet for protocol debugging - const auto& data = packet.getData(); - std::string hexDump; - for (size_t i = 0; i < data.size(); ++i) { - char buf[4]; - snprintf(buf, sizeof(buf), "%02x ", data[i]); - hexDump += buf; - if ((i + 1) % 16 == 0) hexDump += "\n"; - } - LOG_DEBUG("CMSG_AUTH_SESSION full dump:\n", hexDump); + LOG_DEBUG("CMSG_AUTH_SESSION full dump:\n", + core::toHexString(packet.getData().data(), packet.getData().size(), true)); return packet; } @@ -220,33 +242,14 @@ std::vector AuthSessionPacket::computeAuthHash( hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end()); // Diagnostic: dump auth hash inputs for debugging AUTH_REJECT - { - auto toHex = [](const uint8_t* data, size_t len) { - std::string s; - for (size_t i = 0; i < len; ++i) { - char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf; - } - return s; - }; - LOG_DEBUG("AUTH HASH: account='", accountName, "' clientSeed=0x", std::hex, clientSeed, - " serverSeed=0x", serverSeed, std::dec); - LOG_DEBUG("AUTH HASH: sessionKey=", toHex(sessionKey.data(), sessionKey.size())); - LOG_DEBUG("AUTH HASH: input(", hashInput.size(), ")=", toHex(hashInput.data(), hashInput.size())); - } + LOG_DEBUG("AUTH HASH: account='", accountName, "' clientSeed=0x", std::hex, clientSeed, + " serverSeed=0x", serverSeed, std::dec); + LOG_DEBUG("AUTH HASH: sessionKey=", core::toHexString(sessionKey.data(), sessionKey.size())); + LOG_DEBUG("AUTH HASH: input(", hashInput.size(), ")=", core::toHexString(hashInput.data(), hashInput.size())); // Compute SHA1 hash auto result = auth::Crypto::sha1(hashInput); - - { - auto toHex = [](const uint8_t* data, size_t len) { - std::string s; - for (size_t i = 0; i < len; ++i) { - char buf[4]; snprintf(buf, sizeof(buf), "%02x", data[i]); s += buf; - } - return s; - }; - LOG_DEBUG("AUTH HASH: digest=", toHex(result.data(), result.size())); - } + LOG_DEBUG("AUTH HASH: digest=", core::toHexString(result.data(), result.size())); return result; } @@ -391,21 +394,15 @@ network::Packet CharCreatePacket::build(const CharCreateData& data) { " facial=", static_cast(data.facialHair)); // Dump full packet for protocol debugging - const auto& pktData = packet.getData(); - std::string hexDump; - for (size_t i = 0; i < pktData.size(); ++i) { - char buf[4]; - snprintf(buf, sizeof(buf), "%02x ", pktData[i]); - hexDump += buf; - } - LOG_DEBUG("CMSG_CHAR_CREATE full dump: ", hexDump); + LOG_DEBUG("CMSG_CHAR_CREATE full dump: ", + core::toHexString(packet.getData().data(), packet.getData().size(), true)); return packet; } bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponseData& data) { // Validate minimum packet size: result(1) - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { LOG_WARNING("SMSG_CHAR_CREATE: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -426,12 +423,12 @@ network::Packet CharEnumPacket::build() { bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) { // Upfront validation: count(1) + at least minimal character data - if (packet.getSize() - packet.getReadPos() < 1) return false; + if (!packet.hasRemaining(1)) return false; // Read character count uint8_t count = packet.readUInt8(); - LOG_INFO("Parsing SMSG_CHAR_ENUM: ", (int)count, " characters"); + LOG_INFO("Parsing SMSG_CHAR_ENUM: ", static_cast(count), " characters"); response.characters.clear(); response.characters.reserve(count); @@ -445,8 +442,8 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) // x(4) + y(4) + z(4) + guildId(4) + flags(4) + customization(4) + unknown(1) + // petDisplayModel(4) + petLevel(4) + petFamily(4) + 23items*(dispModel(4)+invType(1)+enchant(4)) = 207 bytes const size_t minCharacterSize = 8 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 1 + 4 + 4 + 4 + (23 * 9); - if (packet.getReadPos() + minCharacterSize > packet.getSize()) { - LOG_WARNING("CharEnumParser: truncated character at index ", (int)i); + if (!packet.hasRemaining(minCharacterSize)) { + LOG_WARNING("CharEnumParser: truncated character at index ", static_cast(i)); break; } @@ -454,27 +451,27 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) character.guid = packet.readUInt64(); // Read name (null-terminated string) - validate before reading - if (packet.getReadPos() >= packet.getSize()) { - LOG_WARNING("CharEnumParser: no bytes for name at index ", (int)i); + if (!packet.hasData()) { + LOG_WARNING("CharEnumParser: no bytes for name at index ", static_cast(i)); break; } character.name = packet.readString(); // Validate remaining bytes before reading fixed-size fields - if (packet.getReadPos() + 1 > packet.getSize()) { - LOG_WARNING("CharEnumParser: truncated before race/class/gender at index ", (int)i); + if (!packet.hasRemaining(1)) { + LOG_WARNING("CharEnumParser: truncated before race/class/gender at index ", static_cast(i)); character.race = Race::HUMAN; character.characterClass = Class::WARRIOR; character.gender = Gender::MALE; } else { // Read race, class, gender character.race = static_cast(packet.readUInt8()); - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { character.characterClass = Class::WARRIOR; character.gender = Gender::MALE; } else { character.characterClass = static_cast(packet.readUInt8()); - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { character.gender = Gender::MALE; } else { character.gender = static_cast(packet.readUInt8()); @@ -483,13 +480,13 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) } // Validate before reading appearance data - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { character.appearanceBytes = 0; character.facialFeatures = 0; } else { // Read appearance data character.appearanceBytes = packet.readUInt32(); - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { character.facialFeatures = 0; } else { character.facialFeatures = packet.readUInt8(); @@ -497,14 +494,14 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) } // Read level - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { character.level = 1; } else { character.level = packet.readUInt8(); } // Read location - if (packet.getReadPos() + 12 > packet.getSize()) { + if (!packet.hasRemaining(12)) { character.zoneId = 0; character.mapId = 0; character.x = 0.0f; @@ -519,25 +516,25 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) } // Read affiliations - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { character.guildId = 0; } else { character.guildId = packet.readUInt32(); } // Read flags - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { character.flags = 0; } else { character.flags = packet.readUInt32(); } // Skip customization flag (uint32) and unknown byte - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { // Customization missing, skip unknown } else { packet.readUInt32(); // Customization - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { // Unknown missing } else { packet.readUInt8(); // Unknown @@ -545,7 +542,7 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) } // Read pet data (always present, even if no pet) - if (packet.getReadPos() + 12 > packet.getSize()) { + if (!packet.hasRemaining(12)) { character.pet.displayModel = 0; character.pet.level = 0; character.pet.family = 0; @@ -558,7 +555,7 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) // Read equipment (23 items) character.equipment.reserve(23); for (int j = 0; j < 23; ++j) { - if (packet.getReadPos() + 9 > packet.getSize()) break; + if (!packet.hasRemaining(9)) break; EquipmentItem item; item.displayModel = packet.readUInt32(); item.inventoryType = packet.readUInt8(); @@ -566,9 +563,9 @@ bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) character.equipment.push_back(item); } - LOG_DEBUG(" Character ", (int)(i + 1), ": ", character.name, + LOG_DEBUG(" Character ", static_cast(i + 1), ": ", character.name, " (", getRaceName(character.race), " ", getClassName(character.characterClass), - " level ", (int)character.level, " zone ", character.zoneId, ")"); + " level ", static_cast(character.level), " zone ", character.zoneId, ")"); response.characters.push_back(character); } @@ -632,18 +629,18 @@ bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData data.serverTime = packet.readUInt32(); data.unknown = packet.readUInt8(); - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); uint32_t mask = 0xFF; if (remaining >= 4 && ((remaining - 4) % 4) == 0) { // Treat first dword as slot mask when payload shape matches. mask = packet.readUInt32(); } - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); size_t slotWords = std::min(8, remaining / 4); LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:"); LOG_DEBUG(" Server time: ", data.serverTime); - LOG_DEBUG(" Unknown: ", (int)data.unknown); + LOG_DEBUG(" Unknown: ", static_cast(data.unknown)); LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords); for (size_t i = 0; i < slotWords; ++i) { @@ -653,8 +650,8 @@ bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData } } if (packet.getReadPos() != packet.getSize()) { - LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getSize() - packet.getReadPos()); - packet.setReadPos(packet.getSize()); + LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getRemainingSize()); + packet.skipAll(); } return true; @@ -686,7 +683,7 @@ bool MotdParser::parse(network::Packet& packet, MotdData& data) { for (uint32_t i = 0; i < lineCount; ++i) { // Validate at least 1 byte available for the string - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { LOG_WARNING("MotdParser: truncated at line ", i + 1); break; } @@ -731,23 +728,6 @@ bool PongParser::parse(network::Packet& packet, PongData& data) { return true; } -void MovementPacket::writePackedGuid(network::Packet& packet, uint64_t guid) { - uint8_t mask = 0; - uint8_t guidBytes[8]; - int guidByteCount = 0; - for (int i = 0; i < 8; i++) { - uint8_t byte = static_cast((guid >> (i * 8)) & 0xFF); - if (byte != 0) { - mask |= (1 << i); - guidBytes[guidByteCount++] = byte; - } - } - packet.writeUInt8(mask); - for (int i = 0; i < guidByteCount; i++) { - packet.writeUInt8(guidBytes[i]); - } -} - void MovementPacket::writeMovementPayload(network::Packet& packet, const MovementInfo& info) { // Movement packet format (WoW 3.3.5a) payload: // uint32 flags @@ -764,37 +744,24 @@ void MovementPacket::writeMovementPayload(network::Packet& packet, const Movemen packet.writeUInt32(info.time); // Write position - packet.writeBytes(reinterpret_cast(&info.x), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.y), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.z), sizeof(float)); + packet.writeFloat(info.x); + packet.writeFloat(info.y); + packet.writeFloat(info.z); // Write orientation - packet.writeBytes(reinterpret_cast(&info.orientation), sizeof(float)); + packet.writeFloat(info.orientation); // Write transport data if on transport. // 3.3.5a ordering: transport block appears before pitch/fall/jump. if (info.hasFlag(MovementFlags::ONTRANSPORT)) { // Write packed transport GUID - uint8_t transMask = 0; - uint8_t transGuidBytes[8]; - int transGuidByteCount = 0; - for (int i = 0; i < 8; i++) { - uint8_t byte = static_cast((info.transportGuid >> (i * 8)) & 0xFF); - if (byte != 0) { - transMask |= (1 << i); - transGuidBytes[transGuidByteCount++] = byte; - } - } - packet.writeUInt8(transMask); - for (int i = 0; i < transGuidByteCount; i++) { - packet.writeUInt8(transGuidBytes[i]); - } + packet.writePackedGuid(info.transportGuid); // Write transport local position - packet.writeBytes(reinterpret_cast(&info.transportX), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportY), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportZ), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.transportO), sizeof(float)); + packet.writeFloat(info.transportX); + packet.writeFloat(info.transportY); + packet.writeFloat(info.transportZ); + packet.writeFloat(info.transportO); // Write transport time packet.writeUInt32(info.transportTime); @@ -803,14 +770,14 @@ 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); } } // Write pitch if swimming/flying if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) { - packet.writeBytes(reinterpret_cast(&info.pitch), sizeof(float)); + packet.writeFloat(info.pitch); } // Fall time is ALWAYS present in the packet (server reads it unconditionally). @@ -818,10 +785,10 @@ void MovementPacket::writeMovementPayload(network::Packet& packet, const Movemen packet.writeUInt32(info.fallTime); if (info.hasFlag(MovementFlags::FALLING)) { - packet.writeBytes(reinterpret_cast(&info.jumpVelocity), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpSinAngle), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpCosAngle), sizeof(float)); - packet.writeBytes(reinterpret_cast(&info.jumpXYSpeed), sizeof(float)); + packet.writeFloat(info.jumpVelocity); + packet.writeFloat(info.jumpSinAngle); + packet.writeFloat(info.jumpCosAngle); + packet.writeFloat(info.jumpXYSpeed); } } @@ -830,7 +797,7 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u // Movement packet format (WoW 3.3.5a): // packed GUID + movement payload - writePackedGuid(packet, playerGuid); + packet.writePackedGuid(playerGuid); writeMovementPayload(packet, info); // Detailed hex dump for debugging @@ -859,31 +826,14 @@ network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, u return packet; } -uint64_t UpdateObjectParser::readPackedGuid(network::Packet& packet) { - // Read packed GUID format: - // First byte is a mask indicating which bytes are present - uint8_t mask = packet.readUInt8(); - - if (mask == 0) { - return 0; - } - - uint64_t guid = 0; - for (int i = 0; i < 8; ++i) { - if (mask & (1 << i)) { - uint8_t byte = packet.readUInt8(); - guid |= (static_cast(byte) << (i * 8)); - } - } - - return guid; -} - bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { // WoW 3.3.5a UPDATE_OBJECT movement block structure: // 1. UpdateFlags (1 byte, sometimes 2) // 2. Movement data depends on update flags + auto rem = [&]() -> size_t { return packet.getRemainingSize(); }; + if (rem() < 2) return false; + // Update flags (3.3.5a uses 2 bytes for flags) uint16_t updateFlags = packet.readUInt16(); block.updateFlags = updateFlags; @@ -928,6 +878,9 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock const uint16_t UPDATEFLAG_HIGHGUID = 0x0010; if (updateFlags & UPDATEFLAG_LIVING) { + // Minimum: moveFlags(4)+moveFlags2(2)+time(4)+position(16)+fallTime(4)+speeds(36) = 66 + if (rem() < 66) return false; + // Full movement block for living units uint32_t moveFlags = packet.readUInt32(); uint16_t moveFlags2 = packet.readUInt16(); @@ -945,8 +898,10 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Transport data (if on transport) if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT + if (rem() < 1) return false; block.onTransport = true; - block.transportGuid = readPackedGuid(packet); + block.transportGuid = packet.readPackedGuid(); + if (rem() < 21) return false; // 4 floats + uint32 + uint8 block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); @@ -957,33 +912,38 @@ 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(); } // Fall time + if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING + if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); @@ -992,10 +952,12 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Spline elevation if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION + if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } - // Speeds (7 speed values) + // Speeds (9 values in WotLK: walk/run/runBack/swim/swimBack/flight/flightBack/turn/pitch) + if (rem() < 36) return false; /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); @@ -1011,7 +973,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Spline data if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED - auto bytesAvailable = [&](size_t n) -> bool { return packet.getReadPos() + n <= packet.getSize(); }; + auto bytesAvailable = [&](size_t n) -> bool { return packet.hasRemaining(n); }; if (!bytesAvailable(4)) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec); @@ -1029,86 +991,88 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock /*float finalAngle =*/ packet.readFloat(); } - // Legacy UPDATE_OBJECT spline layout used by many servers: - // timePassed, duration, splineId, durationMod, durationModNext, - // verticalAccel, effectStartTime, pointCount, points, splineMode, endPoint. - const size_t legacyStart = packet.getReadPos(); - if (!bytesAvailable(12 + 8 + 8 + 4)) return false; + // Spline data layout varies by expansion: + // Classic/Vanilla: timePassed(4)+duration(4)+splineId(4)+pointCount(4)+points+mode(1)+endPoint(12) + // WotLK: timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4) + // +[ANIMATION(5)]+[PARABOLIC(8)]+pointCount(4)+points+mode(1)+endPoint(12) + // Since the parser has no expansion context, auto-detect by trying Classic first. + if (!bytesAvailable(16)) return false; // minimum: 12 common + 4 pointCount /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); - /*float durationMod =*/ packet.readFloat(); - /*float durationModNext =*/ packet.readFloat(); - /*float verticalAccel =*/ packet.readFloat(); - /*uint32_t effectStartTime =*/ packet.readUInt32(); - uint32_t pointCount = packet.readUInt32(); + const size_t afterSplineId = packet.getReadPos(); - const size_t remainingAfterCount = packet.getSize() - packet.getReadPos(); - const bool legacyCountLooksValid = (pointCount <= 256); - const size_t legacyPointsBytes = static_cast(pointCount) * 12ull; - const bool legacyPayloadFits = (legacyPointsBytes + 13ull) <= remainingAfterCount; - - if (legacyCountLooksValid && legacyPayloadFits) { - for (uint32_t i = 0; i < pointCount; i++) { - /*float px =*/ packet.readFloat(); - /*float py =*/ packet.readFloat(); - /*float pz =*/ packet.readFloat(); - } - /*uint8_t splineMode =*/ packet.readUInt8(); - /*float endPointX =*/ packet.readFloat(); - /*float endPointY =*/ packet.readFloat(); - /*float endPointZ =*/ packet.readFloat(); - LOG_DEBUG(" Spline pointCount=", pointCount, " (legacy)"); - } else { - // Legacy pointCount looks invalid; try compact WotLK layout as recovery. - // This keeps malformed/variant packets from desyncing the whole update block. - packet.setReadPos(legacyStart); - const size_t afterFinalFacingPos = packet.getReadPos(); - if (splineFlags & 0x00400000) { // Animation - if (!bytesAvailable(5)) return false; - /*uint8_t animType =*/ packet.readUInt8(); - /*uint32_t animStart =*/ packet.readUInt32(); - } - if (!bytesAvailable(4)) return false; - /*uint32_t duration =*/ packet.readUInt32(); - if (splineFlags & 0x00000800) { // Parabolic - if (!bytesAvailable(8)) return false; - /*float verticalAccel =*/ packet.readFloat(); - /*uint32_t effectStartTime =*/ packet.readUInt32(); - } - if (!bytesAvailable(4)) return false; - const uint32_t compactPointCount = packet.readUInt32(); - if (compactPointCount > 16384) { - static uint32_t badSplineCount = 0; - ++badSplineCount; - if (badSplineCount <= 5 || (badSplineCount % 100) == 0) { - LOG_WARNING(" Spline pointCount=", pointCount, - " invalid (legacy+compact) at readPos=", - afterFinalFacingPos, "/", packet.getSize(), - ", occurrence=", badSplineCount); - } - return false; - } - const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0; - size_t compactPayloadBytes = 0; - if (compactPointCount > 0) { - if (uncompressed) { - compactPayloadBytes = static_cast(compactPointCount) * 12ull; + // Helper: parse spline points + splineMode + endPoint. + // WotLK uses compressed points by default (first=12 bytes, rest=4 bytes packed). + // Classic/Turtle uses all uncompressed (12 bytes each). + // The 'compressed' parameter selects which format. + auto tryParseSplinePoints = [&](bool compressed, const char* tag) -> bool { + if (!bytesAvailable(4)) return false; + size_t prePointCount = packet.getReadPos(); + uint32_t pc = packet.readUInt32(); + if (pc > 256) return false; + size_t pointsBytes; + if (compressed && pc > 0) { + // First point = 3 floats (12 bytes), rest = packed uint32 (4 bytes each) + pointsBytes = 12ull + (pc > 1 ? static_cast(pc - 1) * 4ull : 0ull); } else { - compactPayloadBytes = 12ull; - if (compactPointCount > 1) { - compactPayloadBytes += static_cast(compactPointCount - 1) * 4ull; + // All uncompressed: 3 floats each + pointsBytes = static_cast(pc) * 12ull; + } + size_t needed = pointsBytes + 13ull; // + splineMode(1) + endPoint(12) + if (!bytesAvailable(needed)) { + packet.setReadPos(prePointCount); + return false; + } + packet.setReadPos(packet.getReadPos() + pointsBytes); + uint8_t splineMode = packet.readUInt8(); + if (splineMode > 3) { + packet.setReadPos(prePointCount); + return false; + } + packet.readFloat(); packet.readFloat(); packet.readFloat(); // endPoint + LOG_DEBUG(" Spline pointCount=", pc, " compressed=", compressed, " (", tag, ")"); + return true; + }; + + // --- Try 1: Classic format (uncompressed points immediately after splineId) --- + bool splineParsed = tryParseSplinePoints(false, "classic"); + + // --- Try 2: WotLK format (durationMod+durationModNext+conditional+compressed points) --- + if (!splineParsed) { + packet.setReadPos(afterSplineId); + bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext + if (wotlkOk) { + /*float durationMod =*/ packet.readFloat(); + /*float durationModNext =*/ packet.readFloat(); + if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION + if (!bytesAvailable(5)) { wotlkOk = false; } + else { packet.readUInt8(); packet.readUInt32(); } + } + } + // AzerothCore/ChromieCraft always writes verticalAcceleration(float) + // + effectStartTime(uint32) unconditionally — NOT gated by PARABOLIC flag. + if (wotlkOk) { + if (!bytesAvailable(8)) { wotlkOk = false; } + else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); } + } + if (wotlkOk) { + // WotLK: compressed unless CYCLIC(0x80000) or ENTER_CYCLE(0x2000) set + bool useCompressed = (splineFlags & (0x00080000 | 0x00002000)) == 0; + splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed"); + // Fallback: try uncompressed WotLK if compressed didn't work + if (!splineParsed) { + splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed"); } } - if (!bytesAvailable(compactPayloadBytes)) return false; - packet.setReadPos(packet.getReadPos() + compactPayloadBytes); } - } // end else (compact fallback) } } else if (updateFlags & UPDATEFLAG_POSITION) { // Transport position update (UPDATEFLAG_POSITION = 0x0100) - uint64_t transportGuid = readPackedGuid(packet); + if (rem() < 1) return false; + uint64_t transportGuid = packet.readPackedGuid(); + if (rem() < 32) return false; // 8 floats block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -1137,7 +1101,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock } } else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) { - // Simple stationary position (4 floats) + if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); @@ -1149,32 +1113,38 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Target GUID (for units with target) if (updateFlags & UPDATEFLAG_HAS_TARGET) { - /*uint64_t targetGuid =*/ readPackedGuid(packet); + if (rem() < 1) return false; + /*uint64_t targetGuid =*/ packet.readPackedGuid(); } // Transport time if (updateFlags & UPDATEFLAG_TRANSPORT) { + if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } // Vehicle if (updateFlags & UPDATEFLAG_VEHICLE) { + if (rem() < 8) return false; /*uint32_t vehicleId =*/ packet.readUInt32(); /*float vehicleOrientation =*/ packet.readFloat(); } // Rotation (GameObjects) if (updateFlags & UPDATEFLAG_ROTATION) { + if (rem() < 8) return false; /*int64_t rotation =*/ packet.readUInt64(); } // Low GUID if (updateFlags & UPDATEFLAG_LOWGUID) { + if (rem() < 4) return false; /*uint32_t lowGuid =*/ packet.readUInt32(); } // High GUID if (updateFlags & UPDATEFLAG_HIGHGUID) { + if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } @@ -1184,6 +1154,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) { size_t startPos = packet.getReadPos(); + if (!packet.hasData()) return false; + // Read number of blocks (each block is 32 fields = 32 bits) uint8_t blockCount = packet.readUInt8(); @@ -1191,9 +1163,27 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& return true; // No fields to update } + // Sanity check: UNIT_END=148 needs 5 mask blocks, PLAYER_END=1472 needs 46. + // VALUES updates don't carry objectType (defaults to 0), so allow up to 55 + // for any VALUES update (could be a PLAYER). Only flag CREATE_OBJECT blocks + // with genuinely excessive block counts. + bool isCreateBlock = (block.updateType == UpdateType::CREATE_OBJECT || + block.updateType == UpdateType::CREATE_OBJECT2); + uint8_t maxExpectedBlocks = isCreateBlock + ? ((block.objectType == ObjectType::PLAYER) ? 55 : 10) + : 55; // VALUES: allow PLAYER-sized masks + if (blockCount > maxExpectedBlocks) { + LOG_WARNING("UpdateObjectParser: suspicious maskBlockCount=", static_cast(blockCount), + " for objectType=", static_cast(block.objectType), + " guid=0x", std::hex, block.guid, std::dec, + " updateFlags=0x", std::hex, block.updateFlags, std::dec, + " moveFlags=0x", std::hex, block.moveFlags, std::dec, + " readPos=", packet.getReadPos(), " size=", packet.getSize()); + } + uint32_t fieldsCapacity = blockCount * 32; LOG_DEBUG(" UPDATE MASK PARSE:"); - LOG_DEBUG(" maskBlockCount = ", (int)blockCount); + LOG_DEBUG(" maskBlockCount = ", static_cast(blockCount)); LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity); // Read update mask into a reused scratch buffer to avoid per-block allocations. @@ -1201,8 +1191,14 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& updateMask.resize(blockCount); for (int i = 0; i < blockCount; ++i) { // Validate 4 bytes available before each block read - if (packet.getReadPos() + 4 > packet.getSize()) { - LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i); + if (!packet.hasRemaining(4)) { + LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i, + " type=", updateTypeName(block.updateType), + " objectType=", static_cast(block.objectType), + " guid=0x", std::hex, block.guid, std::dec, + " readPos=", packet.getReadPos(), + " size=", packet.getSize(), + " maskBlockCount=", static_cast(blockCount)); return false; } updateMask[i] = packet.readUInt32(); @@ -1230,8 +1226,15 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& highestSetBit = fieldIndex; } // Validate 4 bytes available before reading field value - if (packet.getReadPos() + 4 > packet.getSize()) { - LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex); + if (!packet.hasRemaining(4)) { + LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex, + " type=", updateTypeName(block.updateType), + " objectType=", static_cast(block.objectType), + " guid=0x", std::hex, block.guid, std::dec, + " readPos=", packet.getReadPos(), + " size=", packet.getSize(), + " maskBlockIndex=", blockIdx, + " maskBlock=0x", std::hex, updateMask[blockIdx], std::dec); return false; } uint32_t value = packet.readUInt32(); @@ -1258,16 +1261,19 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& } bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) { + if (!packet.hasData()) return false; + // Read update type uint8_t updateTypeVal = packet.readUInt8(); block.updateType = static_cast(updateTypeVal); - LOG_DEBUG("Update block: type=", (int)updateTypeVal); + LOG_DEBUG("Update block: type=", static_cast(updateTypeVal)); switch (block.updateType) { case UpdateType::VALUES: { // Partial update - changed fields only - block.guid = readPackedGuid(packet); + if (!packet.hasData()) return false; + block.guid = packet.readPackedGuid(); LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec); return parseUpdateFields(packet, block); @@ -1275,7 +1281,8 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::MOVEMENT: { // Movement update - block.guid = readPackedGuid(packet); + if (!packet.hasRemaining(8)) return false; + block.guid = packet.readUInt64(); LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec); return parseMovementBlock(packet, block); @@ -1284,13 +1291,15 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::CREATE_OBJECT: case UpdateType::CREATE_OBJECT2: { // Create new object with full data - block.guid = readPackedGuid(packet); + if (!packet.hasData()) return false; + block.guid = packet.readPackedGuid(); LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec); // Read object type + if (!packet.hasData()) return false; uint8_t objectTypeVal = packet.readUInt8(); block.objectType = static_cast(objectTypeVal); - LOG_DEBUG(" Object type: ", (int)objectTypeVal); + LOG_DEBUG(" Object type: ", static_cast(objectTypeVal)); // Parse movement if present bool hasMovement = parseMovementBlock(packet, block); @@ -1315,14 +1324,16 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& } default: - LOG_WARNING("Unknown update type: ", (int)updateTypeVal); + LOG_WARNING("Unknown update type: ", static_cast(updateTypeVal)); return false; } } bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) { - constexpr uint32_t kMaxReasonableUpdateBlocks = 4096; - constexpr uint32_t kMaxReasonableOutOfRangeGuids = 16384; + // Keep worst-case packet parsing bounded. Extremely large counts are typically + // malformed/desynced and can stall a frame long enough to trigger disconnects. + constexpr uint32_t kMaxReasonableUpdateBlocks = 1024; + constexpr uint32_t kMaxReasonableOutOfRangeGuids = 4096; // Read block count data.blockCount = packet.readUInt32(); @@ -1336,11 +1347,18 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) LOG_DEBUG(" objectCount = ", data.blockCount); LOG_DEBUG(" packetSize = ", packet.getSize()); + uint32_t remainingBlockCount = data.blockCount; + // Check for out-of-range objects first - if (packet.getReadPos() + 1 <= packet.getSize()) { + if (packet.hasRemaining(1)) { uint8_t firstByte = packet.readUInt8(); if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { + if (remainingBlockCount == 0) { + LOG_ERROR("SMSG_UPDATE_OBJECT rejected: OUT_OF_RANGE_OBJECTS with zero blockCount"); + return false; + } + --remainingBlockCount; // Read out-of-range GUID count uint32_t count = packet.readUInt32(); if (count > kMaxReasonableOutOfRangeGuids) { @@ -1350,7 +1368,7 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) } for (uint32_t i = 0; i < count; ++i) { - uint64_t guid = readPackedGuid(packet); + uint64_t guid = packet.readPackedGuid(); data.outOfRangeGuids.push_back(guid); LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec); } @@ -1364,6 +1382,7 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) } // Parse update blocks + data.blockCount = remainingBlockCount; data.blocks.reserve(data.blockCount); for (uint32_t i = 0; i < data.blockCount; ++i) { @@ -1402,7 +1421,7 @@ bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data data.guid = packet.readUInt64(); // WotLK adds isDeath byte; vanilla/TBC packets are exactly 8 bytes - if (packet.getReadPos() < packet.getSize()) { + if (packet.hasData()) { data.isDeath = (packet.readUInt8() != 0); } else { data.isDeath = false; @@ -1475,6 +1494,47 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { // Read unknown field packet.readUInt32(); + auto tryReadSizedCString = [&](std::string& out, uint32_t maxLen, size_t minTrailingBytes) -> bool { + size_t start = packet.getReadPos(); + size_t remaining = packet.getSize() - start; + if (remaining < 4 + minTrailingBytes) return false; + + uint32_t len = packet.readUInt32(); + if (len < 2 || len > maxLen) { + packet.setReadPos(start); + return false; + } + if (!packet.hasRemaining(static_cast(len) + minTrailingBytes)) { + packet.setReadPos(start); + return false; + } + + std::string tmp; + tmp.resize(len); + for (uint32_t i = 0; i < len; ++i) { + tmp[i] = static_cast(packet.readUInt8()); + } + if (tmp.empty() || tmp.back() != '\0') { + packet.setReadPos(start); + return false; + } + tmp.pop_back(); + if (tmp.empty()) { + packet.setReadPos(start); + return false; + } + for (char c : tmp) { + unsigned char uc = static_cast(c); + if (uc < 32 || uc > 126) { + packet.setReadPos(start); + return false; + } + } + + out = std::move(tmp); + return true; + }; + // Type-specific data // WoW 3.3.5 SMSG_MESSAGECHAT format: after senderGuid+unk, most types // have a receiverGuid (uint64). Some types have extra fields before it. @@ -1530,6 +1590,27 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { break; } + case ChatType::WHISPER: + case ChatType::WHISPER_INFORM: { + // Some cores include an explicit sized sender/receiver name for whisper chat. + // Consume it when present so /r has a reliable last whisper sender. + if (data.type == ChatType::WHISPER) { + tryReadSizedCString(data.senderName, 128, 8 + 4 + 1); + } else { + tryReadSizedCString(data.receiverName, 128, 8 + 4 + 1); + } + + data.receiverGuid = packet.readUInt64(); + + // Optional trailing whisper target/source name on some formats. + if (data.type == ChatType::WHISPER && data.receiverName.empty()) { + tryReadSizedCString(data.receiverName, 128, 4 + 1); + } else if (data.type == ChatType::WHISPER_INFORM && data.senderName.empty()) { + tryReadSizedCString(data.senderName, 128, 4 + 1); + } + break; + } + case ChatType::BG_SYSTEM_NEUTRAL: case ChatType::BG_SYSTEM_ALLIANCE: case ChatType::BG_SYSTEM_HORDE: @@ -1574,7 +1655,7 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { LOG_DEBUG(" Channel: ", data.channelName); } LOG_DEBUG(" Message: ", data.message); - LOG_DEBUG(" Chat tag: 0x", std::hex, (int)data.chatTag, std::dec); + LOG_DEBUG(" Chat tag: 0x", std::hex, static_cast(data.chatTag), std::dec); return true; } @@ -1630,7 +1711,7 @@ network::Packet TextEmotePacket::build(uint32_t textEmoteId, uint64_t targetGuid } bool TextEmoteParser::parse(network::Packet& packet, TextEmoteData& data, bool legacyFormat) { - size_t bytesLeft = packet.getSize() - packet.getReadPos(); + size_t bytesLeft = packet.getRemainingSize(); if (bytesLeft < 20) { LOG_WARNING("SMSG_TEXT_EMOTE too short: ", bytesLeft, " bytes"); return false; @@ -1682,7 +1763,7 @@ network::Packet LeaveChannelPacket::build(const std::string& channelName) { } bool ChannelNotifyParser::parse(network::Packet& packet, ChannelNotifyData& data) { - size_t bytesLeft = packet.getSize() - packet.getReadPos(); + size_t bytesLeft = packet.getRemainingSize(); if (bytesLeft < 2) { LOG_WARNING("SMSG_CHANNEL_NOTIFY too short"); return false; @@ -1690,7 +1771,7 @@ bool ChannelNotifyParser::parse(network::Packet& packet, ChannelNotifyData& data data.notifyType = static_cast(packet.readUInt8()); data.channelName = packet.readString(); // Some notification types have additional fields (guid, etc.) - bytesLeft = packet.getSize() - packet.getReadPos(); + bytesLeft = packet.getRemainingSize(); if (bytesLeft >= 8) { data.senderGuid = packet.readUInt64(); } @@ -1722,6 +1803,15 @@ network::Packet InspectPacket::build(uint64_t targetGuid) { return packet; } +network::Packet QueryInspectAchievementsPacket::build(uint64_t targetGuid) { + // CMSG_QUERY_INSPECT_ACHIEVEMENTS: uint64 targetGuid + uint8 unk (always 0) + network::Packet packet(wireOpcode(Opcode::CMSG_QUERY_INSPECT_ACHIEVEMENTS)); + packet.writeUInt64(targetGuid); + packet.writeUInt8(0); // unk / achievementSlot — always 0 for WotLK + LOG_DEBUG("Built CMSG_QUERY_INSPECT_ACHIEVEMENTS: target=0x", std::hex, targetGuid, std::dec); + return packet; +} + // ============================================================ // Server Info Commands // ============================================================ @@ -1734,7 +1824,7 @@ network::Packet QueryTimePacket::build() { bool QueryTimeResponseParser::parse(network::Packet& packet, QueryTimeResponseData& data) { // Validate minimum packet size: serverTime(4) + timeOffset(4) - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("SMSG_QUERY_TIME_RESPONSE: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -1753,15 +1843,16 @@ network::Packet RequestPlayedTimePacket::build(bool sendToChat) { } bool PlayedTimeParser::parse(network::Packet& packet, PlayedTimeData& data) { - // Validate minimum packet size: totalTime(4) + levelTime(4) + triggerMsg(1) - if (packet.getSize() - packet.getReadPos() < 9) { + // Classic/Turtle may omit the trailing trigger-message byte and send only + // totalTime(4) + levelTime(4). Later expansions append triggerMsg(1). + if (!packet.hasRemaining(8)) { LOG_WARNING("SMSG_PLAYED_TIME: packet too small (", packet.getSize(), " bytes)"); return false; } data.totalTimePlayed = packet.readUInt32(); data.levelTimePlayed = packet.readUInt32(); - data.triggerMessage = packet.readUInt8() != 0; + data.triggerMessage = (packet.hasRemaining(1)) && (packet.readUInt8() != 0); LOG_DEBUG("Parsed SMSG_PLAYED_TIME: total=", data.totalTimePlayed, " level=", data.levelTimePlayed); return true; } @@ -1815,7 +1906,7 @@ network::Packet SetContactNotesPacket::build(uint64_t friendGuid, const std::str bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data) { // Validate minimum packet size: status(1) + guid(8) - if (packet.getSize() - packet.getReadPos() < 9) { + if (!packet.hasRemaining(9)) { LOG_WARNING("SMSG_FRIEND_STATUS: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -1824,14 +1915,14 @@ bool FriendStatusParser::parse(network::Packet& packet, FriendStatusData& data) data.guid = packet.readUInt64(); if (data.status == 1) { // Online // Conditional: note (string) + chatFlag (1) - if (packet.getReadPos() < packet.getSize()) { + if (packet.hasData()) { data.note = packet.readString(); - if (packet.getReadPos() + 1 <= packet.getSize()) { + if (packet.hasRemaining(1)) { data.chatFlag = packet.readUInt8(); } } } - LOG_DEBUG("Parsed SMSG_FRIEND_STATUS: status=", (int)data.status, " guid=0x", std::hex, data.guid, std::dec); + LOG_DEBUG("Parsed SMSG_FRIEND_STATUS: status=", static_cast(data.status), " guid=0x", std::hex, data.guid, std::dec); return true; } @@ -1867,14 +1958,14 @@ network::Packet LogoutCancelPacket::build() { bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& data) { // Validate minimum packet size: result(4) + instant(1) - if (packet.getSize() - packet.getReadPos() < 5) { + if (!packet.hasRemaining(5)) { LOG_WARNING("SMSG_LOGOUT_RESPONSE: packet too small (", packet.getSize(), " bytes)"); return false; } data.result = packet.readUInt32(); data.instant = packet.readUInt8(); - LOG_DEBUG("Parsed SMSG_LOGOUT_RESPONSE: result=", data.result, " instant=", (int)data.instant); + LOG_DEBUG("Parsed SMSG_LOGOUT_RESPONSE: result=", data.result, " instant=", static_cast(data.instant)); return true; } @@ -1885,7 +1976,43 @@ bool LogoutResponseParser::parse(network::Packet& packet, LogoutResponseData& da network::Packet StandStateChangePacket::build(uint8_t state) { network::Packet packet(wireOpcode(Opcode::CMSG_STANDSTATECHANGE)); packet.writeUInt32(state); - LOG_DEBUG("Built CMSG_STANDSTATECHANGE: state=", (int)state); + LOG_DEBUG("Built CMSG_STANDSTATECHANGE: state=", static_cast(state)); + return packet; +} + +// ============================================================ +// Action Bar +// ============================================================ + +network::Packet SetActionButtonPacket::build(uint8_t button, uint8_t type, uint32_t id, bool isClassic) { + // Classic/Turtle (1.12): uint8 button + uint16 id + uint8 type + uint8 misc(0) + // type encoding: 0=spell, 1=item, 64=macro + // TBC/WotLK: uint8 button + uint32 packed (type<<24 | id) + // type encoding: 0x00=spell, 0x80=item, 0x40=macro + // packed=0 means clear the slot + network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTION_BUTTON)); + packet.writeUInt8(button); + if (isClassic) { + // Classic: 16-bit id, 8-bit type code, 8-bit misc + // Map ActionBarSlot::Type (0=EMPTY,1=SPELL,2=ITEM,3=MACRO) → classic type byte + uint8_t classicType = 0; // 0 = spell + if (type == 2 /* ITEM */) classicType = 1; + if (type == 3 /* MACRO */) classicType = 64; + packet.writeUInt16(static_cast(id)); + packet.writeUInt8(classicType); + packet.writeUInt8(0); // misc + LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (Classic): button=", static_cast(button), + " id=", id, " type=", static_cast(classicType)); + } else { + // TBC/WotLK: type in bits 24–31, id in bits 0–23; packed=0 clears slot + uint8_t packedType = 0x00; // spell + if (type == 2 /* ITEM */) packedType = 0x80; + if (type == 3 /* MACRO */) packedType = 0x40; + uint32_t packed = (id == 0) ? 0 : (static_cast(packedType) << 24) | (id & 0x00FFFFFF); + packet.writeUInt32(packed); + LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (TBC/WotLK): button=", static_cast(button), + " packed=0x", std::hex, packed, std::dec); + } return packet; } @@ -2082,7 +2209,7 @@ bool PetitionShowlistParser::parse(network::Packet& packet, PetitionShowlistData data.displayId = packet.readUInt32(); data.cost = packet.readUInt32(); // Skip unused fields if present - if ((packet.getSize() - packet.getReadPos()) >= 8) { + if (packet.hasRemaining(8)) { data.charterType = packet.readUInt32(); data.requiredSigs = packet.readUInt32(); } @@ -2109,7 +2236,7 @@ bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponse data.guildId = packet.readUInt32(); // Validate before reading guild name - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { LOG_WARNING("GuildQueryResponseParser: truncated before guild name"); data.guildName.clear(); return true; @@ -2118,7 +2245,7 @@ bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponse // Read 10 rank names with validation for (int i = 0; i < 10; ++i) { - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { LOG_WARNING("GuildQueryResponseParser: truncated at rank name ", i); data.rankNames[i].clear(); } else { @@ -2127,7 +2254,7 @@ bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponse } // Validate before reading emblem fields (5 uint32s = 20 bytes) - if (packet.getReadPos() + 20 > packet.getSize()) { + if (!packet.hasRemaining(20)) { LOG_WARNING("GuildQueryResponseParser: truncated before emblem data"); data.emblemStyle = 0; data.emblemColor = 0; @@ -2143,7 +2270,7 @@ bool GuildQueryResponseParser::parse(network::Packet& packet, GuildQueryResponse data.borderColor = packet.readUInt32(); data.backgroundColor = packet.readUInt32(); - if ((packet.getSize() - packet.getReadPos()) >= 4) { + if (packet.hasRemaining(4)) { data.rankCount = packet.readUInt32(); } LOG_INFO("Parsed SMSG_GUILD_QUERY_RESPONSE: guild=", data.guildName, " id=", data.guildId); @@ -2182,7 +2309,7 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { data.motd = packet.readString(); data.guildInfo = packet.readString(); - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("GuildRosterParser: truncated before rankCount"); data.ranks.clear(); data.members.clear(); @@ -2201,19 +2328,19 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { data.ranks.resize(rankCount); for (uint32_t i = 0; i < rankCount; ++i) { // Validate 4 bytes before each rank rights read - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("GuildRosterParser: truncated rank at index ", i); break; } data.ranks[i].rights = packet.readUInt32(); - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { data.ranks[i].goldLimit = 0; } else { data.ranks[i].goldLimit = packet.readUInt32(); } // 6 bank tab flags + 6 bank tab items per day for (int t = 0; t < 6; ++t) { - if (packet.getReadPos() + 8 > packet.getSize()) break; + if (!packet.hasRemaining(8)) break; packet.readUInt32(); // tabFlags packet.readUInt32(); // tabItemsPerDay } @@ -2222,7 +2349,7 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { data.members.resize(numMembers); for (uint32_t i = 0; i < numMembers; ++i) { // Validate minimum bytes before reading member (guid+online+name at minimum is 9+ bytes) - if (packet.getReadPos() + 9 > packet.getSize()) { + if (!packet.hasRemaining(9)) { LOG_WARNING("GuildRosterParser: truncated member at index ", i); break; } @@ -2231,14 +2358,14 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { m.online = (packet.readUInt8() != 0); // Validate before reading name string - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { m.name.clear(); } else { m.name = packet.readString(); } // Validate before reading rank/level/class/gender/zone - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { m.rankIndex = 0; m.level = 1; m.classId = 0; @@ -2246,7 +2373,7 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { m.zoneId = 0; } else { m.rankIndex = packet.readUInt32(); - if (packet.getReadPos() + 3 > packet.getSize()) { + if (!packet.hasRemaining(3)) { m.level = 1; m.classId = 0; m.gender = 0; @@ -2255,7 +2382,7 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { m.classId = packet.readUInt8(); m.gender = packet.readUInt8(); } - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { m.zoneId = 0; } else { m.zoneId = packet.readUInt32(); @@ -2264,7 +2391,7 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { // Online status affects next fields if (!m.online) { - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { m.lastOnline = 0.0f; } else { m.lastOnline = packet.readFloat(); @@ -2272,12 +2399,12 @@ bool GuildRosterParser::parse(network::Packet& packet, GuildRosterData& data) { } // Read notes - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { m.publicNote.clear(); m.officerNote.clear(); } else { m.publicNote = packet.readString(); - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { m.officerNote.clear(); } else { m.officerNote = packet.readString(); @@ -2298,10 +2425,10 @@ bool GuildEventParser::parse(network::Packet& packet, GuildEventData& data) { for (uint8_t i = 0; i < data.numStrings && i < 3; ++i) { data.strings[i] = packet.readString(); } - if ((packet.getSize() - packet.getReadPos()) >= 8) { + if (packet.hasRemaining(8)) { data.guid = packet.readUInt64(); } - LOG_INFO("Parsed SMSG_GUILD_EVENT: type=", (int)data.eventType, " strings=", (int)data.numStrings); + LOG_INFO("Parsed SMSG_GUILD_EVENT: type=", static_cast(data.eventType), " strings=", static_cast(data.numStrings)); return true; } @@ -2382,7 +2509,7 @@ network::Packet RaidTargetUpdatePacket::build(uint8_t targetIndex, uint64_t targ network::Packet packet(wireOpcode(Opcode::MSG_RAID_TARGET_UPDATE)); packet.writeUInt8(targetIndex); packet.writeUInt64(targetGuid); - LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", (uint32_t)targetIndex, ", guid: 0x", std::hex, targetGuid, std::dec); + LOG_DEBUG("Built MSG_RAID_TARGET_UPDATE, index: ", static_cast(targetIndex), ", guid: 0x", std::hex, targetGuid, std::dec); return packet; } @@ -2427,14 +2554,14 @@ network::Packet SetTradeItemPacket::build(uint8_t tradeSlot, uint8_t bag, uint8_ packet.writeUInt8(tradeSlot); packet.writeUInt8(bag); packet.writeUInt8(bagSlot); - LOG_DEBUG("Built CMSG_SET_TRADE_ITEM slot=", (int)tradeSlot, " bag=", (int)bag, " bagSlot=", (int)bagSlot); + LOG_DEBUG("Built CMSG_SET_TRADE_ITEM slot=", static_cast(tradeSlot), " bag=", static_cast(bag), " bagSlot=", static_cast(bagSlot)); return packet; } network::Packet ClearTradeItemPacket::build(uint8_t tradeSlot) { network::Packet packet(wireOpcode(Opcode::CMSG_CLEAR_TRADE_ITEM)); packet.writeUInt8(tradeSlot); - LOG_DEBUG("Built CMSG_CLEAR_TRADE_ITEM slot=", (int)tradeSlot); + LOG_DEBUG("Built CMSG_CLEAR_TRADE_ITEM slot=", static_cast(tradeSlot)); return packet; } @@ -2493,7 +2620,7 @@ network::Packet RandomRollPacket::build(uint32_t minRoll, uint32_t maxRoll) { bool RandomRollParser::parse(network::Packet& packet, RandomRollData& data) { // Validate minimum packet size: rollerGuid(8) + targetGuid(8) + minRoll(4) + maxRoll(4) + result(4) - if (packet.getSize() - packet.getReadPos() < 28) { + if (!packet.hasRemaining(28)) { LOG_WARNING("SMSG_RANDOM_ROLL: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -2519,13 +2646,13 @@ bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseDa // 3.3.5a: packedGuid, uint8 found // If found==0: CString name, CString realmName, uint8 race, uint8 gender, uint8 classId // Validation: packed GUID (1-8 bytes) + found flag (1 byte minimum) - if (packet.getSize() - packet.getReadPos() < 2) return false; // At least 1 for packed GUID + 1 for found + if (!packet.hasRemaining(2)) return false; // At least 1 for packed GUID + 1 for found size_t startPos = packet.getReadPos(); - data.guid = UpdateObjectParser::readPackedGuid(packet); + data.guid = packet.readPackedGuid(); // Validate found flag read - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { packet.setReadPos(startPos); return false; } @@ -2537,7 +2664,7 @@ bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseDa } // Validate strings: need at least 2 null terminators for empty strings - if (packet.getSize() - packet.getReadPos() < 2) { + if (!packet.hasRemaining(2)) { data.name.clear(); data.realmName.clear(); return !data.name.empty(); // Fail if name was required @@ -2547,7 +2674,7 @@ bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseDa data.realmName = packet.readString(); // Validate final 3 uint8 fields (race, gender, classId) - if (packet.getSize() - packet.getReadPos() < 3) { + if (!packet.hasRemaining(3)) { LOG_WARNING("Name query: truncated fields after realmName, expected 3 uint8s"); data.race = 0; data.gender = 0; @@ -2559,8 +2686,8 @@ bool NameQueryResponseParser::parse(network::Packet& packet, NameQueryResponseDa data.gender = packet.readUInt8(); data.classId = packet.readUInt8(); - LOG_DEBUG("Name query response: ", data.name, " (race=", (int)data.race, - " class=", (int)data.classId, ")"); + LOG_DEBUG("Name query response: ", data.name, " (race=", static_cast(data.race), + " class=", static_cast(data.classId), ")"); return true; } @@ -2599,7 +2726,7 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe // WotLK: 4 fixed fields after iconName (typeFlags, creatureType, family, rank) // Validate minimum size for these fields: 4×4 = 16 bytes - if (packet.getSize() - packet.getReadPos() < 16) { + if (!packet.hasRemaining(16)) { LOG_WARNING("SMSG_CREATURE_QUERY_RESPONSE: truncated before typeFlags (entry=", data.entry, ")"); data.typeFlags = 0; data.creatureType = 0; @@ -2649,7 +2776,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue } // Validate minimum size for fixed fields: type(4) + displayId(4) - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_ERROR("SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")"); return false; } @@ -2669,7 +2796,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue packet.readString(); // unk1 // Read 24 type-specific data fields - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining >= 24 * 4) { for (int i = 0; i < 24; i++) { data.data[i] = packet.readUInt32(); @@ -2699,10 +2826,10 @@ network::Packet PageTextQueryPacket::build(uint32_t pageId, uint64_t guid) { } bool PageTextQueryResponseParser::parse(network::Packet& packet, PageTextQueryResponseData& data) { - if (packet.getSize() - packet.getReadPos() < 4) return false; + if (!packet.hasRemaining(4)) return false; data.pageId = packet.readUInt32(); data.text = normalizeWowTextTokens(packet.readString()); - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasRemaining(4)) { data.nextPageId = packet.readUInt32(); } else { data.nextPageId = 0; @@ -2720,7 +2847,7 @@ network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) { return packet; } -static const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass) { +const char* getItemSubclassName(uint32_t itemClass, uint32_t subClass) { if (itemClass == 2) { // Weapon switch (subClass) { case 0: return "Axe"; case 1: return "Axe"; @@ -2764,7 +2891,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa // Validate minimum size for fixed fields before reading: itemClass(4) + subClass(4) + soundOverride(4) // + 4 name strings + displayInfoId(4) + quality(4) = at least 24 bytes more - if (packet.getSize() - packet.getReadPos() < 24) { + if (!packet.hasRemaining(24)) { LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before displayInfoId (entry=", data.entry, ")"); return false; } @@ -2790,11 +2917,11 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa // Some server variants omit BuyCount (4 fields instead of 5). // Read 5 fields and validate InventoryType; if it looks implausible, rewind and try 4. const size_t postQualityPos = packet.getReadPos(); - if (packet.getSize() - packet.getReadPos() < 24) { + if (!packet.hasRemaining(24)) { LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before flags (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // Flags + data.itemFlags = packet.readUInt32(); // Flags packet.readUInt32(); // Flags2 packet.readUInt32(); // BuyCount packet.readUInt32(); // BuyPrice @@ -2804,7 +2931,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa if (data.inventoryType > 28) { // inventoryType out of range — BuyCount probably not present; rewind and try 4 fields packet.setReadPos(postQualityPos); - packet.readUInt32(); // Flags + data.itemFlags = packet.readUInt32(); // Flags packet.readUInt32(); // Flags2 packet.readUInt32(); // BuyPrice data.sellPrice = packet.readUInt32(); // SellPrice @@ -2812,27 +2939,27 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa } // Validate minimum size for remaining fixed fields before inventoryType through containerSlots: 13×4 = 52 bytes - if (packet.getSize() - packet.getReadPos() < 52) { + if (!packet.hasRemaining(52)) { LOG_ERROR("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before statsCount (entry=", data.entry, ")"); return false; } - packet.readUInt32(); // AllowableClass - packet.readUInt32(); // AllowableRace + data.allowableClass = packet.readUInt32(); // AllowableClass + data.allowableRace = packet.readUInt32(); // AllowableRace data.itemLevel = packet.readUInt32(); data.requiredLevel = packet.readUInt32(); - packet.readUInt32(); // RequiredSkill - packet.readUInt32(); // RequiredSkillRank + data.requiredSkill = packet.readUInt32(); // RequiredSkill + data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank packet.readUInt32(); // RequiredSpell packet.readUInt32(); // RequiredHonorRank packet.readUInt32(); // RequiredCityRank - packet.readUInt32(); // RequiredReputationFaction - packet.readUInt32(); // RequiredReputationRank - packet.readUInt32(); // MaxCount + data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction + data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank + data.maxCount = static_cast(packet.readUInt32()); // MaxCount (1 = Unique) data.maxStack = static_cast(packet.readUInt32()); // Stackable data.containerSlots = packet.readUInt32(); // Read statsCount with bounds validation - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated at statsCount (entry=", data.entry, ")"); return true; // Have enough for core fields; stats are optional } @@ -2850,7 +2977,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa uint32_t statsToRead = std::min(statsCount, 10u); for (uint32_t i = 0; i < statsToRead; i++) { // Each stat is 2 uint32s (type + value) = 8 bytes - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")"); break; } @@ -2870,7 +2997,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa } // ScalingStatDistribution and ScalingStatValue - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before scaling stats (entry=", data.entry, ")"); return true; // Have core fields; scaling is optional } @@ -2893,19 +3020,19 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa } data.armor = static_cast(packet.readUInt32()); - packet.readUInt32(); // HolyRes - packet.readUInt32(); // FireRes - packet.readUInt32(); // NatureRes - packet.readUInt32(); // FrostRes - packet.readUInt32(); // ShadowRes - packet.readUInt32(); // ArcaneRes + data.holyRes = static_cast(packet.readUInt32()); // HolyRes + data.fireRes = static_cast(packet.readUInt32()); // FireRes + data.natureRes = static_cast(packet.readUInt32()); // NatureRes + data.frostRes = static_cast(packet.readUInt32()); // FrostRes + data.shadowRes = static_cast(packet.readUInt32()); // ShadowRes + data.arcaneRes = static_cast(packet.readUInt32()); // ArcaneRes data.delayMs = packet.readUInt32(); packet.readUInt32(); // AmmoType packet.readFloat(); // RangedModRange // 5 item spells: SpellId, SpellTrigger, SpellCharges, SpellCooldown, SpellCategory, SpellCategoryCooldown for (int i = 0; i < 5; i++) { - if (packet.getReadPos() + 24 > packet.getSize()) break; + if (!packet.hasRemaining(24)) break; data.spells[i].spellId = packet.readUInt32(); data.spells[i].spellTrigger = packet.readUInt32(); packet.readUInt32(); // SpellCharges @@ -2915,21 +3042,44 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa } // Bonding type (0=none, 1=BoP, 2=BoE, 3=BoU, 4=BoQ) - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.bindType = packet.readUInt32(); // Flavor/lore text (Description cstring) - if (packet.getReadPos() < packet.getSize()) + if (packet.hasData()) data.description = packet.readString(); // Post-description fields: PageText, LanguageID, PageMaterial, StartQuest - if (packet.getReadPos() + 16 <= packet.getSize()) { + if (packet.hasRemaining(16)) { packet.readUInt32(); // PageText packet.readUInt32(); // LanguageID packet.readUInt32(); // PageMaterial data.startQuestId = packet.readUInt32(); // StartQuest } + // WotLK 3.3.5a: additional fields after StartQuest (read up to socket data) + // LockID(4), Material(4), Sheath(4), RandomProperty(4), RandomSuffix(4), + // Block(4), ItemSet(4), MaxDurability(4), Area(4), Map(4), BagFamily(4), + // TotemCategory(4) = 48 bytes before sockets + constexpr size_t kPreSocketSkip = 48; + if (packet.getReadPos() + kPreSocketSkip + 28 <= packet.getSize()) { + // LockID(0), Material(1), Sheath(2), RandomProperty(3), RandomSuffix(4), Block(5) + for (size_t i = 0; i < 6; ++i) packet.readUInt32(); + data.itemSetId = packet.readUInt32(); // ItemSet(6) + // MaxDurability(7), Area(8), Map(9), BagFamily(10), TotemCategory(11) + for (size_t i = 0; i < 5; ++i) packet.readUInt32(); + // 3 socket slots: socketColor (4 bytes each) + data.socketColor[0] = packet.readUInt32(); + data.socketColor[1] = packet.readUInt32(); + data.socketColor[2] = packet.readUInt32(); + // 3 socket content (gem enchantment IDs — skip, not currently displayed) + packet.readUInt32(); + packet.readUInt32(); + packet.readUInt32(); + // socketBonus (enchantmentId) + data.socketBonus = packet.readUInt32(); + } + data.valid = !data.name.empty(); return true; } @@ -2940,25 +3090,25 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { // PackedGuid - data.guid = UpdateObjectParser::readPackedGuid(packet); + data.guid = packet.readPackedGuid(); if (data.guid == 0) return false; // uint8 unk (toggle for MOVEMENTFLAG2_UNK7) - if (packet.getReadPos() >= packet.getSize()) return false; + if (!packet.hasData()) return false; packet.readUInt8(); // Current position (server coords: float x, y, z) - if (packet.getReadPos() + 12 > packet.getSize()) return false; + if (!packet.hasRemaining(12)) return false; data.x = packet.readFloat(); data.y = packet.readFloat(); data.z = packet.readFloat(); // uint32 splineId - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; packet.readUInt32(); // uint8 moveType - if (packet.getReadPos() >= packet.getSize()) return false; + if (!packet.hasData()) return false; data.moveType = packet.readUInt8(); if (data.moveType == 1) { @@ -2973,20 +3123,20 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { // Read facing data based on move type if (data.moveType == 2) { // FacingSpot: float x, y, z - if (packet.getReadPos() + 12 > packet.getSize()) return false; + if (!packet.hasRemaining(12)) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } else if (data.moveType == 3) { // FacingTarget: uint64 guid - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; data.facingTarget = packet.readUInt64(); } else if (data.moveType == 4) { // FacingAngle: float angle - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.facingAngle = packet.readFloat(); } // uint32 splineFlags - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.splineFlags = packet.readUInt32(); // WotLK 3.3.5a SplineFlags (from TrinityCore/MaNGOS MoveSplineFlag.h): @@ -2997,34 +3147,33 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { // [if Animation] uint8 animationType + int32 effectStartTime (5 bytes) if (data.splineFlags & 0x00400000) { - if (packet.getReadPos() + 5 > packet.getSize()) return false; + if (!packet.hasRemaining(5)) return false; packet.readUInt8(); // animationType packet.readUInt32(); // effectStartTime (int32, read as uint32 same size) } // uint32 duration - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.duration = packet.readUInt32(); // [if Parabolic] float verticalAcceleration + int32 effectStartTime (8 bytes) if (data.splineFlags & 0x00000800) { - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; packet.readFloat(); // verticalAcceleration packet.readUInt32(); // effectStartTime } // uint32 pointCount - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; uint32_t pointCount = packet.readUInt32(); if (pointCount == 0) return true; - // Cap pointCount to prevent excessive iteration from malformed packets constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, "), capping"); - pointCount = kMaxSplinePoints; + " (guid=0x", std::hex, data.guid, std::dec, ")"); + return false; } // Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed). @@ -3035,17 +3184,17 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { // Read last point as destination // Skip to last point: each point is 12 bytes for (uint32_t i = 0; i < pointCount - 1; i++) { - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (!packet.hasRemaining(12)) return true; packet.readFloat(); packet.readFloat(); packet.readFloat(); } - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (!packet.hasRemaining(12)) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); data.hasDest = true; } else { // Compressed: first 3 floats are the destination (final point) - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (!packet.hasRemaining(12)) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -3053,17 +3202,17 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { } LOG_DEBUG("MonsterMove: guid=0x", std::hex, data.guid, std::dec, - " type=", (int)data.moveType, " dur=", data.duration, "ms", + " type=", static_cast(data.moveType), " dur=", data.duration, "ms", " dest=(", data.destX, ",", data.destY, ",", data.destZ, ")"); return true; } bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& data) { - data.guid = UpdateObjectParser::readPackedGuid(packet); + data.guid = packet.readPackedGuid(); if (data.guid == 0) return false; - if (packet.getReadPos() + 12 > packet.getSize()) return false; + if (!packet.hasRemaining(12)) return false; data.x = packet.readFloat(); data.y = packet.readFloat(); data.z = packet.readFloat(); @@ -3079,10 +3228,10 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d // uint32 pointCount // float[3] dest // uint32 packedPoints[pointCount-1] - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; /*uint32_t splineIdOrTick =*/ packet.readUInt32(); - if (packet.getReadPos() >= packet.getSize()) return false; + if (!packet.hasData()) return false; data.moveType = packet.readUInt8(); if (data.moveType == 1) { @@ -3094,51 +3243,54 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d } if (data.moveType == 2) { - if (packet.getReadPos() + 12 > packet.getSize()) return false; + if (!packet.hasRemaining(12)) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } else if (data.moveType == 3) { - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; data.facingTarget = packet.readUInt64(); } else if (data.moveType == 4) { - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.facingAngle = packet.readFloat(); } - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.splineFlags = packet.readUInt32(); // Animation flag (same bit as WotLK MoveSplineFlag::Animation) if (data.splineFlags & 0x00400000) { - if (packet.getReadPos() + 5 > packet.getSize()) return false; + if (!packet.hasRemaining(5)) return false; packet.readUInt8(); packet.readUInt32(); } - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; data.duration = packet.readUInt32(); // Parabolic flag (same bit as WotLK MoveSplineFlag::Parabolic) if (data.splineFlags & 0x00000800) { - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; packet.readFloat(); packet.readUInt32(); } - if (packet.getReadPos() + 4 > packet.getSize()) return false; + if (!packet.hasRemaining(4)) return false; uint32_t pointCount = packet.readUInt32(); if (pointCount == 0) return true; - // Cap pointCount to prevent excessive iteration from malformed packets + // Reject extreme point counts from malformed packets. constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { - LOG_WARNING("SMSG_MONSTER_MOVE(Vanilla): pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, "), capping"); - pointCount = kMaxSplinePoints; + return false; } + size_t requiredBytes = 12; + if (pointCount > 1) { + requiredBytes += static_cast(pointCount - 1) * 4ull; + } + if (!packet.hasRemaining(requiredBytes)) return false; + // First float[3] is destination. - if (packet.getReadPos() + 12 > packet.getSize()) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -3148,13 +3300,12 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d if (pointCount > 1) { size_t skipBytes = static_cast(pointCount - 1) * 4; size_t newPos = packet.getReadPos() + skipBytes; - if (newPos <= packet.getSize()) { - packet.setReadPos(newPos); - } + if (newPos > packet.getSize()) return false; + packet.setReadPos(newPos); } LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec, - " type=", (int)data.moveType, " dur=", data.duration, "ms", + " type=", static_cast(data.moveType), " dur=", data.duration, "ms", " dest=(", data.destX, ",", data.destY, ",", data.destZ, ")"); return true; @@ -3175,9 +3326,9 @@ bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) { } bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) { - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); - data.victimGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getReadPos() < packet.getSize()) { + data.attackerGuid = packet.readPackedGuid(); + data.victimGuid = packet.readPackedGuid(); + if (packet.hasData()) { data.unknown = packet.readUInt32(); } LOG_DEBUG("Attack stopped: 0x", std::hex, data.attackerGuid, std::dec); @@ -3186,15 +3337,23 @@ bool AttackStopParser::parse(network::Packet& packet, AttackStopData& data) { bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpdateData& data) { // Upfront validation: hitInfo(4) + packed GUIDs(1-8 each) + totalDamage(4) + subDamageCount(1) = 13 bytes minimum - if (packet.getSize() - packet.getReadPos() < 13) return false; + if (!packet.hasRemaining(13)) return false; size_t startPos = packet.getReadPos(); data.hitInfo = packet.readUInt32(); - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.attackerGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.targetGuid = packet.readPackedGuid(); // Validate totalDamage + subDamageCount can be read (5 bytes) - if (packet.getSize() - packet.getReadPos() < 5) { + if (!packet.hasRemaining(5)) { packet.setReadPos(startPos); return false; } @@ -3202,17 +3361,25 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); - // Cap subDamageCount to prevent OOM (each entry is 20 bytes: 4+4+4+4+4) - if (data.subDamageCount > 64) { - LOG_WARNING("AttackerStateUpdate: subDamageCount capped (requested=", (int)data.subDamageCount, ")"); - data.subDamageCount = 64; + // Cap subDamageCount: each entry is 20 bytes. If the claimed count + // exceeds what the remaining bytes can hold, a GUID was mis-parsed + // (off by one byte), causing the school-mask byte to be read as count. + // In that case clamp to the number of full entries that fit. + { + size_t remaining = packet.getRemainingSize(); + size_t maxFit = remaining / 20; + if (data.subDamageCount > maxFit) { + data.subDamageCount = static_cast(std::min(maxFit, 64)); + } else if (data.subDamageCount > 64) { + data.subDamageCount = 64; + } } + if (data.subDamageCount == 0) return false; data.subDamages.reserve(data.subDamageCount); for (uint8_t i = 0; i < data.subDamageCount; ++i) { // Each sub-damage entry needs 20 bytes: schoolMask(4) + damage(4) + intDamage(4) + absorbed(4) + resisted(4) - if (packet.getSize() - packet.getReadPos() < 20) { - LOG_WARNING("AttackerStateUpdate: truncated subDamage at index ", (int)i, "/", (int)data.subDamageCount); + if (!packet.hasRemaining(20)) { data.subDamageCount = i; break; } @@ -3226,22 +3393,26 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda } // Validate victimState + overkill fields (8 bytes) - if (packet.getSize() - packet.getReadPos() < 8) { - LOG_WARNING("AttackerStateUpdate: truncated victimState/overkill"); + if (!packet.hasRemaining(8)) { data.victimState = 0; data.overkill = 0; return !data.subDamages.empty(); } data.victimState = packet.readUInt32(); - data.overkill = static_cast(packet.readUInt32()); + // WotLK (AzerothCore): two unknown uint32 fields follow victimState before overkill. + // Older parsers omitted these, reading overkill from the wrong offset. + auto rem = [&]() { return packet.getRemainingSize(); }; + if (rem() >= 4) packet.readUInt32(); // unk1 (always 0) + if (rem() >= 4) packet.readUInt32(); // unk2 (melee spell ID, 0 for auto-attack) + data.overkill = (rem() >= 4) ? static_cast(packet.readUInt32()) : -1; - // Read blocked amount (optional, 4 bytes) - if (packet.getSize() - packet.getReadPos() >= 4) { - data.blocked = packet.readUInt32(); - } else { - data.blocked = 0; - } + // hitInfo-conditional fields: HITINFO_BLOCK(0x2000), RAGE_GAIN(0x20000), FAKE_DAMAGE(0x40) + if ((data.hitInfo & 0x2000) && rem() >= 4) data.blocked = packet.readUInt32(); + else data.blocked = 0; + // RAGE_GAIN and FAKE_DAMAGE both add a uint32 we can skip + if ((data.hitInfo & 0x20000) && rem() >= 4) packet.readUInt32(); // rage gain + if ((data.hitInfo & 0x40) && rem() >= 4) packet.readUInt32(); // fake damage total LOG_DEBUG("Melee hit: ", data.totalDamage, " damage", data.isCrit() ? " (CRIT)" : "", @@ -3250,15 +3421,26 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda } bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) { - // Upfront validation: packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + absorbed(4) + resisted(4) = 30 bytes minimum - if (packet.getSize() - packet.getReadPos() < 30) return false; + // Upfront validation: + // packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) + // = 33 bytes minimum. + if (!packet.hasRemaining(33)) return false; size_t startPos = packet.getReadPos(); - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.targetGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.attackerGuid = packet.readPackedGuid(); // Validate core fields (spellId + damage + overkill + schoolMask + absorbed + resisted = 21 bytes) - if (packet.getSize() - packet.getReadPos() < 21) { + if (!packet.hasRemaining(21)) { packet.setReadPos(startPos); return false; } @@ -3270,11 +3452,11 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da data.absorbed = packet.readUInt32(); data.resisted = packet.readUInt32(); - // Skip remaining fields (periodicLog + unused + blocked + flags = 10 bytes) - if (packet.getSize() - packet.getReadPos() < 10) { - LOG_WARNING("SpellDamageLog: truncated trailing fields"); - data.isCrit = false; - return true; + // Remaining fields are required for a complete event. + // Reject truncated packets so we do not emit partial/incorrect combat entries. + if (!packet.hasRemaining(10)) { + packet.setReadPos(startPos); + return false; } uint8_t periodicLog = packet.readUInt8(); @@ -3293,14 +3475,22 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) { // Upfront validation: packed GUIDs(1-8 each) + spellId(4) + heal(4) + overheal(4) + absorbed(4) + critFlag(1) = 21 bytes minimum - if (packet.getSize() - packet.getReadPos() < 21) return false; + if (!packet.hasRemaining(21)) return false; size_t startPos = packet.getReadPos(); - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.targetGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.casterGuid = packet.readPackedGuid(); // Validate remaining fields (spellId + heal + overheal + absorbed + critFlag = 17 bytes) - if (packet.getSize() - packet.getReadPos() < 17) { + if (!packet.hasRemaining(17)) { packet.setReadPos(startPos); return false; } @@ -3323,7 +3513,7 @@ bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) bool XpGainParser::parse(network::Packet& packet, XpGainData& data) { // Validate minimum packet size: victimGuid(8) + totalXp(4) + type(1) - if (packet.getSize() - packet.getReadPos() < 13) { + if (!packet.hasRemaining(13)) { LOG_WARNING("SMSG_LOG_XPGAIN: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -3334,7 +3524,7 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) { if (data.type == 0) { // Kill XP: float groupRate (1.0 = solo) + uint8 RAF flag // Validate before reading conditional fields - if (packet.getReadPos() + 5 <= packet.getSize()) { + if (packet.hasRemaining(5)) { float groupRate = packet.readFloat(); packet.readUInt8(); // RAF bonus flag // Group bonus = total - (total / rate); only if grouped (rate > 1) @@ -3354,7 +3544,7 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) { bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data, bool vanillaFormat) { // Validate minimum packet size for header: talentSpec(1) + spellCount(2) - if (packet.getSize() - packet.getReadPos() < 3) { + if (!packet.hasRemaining(3)) { LOG_ERROR("SMSG_INITIAL_SPELLS: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -3362,8 +3552,10 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data data.talentSpec = packet.readUInt8(); uint16_t spellCount = packet.readUInt16(); - // Cap spell count to prevent excessive iteration - constexpr uint16_t kMaxSpells = 256; + // Cap spell count to prevent excessive iteration. + // WotLK characters with all ranks, mounts, professions, and racials can + // know 400-600 spells; 1024 covers all practical cases with headroom. + constexpr uint16_t kMaxSpells = 1024; if (spellCount > kMaxSpells) { LOG_WARNING("SMSG_INITIAL_SPELLS: spellCount=", spellCount, " exceeds max ", kMaxSpells, ", capping"); @@ -3378,7 +3570,7 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data // Vanilla spell: spellId(2) + slot(2) = 4 bytes // TBC/WotLK spell: spellId(4) + unknown(2) = 6 bytes size_t spellEntrySize = vanillaFormat ? 4 : 6; - if (packet.getSize() - packet.getReadPos() < spellEntrySize) { + if (!packet.hasRemaining(spellEntrySize)) { LOG_WARNING("SMSG_INITIAL_SPELLS: spell ", i, " truncated (", spellCount, " expected)"); break; } @@ -3397,7 +3589,7 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data } // Validate minimum packet size for cooldownCount (2 bytes) - if (packet.getSize() - packet.getReadPos() < 2) { + if (!packet.hasRemaining(2)) { LOG_WARNING("SMSG_INITIAL_SPELLS: truncated before cooldownCount (parsed ", data.spellIds.size(), " spells)"); return true; // Have spells; cooldowns are optional @@ -3405,8 +3597,10 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data uint16_t cooldownCount = packet.readUInt16(); - // Cap cooldown count to prevent excessive iteration - constexpr uint16_t kMaxCooldowns = 256; + // Cap cooldown count to prevent excessive iteration. + // Some servers include entries for all spells (even with zero remaining time) + // to communicate category cooldown data, so the count can be high. + constexpr uint16_t kMaxCooldowns = 1024; if (cooldownCount > kMaxCooldowns) { LOG_WARNING("SMSG_INITIAL_SPELLS: cooldownCount=", cooldownCount, " exceeds max ", kMaxCooldowns, ", capping"); @@ -3418,7 +3612,7 @@ bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data // Vanilla cooldown: spellId(2) + itemId(2) + categoryId(2) + cooldownMs(4) + categoryCooldownMs(4) = 14 bytes // TBC/WotLK cooldown: spellId(4) + itemId(2) + categoryId(2) + cooldownMs(4) + categoryCooldownMs(4) = 16 bytes size_t cooldownEntrySize = vanillaFormat ? 14 : 16; - if (packet.getSize() - packet.getReadPos() < cooldownEntrySize) { + if (!packet.hasRemaining(cooldownEntrySize)) { LOG_WARNING("SMSG_INITIAL_SPELLS: cooldown ", i, " truncated (", cooldownCount, " expected)"); break; } @@ -3504,25 +3698,35 @@ network::Packet PetActionPacket::build(uint64_t petGuid, uint32_t action, uint64 bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) { // WotLK format: castCount(1) + spellId(4) + result(1) = 6 bytes minimum - if (packet.getSize() - packet.getReadPos() < 6) return false; + if (!packet.hasRemaining(6)) return false; data.castCount = packet.readUInt8(); data.spellId = packet.readUInt32(); data.result = packet.readUInt8(); - LOG_INFO("Cast failed: spell=", data.spellId, " result=", (int)data.result); + LOG_INFO("Cast failed: spell=", data.spellId, " result=", static_cast(data.result)); return true; } bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { - // Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + castTime(4) = 22 bytes minimum - if (packet.getSize() - packet.getReadPos() < 22) return false; + data = SpellStartData{}; + + // Packed GUIDs are variable-length; only require minimal packet shape up front: + // two GUID masks + castCount(1) + spellId(4) + castFlags(4) + castTime(4). + if (!packet.hasRemaining(15)) return false; size_t startPos = packet.getReadPos(); - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) { + return false; + } + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.casterUnit = packet.readPackedGuid(); - // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 9 bytes) - if (packet.getSize() - packet.getReadPos() < 9) { + // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 13 bytes) + if (!packet.hasRemaining(13)) { packet.setReadPos(startPos); return false; } @@ -3532,12 +3736,51 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { data.castFlags = packet.readUInt32(); data.castTime = packet.readUInt32(); - // Read target flags and target (simplified) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t targetFlags = packet.readUInt32(); - if ((targetFlags & 0x02) && packet.getSize() - packet.getReadPos() >= 1) { // TARGET_FLAG_UNIT, validate packed GUID read - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); - } + // SpellCastTargets starts with target flags and is mandatory. + if (!packet.hasRemaining(4)) { + LOG_WARNING("Spell start: missing targetFlags"); + packet.setReadPos(startPos); + return false; + } + + // WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that + // subsequent fields (e.g. school mask, cast flags 0x20 extra data) are not + // misaligned for ground-targeted or AoE spells. + uint32_t targetFlags = packet.readUInt32(); + + auto readPackedTarget = [&](uint64_t* out) -> bool { + if (!packet.hasFullPackedGuid()) return false; + uint64_t g = packet.readPackedGuid(); + if (out) *out = g; + return true; + }; + auto skipPackedAndFloats3 = [&]() -> bool { + if (!packet.hasFullPackedGuid()) return false; + packet.readPackedGuid(); // transport GUID (may be zero) + if (!packet.hasRemaining(12)) return false; + packet.readFloat(); packet.readFloat(); packet.readFloat(); + return true; + }; + + // UNIT/UNIT_MINIPET/CORPSE_ALLY/GAMEOBJECT share a single object target GUID + if (targetFlags & (0x0002u | 0x0004u | 0x0400u | 0x0800u)) { + readPackedTarget(&data.targetGuid); // best-effort; ignore failure + } + // ITEM/TRADE_ITEM share a single item target GUID + if (targetFlags & (0x0010u | 0x0100u)) { + readPackedTarget(nullptr); + } + // SOURCE_LOCATION: PackedGuid (transport) + float x,y,z + if (targetFlags & 0x0020u) { + skipPackedAndFloats3(); + } + // DEST_LOCATION: PackedGuid (transport) + float x,y,z + if (targetFlags & 0x0040u) { + skipPackedAndFloats3(); + } + // STRING: null-terminated + if (targetFlags & 0x0200u) { + while (packet.hasData() && packet.readUInt8() != 0) {} } LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); @@ -3545,15 +3788,26 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { - // Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1) + missCount(1) = 24 bytes minimum - if (packet.getSize() - packet.getReadPos() < 24) return false; + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + + // Packed GUIDs are variable-length, so only require the smallest possible + // shape up front: 2 GUID masks + fixed fields through hitCount. + if (!packet.hasRemaining(16)) return false; size_t startPos = packet.getReadPos(); - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) { + return false; + } + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { + packet.setReadPos(startPos); + return false; + } + data.casterUnit = packet.readPackedGuid(); // Validate remaining fixed fields up to hitCount/missCount - if (packet.getSize() - packet.getReadPos() < 14) { // castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1) + if (!packet.hasRemaining(14)) { // castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1) packet.setReadPos(startPos); return false; } @@ -3564,67 +3818,166 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { // Timestamp in 3.3.5a packet.readUInt32(); - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent DoS via massive arrays - if (data.hitCount > 128) { - LOG_WARNING("Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("Spell go: hitCount capped (requested=", static_cast(rawHitCount), ")"); } + const uint8_t storedHitLimit = std::min(rawHitCount, 128); - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 8) { - LOG_WARNING("Spell go: truncated hit targets at index ", (int)i, "/", (int)data.hitCount); - data.hitCount = i; + bool truncatedTargets = false; + + data.hitTargets.reserve(storedHitLimit); + for (uint16_t i = 0; i < rawHitCount; ++i) { + // WotLK 3.3.5a hit targets are full uint64 GUIDs (not PackedGuid). + if (!packet.hasRemaining(8)) { + LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", static_cast(rawHitCount)); + truncatedTargets = true; break; } - data.hitTargets.push_back(packet.readUInt64()); + const uint64_t targetGuid = packet.readUInt64(); + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } + } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.hitCount = static_cast(data.hitTargets.size()); + + // missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. + if (!packet.hasRemaining(1)) { + LOG_WARNING("Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; } - // Validate missCount field exists - if (packet.getSize() - packet.getReadPos() < 1) { - return true; // Valid, just no misses + const size_t missCountPos = packet.getReadPos(); + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 20) { + // Likely offset error — dump context bytes for diagnostics. + const auto& raw = packet.getData(); + std::string hexCtx; + size_t dumpStart = (missCountPos >= 8) ? missCountPos - 8 : startPos; + size_t dumpEnd = std::min(missCountPos + 16, raw.size()); + for (size_t i = dumpStart; i < dumpEnd; ++i) { + char buf[4]; + std::snprintf(buf, sizeof(buf), "%02x ", raw[i]); + hexCtx += buf; + if (i == missCountPos - 1) hexCtx += "["; + if (i == missCountPos) hexCtx += "] "; + } + LOG_WARNING("Spell go: suspect missCount=", static_cast(rawMissCount), + " spell=", data.spellId, " hits=", static_cast(data.hitCount), + " castFlags=0x", std::hex, data.castFlags, std::dec, + " missCountPos=", missCountPos, " pktSize=", packet.getSize(), + " ctx=", hexCtx); } - - data.missCount = packet.readUInt8(); - // Cap miss count to prevent DoS - if (data.missCount > 128) { - LOG_WARNING("Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + if (rawMissCount > 128) { + LOG_WARNING("Spell go: missCount capped (requested=", static_cast(rawMissCount), + ") spell=", data.spellId, " hits=", static_cast(data.hitCount), + " remaining=", packet.getRemainingSize()); } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount; ++i) { - // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte), validate before reading - if (packet.getSize() - packet.getReadPos() < 2) { - LOG_WARNING("Spell go: truncated miss targets at index ", (int)i, "/", (int)data.missCount); - data.missCount = i; + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { + // WotLK 3.3.5a miss targets are full uint64 GUIDs + uint8 missType. + // REFLECT additionally appends uint8 reflectResult. + if (!packet.hasRemaining(9)) { // 8 GUID + 1 missType + LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", static_cast(rawMissCount), + " spell=", data.spellId, " hits=", static_cast(data.hitCount)); + truncatedTargets = true; break; } SpellGoMissEntry m; - m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK - m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; - data.missTargets.push_back(m); + m.targetGuid = packet.readUInt64(); + m.missType = packet.readUInt8(); + if (m.missType == 11) { // SPELL_MISS_REFLECT + if (!packet.hasRemaining(1)) { + LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", static_cast(rawMissCount)); + truncatedTargets = true; + break; + } + (void)packet.readUInt8(); // reflectResult + } + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } + } + data.missCount = static_cast(data.missTargets.size()); + + // If miss targets were truncated, salvage the successfully-parsed hit data + // rather than discarding the entire spell. The server already applied effects; + // we just need the hit list for UI feedback (combat text, health bars). + if (truncatedTargets) { + LOG_DEBUG("Spell go: salvaging ", static_cast(data.hitCount), " hits despite miss truncation"); + packet.skipAll(); // consume remaining bytes + return true; } - LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, - " misses=", (int)data.missCount); + // WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that + // any trailing fields after the target section are not misaligned for + // ground-targeted or AoE spells. Same layout as SpellStartParser. + if (packet.hasData()) { + if (packet.hasRemaining(4)) { + uint32_t targetFlags = packet.readUInt32(); + + auto readPackedTarget = [&](uint64_t* out) -> bool { + if (!packet.hasFullPackedGuid()) return false; + uint64_t g = packet.readPackedGuid(); + if (out) *out = g; + return true; + }; + auto skipPackedAndFloats3 = [&]() -> bool { + if (!packet.hasFullPackedGuid()) return false; + packet.readPackedGuid(); // transport GUID + if (!packet.hasRemaining(12)) return false; + packet.readFloat(); packet.readFloat(); packet.readFloat(); + return true; + }; + + // UNIT/UNIT_MINIPET/CORPSE_ALLY/GAMEOBJECT share one object target GUID + if (targetFlags & (0x0002u | 0x0004u | 0x0400u | 0x0800u)) { + readPackedTarget(&data.targetGuid); + } + // ITEM/TRADE_ITEM share one item target GUID + if (targetFlags & (0x0010u | 0x0100u)) { + readPackedTarget(nullptr); + } + // SOURCE_LOCATION: PackedGuid (transport) + float x,y,z + if (targetFlags & 0x0020u) { + skipPackedAndFloats3(); + } + // DEST_LOCATION: PackedGuid (transport) + float x,y,z + if (targetFlags & 0x0040u) { + skipPackedAndFloats3(); + } + // STRING: null-terminated + if (targetFlags & 0x0200u) { + while (packet.hasData() && packet.readUInt8() != 0) {} + } + } + } + + LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", static_cast(data.hitCount), + " misses=", static_cast(data.missCount)); return true; } bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool isAll) { // Validation: packed GUID (1-8 bytes minimum for reading) - if (packet.getSize() - packet.getReadPos() < 1) return false; + if (!packet.hasRemaining(1)) return false; - data.guid = UpdateObjectParser::readPackedGuid(packet); + data.guid = packet.readPackedGuid(); // Cap number of aura entries to prevent unbounded loop DoS uint32_t maxAuras = isAll ? 512 : 1; uint32_t auraCount = 0; - while (packet.getReadPos() < packet.getSize() && auraCount < maxAuras) { + while (packet.hasData() && auraCount < maxAuras) { // Validate we can read slot (1) + spellId (4) = 5 bytes minimum - if (packet.getSize() - packet.getReadPos() < 5) { + if (!packet.hasRemaining(5)) { LOG_DEBUG("Aura update: truncated entry at position ", auraCount); break; } @@ -3638,7 +3991,7 @@ bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool aura.spellId = spellId; // Validate flags + level + charges (3 bytes) - if (packet.getSize() - packet.getReadPos() < 3) { + if (!packet.hasRemaining(3)) { LOG_WARNING("Aura update: truncated flags/level/charges at entry ", auraCount); aura.flags = 0; aura.level = 0; @@ -3651,15 +4004,15 @@ bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool if (!(aura.flags & 0x08)) { // NOT_CASTER flag // Validate space for packed GUID read (minimum 1 byte) - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { aura.casterGuid = 0; } else { - aura.casterGuid = UpdateObjectParser::readPackedGuid(packet); + aura.casterGuid = packet.readPackedGuid(); } } if (aura.flags & 0x20) { // DURATION - need 8 bytes (two uint32s) - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_WARNING("Aura update: truncated duration fields at entry ", auraCount); aura.maxDurationMs = 0; aura.durationMs = 0; @@ -3673,7 +4026,7 @@ bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool // Only read amounts for active effect indices (flags 0x01, 0x02, 0x04) for (int i = 0; i < 3; ++i) { if (aura.flags & (1 << i)) { - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasRemaining(4)) { packet.readUInt32(); } else { LOG_WARNING("Aura update: truncated effect amount ", i, " at entry ", auraCount); @@ -3690,7 +4043,7 @@ bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool if (!isAll) break; } - if (auraCount >= maxAuras && packet.getReadPos() < packet.getSize()) { + if (auraCount >= maxAuras && packet.hasData()) { LOG_WARNING("Aura update: capped at ", maxAuras, " entries, remaining data ignored"); } @@ -3701,7 +4054,7 @@ bool AuraUpdateParser::parse(network::Packet& packet, AuraUpdateData& data, bool bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data) { // Upfront validation: guid(8) + flags(1) = 9 bytes minimum - if (packet.getSize() - packet.getReadPos() < 9) return false; + if (!packet.hasRemaining(9)) return false; data.guid = packet.readUInt64(); data.flags = packet.readUInt8(); @@ -3710,14 +4063,14 @@ bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data uint32_t maxCooldowns = 512; uint32_t cooldownCount = 0; - while (packet.getReadPos() + 8 <= packet.getSize() && cooldownCount < maxCooldowns) { + while (packet.hasRemaining(8) && cooldownCount < maxCooldowns) { uint32_t spellId = packet.readUInt32(); uint32_t cooldownMs = packet.readUInt32(); data.cooldowns.push_back({spellId, cooldownMs}); cooldownCount++; } - if (cooldownCount >= maxCooldowns && packet.getReadPos() + 8 <= packet.getSize()) { + if (cooldownCount >= maxCooldowns && packet.hasRemaining(8)) { LOG_WARNING("Spell cooldowns: capped at ", maxCooldowns, " entries, remaining data ignored"); } @@ -3739,7 +4092,7 @@ network::Packet GroupInvitePacket::build(const std::string& playerName) { bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteResponseData& data) { // Validate minimum packet size: canAccept(1) - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { LOG_WARNING("SMSG_GROUP_INVITE: packet too small (", packet.getSize(), " bytes)"); return false; } @@ -3747,7 +4100,7 @@ bool GroupInviteResponseParser::parse(network::Packet& packet, GroupInviteRespon data.canAccept = packet.readUInt8(); // Note: inviterName is a string, which is always safe to read even if empty data.inviterName = packet.readString(); - LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", (int)data.canAccept, ")"); + LOG_INFO("Group invite from: ", data.inviterName, " (canAccept=", static_cast(data.canAccept), ")"); return true; } @@ -3763,7 +4116,7 @@ network::Packet GroupDeclinePacket::build() { } bool GroupListParser::parse(network::Packet& packet, GroupListData& data, bool hasRoles) { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 3) return false; data.groupType = packet.readUInt8(); @@ -3847,25 +4200,25 @@ bool GroupListParser::parse(network::Packet& packet, GroupListData& data, bool h bool PartyCommandResultParser::parse(network::Packet& packet, PartyCommandResultData& data) { // Upfront validation: command(4) + name(var) + result(4) = 8 bytes minimum (plus name string) - if (packet.getSize() - packet.getReadPos() < 8) return false; + if (!packet.hasRemaining(8)) return false; data.command = static_cast(packet.readUInt32()); data.name = packet.readString(); // Validate result field exists (4 bytes) - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { data.result = static_cast(0); return true; // Partial read is acceptable } data.result = static_cast(packet.readUInt32()); - LOG_DEBUG("Party command result: ", (int)data.result); + LOG_DEBUG("Party command result: ", static_cast(data.result)); return true; } bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData& data) { // Upfront validation: playerName is a CString (minimum 1 null terminator) - if (packet.getSize() - packet.getReadPos() < 1) return false; + if (!packet.hasRemaining(1)) return false; data.playerName = packet.readString(); LOG_INFO("Group decline from: ", data.playerName); @@ -3903,6 +4256,13 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64 return packet; } +network::Packet OpenItemPacket::build(uint8_t bagIndex, uint8_t slotIndex) { + network::Packet packet(wireOpcode(Opcode::CMSG_OPEN_ITEM)); + packet.writeUInt8(bagIndex); + packet.writeUInt8(slotIndex); + return packet; +} + network::Packet AutoEquipItemPacket::build(uint8_t srcBag, uint8_t srcSlot) { network::Packet packet(wireOpcode(Opcode::CMSG_AUTOEQUIP_ITEM)); packet.writeUInt8(srcBag); @@ -3919,6 +4279,17 @@ network::Packet SwapItemPacket::build(uint8_t dstBag, uint8_t dstSlot, uint8_t s return packet; } +network::Packet SplitItemPacket::build(uint8_t srcBag, uint8_t srcSlot, + uint8_t dstBag, uint8_t dstSlot, uint8_t count) { + network::Packet packet(wireOpcode(Opcode::CMSG_SPLIT_ITEM)); + packet.writeUInt8(srcBag); + packet.writeUInt8(srcSlot); + packet.writeUInt8(dstBag); + packet.writeUInt8(dstSlot); + packet.writeUInt8(count); + return packet; +} + network::Packet SwapInvItemPacket::build(uint8_t srcSlot, uint8_t dstSlot) { network::Packet packet(wireOpcode(Opcode::CMSG_SWAP_INV_ITEM)); packet.writeUInt8(srcSlot); @@ -3939,44 +4310,50 @@ network::Packet LootReleasePacket::build(uint64_t lootGuid) { bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat) { data = LootResponseData{}; - if (packet.getSize() - packet.getReadPos() < 14) { - LOG_WARNING("LootResponseParser: packet too short"); + size_t avail = packet.getRemainingSize(); + + // Minimum is guid(8)+lootType(1) = 9 bytes. Servers send a short packet with + // lootType=0 (LOOT_NONE) when loot is unavailable (e.g. chest not yet opened, + // needs a key, or another player is looting). We treat this as an empty-loot + // signal and return false so the caller knows not to open the loot window. + if (avail < 9) { + LOG_WARNING("LootResponseParser: packet too short (", avail, " bytes)"); return false; } data.lootGuid = packet.readUInt64(); data.lootType = packet.readUInt8(); + + // Short failure packet — no gold/item data follows. + avail = packet.getRemainingSize(); + if (avail < 5) { + LOG_DEBUG("LootResponseParser: lootType=", static_cast(data.lootType), " (empty/failure response)"); + return false; + } + data.gold = packet.readUInt32(); uint8_t itemCount = packet.readUInt8(); - // Item wire size: - // WotLK 3.3.5a: slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22 - // Classic/TBC: slot(1)+itemId(4)+count(4)+displayInfo(4)+slotType(1) = 14 - const size_t kItemSize = isWotlkFormat ? 22u : 14u; + // Per-item wire size is 22 bytes across all expansions: + // slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22 + constexpr size_t kItemSize = 22u; auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool { for (uint8_t i = 0; i < listCount; ++i) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < kItemSize) { return false; } LootItem item; - item.slotIndex = packet.readUInt8(); - item.itemId = packet.readUInt32(); - item.count = packet.readUInt32(); - item.displayInfoId = packet.readUInt32(); - - if (isWotlkFormat) { - item.randomSuffix = packet.readUInt32(); - item.randomPropertyId = packet.readUInt32(); - } else { - item.randomSuffix = 0; - item.randomPropertyId = 0; - } - - item.lootSlotType = packet.readUInt8(); - item.isQuestItem = markQuestItems; + item.slotIndex = packet.readUInt8(); + item.itemId = packet.readUInt32(); + item.count = packet.readUInt32(); + item.displayInfoId = packet.readUInt32(); + item.randomSuffix = packet.readUInt32(); + item.randomPropertyId = packet.readUInt32(); + item.lootSlotType = packet.readUInt8(); + item.isQuestItem = markQuestItems; data.items.push_back(item); } return true; @@ -3988,8 +4365,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, return false; } + // Quest item section only present in WotLK 3.3.5a uint8_t questItemCount = 0; - if (packet.getSize() - packet.getReadPos() >= 1) { + if (isWotlkFormat && packet.hasRemaining(1)) { questItemCount = packet.readUInt8(); data.items.reserve(data.items.size() + questItemCount); if (!parseLootItemList(questItemCount, true)) { @@ -3998,7 +4376,7 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, } } - LOG_DEBUG("Loot response: ", (int)itemCount, " regular + ", (int)questItemCount, + LOG_DEBUG("Loot response: ", static_cast(itemCount), " regular + ", static_cast(questItemCount), " quest items, ", data.gold, " copper"); return true; } @@ -4066,7 +4444,7 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) data.details = normalizeWowTextTokens(packet.readString()); data.objectives = normalizeWowTextTokens(packet.readString()); - if (packet.getReadPos() + 10 > packet.getSize()) { + if (!packet.hasRemaining(10)) { LOG_DEBUG("Quest details (short): id=", data.questId, " title='", data.title, "'"); return true; } @@ -4077,10 +4455,10 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) /*isFinished*/ packet.readUInt8(); // Reward choice items: server always writes 6 entries (QUEST_REWARD_CHOICES_COUNT) - if (packet.getReadPos() + 4 <= packet.getSize()) { + if (packet.hasRemaining(4)) { /*choiceCount*/ packet.readUInt32(); for (int i = 0; i < 6; i++) { - if (packet.getReadPos() + 12 > packet.getSize()) break; + if (!packet.hasRemaining(12)) break; uint32_t itemId = packet.readUInt32(); uint32_t count = packet.readUInt32(); uint32_t dispId = packet.readUInt32(); @@ -4094,10 +4472,10 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) } // Reward items: server always writes 4 entries (QUEST_REWARDS_COUNT) - if (packet.getReadPos() + 4 <= packet.getSize()) { + if (packet.hasRemaining(4)) { /*rewardCount*/ packet.readUInt32(); for (int i = 0; i < 4; i++) { - if (packet.getReadPos() + 12 > packet.getSize()) break; + if (!packet.hasRemaining(12)) break; uint32_t itemId = packet.readUInt32(); uint32_t count = packet.readUInt32(); uint32_t dispId = packet.readUInt32(); @@ -4110,9 +4488,9 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) } // Money and XP rewards - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.rewardMoney = packet.readUInt32(); - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) data.rewardXp = packet.readUInt32(); LOG_DEBUG("Quest details: id=", data.questId, " title='", data.title, "'"); @@ -4121,7 +4499,7 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) { // Upfront validation: npcGuid(8) + menuId(4) + titleTextId(4) + optionCount(4) = 20 bytes minimum - if (packet.getSize() - packet.getReadPos() < 20) return false; + if (!packet.hasRemaining(20)) return false; data.npcGuid = packet.readUInt64(); data.menuId = packet.readUInt32(); @@ -4140,7 +4518,7 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data for (uint32_t i = 0; i < optionCount; ++i) { // Each option: id(4) + icon(1) + isCoded(1) + boxMoney(4) + text(var) + boxText(var) // Minimum: 10 bytes + 2 empty strings (2 null terminators) = 12 bytes - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { LOG_WARNING("GossipMessageParser: truncated options at index ", i, "/", optionCount); break; } @@ -4155,7 +4533,7 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data } // Validate questCount field exists (4 bytes) - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_DEBUG("Gossip: ", data.options.size(), " options (no quest data)"); return true; } @@ -4173,7 +4551,7 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data for (uint32_t i = 0; i < questCount; ++i) { // Each quest: questId(4) + questIcon(4) + questLevel(4) + questFlags(4) + isRepeatable(1) + title(var) // Minimum: 17 bytes + empty string (1 null terminator) = 18 bytes - if (packet.getSize() - packet.getReadPos() < 18) { + if (!packet.hasRemaining(18)) { LOG_WARNING("GossipMessageParser: truncated quests at index ", i, "/", questCount); break; } @@ -4212,13 +4590,13 @@ bool BindPointUpdateParser::parse(network::Packet& packet, BindPointUpdateData& } bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsData& data) { - if (packet.getSize() - packet.getReadPos() < 20) return false; + if (!packet.hasRemaining(20)) return false; data.npcGuid = packet.readUInt64(); data.questId = packet.readUInt32(); data.title = normalizeWowTextTokens(packet.readString()); data.completionText = normalizeWowTextTokens(packet.readString()); - if (packet.getReadPos() + 9 > packet.getSize()) { + if (!packet.hasRemaining(9)) { LOG_DEBUG("Quest request items (short): id=", data.questId, " title='", data.title, "'"); return true; } @@ -4235,17 +4613,17 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa ParsedTail out; packet.setReadPos(startPos); - if (packet.getReadPos() + prefixSkip > packet.getSize()) return out; + if (!packet.hasRemaining(prefixSkip)) return out; packet.setReadPos(packet.getReadPos() + prefixSkip); - if (packet.getReadPos() + 8 > packet.getSize()) return out; + if (!packet.hasRemaining(8)) return out; out.requiredMoney = packet.readUInt32(); uint32_t requiredItemCount = packet.readUInt32(); if (requiredItemCount > 64) return out; // sanity guard against misalignment out.requiredItems.reserve(requiredItemCount); for (uint32_t i = 0; i < requiredItemCount; ++i) { - if (packet.getReadPos() + 12 > packet.getSize()) return out; + if (!packet.hasRemaining(12)) return out; QuestRewardItem item; item.itemId = packet.readUInt32(); item.count = packet.readUInt32(); @@ -4253,7 +4631,7 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa if (item.itemId != 0) out.requiredItems.push_back(item); } - if (packet.getReadPos() + 4 > packet.getSize()) return out; + if (!packet.hasRemaining(4)) return out; out.completableFlags = packet.readUInt32(); out.ok = true; @@ -4266,7 +4644,7 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa else if (out.requiredMoney <= 100000) out.score += 2; // <=10g is common else if (out.requiredMoney >= 1000000) out.score -= 3; // implausible for most quests if (!out.requiredItems.empty()) out.score += 1; - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining <= 16) out.score += 3; else if (remaining <= 32) out.score += 2; else if (remaining <= 64) out.score += 1; @@ -4301,13 +4679,13 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa } bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData& data) { - if (packet.getSize() - packet.getReadPos() < 20) return false; + if (!packet.hasRemaining(20)) return false; data.npcGuid = packet.readUInt64(); data.questId = packet.readUInt32(); data.title = normalizeWowTextTokens(packet.readString()); data.rewardText = normalizeWowTextTokens(packet.readString()); - if (packet.getReadPos() + 8 > packet.getSize()) { + if (!packet.hasRemaining(8)) { LOG_DEBUG("Quest offer reward (short): id=", data.questId, " title='", data.title, "'"); return true; } @@ -4338,26 +4716,26 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData packet.setReadPos(startPos); // Skip the prefix bytes (autoFinish + optional suggestedPlayers before emoteCount) - if (packet.getReadPos() + prefixSkip > packet.getSize()) return out; + if (!packet.hasRemaining(prefixSkip)) return out; packet.setReadPos(packet.getReadPos() + prefixSkip); - if (packet.getReadPos() + 4 > packet.getSize()) return out; + if (!packet.hasRemaining(4)) return out; uint32_t emoteCount = packet.readUInt32(); if (emoteCount > 32) return out; // guard against misalignment for (uint32_t i = 0; i < emoteCount; ++i) { - if (packet.getReadPos() + 8 > packet.getSize()) return out; + if (!packet.hasRemaining(8)) return out; packet.readUInt32(); // delay packet.readUInt32(); // emote type } - if (packet.getReadPos() + 4 > packet.getSize()) return out; + if (!packet.hasRemaining(4)) return out; uint32_t choiceCount = packet.readUInt32(); if (choiceCount > 6) return out; uint32_t choiceSlots = fixedArrays ? 6u : choiceCount; out.choiceRewards.reserve(choiceCount); uint32_t nonZeroChoice = 0; for (uint32_t i = 0; i < choiceSlots; ++i) { - if (packet.getReadPos() + 12 > packet.getSize()) return out; + if (!packet.hasRemaining(12)) return out; QuestRewardItem item; item.itemId = packet.readUInt32(); item.count = packet.readUInt32(); @@ -4369,14 +4747,14 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData } } - if (packet.getReadPos() + 4 > packet.getSize()) return out; + if (!packet.hasRemaining(4)) return out; uint32_t rewardCount = packet.readUInt32(); if (rewardCount > 4) return out; uint32_t rewardSlots = fixedArrays ? 4u : rewardCount; out.fixedRewards.reserve(rewardCount); uint32_t nonZeroFixed = 0; for (uint32_t i = 0; i < rewardSlots; ++i) { - if (packet.getReadPos() + 12 > packet.getSize()) return out; + if (!packet.hasRemaining(12)) return out; QuestRewardItem item; item.itemId = packet.readUInt32(); item.count = packet.readUInt32(); @@ -4387,9 +4765,9 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData } } - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) out.rewardMoney = packet.readUInt32(); - if (packet.getReadPos() + 4 <= packet.getSize()) + if (packet.hasRemaining(4)) out.rewardXp = packet.readUInt32(); out.ok = true; @@ -4406,7 +4784,7 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData if (nonZeroChoice <= choiceCount) out.score += 2; if (nonZeroFixed <= rewardCount) out.score += 2; // No bytes left over (or only a few) - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining == 0) out.score += 5; else if (remaining <= 4) out.score += 3; else if (remaining <= 8) out.score += 2; @@ -4509,7 +4887,7 @@ network::Packet BuybackItemPacket::build(uint64_t vendorGuid, uint32_t slot) { bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) { data = ListInventoryData{}; - if (packet.getSize() - packet.getReadPos() < 9) { + if (!packet.hasRemaining(9)) { LOG_WARNING("ListInventoryParser: packet too short"); return false; } @@ -4525,12 +4903,12 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data // Auto-detect whether server sends 7 fields (28 bytes/item) or 8 fields (32 bytes/item). // Some servers omit the extendedCost field entirely; reading 8 fields on a 7-field packet // misaligns every item after the first and produces garbage prices. - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); const size_t bytesPerItemNoExt = 28; const size_t bytesPerItemWithExt = 32; bool hasExtendedCost = false; if (remaining < static_cast(itemCount) * bytesPerItemNoExt) { - LOG_WARNING("ListInventoryParser: truncated packet (items=", (int)itemCount, + LOG_WARNING("ListInventoryParser: truncated packet (items=", static_cast(itemCount), ", remaining=", remaining, ")"); return false; } @@ -4541,8 +4919,8 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data data.items.reserve(itemCount); for (uint8_t i = 0; i < itemCount; ++i) { const size_t perItemBytes = hasExtendedCost ? bytesPerItemWithExt : bytesPerItemNoExt; - if (packet.getSize() - packet.getReadPos() < perItemBytes) { - LOG_WARNING("ListInventoryParser: item ", (int)i, " truncated"); + if (!packet.hasRemaining(perItemBytes)) { + LOG_WARNING("ListInventoryParser: item ", static_cast(i), " truncated"); return false; } VendorItem item; @@ -4557,7 +4935,7 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data data.items.push_back(item); } - LOG_DEBUG("Vendor inventory: ", (int)itemCount, " items (extendedCost: ", hasExtendedCost ? "yes" : "no", ")"); + LOG_DEBUG("Vendor inventory: ", static_cast(itemCount), " items (extendedCost: ", hasExtendedCost ? "yes" : "no", ")"); return true; } @@ -4571,7 +4949,7 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data, bo // Classic per-entry: spellId(4) + state(1) + cost(4) + reqLevel(1) + // reqSkill(4) + reqSkillValue(4) + chain×3(12) + unk(4) = 34 bytes data = TrainerListData{}; - if (packet.getSize() - packet.getReadPos() < 16) return false; // guid(8) + type(4) + count(4) + if (!packet.hasRemaining(16)) return false; // guid(8) + type(4) + count(4) data.trainerGuid = packet.readUInt64(); data.trainerType = packet.readUInt32(); @@ -4586,7 +4964,7 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data, bo for (uint32_t i = 0; i < spellCount; ++i) { // Validate minimum entry size before reading const size_t minEntrySize = isClassic ? 34 : 38; - if (packet.getReadPos() + minEntrySize > packet.getSize()) { + if (!packet.hasRemaining(minEntrySize)) { LOG_WARNING("TrainerListParser: truncated at spell ", i); break; } @@ -4617,7 +4995,7 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data, bo data.spells.push_back(spell); } - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { LOG_WARNING("TrainerListParser: truncated before greeting"); data.greeting.clear(); } else { @@ -4678,8 +5056,8 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { return false; } - LOG_INFO("SMSG_TALENTS_INFO: spec=", (int)data.talentSpec, - " unspent=", (int)data.unspentPoints, + LOG_INFO("SMSG_TALENTS_INFO: spec=", static_cast(data.talentSpec), + " unspent=", static_cast(data.unspentPoints), " talentCount=", talentCount, " entryCount=", entryCount); @@ -4689,7 +5067,7 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { data.talents.reserve(entryCount); for (uint16_t i = 0; i < entryCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 5) { + if (!packet.hasRemaining(5)) { LOG_ERROR("SMSG_TALENTS_INFO: truncated entry list at i=", i); return false; } @@ -4697,11 +5075,11 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { uint8_t rank = packet.readUInt8(); data.talents.push_back({id, rank}); - LOG_INFO(" Entry: id=", id, " rank=", (int)rank); + LOG_INFO(" Entry: id=", id, " rank=", static_cast(rank)); } // Parse glyph tail: glyphSlots + glyphIds[] - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { LOG_WARNING("SMSG_TALENTS_INFO: no glyph tail data"); return true; // Not fatal, older formats may not have glyphs } @@ -4710,17 +5088,17 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { // Sanity check: Wrath has 6 glyph slots, cap at 12 for safety if (glyphSlots > 12) { - LOG_WARNING("SMSG_TALENTS_INFO: glyphSlots too large (", (int)glyphSlots, "), clamping to 12"); + LOG_WARNING("SMSG_TALENTS_INFO: glyphSlots too large (", static_cast(glyphSlots), "), clamping to 12"); glyphSlots = 12; } - LOG_INFO(" GlyphSlots: ", (int)glyphSlots); + LOG_INFO(" GlyphSlots: ", static_cast(glyphSlots)); data.glyphs.clear(); data.glyphs.reserve(glyphSlots); for (uint8_t i = 0; i < glyphSlots; ++i) { - if (packet.getSize() - packet.getReadPos() < 2) { + if (!packet.hasRemaining(2)) { LOG_ERROR("SMSG_TALENTS_INFO: truncated glyph list at i=", i); return false; } @@ -4732,7 +5110,7 @@ bool TalentsInfoParser::parse(network::Packet& packet, TalentsInfoData& data) { } LOG_INFO("SMSG_TALENTS_INFO: bytesConsumed=", (packet.getReadPos() - startPos), - " bytesRemaining=", (packet.getSize() - packet.getReadPos())); + " bytesRemaining=", (packet.getRemainingSize())); return true; } @@ -4768,6 +5146,12 @@ network::Packet RepopRequestPacket::build() { return packet; } +network::Packet ReclaimCorpsePacket::build(uint64_t guid) { + network::Packet packet(wireOpcode(Opcode::CMSG_RECLAIM_CORPSE)); + packet.writeUInt64(guid); + return packet; +} + network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) { network::Packet packet(wireOpcode(Opcode::CMSG_SPIRIT_HEALER_ACTIVATE)); packet.writeUInt64(npcGuid); @@ -4787,7 +5171,7 @@ network::Packet ResurrectResponsePacket::build(uint64_t casterGuid, bool accept) bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data) { // Minimum: windowInfo(4) + npcGuid(8) + nearestNode(4) + at least 1 mask uint32(4) - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 4 + 8 + 4 + 4) { LOG_ERROR("ShowTaxiNodesParser: packet too short (", remaining, " bytes)"); return false; @@ -4796,7 +5180,7 @@ bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data data.npcGuid = packet.readUInt64(); data.nearestNode = packet.readUInt32(); // Read as many mask uint32s as available (Classic/Vanilla=4, WotLK=12) - size_t maskBytes = packet.getSize() - packet.getReadPos(); + size_t maskBytes = packet.getRemainingSize(); uint32_t maskCount = static_cast(maskBytes / 4); if (maskCount > TLK_TAXI_MASK_SIZE) maskCount = TLK_TAXI_MASK_SIZE; for (uint32_t i = 0; i < maskCount; ++i) { @@ -4808,7 +5192,7 @@ bool ShowTaxiNodesParser::parse(network::Packet& packet, ShowTaxiNodesData& data } bool ActivateTaxiReplyParser::parse(network::Packet& packet, ActivateTaxiReplyData& data) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining >= 4) { data.result = packet.readUInt32(); } else if (remaining >= 1) { @@ -4888,11 +5272,12 @@ network::Packet MailTakeMoneyPacket::build(uint64_t mailboxGuid, uint32_t mailId return packet; } -network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemIndex) { +network::Packet MailTakeItemPacket::build(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemGuidLow) { network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM)); packet.writeUInt64(mailboxGuid); packet.writeUInt32(mailId); - packet.writeUInt32(itemIndex); + // WotLK expects attachment item GUID low, not attachment slot index. + packet.writeUInt32(itemGuidLow); return packet; } @@ -4915,20 +5300,20 @@ network::Packet MailMarkAsReadPacket::build(uint64_t mailboxGuid, uint32_t mailI // PacketParsers::parseMailList — WotLK 3.3.5a format (base/default) // ============================================================================ bool PacketParsers::parseMailList(network::Packet& packet, std::vector& inbox) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 5) return false; uint32_t totalCount = packet.readUInt32(); uint8_t shownCount = packet.readUInt8(); (void)totalCount; - LOG_INFO("SMSG_MAIL_LIST_RESULT (WotLK): total=", totalCount, " shown=", (int)shownCount); + LOG_INFO("SMSG_MAIL_LIST_RESULT (WotLK): total=", totalCount, " shown=", static_cast(shownCount)); inbox.clear(); inbox.reserve(shownCount); for (uint8_t i = 0; i < shownCount; ++i) { - remaining = packet.getSize() - packet.getReadPos(); + remaining = packet.getRemainingSize(); if (remaining < 2) break; uint16_t msgSize = packet.readUInt16(); @@ -4959,10 +5344,9 @@ bool PacketParsers::parseMailList(network::Packet& packet, std::vector packet.getSize()) { + if (!packet.hasRemaining(1)) { LOG_WARNING("GuildBankListParser: truncated before tabCount"); data.tabs.clear(); } else { uint8_t tabCount = packet.readUInt8(); // Cap at 8 (normal guild bank tab limit in WoW) if (tabCount > 8) { - LOG_WARNING("GuildBankListParser: tabCount capped (requested=", (int)tabCount, ")"); + LOG_WARNING("GuildBankListParser: tabCount capped (requested=", static_cast(tabCount), ")"); tabCount = 8; } data.tabs.resize(tabCount); for (uint8_t i = 0; i < tabCount; ++i) { // Validate before reading strings - if (packet.getReadPos() >= packet.getSize()) { - LOG_WARNING("GuildBankListParser: truncated tab at index ", (int)i); + if (!packet.hasData()) { + LOG_WARNING("GuildBankListParser: truncated tab at index ", static_cast(i)); break; } data.tabs[i].tabName = packet.readString(); - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { data.tabs[i].tabIcon.clear(); } else { data.tabs[i].tabIcon = packet.readString(); @@ -5145,7 +5531,7 @@ bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) { } } - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { LOG_WARNING("GuildBankListParser: truncated before numSlots"); data.tabItems.clear(); return true; @@ -5155,8 +5541,8 @@ bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) { data.tabItems.clear(); for (uint8_t i = 0; i < numSlots; ++i) { // Validate minimum bytes before reading slot (slotId(1) + itemEntry(4) = 5) - if (packet.getReadPos() + 5 > packet.getSize()) { - LOG_WARNING("GuildBankListParser: truncated slot at index ", (int)i); + if (!packet.hasRemaining(5)) { + LOG_WARNING("GuildBankListParser: truncated slot at index ", static_cast(i)); break; } GuildBankItemSlot slot; @@ -5164,12 +5550,12 @@ bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) { slot.itemEntry = packet.readUInt32(); if (slot.itemEntry != 0) { // Validate before reading enchant mask - if (packet.getReadPos() + 4 > packet.getSize()) break; + if (!packet.hasRemaining(4)) break; // Enchant info uint32_t enchantMask = packet.readUInt32(); for (int bit = 0; bit < 10; ++bit) { if (enchantMask & (1u << bit)) { - if (packet.getReadPos() + 12 > packet.getSize()) { + if (!packet.hasRemaining(12)) { LOG_WARNING("GuildBankListParser: truncated enchant data"); break; } @@ -5181,7 +5567,7 @@ bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) { } } // Validate before reading remaining item fields - if (packet.getReadPos() + 12 > packet.getSize()) { + if (!packet.hasRemaining(12)) { LOG_WARNING("GuildBankListParser: truncated item fields"); break; } @@ -5189,7 +5575,7 @@ bool GuildBankListParser::parse(network::Packet& packet, GuildBankData& data) { /*spare=*/ packet.readUInt32(); slot.randomPropertyId = packet.readUInt32(); if (slot.randomPropertyId) { - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("GuildBankListParser: truncated suffix factor"); break; } @@ -5212,7 +5598,7 @@ network::Packet AuctionHelloPacket::build(uint64_t guid) { } bool AuctionHelloParser::parse(network::Packet& packet, AuctionHelloData& data) { - size_t remaining = packet.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); if (remaining < 12) { LOG_WARNING("AuctionHelloParser: too small, remaining=", remaining); return false; @@ -5220,7 +5606,7 @@ bool AuctionHelloParser::parse(network::Packet& packet, AuctionHelloData& data) data.auctioneerGuid = packet.readUInt64(); data.auctionHouseId = packet.readUInt32(); // WotLK has an extra uint8 enabled field; Vanilla does not - if (packet.getReadPos() < packet.getSize()) { + if (packet.hasData()) { data.enabled = packet.readUInt8(); } else { data.enabled = 1; @@ -5312,7 +5698,7 @@ bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& // bidderGuid(8) + curBid(4) // Classic: numEnchantSlots=1 → 80 bytes/entry // TBC/WotLK: numEnchantSlots=3 → 104 bytes/entry - if (packet.getSize() - packet.getReadPos() < 4) return false; + if (!packet.hasRemaining(4)) return false; uint32_t count = packet.readUInt32(); // Cap auction count to prevent unbounded memory allocation @@ -5327,7 +5713,7 @@ bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& const size_t minPerEntry = static_cast(8 + numEnchantSlots * 12 + 28 + 8 + 8); for (uint32_t i = 0; i < count; ++i) { - if (packet.getReadPos() + minPerEntry > packet.getSize()) break; + if (!packet.hasRemaining(minPerEntry)) break; AuctionEntry e; e.auctionId = packet.readUInt32(); e.itemEntry = packet.readUInt32(); @@ -5356,7 +5742,7 @@ bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& data.auctions.push_back(e); } - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { data.totalCount = packet.readUInt32(); data.searchDelay = packet.readUInt32(); } @@ -5364,15 +5750,63 @@ bool AuctionListResultParser::parse(network::Packet& packet, AuctionListResult& } bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandResult& data) { - if (packet.getSize() - packet.getReadPos() < 12) return false; + if (!packet.hasRemaining(12)) return false; data.auctionId = packet.readUInt32(); data.action = packet.readUInt32(); data.errorCode = packet.readUInt32(); - if (data.errorCode != 0 && data.action == 2 && packet.getReadPos() + 4 <= packet.getSize()) { + if (data.errorCode != 0 && data.action == 2 && packet.hasRemaining(4)) { data.bidError = packet.readUInt32(); } return true; } +// ============================================================ +// Pet Stable System +// ============================================================ + +network::Packet ListStabledPetsPacket::build(uint64_t stableMasterGuid) { + network::Packet p(wireOpcode(Opcode::MSG_LIST_STABLED_PETS)); + p.writeUInt64(stableMasterGuid); + return p; +} + +network::Packet StablePetPacket::build(uint64_t stableMasterGuid, uint8_t slot) { + network::Packet p(wireOpcode(Opcode::CMSG_STABLE_PET)); + p.writeUInt64(stableMasterGuid); + p.writeUInt8(slot); + return p; +} + +network::Packet UnstablePetPacket::build(uint64_t stableMasterGuid, uint32_t petNumber) { + network::Packet p(wireOpcode(Opcode::CMSG_UNSTABLE_PET)); + p.writeUInt64(stableMasterGuid); + p.writeUInt32(petNumber); + return p; +} + +network::Packet PetRenamePacket::build(uint64_t petGuid, const std::string& name, uint8_t isDeclined) { + network::Packet p(wireOpcode(Opcode::CMSG_PET_RENAME)); + p.writeUInt64(petGuid); + p.writeString(name); // null-terminated + p.writeUInt8(isDeclined); + return p; +} + +network::Packet SetTitlePacket::build(int32_t titleBit) { + // CMSG_SET_TITLE: int32 titleBit (-1 = remove active title) + network::Packet p(wireOpcode(Opcode::CMSG_SET_TITLE)); + p.writeUInt32(static_cast(titleBit)); + return p; +} + +network::Packet AlterAppearancePacket::build(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { + // CMSG_ALTER_APPEARANCE: uint32 hairStyle + uint32 hairColor + uint32 facialHair + network::Packet p(wireOpcode(Opcode::CMSG_ALTER_APPEARANCE)); + p.writeUInt32(hairStyle); + p.writeUInt32(hairColor); + p.writeUInt32(facialHair); + return p; +} + } // namespace game } // namespace wowee 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/network/packet.cpp b/src/network/packet.cpp index d82469b9..2a20298e 100644 --- a/src/network/packet.cpp +++ b/src/network/packet.cpp @@ -86,6 +86,33 @@ float Packet::readFloat() { return value; } +uint64_t Packet::readPackedGuid() { + uint8_t mask = readUInt8(); + if (mask == 0) return 0; + uint64_t guid = 0; + for (int i = 0; i < 8; ++i) { + if (mask & (1 << i)) + guid |= static_cast(readUInt8()) << (i * 8); + } + return guid; +} + +void Packet::writePackedGuid(uint64_t guid) { + uint8_t mask = 0; + uint8_t guidBytes[8]; + int count = 0; + for (int i = 0; i < 8; ++i) { + uint8_t byte = static_cast((guid >> (i * 8)) & 0xFF); + if (byte != 0) { + mask |= (1 << i); + guidBytes[count++] = byte; + } + } + writeUInt8(mask); + for (int i = 0; i < count; ++i) + writeUInt8(guidBytes[i]); +} + std::string Packet::readString() { std::string result; while (readPos < data.size()) { diff --git a/src/network/tcp_socket.cpp b/src/network/tcp_socket.cpp index 2dbf1b57..e149d0ef 100644 --- a/src/network/tcp_socket.cpp +++ b/src/network/tcp_socket.cpp @@ -185,7 +185,7 @@ void TCPSocket::tryParsePackets() { if (expectedSize == 0) { // Unknown opcode or need more data to determine size - LOG_WARNING("Unknown opcode or indeterminate size: 0x", std::hex, (int)opcode, std::dec); + LOG_WARNING("Unknown opcode or indeterminate size: 0x", std::hex, static_cast(opcode), std::dec); break; } @@ -197,7 +197,7 @@ void TCPSocket::tryParsePackets() { } // We have a complete packet! - LOG_DEBUG("Parsing packet: opcode=0x", std::hex, (int)opcode, std::dec, + LOG_DEBUG("Parsing packet: opcode=0x", std::hex, static_cast(opcode), std::dec, " size=", expectedSize, " bytes"); // Create packet from buffer data @@ -285,7 +285,7 @@ size_t TCPSocket::getExpectedPacketSize(uint8_t opcode) { return 0; // Need more data to read size field default: - LOG_WARNING("Unknown auth packet opcode: 0x", std::hex, (int)opcode, std::dec); + LOG_WARNING("Unknown auth packet opcode: 0x", std::hex, static_cast(opcode), std::dec); return 0; } } diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 78c90c8e..4482e3f3 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -1,6 +1,7 @@ #include "network/world_socket.hpp" #include "network/packet.hpp" #include "network/net_platform.hpp" +#include "game/opcode_table.hpp" #include "auth/crypto.hpp" #include "core/logger.hpp" #include @@ -9,10 +10,52 @@ #include #include #include +#include +#include namespace { constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024; -constexpr int kMaxParsedPacketsPerUpdate = 220; +constexpr int kDefaultMaxParsedPacketsPerUpdate = 16; +constexpr int kAbsoluteMaxParsedPacketsPerUpdate = 220; +constexpr int kMinParsedPacketsPerUpdate = 8; +constexpr int kDefaultMaxPacketCallbacksPerUpdate = 6; +constexpr int kAbsoluteMaxPacketCallbacksPerUpdate = 64; +constexpr int kMinPacketCallbacksPerUpdate = 1; +constexpr int kMaxRecvCallsPerUpdate = 64; +constexpr size_t kMaxRecvBytesPerUpdate = 512 * 1024; +constexpr size_t kMaxQueuedPacketCallbacks = 4096; +constexpr int kAsyncPumpSleepMs = 2; +constexpr size_t kRecentPacketHistoryLimit = 96; +constexpr auto kRecentPacketHistoryWindow = std::chrono::seconds(15); +constexpr const char* kCloseTraceEnv = "WOWEE_NET_CLOSE_TRACE"; + +inline int parsedPacketsBudgetPerUpdate() { + static int budget = []() { + const char* raw = std::getenv("WOWEE_NET_MAX_PARSED_PACKETS"); + if (!raw || !*raw) return kDefaultMaxParsedPacketsPerUpdate; + char* end = nullptr; + long parsed = std::strtol(raw, &end, 10); + if (end == raw) return kDefaultMaxParsedPacketsPerUpdate; + if (parsed < kMinParsedPacketsPerUpdate) return kMinParsedPacketsPerUpdate; + if (parsed > kAbsoluteMaxParsedPacketsPerUpdate) return kAbsoluteMaxParsedPacketsPerUpdate; + return static_cast(parsed); + }(); + return budget; +} + +inline int packetCallbacksBudgetPerUpdate() { + static int budget = []() { + const char* raw = std::getenv("WOWEE_NET_MAX_PACKET_CALLBACKS"); + if (!raw || !*raw) return kDefaultMaxPacketCallbacksPerUpdate; + char* end = nullptr; + long parsed = std::strtol(raw, &end, 10); + if (end == raw) return kDefaultMaxPacketCallbacksPerUpdate; + if (parsed < kMinPacketCallbacksPerUpdate) return kMinPacketCallbacksPerUpdate; + if (parsed > kAbsoluteMaxPacketCallbacksPerUpdate) return kAbsoluteMaxPacketCallbacksPerUpdate; + return static_cast(parsed); + }(); + return budget; +} inline bool isLoginPipelineSmsg(uint16_t opcode) { switch (opcode) { @@ -49,6 +92,14 @@ inline bool envFlagEnabled(const char* key, bool defaultValue = false) { return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' || raw[0] == 'n' || raw[0] == 'N'); } + +const char* opcodeNameForTrace(uint16_t wireOpcode) { + const auto* table = wowee::game::getActiveOpcodeTable(); + if (!table) return "UNKNOWN"; + auto logical = table->fromWire(wireOpcode); + if (!logical) return "UNKNOWN"; + return wowee::game::OpcodeTable::logicalToName(*logical); +} } // namespace namespace wowee { @@ -71,6 +122,7 @@ WorldSocket::WorldSocket() { receiveBuffer.reserve(64 * 1024); useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", true); useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false); + useAsyncPump_ = envFlagEnabled("WOWEE_NET_ASYNC_PUMP", true); if (useParseScratchQueue_) { LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off"); useParseScratchQueue_ = false; @@ -79,7 +131,10 @@ WorldSocket::WorldSocket() { parsedPacketsScratch_.reserve(64); } LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off", - " parse_scratch=", useParseScratchQueue_ ? "on" : "off"); + " async_pump=", useAsyncPump_ ? "on" : "off", + " parse_scratch=", useParseScratchQueue_ ? "on" : "off", + " max_parsed_packets=", parsedPacketsBudgetPerUpdate(), + " max_packet_callbacks=", packetCallbacksBudgetPerUpdate()); } WorldSocket::~WorldSocket() { @@ -89,6 +144,8 @@ WorldSocket::~WorldSocket() { bool WorldSocket::connect(const std::string& host, uint16_t port) { LOG_INFO("Connecting to world server: ", host, ":", port); + stopAsyncPump(); + // Create socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == INVALID_SOCK) { @@ -165,32 +222,90 @@ bool WorldSocket::connect(const std::string& host, uint16_t port) { connected = true; LOG_INFO("Connected to world server: ", host, ":", port); + startAsyncPump(); return true; } void WorldSocket::disconnect() { + stopAsyncPump(); + { + std::lock_guard lock(ioMutex_); + closeSocketNoJoin(); + encryptionEnabled = false; + useVanillaCrypt = false; + receiveBuffer.clear(); + receiveReadOffset_ = 0; + parsedPacketsScratch_.clear(); + headerBytesDecrypted = 0; + packetTraceStart_ = {}; + packetTraceUntil_ = {}; + packetTraceReason_.clear(); + } + { + std::lock_guard lock(callbackMutex_); + pendingPacketCallbacks_.clear(); + } + LOG_INFO("Disconnected from world server"); +} + +void WorldSocket::tracePacketsFor(std::chrono::milliseconds duration, const std::string& reason) { + std::lock_guard lock(ioMutex_); + packetTraceStart_ = std::chrono::steady_clock::now(); + packetTraceUntil_ = packetTraceStart_ + duration; + packetTraceReason_ = reason; + LOG_DEBUG("WS TRACE enabled: reason='", packetTraceReason_, + "' durationMs=", duration.count()); +} + +bool WorldSocket::isConnected() const { + std::lock_guard lock(ioMutex_); + return connected; +} + +void WorldSocket::closeSocketNoJoin() { if (sockfd != INVALID_SOCK) { net::closeSocket(sockfd); sockfd = INVALID_SOCK; } connected = false; - encryptionEnabled = false; - useVanillaCrypt = false; - receiveBuffer.clear(); - receiveReadOffset_ = 0; - parsedPacketsScratch_.clear(); - headerBytesDecrypted = 0; - LOG_INFO("Disconnected from world server"); } -bool WorldSocket::isConnected() const { - return connected; +void WorldSocket::recordRecentPacket(bool outbound, uint16_t opcode, uint16_t payloadLen) { + const auto now = std::chrono::steady_clock::now(); + recentPacketHistory_.push_back(RecentPacketTrace{now, outbound, opcode, payloadLen}); + while (!recentPacketHistory_.empty() && + (recentPacketHistory_.size() > kRecentPacketHistoryLimit || + (now - recentPacketHistory_.front().when) > kRecentPacketHistoryWindow)) { + recentPacketHistory_.pop_front(); + } +} + +void WorldSocket::dumpRecentPacketHistoryLocked(const char* reason, size_t bufferedBytes) { + if (recentPacketHistory_.empty()) { + LOG_WARNING("WS CLOSE TRACE reason='", reason, "' buffered=", bufferedBytes, + " no recent packet history"); + return; + } + + const auto lastWhen = recentPacketHistory_.back().when; + LOG_WARNING("WS CLOSE TRACE reason='", reason, "' buffered=", bufferedBytes, + " recentPackets=", recentPacketHistory_.size()); + for (const auto& entry : recentPacketHistory_) { + const auto ageMs = std::chrono::duration_cast( + lastWhen - entry.when).count(); + LOG_WARNING("WS CLOSE TRACE ", entry.outbound ? "TX" : "RX", + " -", ageMs, "ms opcode=0x", + std::hex, entry.opcode, std::dec, + " logical=", opcodeNameForTrace(entry.opcode), + " payload=", entry.payloadLen); + } } void WorldSocket::send(const Packet& packet) { - if (!connected) return; static const bool kLogCharCreatePayload = envFlagEnabled("WOWEE_NET_LOG_CHAR_CREATE", false); static const bool kLogSwapItemPackets = envFlagEnabled("WOWEE_NET_LOG_SWAP_ITEM", false); + std::lock_guard lock(ioMutex_); + if (!connected || sockfd == INVALID_SOCK) return; const auto& data = packet.getData(); uint16_t opcode = packet.getOpcode(); @@ -217,24 +332,24 @@ void WorldSocket::send(const Packet& packet) { rd8(skin) && rd8(face) && rd8(hairStyle) && rd8(hairColor) && rd8(facial) && rd8(outfit); if (ok) { LOG_INFO("CMSG_CHAR_CREATE payload: name='", name, - "' race=", (int)race, " class=", (int)cls, " gender=", (int)gender, - " skin=", (int)skin, " face=", (int)face, - " hairStyle=", (int)hairStyle, " hairColor=", (int)hairColor, - " facial=", (int)facial, " outfit=", (int)outfit, + "' race=", static_cast(race), " class=", static_cast(cls), " gender=", static_cast(gender), + " skin=", static_cast(skin), " face=", static_cast(face), + " hairStyle=", static_cast(hairStyle), " hairColor=", static_cast(hairColor), + " facial=", static_cast(facial), " outfit=", static_cast(outfit), " payloadLen=", payloadLen); // Persist to disk so we can compare TX vs DB even if the console scrolls away. std::ofstream f("charcreate_payload.log", std::ios::app); if (f.is_open()) { f << "name='" << name << "'" - << " race=" << (int)race - << " class=" << (int)cls - << " gender=" << (int)gender - << " skin=" << (int)skin - << " face=" << (int)face - << " hairStyle=" << (int)hairStyle - << " hairColor=" << (int)hairColor - << " facial=" << (int)facial - << " outfit=" << (int)outfit + << " race=" << static_cast(race) + << " class=" << static_cast(cls) + << " gender=" << static_cast(gender) + << " skin=" << static_cast(skin) + << " face=" << static_cast(face) + << " hairStyle=" << static_cast(hairStyle) + << " hairColor=" << static_cast(hairColor) + << " facial=" << static_cast(facial) + << " outfit=" << static_cast(outfit) << " payloadLen=" << payloadLen << "\n"; } @@ -245,13 +360,20 @@ void WorldSocket::send(const Packet& packet) { } if (kLogSwapItemPackets && (opcode == 0x10C || opcode == 0x10D)) { // CMSG_SWAP_ITEM / CMSG_SWAP_INV_ITEM - std::string hex; - for (size_t i = 0; i < data.size(); i++) { - char buf[4]; - snprintf(buf, sizeof(buf), "%02x ", data[i]); - hex += buf; - } - LOG_INFO("WS TX opcode=0x", std::hex, opcode, std::dec, " payloadLen=", payloadLen, " data=[", hex, "]"); + LOG_INFO("WS TX opcode=0x", std::hex, opcode, std::dec, " payloadLen=", payloadLen, + " data=[", core::toHexString(data.data(), data.size(), true), "]"); + } + + const auto traceNow = std::chrono::steady_clock::now(); + recordRecentPacket(true, opcode, payloadLen); + if (packetTraceUntil_ > traceNow) { + const auto elapsedMs = std::chrono::duration_cast( + traceNow - packetTraceStart_).count(); + LOG_DEBUG("WS TRACE TX +", elapsedMs, "ms opcode=0x", + std::hex, opcode, std::dec, + " logical=", opcodeNameForTrace(opcode), + " payload=", payloadLen, + " reason='", packetTraceReason_, "'"); } // WotLK 3.3.5 CMSG header (6 bytes total): @@ -291,14 +413,8 @@ void WorldSocket::send(const Packet& packet) { // Debug: dump packet bytes for AUTH_SESSION if (opcode == 0x1ED) { - std::string hexDump = "AUTH_SESSION raw bytes: "; - for (size_t i = 0; i < sendData.size(); ++i) { - char buf[4]; - snprintf(buf, sizeof(buf), "%02x ", sendData[i]); - hexDump += buf; - if ((i + 1) % 32 == 0) hexDump += "\n"; - } - LOG_DEBUG(hexDump); + LOG_DEBUG("AUTH_SESSION raw bytes: ", + core::toHexString(sendData.data(), sendData.size(), true)); } if (isLoginPipelineCmsg(opcode)) { LOG_INFO("WS TX LOGIN opcode=0x", std::hex, opcode, std::dec, @@ -317,7 +433,46 @@ void WorldSocket::send(const Packet& packet) { } void WorldSocket::update() { - if (!connected) return; + if (!useAsyncPump_) { + pumpNetworkIO(); + } + dispatchQueuedPackets(); +} + +void WorldSocket::startAsyncPump() { + if (!useAsyncPump_ || asyncPumpRunning_.load(std::memory_order_acquire)) { + return; + } + asyncPumpStop_.store(false, std::memory_order_release); + asyncPumpThread_ = std::thread(&WorldSocket::asyncPumpLoop, this); +} + +void WorldSocket::stopAsyncPump() { + asyncPumpStop_.store(true, std::memory_order_release); + if (asyncPumpThread_.joinable()) { + asyncPumpThread_.join(); + } + asyncPumpRunning_.store(false, std::memory_order_release); +} + +void WorldSocket::asyncPumpLoop() { + asyncPumpRunning_.store(true, std::memory_order_release); + while (!asyncPumpStop_.load(std::memory_order_acquire)) { + pumpNetworkIO(); + { + std::lock_guard lock(ioMutex_); + if (!connected) { + break; + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(kAsyncPumpSleepMs)); + } + asyncPumpRunning_.store(false, std::memory_order_release); +} + +void WorldSocket::pumpNetworkIO() { + std::lock_guard lock(ioMutex_); + if (!connected || sockfd == INVALID_SOCK) return; auto bufferedBytes = [&]() -> size_t { return (receiveBuffer.size() >= receiveReadOffset_) ? (receiveBuffer.size() - receiveReadOffset_) @@ -343,7 +498,8 @@ void WorldSocket::update() { bool receivedAny = false; size_t bytesReadThisTick = 0; int readOps = 0; - while (connected) { + while (connected && readOps < kMaxRecvCallsPerUpdate && + bytesReadThisTick < kMaxRecvBytesPerUpdate) { uint8_t buffer[4096]; ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer)); @@ -362,7 +518,7 @@ void WorldSocket::update() { LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes, " incoming=", receivedSize, " max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing."); - disconnect(); + closeSocketNoJoin(); return; } const size_t oldSize = receiveBuffer.size(); @@ -375,7 +531,7 @@ void WorldSocket::update() { if (newCap < needed) { LOG_ERROR("World socket receive buffer capacity growth failed (needed=", needed, " max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing."); - disconnect(); + closeSocketNoJoin(); return; } receiveBuffer.reserve(newCap); @@ -387,7 +543,7 @@ void WorldSocket::update() { if (bufferedBytes() > kMaxReceiveBufferBytes) { LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(), " bytes). Disconnecting to recover framing."); - disconnect(); + closeSocketNoJoin(); return; } continue; @@ -409,7 +565,7 @@ void WorldSocket::update() { } LOG_ERROR("Receive failed: ", net::errorString(err)); - disconnect(); + closeSocketNoJoin(); return; } @@ -421,11 +577,9 @@ void WorldSocket::update() { } // Hex dump received bytes for auth debugging (debug-only to avoid per-frame string work) if (debugLog && bytesReadThisTick <= 128) { - std::string hex; - for (size_t i = receiveReadOffset_; i < receiveBuffer.size(); ++i) { - char buf[4]; snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]); hex += buf; - } - LOG_DEBUG("World socket raw bytes: ", hex); + LOG_DEBUG("World socket raw bytes: ", + core::toHexString(receiveBuffer.data() + receiveReadOffset_, + receiveBuffer.size() - receiveReadOffset_, true)); } tryParsePackets(); if (debugLog && connected && bufferedBytes() > 0) { @@ -434,10 +588,16 @@ void WorldSocket::update() { } } + if (connected && (readOps >= kMaxRecvCallsPerUpdate || bytesReadThisTick >= kMaxRecvBytesPerUpdate)) { + LOG_DEBUG("World socket recv budget reached (calls=", readOps, + ", bytes=", bytesReadThisTick, "), deferring remaining socket drain"); + } + if (sawClose) { - LOG_INFO("World server connection closed (receivedAny=", receivedAny, + dumpRecentPacketHistoryLocked("peer_closed", bufferedBytes()); + LOG_WARNING("World server connection closed by peer (receivedAny=", receivedAny, " buffered=", bufferedBytes(), ")"); - disconnect(); + closeSocketNoJoin(); return; } } @@ -462,7 +622,8 @@ void WorldSocket::tryParsePackets() { } else { parsedPacketsLocal.reserve(32); } - while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) { + const int maxParsedThisTick = parsedPacketsBudgetPerUpdate(); + while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < maxParsedThisTick) { uint8_t rawHeader[4] = {0, 0, 0, 0}; std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4); @@ -491,10 +652,10 @@ void WorldSocket::tryParsePackets() { static_cast(rawHeader[2]), " ", static_cast(rawHeader[3]), std::dec, " enc=", encryptionEnabled, ". Disconnecting to recover stream."); - disconnect(); + closeSocketNoJoin(); return; } - constexpr uint16_t kMaxWorldPacketSize = 0x4000; + constexpr uint16_t kMaxWorldPacketSize = 0x8000; // 32KB — allows large guild rosters, auction lists if (size > kMaxWorldPacketSize) { LOG_ERROR("World packet framing desync: oversized packet size=", size, " rawHdr=", std::hex, @@ -503,7 +664,7 @@ void WorldSocket::tryParsePackets() { static_cast(rawHeader[2]), " ", static_cast(rawHeader[3]), std::dec, " enc=", encryptionEnabled, ". Disconnecting to recover stream."); - disconnect(); + closeSocketNoJoin(); return; } @@ -535,6 +696,17 @@ void WorldSocket::tryParsePackets() { " buffered=", (receiveBuffer.size() - parseOffset), " enc=", encryptionEnabled ? "yes" : "no"); } + recordRecentPacket(false, opcode, payloadLen); + const auto traceNow = std::chrono::steady_clock::now(); + if (packetTraceUntil_ > traceNow) { + const auto elapsedMs = std::chrono::duration_cast( + traceNow - packetTraceStart_).count(); + LOG_DEBUG("WS TRACE RX +", elapsedMs, "ms opcode=0x", + std::hex, opcode, std::dec, + " logical=", opcodeNameForTrace(opcode), + " payload=", payloadLen, + " reason='", packetTraceReason_, "'"); + } if ((receiveBuffer.size() - parseOffset) < totalSize) { // Not enough data yet - header stays decrypted in buffer @@ -555,7 +727,7 @@ void WorldSocket::tryParsePackets() { " payload=", payloadLen, " buffered=", receiveBuffer.size(), " parseOffset=", parseOffset, " what=", e.what(), ". Disconnecting to recover."); - disconnect(); + closeSocketNoJoin(); return; } parseOffset += totalSize; @@ -578,23 +750,57 @@ void WorldSocket::tryParsePackets() { } headerBytesDecrypted = localHeaderBytesDecrypted; - if (packetCallback) { - for (const auto& packet : *parsedPackets) { - if (!connected) break; - packetCallback(packet); + // Queue parsed packets for main-thread dispatch. + if (!parsedPackets->empty()) { + std::lock_guard callbackLock(callbackMutex_); + for (auto& packet : *parsedPackets) { + pendingPacketCallbacks_.push_back(std::move(packet)); + } + if (pendingPacketCallbacks_.size() > kMaxQueuedPacketCallbacks) { + LOG_ERROR("World socket callback queue overflow (", pendingPacketCallbacks_.size(), + " packets). Disconnecting to recover."); + pendingPacketCallbacks_.clear(); + closeSocketNoJoin(); + return; } } const size_t buffered = (receiveBuffer.size() >= receiveReadOffset_) ? (receiveBuffer.size() - receiveReadOffset_) : 0; - if (parsedThisTick >= kMaxParsedPacketsPerUpdate && buffered >= 4) { + if (parsedThisTick >= maxParsedThisTick && buffered >= 4) { LOG_DEBUG("World socket parse budget reached (", parsedThisTick, " packets); deferring remaining buffered data=", buffered, " bytes"); } } +void WorldSocket::dispatchQueuedPackets() { + std::deque localPackets; + { + std::lock_guard lock(callbackMutex_); + if (!packetCallback || pendingPacketCallbacks_.empty()) { + return; + } + const int maxCallbacksThisTick = packetCallbacksBudgetPerUpdate(); + for (int i = 0; i < maxCallbacksThisTick && !pendingPacketCallbacks_.empty(); ++i) { + localPackets.push_back(std::move(pendingPacketCallbacks_.front())); + pendingPacketCallbacks_.pop_front(); + } + if (!pendingPacketCallbacks_.empty()) { + LOG_DEBUG("World socket callback budget reached (", localPackets.size(), + " callbacks); deferring ", pendingPacketCallbacks_.size(), + " queued packet callbacks"); + } + } + + while (!localPackets.empty()) { + packetCallback(localPackets.front()); + localPackets.pop_front(); + } +} + void WorldSocket::initEncryption(const std::vector& sessionKey, uint32_t build) { + std::lock_guard lock(ioMutex_); if (sessionKey.size() != 40) { LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)"); return; diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 469df669..dd311e2e 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -117,9 +117,9 @@ void AssetManager::shutdown() { LOG_INFO("Shutting down asset manager"); if (fileCacheHits + fileCacheMisses > 0) { - float hitRate = (float)fileCacheHits / (fileCacheHits + fileCacheMisses) * 100.0f; + float hitRate = static_cast(fileCacheHits) / (fileCacheHits + fileCacheMisses) * 100.0f; LOG_INFO("File cache stats: ", fileCacheHits, " hits, ", fileCacheMisses, " misses (", - (int)hitRate, "% hit rate), ", fileCacheTotalBytes / 1024 / 1024, " MB cached"); + static_cast(hitRate), "% hit rate), ", fileCacheTotalBytes / 1024 / 1024, " MB cached"); } clearCache(); diff --git a/src/pipeline/blp_loader.cpp b/src/pipeline/blp_loader.cpp index 8c817890..7aaaf7f3 100644 --- a/src/pipeline/blp_loader.cpp +++ b/src/pipeline/blp_loader.cpp @@ -126,8 +126,8 @@ BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) { LOG_DEBUG("Loading BLP2: ", image.width, "x", image.height, " ", getCompressionName(image.compression), - " (comp=", (int)header.compression, " alphaDepth=", (int)header.alphaDepth, - " alphaEnc=", (int)header.alphaEncoding, " mipOfs=", header.mipOffsets[0], + " (comp=", static_cast(header.compression), " alphaDepth=", static_cast(header.alphaDepth), + " alphaEnc=", static_cast(header.alphaEncoding), " mipOfs=", header.mipOffsets[0], " mipSize=", header.mipSizes[0], ")"); // Get first mipmap (full resolution) @@ -253,7 +253,7 @@ void BLPLoader::decompressDXT3(const uint8_t* src, uint8_t* dst, int width, int // First 8 bytes: 4-bit alpha values uint64_t alphaBlock = 0; for (int i = 0; i < 8; i++) { - alphaBlock |= (uint64_t)block[i] << (i * 8); + alphaBlock |= static_cast(block[i]) << (i * 8); } // Color block (same as DXT1) starts at byte 8 @@ -336,7 +336,7 @@ void BLPLoader::decompressDXT5(const uint8_t* src, uint8_t* dst, int width, int // Alpha indices (48 bits for 16 pixels, 3 bits each) uint64_t alphaIndices = 0; for (int i = 2; i < 8; i++) { - alphaIndices |= (uint64_t)block[i] << ((i - 2) * 8); + alphaIndices |= static_cast(block[i]) << ((i - 2) * 8); } // Color block (same as DXT1) starts at byte 8 diff --git a/src/pipeline/dbc_layout.cpp b/src/pipeline/dbc_layout.cpp index 08730536..7d3878fe 100644 --- a/src/pipeline/dbc_layout.cpp +++ b/src/pipeline/dbc_layout.cpp @@ -1,7 +1,9 @@ #include "pipeline/dbc_layout.hpp" +#include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" #include #include +#include namespace wowee { namespace pipeline { @@ -94,5 +96,69 @@ const DBCFieldMap* DBCLayout::getLayout(const std::string& dbcName) const { return (it != layouts_.end()) ? &it->second : nullptr; } +CharSectionsFields detectCharSectionsFields(const DBCFile* dbc, const DBCFieldMap* csL) { + // Cache: avoid re-probing the same DBC on every call. + static const DBCFile* s_cachedDbc = nullptr; + static CharSectionsFields s_cachedResult; + if (dbc && dbc == s_cachedDbc) return s_cachedResult; + + CharSectionsFields f; + if (!dbc || dbc->getRecordCount() == 0) return f; + + // Start from the JSON layout (or defaults matching Classic-style: variation-first) + f.raceId = csL ? (*csL)["RaceID"] : 1; + f.sexId = csL ? (*csL)["SexID"] : 2; + f.baseSection = csL ? (*csL)["BaseSection"] : 3; + f.variationIndex = csL ? (*csL)["VariationIndex"] : 4; + f.colorIndex = csL ? (*csL)["ColorIndex"] : 5; + f.texture1 = csL ? (*csL)["Texture1"] : 6; + f.texture2 = csL ? (*csL)["Texture2"] : 7; + f.texture3 = csL ? (*csL)["Texture3"] : 8; + f.flags = csL ? (*csL)["Flags"] : 9; + + // Auto-detect: probe the field that the JSON layout says is VariationIndex. + // In Classic-style layout, VariationIndex (field 4) holds small integers 0-15. + // In stock WotLK layout, field 4 is actually Texture1 (a string block offset, typically > 100). + // Sample up to 20 records and check if all field-4 values are small integers. + uint32_t probeField = f.variationIndex; + if (probeField >= dbc->getFieldCount()) { + s_cachedDbc = dbc; + s_cachedResult = f; + return f; // safety + } + + uint32_t sampleCount = std::min(dbc->getRecordCount(), 20u); + uint32_t largeCount = 0; + uint32_t smallCount = 0; + for (uint32_t r = 0; r < sampleCount; r++) { + uint32_t val = dbc->getUInt32(r, probeField); + if (val > 50) { + ++largeCount; + } else { + ++smallCount; + } + } + + // If most sampled values are large, the JSON layout's VariationIndex field + // actually contains string offsets => this is stock WotLK (texture-first). + // Swap to texture-first layout: Tex1=4, Tex2=5, Tex3=6, Flags=7, Var=8, Color=9. + if (largeCount > smallCount) { + uint32_t base = probeField; // the field index the JSON calls VariationIndex (typically 4) + f.texture1 = base; + f.texture2 = base + 1; + f.texture3 = base + 2; + f.flags = base + 3; + f.variationIndex = base + 4; + f.colorIndex = base + 5; + LOG_INFO("CharSections.dbc: detected stock WotLK layout (textures-first at field ", base, ")"); + } else { + LOG_INFO("CharSections.dbc: detected Classic-style layout (variation-first at field ", probeField, ")"); + } + + s_cachedDbc = dbc; + s_cachedResult = f; + return f; +} + } // namespace pipeline } // namespace wowee diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index b3d057d6..b1f82973 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -1258,6 +1258,125 @@ M2Model M2Loader::load(const std::vector& m2Data) { } // end size check } + // Parse ribbon emitters (WotLK only; vanilla format TBD). + // WotLK M2RibbonEmitter = 0xAC (172) bytes per entry. + static constexpr uint32_t RIBBON_SIZE_WOTLK = 0xAC; + if (header.nRibbonEmitters > 0 && header.ofsRibbonEmitters > 0 && + header.nRibbonEmitters < 64 && header.version >= 264) { + + if (static_cast(header.ofsRibbonEmitters) + + static_cast(header.nRibbonEmitters) * RIBBON_SIZE_WOTLK <= m2Data.size()) { + + // Build sequence flags for parseAnimTrack + std::vector ribSeqFlags; + ribSeqFlags.reserve(model.sequences.size()); + for (const auto& seq : model.sequences) { + ribSeqFlags.push_back(seq.flags); + } + + for (uint32_t ri = 0; ri < header.nRibbonEmitters; ri++) { + uint32_t base = header.ofsRibbonEmitters + ri * RIBBON_SIZE_WOTLK; + + M2RibbonEmitter rib; + rib.ribbonId = readValue(m2Data, base + 0x00); + rib.bone = readValue(m2Data, base + 0x04); + rib.position.x = readValue(m2Data, base + 0x08); + rib.position.y = readValue(m2Data, base + 0x0C); + rib.position.z = readValue(m2Data, base + 0x10); + + // textureIndices M2Array (0x14): count + offset → first element = texture lookup index + { + uint32_t nTex = readValue(m2Data, base + 0x14); + uint32_t ofsTex = readValue(m2Data, base + 0x18); + if (nTex > 0 && ofsTex + sizeof(uint16_t) <= m2Data.size()) { + rib.textureIndex = readValue(m2Data, ofsTex); + } + } + + // materialIndices M2Array (0x1C): count + offset → first element = material index + { + uint32_t nMat = readValue(m2Data, base + 0x1C); + uint32_t ofsMat = readValue(m2Data, base + 0x20); + if (nMat > 0 && ofsMat + sizeof(uint16_t) <= m2Data.size()) { + rib.materialIndex = readValue(m2Data, ofsMat); + } + } + + // colorTrack M2TrackDisk at 0x24 (vec3 RGB 0..1) + if (base + 0x24 + sizeof(M2TrackDisk) <= m2Data.size()) { + M2TrackDisk disk = readValue(m2Data, base + 0x24); + parseAnimTrack(m2Data, disk, rib.colorTrack, TrackType::VEC3, ribSeqFlags); + } + + // alphaTrack M2TrackDisk at 0x38 (fixed16: int16/32767) + // Same nested-array layout as parseAnimTrack but keys are int16. + if (base + 0x38 + sizeof(M2TrackDisk) <= m2Data.size()) { + M2TrackDisk disk = readValue(m2Data, base + 0x38); + auto& track = rib.alphaTrack; + track.interpolationType = disk.interpolationType; + track.globalSequence = disk.globalSequence; + uint32_t nSeqs = disk.nTimestamps; + if (nSeqs > 0 && nSeqs <= 4096) { + track.sequences.resize(nSeqs); + for (uint32_t s = 0; s < nSeqs; s++) { + if (s < ribSeqFlags.size() && !(ribSeqFlags[s] & 0x20)) continue; + uint32_t tsHdr = disk.ofsTimestamps + s * 8; + uint32_t keyHdr = disk.ofsKeys + s * 8; + if (tsHdr + 8 > m2Data.size() || keyHdr + 8 > m2Data.size()) continue; + uint32_t tsCount = readValue(m2Data, tsHdr); + uint32_t tsOfs = readValue(m2Data, tsHdr + 4); + uint32_t kCount = readValue(m2Data, keyHdr); + uint32_t kOfs = readValue(m2Data, keyHdr + 4); + if (tsCount == 0 || kCount == 0) continue; + if (tsOfs + tsCount * 4 > m2Data.size()) continue; + if (kOfs + kCount * sizeof(int16_t) > m2Data.size()) continue; + track.sequences[s].timestamps = readArray(m2Data, tsOfs, tsCount); + auto raw = readArray(m2Data, kOfs, kCount); + track.sequences[s].floatValues.reserve(raw.size()); + for (auto v : raw) { + track.sequences[s].floatValues.push_back( + static_cast(v) / 32767.0f); + } + } + } + } + + // heightAboveTrack M2TrackDisk at 0x4C (float) + if (base + 0x4C + sizeof(M2TrackDisk) <= m2Data.size()) { + M2TrackDisk disk = readValue(m2Data, base + 0x4C); + parseAnimTrack(m2Data, disk, rib.heightAboveTrack, TrackType::FLOAT, ribSeqFlags); + } + + // heightBelowTrack M2TrackDisk at 0x60 (float) + if (base + 0x60 + sizeof(M2TrackDisk) <= m2Data.size()) { + M2TrackDisk disk = readValue(m2Data, base + 0x60); + parseAnimTrack(m2Data, disk, rib.heightBelowTrack, TrackType::FLOAT, ribSeqFlags); + } + + rib.edgesPerSecond = readValue(m2Data, base + 0x74); + rib.edgeLifetime = readValue(m2Data, base + 0x78); + rib.gravity = readValue(m2Data, base + 0x7C); + rib.textureRows = readValue(m2Data, base + 0x80); + rib.textureCols = readValue(m2Data, base + 0x82); + if (rib.textureRows == 0) rib.textureRows = 1; + if (rib.textureCols == 0) rib.textureCols = 1; + + // Clamp to sane values + if (rib.edgesPerSecond < 1.0f || rib.edgesPerSecond > 200.0f) rib.edgesPerSecond = 15.0f; + if (rib.edgeLifetime < 0.05f || rib.edgeLifetime > 10.0f) rib.edgeLifetime = 0.5f; + + // visibilityTrack M2TrackDisk at 0x98 (uint8, treat as float 0/1) + if (base + 0x98 + sizeof(M2TrackDisk) <= m2Data.size()) { + M2TrackDisk disk = readValue(m2Data, base + 0x98); + parseAnimTrack(m2Data, disk, rib.visibilityTrack, TrackType::FLOAT, ribSeqFlags); + } + + model.ribbonEmitters.push_back(std::move(rib)); + } + core::Logger::getInstance().debug(" Ribbon emitters: ", model.ribbonEmitters.size()); + } + } + // Read collision mesh (bounding triangles/vertices/normals) if (header.nBoundingVertices > 0 && header.ofsBoundingVertices > 0) { struct Vec3Disk { float x, y, z; }; diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index 076e4579..3e3a7e19 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -578,7 +578,7 @@ bool WMOLoader::loadGroup(const std::vector& groupData, if (batchLogCount < 15) { core::Logger::getInstance().debug(" Batch[", i, "]: start=", batch.startIndex, " count=", batch.indexCount, " verts=[", batch.startVertex, "-", - batch.lastVertex, "] mat=", (int)batch.materialId, " flags=", (int)batch.flags); + batch.lastVertex, "] mat=", static_cast(batch.materialId), " flags=", static_cast(batch.flags)); batchLogCount++; } } diff --git a/src/rendering/amd_fsr3_runtime.cpp b/src/rendering/amd_fsr3_runtime.cpp index 26fc5ce1..469056d9 100644 --- a/src/rendering/amd_fsr3_runtime.cpp +++ b/src/rendering/amd_fsr3_runtime.cpp @@ -341,7 +341,13 @@ bool AmdFsr3Runtime::initialize(const AmdFsr3RuntimeInitDesc& desc) { const std::string loadedPath = loadedLibraryPath_; lastError_ = "ffxCreateContext (upscale) failed rc=" + std::to_string(upCreateRc) + " (" + ffxApiReturnCodeName(upCreateRc) + "), runtimeLib=" + loadedPath; - shutdown(); + LOG_ERROR("FSR3 runtime/API: FSR3 Upscale create failed at ffxCreateContext: rc=", upCreateRc); + // Don't call full shutdown() here — dlclose() on the AMD runtime library + // can hang on some drivers (notably NVIDIA) when context creation failed. + // Just clean up local state; library stays loaded (harmless leak). + delete fns_; fns_ = nullptr; + ready_ = false; + apiMode_ = ApiMode::LegacyFsr3; return false; } genericUpscaleContext_ = upscaleCtx; diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 50872d46..53be1a25 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -140,6 +140,17 @@ std::optional CameraController::getCachedFloorHeight(float x, float y, fl return result; } +void CameraController::triggerShake(float magnitude, float frequency, float duration) { + // Allow stronger shake to override weaker; don't allow zero magnitude. + if (magnitude <= 0.0f || duration <= 0.0f) return; + if (magnitude > shakeMagnitude_ || shakeElapsed_ >= shakeDuration_) { + shakeMagnitude_ = magnitude; + shakeFrequency_ = frequency; + shakeDuration_ = duration; + shakeElapsed_ = 0.0f; + } +} + void CameraController::update(float deltaTime) { if (!enabled || !camera) { return; @@ -262,8 +273,9 @@ void CameraController::update(float deltaTime) { keyW = keyS = keyA = keyD = keyQ = keyE = nowJump = false; } - // Tilde toggles auto-run; any forward/backward key cancels it - bool tildeDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_GRAVE); + // Tilde or NumLock toggles auto-run; any forward/backward key cancels it + bool tildeDown = !uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_GRAVE) || + input.isKeyPressed(SDL_SCANCODE_NUMLOCKCLEAR)); if (tildeDown && !tildeWasDown) { autoRunning = !autoRunning; } @@ -377,10 +389,11 @@ void CameraController::update(float deltaTime) { if (mounted_) sitting = false; xKeyWasDown = xDown; - // Reset camera with R key (edge-triggered) — only when UI doesn't want keyboard + // Reset camera angles with R key (edge-triggered) — only when UI doesn't want keyboard + // Does NOT move the player; full reset() is reserved for world-entry/respawn. bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R); if (rDown && !rKeyWasDown) { - reset(); + resetAngles(); } rKeyWasDown = rDown; @@ -1859,6 +1872,23 @@ void CameraController::update(float deltaTime) { wasFalling = !grounded && verticalVelocity <= 0.0f; // R key is now handled above with chat safeguard (WantTextInput check) + + // Camera shake (SMSG_CAMERA_SHAKE): apply sinusoidal offset to final camera position. + if (shakeElapsed_ < shakeDuration_) { + shakeElapsed_ += deltaTime; + float t = shakeElapsed_ / shakeDuration_; + // Envelope: fade out over the last 30% of shake duration + float envelope = (t < 0.7f) ? 1.0f : (1.0f - (t - 0.7f) / 0.3f); + float theta = shakeElapsed_ * shakeFrequency_ * 2.0f * 3.14159265f; + glm::vec3 offset( + shakeMagnitude_ * envelope * std::sin(theta), + shakeMagnitude_ * envelope * std::cos(theta * 1.3f), + shakeMagnitude_ * envelope * std::sin(theta * 0.7f) * 0.5f + ); + if (camera) { + camera->setPosition(camera->getPosition() + offset); + } + } } void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) { @@ -1875,7 +1905,9 @@ void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) { // Directly update stored yaw/pitch (no lossy forward-vector derivation) yaw -= event.xrel * mouseSensitivity; - float invert = invertMouse ? -1.0f : 1.0f; + // SDL yrel > 0 = mouse moved DOWN. In WoW, mouse-down = look down = pitch decreases. + // invertMouse flips to flight-sim style (mouse-down = look up). + float invert = invertMouse ? 1.0f : -1.0f; pitch += event.yrel * mouseSensitivity * invert; // WoW-style pitch limits: can look almost straight down, limited upward @@ -1911,6 +1943,14 @@ void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) { mouseButtonDown = anyDown; } +void CameraController::resetAngles() { + if (!camera) return; + yaw = defaultYaw; + facingYaw = defaultYaw; + pitch = defaultPitch; + camera->setRotation(yaw, pitch); +} + void CameraController::reset() { if (!camera) { return; diff --git a/src/rendering/celestial.cpp b/src/rendering/celestial.cpp index 798ac5d5..ad7804ba 100644 --- a/src/rendering/celestial.cpp +++ b/src/rendering/celestial.cpp @@ -90,7 +90,7 @@ bool Celestial::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -162,7 +162,7 @@ void Celestial::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 2cb6278e..041fe8f2 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -332,25 +332,21 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, bool foundUnderwear = false; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL); - uint32_t fRace = csL ? (*csL)["RaceID"] : 1; - uint32_t fSex = csL ? (*csL)["SexID"] : 2; - uint32_t fBase = csL ? (*csL)["BaseSection"] : 3; - uint32_t fVar = csL ? (*csL)["VariationIndex"] : 4; - uint32_t fColor = csL ? (*csL)["ColorIndex"] : 5; for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - uint32_t raceId = charSectionsDbc->getUInt32(r, fRace); - uint32_t sexId = charSectionsDbc->getUInt32(r, fSex); - uint32_t baseSection = charSectionsDbc->getUInt32(r, fBase); - uint32_t variationIndex = charSectionsDbc->getUInt32(r, fVar); - uint32_t colorIndex = charSectionsDbc->getUInt32(r, fColor); + uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId); + uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId); + uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection); + uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex); + uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex); if (raceId != targetRaceId || sexId != targetSexId) continue; // Section 0: Body skin (variation=0, colorIndex = skin color) if (baseSection == 0 && !foundSkin && variationIndex == 0 && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); if (!tex1.empty()) { bodySkinPath_ = tex1; foundSkin = true; @@ -360,8 +356,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 1 && !foundFace && variationIndex == static_cast(face) && colorIndex == static_cast(skin)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); - std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); + std::string tex2 = charSectionsDbc->getString(r, csF.texture2); if (!tex1.empty()) faceLowerPath = tex1; if (!tex2.empty()) faceUpperPath = tex2; foundFace = true; @@ -370,7 +366,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, else if (baseSection == 3 && !foundHair && variationIndex == static_cast(hairStyle) && colorIndex == static_cast(hairColor)) { - std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + std::string tex1 = charSectionsDbc->getString(r, csF.texture1); if (!tex1.empty()) { hairScalpPath = tex1; foundHair = true; @@ -379,8 +375,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, // Section 4: Underwear (variation=0, colorIndex = skin color) else if (baseSection == 4 && !foundUnderwear && variationIndex == 0 && colorIndex == static_cast(skin)) { - uint32_t texBase = csL ? (*csL)["Texture1"] : 6; - for (uint32_t f = texBase; f <= texBase + 2; f++) { + for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) { std::string tex = charSectionsDbc->getString(r, f); if (!tex.empty()) { underwearPaths.push_back(tex); @@ -462,6 +457,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; + } + } + } } } @@ -530,9 +536,9 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, modelLoaded_ = true; LOG_INFO("CharacterPreview: loaded ", m2Path, - " skin=", (int)skin, " face=", (int)face, - " hair=", (int)hairStyle, " hairColor=", (int)hairColor, - " facial=", (int)facialHair); + " skin=", static_cast(skin), " face=", static_cast(face), + " hair=", static_cast(hairStyle), " hairColor=", static_cast(hairColor), + " facial=", static_cast(facialHair)); return true; } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 2fcf2ef7..b5a09c1c 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -264,7 +264,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); @@ -343,6 +343,8 @@ void CharacterRenderer::shutdown() { // Clean up composite cache compositeCache_.clear(); failedTextureCache_.clear(); + failedTextureRetryAt_.clear(); + textureLookupSerial_ = 0; whiteTexture_.reset(); transparentTexture_.reset(); @@ -430,6 +432,8 @@ void CharacterRenderer::clear() { textureCacheBytes_ = 0; textureCacheCounter_ = 0; loggedTextureLoadFails_.clear(); + failedTextureRetryAt_.clear(); + textureLookupSerial_ = 0; // Clear composite and failed caches compositeCache_.clear(); @@ -550,8 +554,8 @@ CharacterRenderer::NormalMapResult CharacterRenderer::generateNormalHeightMapCPU // Step 1.5: Box blur the height map to reduce noise from diffuse textures auto wrapSample = [&](const std::vector& map, int x, int y) -> float { - x = ((x % (int)width) + (int)width) % (int)width; - y = ((y % (int)height) + (int)height) % (int)height; + x = ((x % static_cast(width)) + static_cast(width)) % static_cast(width); + y = ((y % static_cast(height)) + static_cast(height)) % static_cast(height); return map[y * width + x]; }; @@ -572,8 +576,8 @@ CharacterRenderer::NormalMapResult CharacterRenderer::generateNormalHeightMapCPU result.pixels.resize(totalPixels * 4); auto sampleH = [&](int x, int y) -> float { - x = ((x % (int)width) + (int)width) % (int)width; - y = ((y % (int)height) + (int)height) % (int)height; + x = ((x % static_cast(width)) + static_cast(width)) % static_cast(width); + y = ((y % static_cast(height)) + static_cast(height)) % static_cast(height); return heightMap[y * width + x]; }; @@ -604,6 +608,7 @@ CharacterRenderer::NormalMapResult CharacterRenderer::generateNormalHeightMapCPU } VkTexture* CharacterRenderer::loadTexture(const std::string& path) { + constexpr uint64_t kFailedTextureRetryLookups = 512; // Skip empty or whitespace-only paths (type-0 textures have no filename) if (path.empty()) return whiteTexture_.get(); bool allWhitespace = true; @@ -619,6 +624,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) { return key; }; std::string key = normalizeKey(path); + const uint64_t lookupSerial = ++textureLookupSerial_; auto containsToken = [](const std::string& haystack, const char* token) { return haystack.find(token) != std::string::npos; }; @@ -634,6 +640,10 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) { it->second.lastUse = ++textureCacheCounter_; return it->second.texture.get(); } + auto failIt = failedTextureRetryAt_.find(key); + if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) { + return whiteTexture_.get(); + } if (!assetManager || !assetManager->isInitialized()) { return whiteTexture_.get(); @@ -652,8 +662,9 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) { blpImage = assetManager->loadTexture(key); } if (!blpImage.isValid()) { - // Return white fallback but don't cache the failure — allow retry - // on next character load in case the asset becomes available. + // Cache misses briefly to avoid repeated expensive MPQ/disk probes. + failedTextureCache_.insert(key); + failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups; if (loggedTextureLoadFails_.insert(key).second) { core::Logger::getInstance().warning("Failed to load texture: ", path); } @@ -666,6 +677,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) { if (failedTextureCache_.size() < kMaxFailedTextureCache) { // Budget is saturated; avoid repeatedly decoding/uploading this texture. failedTextureCache_.insert(key); + failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups; } if (textureBudgetRejectWarnings_ < 3) { core::Logger::getInstance().warning( @@ -724,6 +736,8 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) { textureHasAlphaByPtr_[texPtr] = hasAlpha; textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint; textureCache[key] = std::move(e); + failedTextureCache_.erase(key); + failedTextureRetryAt_.erase(key); core::Logger::getInstance().debug("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")"); return texPtr; @@ -1623,10 +1637,6 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) { if (t >= 1.0f) { inst.position = inst.moveEnd; inst.isMoving = false; - // Return to idle when movement completes - if (inst.currentAnimationId == 4 || inst.currentAnimationId == 5) { - playAnimation(pair.first, 0, true); - } } else { inst.position = glm::mix(inst.moveStart, inst.moveEnd, t); } @@ -2638,7 +2648,7 @@ bool CharacterRenderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -3305,7 +3315,7 @@ void CharacterRenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; LOG_INFO("CharacterRenderer::recreatePipelines: renderPass=", (void*)mainPass, diff --git a/src/rendering/charge_effect.cpp b/src/rendering/charge_effect.cpp index d6fba4de..32a3b36d 100644 --- a/src/rendering/charge_effect.cpp +++ b/src/rendering/charge_effect.cpp @@ -101,7 +101,7 @@ bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayo .setLayout(ribbonPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -165,7 +165,7 @@ bool ChargeEffect::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayo .setLayout(dustPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -314,7 +314,7 @@ void ChargeEffect::recreatePipelines() { .setLayout(ribbonPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -360,7 +360,7 @@ void ChargeEffect::recreatePipelines() { .setLayout(dustPipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/clouds.cpp b/src/rendering/clouds.cpp index eb2a5a25..6b682850 100644 --- a/src/rendering/clouds.cpp +++ b/src/rendering/clouds.cpp @@ -83,7 +83,7 @@ bool Clouds::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -149,7 +149,7 @@ void Clouds::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/lens_flare.cpp b/src/rendering/lens_flare.cpp index 820641af..e9a9bb04 100644 --- a/src/rendering/lens_flare.cpp +++ b/src/rendering/lens_flare.cpp @@ -109,7 +109,7 @@ bool LensFlare::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayou .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); // Shader modules can be freed after pipeline creation vertModule.destroy(); @@ -198,7 +198,7 @@ void LensFlare::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -313,8 +313,12 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec return; } + // Sun height attenuation — flare weakens when sun is near horizon (sunrise/sunset) + float sunHeight = sunDir.z; // z = up in render space; 0 = horizon, 1 = zenith + float heightFactor = glm::smoothstep(-0.05f, 0.25f, sunHeight); + // Atmospheric attenuation — fog, clouds, and weather reduce lens flare - float atmosphericFactor = 1.0f; + float atmosphericFactor = heightFactor; atmosphericFactor *= (1.0f - glm::clamp(fogDensity * 0.8f, 0.0f, 0.9f)); // Heavy fog nearly kills flare atmosphericFactor *= (1.0f - glm::clamp(cloudDensity * 0.6f, 0.0f, 0.7f)); // Clouds attenuate atmosphericFactor *= (1.0f - glm::clamp(weatherIntensity * 0.9f, 0.0f, 0.95f)); // Rain/snow heavily attenuates @@ -339,6 +343,9 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset); + // Warm tint at sunrise/sunset — shift flare color toward orange/amber when sun is low + float warmTint = 1.0f - glm::smoothstep(0.05f, 0.35f, sunHeight); + // Render each flare element for (const auto& element : flareElements) { // Calculate position along sun-to-center axis @@ -347,12 +354,19 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec // Apply visibility, intensity, and atmospheric attenuation float brightness = element.brightness * visibility * intensityMultiplier * atmosphericFactor; + // Apply warm sunset/sunrise color shift + glm::vec3 tintedColor = element.color; + if (warmTint > 0.01f) { + glm::vec3 warmColor(1.0f, 0.6f, 0.25f); // amber/orange + tintedColor = glm::mix(tintedColor, warmColor, warmTint * 0.5f); + } + // Set push constants FlarePushConstants push{}; push.position = position; push.size = element.size; push.aspectRatio = aspectRatio; - push.colorBrightness = glm::vec4(element.color, brightness); + push.colorBrightness = glm::vec4(tintedColor, brightness); vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, diff --git a/src/rendering/lightning.cpp b/src/rendering/lightning.cpp index 9dbd1b95..b7d28c1d 100644 --- a/src/rendering/lightning.cpp +++ b/src/rendering/lightning.cpp @@ -107,7 +107,7 @@ bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(boltPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -169,7 +169,7 @@ bool Lightning::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(flashPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -306,7 +306,7 @@ void Lightning::recreatePipelines() { .setLayout(boltPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -344,7 +344,7 @@ void Lightning::recreatePipelines() { .setLayout(flashPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/loading_screen.cpp b/src/rendering/loading_screen.cpp index a2e83a2b..8bbf4013 100644 --- a/src/rendering/loading_screen.cpp +++ b/src/rendering/loading_screen.cpp @@ -40,10 +40,7 @@ void LoadingScreen::shutdown() { // ImGui manages descriptor set lifetime bgDescriptorSet = VK_NULL_HANDLE; } - if (bgSampler) { - vkDestroySampler(device, bgSampler, nullptr); - bgSampler = VK_NULL_HANDLE; - } + bgSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; @@ -94,7 +91,7 @@ bool LoadingScreen::loadImage(const std::string& path) { if (bgImage) { VkDevice device = vkCtx->getDevice(); vkDeviceWaitIdle(device); - if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; } + bgSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; } if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; } if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; } @@ -230,7 +227,7 @@ bool LoadingScreen::loadImage(const std::string& path) { samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(device, &samplerInfo, nullptr, &bgSampler); + bgSampler = vkCtx->getOrCreateSampler(samplerInfo); } // Register with ImGui as a texture @@ -261,6 +258,20 @@ void LoadingScreen::renderOverlay() { ImVec2(0, 0), ImVec2(screenW, screenH)); } + // Zone name header + if (!zoneName.empty()) { + ImFont* font = ImGui::GetFont(); + float zoneTextSize = 24.0f; + ImVec2 zoneSize = font->CalcTextSizeA(zoneTextSize, FLT_MAX, 0.0f, zoneName.c_str()); + float zoneX = (screenW - zoneSize.x) * 0.5f; + float zoneY = screenH * 0.06f - 44.0f; + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddText(font, zoneTextSize, ImVec2(zoneX + 2.0f, zoneY + 2.0f), + IM_COL32(0, 0, 0, 200), zoneName.c_str()); + dl->AddText(font, zoneTextSize, ImVec2(zoneX, zoneY), + IM_COL32(255, 220, 120, 255), zoneName.c_str()); + } + // Progress bar { const float barWidthFrac = 0.6f; @@ -332,6 +343,22 @@ void LoadingScreen::render() { ImVec2(0, 0), ImVec2(screenW, screenH)); } + // Zone name header (large text centered above progress bar) + if (!zoneName.empty()) { + ImFont* font = ImGui::GetFont(); + float zoneTextSize = 24.0f; + ImVec2 zoneSize = font->CalcTextSizeA(zoneTextSize, FLT_MAX, 0.0f, zoneName.c_str()); + float zoneX = (screenW - zoneSize.x) * 0.5f; + float zoneY = screenH * 0.06f - 44.0f; // above percentage text + ImDrawList* dl = ImGui::GetWindowDrawList(); + // Drop shadow + dl->AddText(font, zoneTextSize, ImVec2(zoneX + 2.0f, zoneY + 2.0f), + IM_COL32(0, 0, 0, 200), zoneName.c_str()); + // Gold text + dl->AddText(font, zoneTextSize, ImVec2(zoneX, zoneY), + IM_COL32(255, 220, 120, 255), zoneName.c_str()); + } + // Progress bar (top of screen) { const float barWidthFrac = 0.6f; diff --git a/src/rendering/m2_model_classifier.cpp b/src/rendering/m2_model_classifier.cpp new file mode 100644 index 00000000..424bfc42 --- /dev/null +++ b/src/rendering/m2_model_classifier.cpp @@ -0,0 +1,248 @@ +#include "rendering/m2_model_classifier.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +namespace { + +// Returns true if `lower` contains `token` as a substring. +// Caller must provide an already-lowercased string. +inline bool has(const std::string& lower, std::string_view token) noexcept { + return lower.find(token) != std::string::npos; +} + +// Returns true if any token in the compile-time array is a substring of `lower`. +template +bool hasAny(const std::string& lower, + const std::array& tokens) noexcept { + for (auto tok : tokens) + if (lower.find(tok) != std::string::npos) return true; + return false; +} + +} // namespace + +M2ClassificationResult classifyM2Model( + const std::string& name, + const glm::vec3& boundsMin, + const glm::vec3& boundsMax, + std::size_t vertexCount, + std::size_t emitterCount) +{ + // Single lowercased copy — all token checks share it. + std::string n = name; + std::transform(n.begin(), n.end(), n.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + M2ClassificationResult r; + + // --------------------------------------------------------------- + // Geometry metrics + // --------------------------------------------------------------- + const glm::vec3 dims = boundsMax - boundsMin; + const float horiz = std::max(dims.x, dims.y); + const float vert = std::max(0.0f, dims.z); + const bool lowWide = (horiz > 1.4f && vert > 0.2f && vert < horiz * 0.70f); + const bool lowPlat = (horiz > 1.8f && vert > 0.2f && vert < 1.8f); + + // --------------------------------------------------------------- + // Simple single-token flags + // --------------------------------------------------------------- + r.isInvisibleTrap = has(n, "invisibletrap"); + r.isGroundDetail = has(n, "\\nodxt\\detail\\") || has(n, "\\detail\\"); + r.isSmoke = has(n, "smoke"); + r.isLavaModel = has(n, "forgelava") || has(n, "lavapot") || has(n, "lavaflow"); + + r.isInstancePortal = has(n, "instanceportal") || has(n, "instancenewportal") + || has(n, "portalfx") || has(n, "spellportal"); + + r.isWaterVegetation = has(n, "cattail") || has(n, "reed") || has(n, "bulrush") + || has(n, "seaweed") || has(n, "kelp") || has(n, "lilypad"); + + r.isElvenLike = has(n, "elf") || has(n, "elven") || has(n, "quel"); + r.isLanternLike = has(n, "lantern") || has(n, "lamp") || has(n, "light"); + r.isKoboldFlame = has(n, "kobold") + && (has(n, "candle") || has(n, "torch") || has(n, "mine")); + + // --------------------------------------------------------------- + // Collision: shape categories (mirrors original logic ordering) + // --------------------------------------------------------------- + const bool isPlanter = has(n, "planter"); + const bool likelyCurb = isPlanter || has(n, "curb") || has(n, "base") + || has(n, "ring") || has(n, "well"); + const bool knownSwPlanter = has(n, "stormwindplanter") + || has(n, "stormwindwindowplanter"); + const bool bridgeName = has(n, "bridge") || has(n, "plank") || has(n, "walkway"); + const bool statueName = has(n, "statue") || has(n, "monument") || has(n, "sculpture"); + const bool sittable = has(n, "chair") || has(n, "bench") || has(n, "stool") + || has(n, "seat") || has(n, "throne"); + const bool smallSolid = (statueName && !sittable) + || has(n, "crate") || has(n, "box") + || has(n, "chest") || has(n, "barrel"); + const bool chestName = has(n, "chest"); + + r.collisionSteppedFountain = has(n, "fountain"); + r.collisionSteppedLowPlatform = !r.collisionSteppedFountain + && (knownSwPlanter || bridgeName + || (likelyCurb && (lowPlat || lowWide))); + r.collisionBridge = bridgeName; + r.collisionPlanter = isPlanter; + r.collisionStatue = statueName; + + const bool narrowVertName = has(n, "lamp") || has(n, "lantern") + || has(n, "post") || has(n, "pole"); + const bool narrowVertShape = (horiz > 0.12f && horiz < 2.0f + && vert > 2.2f && vert > horiz * 1.8f); + r.collisionNarrowVerticalProp = !r.collisionSteppedFountain + && !r.collisionSteppedLowPlatform + && (narrowVertName || narrowVertShape); + + // --------------------------------------------------------------- + // Foliage token table (sorted alphabetically) + // --------------------------------------------------------------- + static constexpr auto kFoliageTokens = std::to_array({ + "algae", "bamboo", "banana", "branch", "bush", + "cactus", "canopy", "cattail", "coconut", "coral", + "corn", "crop", "dead-grass", "dead_grass", "deadgrass", + "dry-grass", "dry_grass", "drygrass", + "fern", "fireflies", "firefly", "fireflys", + "flower", "frond", "fungus", "gourd", "grass", + "hay", "hedge", "ivy", "kelp", "leaf", + "leaves", "lily", "melon", "moss", "mushroom", + "palm", "pumpkin", "reed", "root", "seaweed", + "shrub", "squash", "stalk", "thorn", "toadstool", + "vine", "watermelon", "weed", "wheat", + }); + + // "plant" is foliage unless "planter" is also present (planters are solid curbs). + const bool foliagePlant = has(n, "plant") && !isPlanter; + const bool foliageName = foliagePlant || hasAny(n, kFoliageTokens); + const bool treeLike = has(n, "tree"); + const bool hardTreePart = has(n, "trunk") || has(n, "stump") || has(n, "log"); + + // Trees wide/tall enough to have a visible trunk → solid cylinder collision. + const bool treeWithTrunk = treeLike && !hardTreePart && !foliageName + && horiz > 6.0f && vert > 4.0f; + const bool softTree = treeLike && !hardTreePart && !treeWithTrunk; + + r.collisionTreeTrunk = treeWithTrunk; + + const bool genericSolid = (horiz > 0.6f && horiz < 6.0f + && vert > 0.30f && vert < 4.0f + && vert > horiz * 0.16f) || statueName; + const bool curbLikeName = has(n, "curb") || has(n, "planter") + || has(n, "ring") || has(n, "well") || has(n, "base"); + const bool lowPlatLikeShape = lowWide || lowPlat; + + r.collisionSmallSolidProp = !r.collisionSteppedFountain + && !r.collisionSteppedLowPlatform + && !r.collisionNarrowVerticalProp + && !r.collisionTreeTrunk + && !curbLikeName + && !lowPlatLikeShape + && (smallSolid + || (genericSolid && !foliageName && !softTree)); + + const bool carpetOrRug = has(n, "carpet") || has(n, "rug"); + const bool forceSolidCurb = r.collisionSteppedLowPlatform || knownSwPlanter + || likelyCurb || r.collisionPlanter; + r.collisionNoBlock = (foliageName || softTree || carpetOrRug) && !forceSolidCurb; + // Ground-clutter detail cards are always non-blocking. + if (r.isGroundDetail) r.collisionNoBlock = true; + + // --------------------------------------------------------------- + // Ambient creatures: fireflies, dragonflies, moths, butterflies + // --------------------------------------------------------------- + static constexpr auto kAmbientTokens = std::to_array({ + "butterfly", "dragonflies", "dragonfly", + "fireflies", "firefly", "fireflys", "moth", + }); + const bool ambientCreature = hasAny(n, kAmbientTokens); + + // --------------------------------------------------------------- + // Animation / foliage rendering flags + // --------------------------------------------------------------- + const bool foliageOrTree = foliageName || treeLike; + r.isFoliageLike = foliageOrTree && !ambientCreature; + r.disableAnimation = r.isFoliageLike || chestName; + r.shadowWindFoliage = r.isFoliageLike; + r.isFireflyEffect = ambientCreature; + + // --------------------------------------------------------------- + // Spell effects (named tokens + particle-dominated geometry heuristic) + // --------------------------------------------------------------- + static constexpr auto kEffectTokens = std::to_array({ + "bubbles", "hazardlight", "instancenewportal", "instanceportal", + "lavabubble", "lavasplash", "lavasteam", "levelup", + "lightshaft", "mageportal", "particleemitter", + "spotlight", "volumetriclight", "wisps", "worldtreeportal", + }); + r.isSpellEffect = hasAny(n, kEffectTokens) + || (emitterCount >= 3 && vertexCount <= 200); + // Instance portals are spell effects too. + if (r.isInstancePortal) r.isSpellEffect = true; + + return r; +} + +// --------------------------------------------------------------------------- +// classifyBatchTexture +// --------------------------------------------------------------------------- + +M2BatchTexClassification classifyBatchTexture(const std::string& lowerTexKey) +{ + M2BatchTexClassification r; + + // Exact paths for well-known lantern / lamp glow-card textures. + static constexpr auto kExactGlowTextures = std::to_array({ + "world\\azeroth\\karazahn\\passivedoodads\\bonfire\\flamelicksmallblue.blp", + "world\\expansion06\\doodads\\nightelf\\7ne_druid_streetlamp01_light.blp", + "world\\generic\\human\\passive doodads\\stormwind\\t_vfx_glow01_64.blp", + "world\\generic\\nightelf\\passive doodads\\lamps\\glowblue32.blp", + "world\\generic\\nightelf\\passive doodads\\magicalimplements\\glow.blp", + }); + for (auto s : kExactGlowTextures) + if (lowerTexKey == s) { r.exactLanternGlowTex = true; break; } + + static constexpr auto kGlowTokens = std::to_array({ + "flare", "glow", "halo", "light", + }); + static constexpr auto kFlameTokens = std::to_array({ + "ember", "fire", "flame", "flamelick", + }); + static constexpr auto kGlowCardTokens = std::to_array({ + "flamelick", "genericglow", "glow", "glowball", + "lensflare", "lightbeam", "t_vfx", + }); + static constexpr auto kLikelyFlameTokens = std::to_array({ + "fire", "flame", "torch", + }); + static constexpr auto kLanternFamilyTokens = std::to_array({ + "elf", "lamp", "lantern", "quel", "silvermoon", "thalas", + }); + static constexpr auto kCoolTintTokens = std::to_array({ + "arcane", "blue", "nightelf", + }); + static constexpr auto kRedTintTokens = std::to_array({ + "red", "ruby", "scarlet", + }); + + r.hasGlowToken = hasAny(lowerTexKey, kGlowTokens); + r.hasFlameToken = hasAny(lowerTexKey, kFlameTokens); + r.hasGlowCardToken = hasAny(lowerTexKey, kGlowCardTokens); + r.likelyFlame = hasAny(lowerTexKey, kLikelyFlameTokens); + r.lanternFamily = hasAny(lowerTexKey, kLanternFamilyTokens); + r.glowTint = hasAny(lowerTexKey, kCoolTintTokens) ? 1 + : hasAny(lowerTexKey, kRedTintTokens) ? 2 + : 0; + + return r; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index c5ef43b2..d33f0ed7 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1,4 +1,5 @@ #include "rendering/m2_renderer.hpp" +#include "rendering/m2_model_classifier.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_buffer.hpp" #include "rendering/vk_texture.hpp" @@ -30,6 +31,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 +370,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 @@ -469,7 +508,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true); @@ -504,7 +543,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(particlePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; particlePipeline_ = buildParticlePipeline(PipelineBuilder::blendAlpha()); @@ -537,7 +576,55 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(smokePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); + } + + // --- Build ribbon pipelines --- + // Vertex format: pos(3) + color(3) + alpha(1) + uv(2) = 9 floats = 36 bytes + { + rendering::VkShaderModule ribVert, ribFrag; + ribVert.loadFromFile(device, "assets/shaders/m2_ribbon.vert.spv"); + ribFrag.loadFromFile(device, "assets/shaders/m2_ribbon.frag.spv"); + if (ribVert.isValid() && ribFrag.isValid()) { + // Reuse particleTexLayout_ for set 1 (single texture sampler) + VkDescriptorSetLayout ribLayouts[] = {perFrameLayout, particleTexLayout_}; + VkPipelineLayoutCreateInfo lci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + lci.setLayoutCount = 2; + lci.pSetLayouts = ribLayouts; + vkCreatePipelineLayout(device, &lci, nullptr, &ribbonPipelineLayout_); + + VkVertexInputBindingDescription rBind{}; + rBind.binding = 0; + rBind.stride = 9 * sizeof(float); + rBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector rAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // pos + {1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * sizeof(float)}, // color + {2, 0, VK_FORMAT_R32_SFLOAT, 6 * sizeof(float)}, // alpha + {3, 0, VK_FORMAT_R32G32_SFLOAT, 7 * sizeof(float)}, // uv + }; + + auto buildRibbonPipeline = [&](VkPipelineColorBlendAttachmentState blend) -> VkPipeline { + return PipelineBuilder() + .setShaders(ribVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + ribFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({rBind}, rAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blend) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(ribbonPipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device, vkCtx_->getPipelineCache()); + }; + + ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha()); + ribbonAdditivePipeline_ = buildRibbonPipeline(PipelineBuilder::blendAdditive()); + } + ribVert.destroy(); ribFrag.destroy(); } // Clean up shader modules @@ -570,6 +657,11 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout bci.size = MAX_GLOW_SPRITES * 9 * sizeof(float); vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &glowVB_, &glowVBAlloc_, &allocInfo); glowVBMapped_ = allocInfo.pMappedData; + + // Ribbon vertex buffer — triangle strip: pos(3)+color(3)+alpha(1)+uv(2)=9 floats/vert + bci.size = MAX_RIBBON_VERTS * 9 * sizeof(float); + vmaCreateBuffer(vkCtx_->getAllocator(), &bci, &aci, &ribbonVB_, &ribbonVBAlloc_, &allocInfo); + ribbonVBMapped_ = allocInfo.pMappedData; } // --- Create white fallback texture --- @@ -661,15 +753,18 @@ void M2Renderer::shutdown() { textureHasAlphaByPtr_.clear(); textureColorKeyBlackByPtr_.clear(); failedTextureCache_.clear(); + failedTextureRetryAt_.clear(); loggedTextureLoadFails_.clear(); + textureLookupSerial_ = 0; textureBudgetRejectWarnings_ = 0; whiteTexture_.reset(); glowTexture_.reset(); - // Clean up particle buffers + // Clean up particle/ribbon buffers if (smokeVB_) { vmaDestroyBuffer(alloc, smokeVB_, smokeVBAlloc_); smokeVB_ = VK_NULL_HANDLE; } if (m2ParticleVB_) { vmaDestroyBuffer(alloc, m2ParticleVB_, m2ParticleVBAlloc_); m2ParticleVB_ = VK_NULL_HANDLE; } if (glowVB_) { vmaDestroyBuffer(alloc, glowVB_, glowVBAlloc_); glowVB_ = VK_NULL_HANDLE; } + if (ribbonVB_) { vmaDestroyBuffer(alloc, ribbonVB_, ribbonVBAlloc_); ribbonVB_ = VK_NULL_HANDLE; } smokeParticles.clear(); // Destroy pipelines @@ -681,12 +776,18 @@ void M2Renderer::shutdown() { destroyPipeline(particlePipeline_); destroyPipeline(particleAdditivePipeline_); destroyPipeline(smokePipeline_); + destroyPipeline(ribbonPipeline_); + destroyPipeline(ribbonAdditivePipeline_); if (pipelineLayout_) { vkDestroyPipelineLayout(device, pipelineLayout_, nullptr); pipelineLayout_ = VK_NULL_HANDLE; } if (particlePipelineLayout_) { vkDestroyPipelineLayout(device, particlePipelineLayout_, nullptr); particlePipelineLayout_ = VK_NULL_HANDLE; } if (smokePipelineLayout_) { vkDestroyPipelineLayout(device, smokePipelineLayout_, nullptr); smokePipelineLayout_ = VK_NULL_HANDLE; } + 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; } @@ -719,6 +820,11 @@ void M2Renderer::destroyModelGPU(M2ModelGPU& model) { if (pSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &pSet); pSet = VK_NULL_HANDLE; } } model.particleTexSets.clear(); + // Free ribbon texture descriptor sets + for (auto& rSet : model.ribbonTexSets) { + if (rSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &rSet); rSet = VK_NULL_HANDLE; } + } + model.ribbonTexSets.clear(); } void M2Renderer::destroyInstanceBones(M2Instance& inst) { @@ -748,7 +854,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; } @@ -758,7 +868,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; } @@ -882,23 +996,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { bool hasGeometry = !model.vertices.empty() && !model.indices.empty(); bool hasParticles = !model.particleEmitters.empty(); - if (!hasGeometry && !hasParticles) { - LOG_WARNING("M2 model has no geometry and no particles: ", model.name); + bool hasRibbons = !model.ribbonEmitters.empty(); + if (!hasGeometry && !hasParticles && !hasRibbons) { + LOG_WARNING("M2 model has no renderable content: ", model.name); return false; } M2ModelGPU gpuModel; gpuModel.name = model.name; - // Detect invisible trap models (event objects that should not render or collide) - std::string lowerName = model.name; - std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - bool isInvisibleTrap = (lowerName.find("invisibletrap") != std::string::npos); - gpuModel.isInvisibleTrap = isInvisibleTrap; - if (isInvisibleTrap) { - LOG_INFO("Loading InvisibleTrap model: ", model.name, " (will be invisible, no collision)"); - } // Use tight bounds from actual vertices for collision/camera occlusion. // Header bounds in some M2s are overly conservative. glm::vec3 tightMin(0.0f); @@ -911,165 +1017,40 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { tightMax = glm::max(tightMax, v.position); } } - bool foliageOrTreeLike = false; - bool chestName = false; - bool groundDetailModel = false; - { - std::string lowerName = model.name; - std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - gpuModel.collisionSteppedFountain = (lowerName.find("fountain") != std::string::npos); - glm::vec3 dims = tightMax - tightMin; - float horiz = std::max(dims.x, dims.y); - float vert = std::max(0.0f, dims.z); - bool lowWideShape = (horiz > 1.4f && vert > 0.2f && vert < horiz * 0.70f); - bool likelyCurbName = - (lowerName.find("planter") != std::string::npos) || - (lowerName.find("curb") != std::string::npos) || - (lowerName.find("base") != std::string::npos) || - (lowerName.find("ring") != std::string::npos) || - (lowerName.find("well") != std::string::npos); - bool knownStormwindPlanter = - (lowerName.find("stormwindplanter") != std::string::npos) || - (lowerName.find("stormwindwindowplanter") != std::string::npos); - bool lowPlatformShape = (horiz > 1.8f && vert > 0.2f && vert < 1.8f); - bool bridgeName = - (lowerName.find("bridge") != std::string::npos) || - (lowerName.find("plank") != std::string::npos) || - (lowerName.find("walkway") != std::string::npos); - gpuModel.collisionSteppedLowPlatform = (!gpuModel.collisionSteppedFountain) && - (knownStormwindPlanter || - bridgeName || - (likelyCurbName && (lowPlatformShape || lowWideShape))); - gpuModel.collisionBridge = bridgeName; - - bool isPlanter = (lowerName.find("planter") != std::string::npos); - gpuModel.collisionPlanter = isPlanter; - bool statueName = - (lowerName.find("statue") != std::string::npos) || - (lowerName.find("monument") != std::string::npos) || - (lowerName.find("sculpture") != std::string::npos); - gpuModel.collisionStatue = statueName; - // Sittable furniture: chairs/benches/stools cause players to get stuck against - // invisible bounding boxes; WMOs already handle room collision. - bool sittableFurnitureName = - (lowerName.find("chair") != std::string::npos) || - (lowerName.find("bench") != std::string::npos) || - (lowerName.find("stool") != std::string::npos) || - (lowerName.find("seat") != std::string::npos) || - (lowerName.find("throne") != std::string::npos); - bool smallSolidPropName = - (statueName && !sittableFurnitureName) || - (lowerName.find("crate") != std::string::npos) || - (lowerName.find("box") != std::string::npos) || - (lowerName.find("chest") != std::string::npos) || - (lowerName.find("barrel") != std::string::npos); - chestName = (lowerName.find("chest") != std::string::npos); - bool foliageName = - (lowerName.find("bush") != std::string::npos) || - (lowerName.find("grass") != std::string::npos) || - (lowerName.find("drygrass") != std::string::npos) || - (lowerName.find("dry_grass") != std::string::npos) || - (lowerName.find("dry-grass") != std::string::npos) || - (lowerName.find("deadgrass") != std::string::npos) || - (lowerName.find("dead_grass") != std::string::npos) || - (lowerName.find("dead-grass") != std::string::npos) || - ((lowerName.find("plant") != std::string::npos) && !isPlanter) || - (lowerName.find("flower") != std::string::npos) || - (lowerName.find("shrub") != std::string::npos) || - (lowerName.find("fern") != std::string::npos) || - (lowerName.find("vine") != std::string::npos) || - (lowerName.find("lily") != std::string::npos) || - (lowerName.find("weed") != std::string::npos) || - (lowerName.find("wheat") != std::string::npos) || - (lowerName.find("pumpkin") != std::string::npos) || - (lowerName.find("firefly") != std::string::npos) || - (lowerName.find("fireflies") != std::string::npos) || - (lowerName.find("fireflys") != std::string::npos) || - (lowerName.find("mushroom") != std::string::npos) || - (lowerName.find("fungus") != std::string::npos) || - (lowerName.find("toadstool") != std::string::npos) || - (lowerName.find("root") != std::string::npos) || - (lowerName.find("branch") != std::string::npos) || - (lowerName.find("thorn") != std::string::npos) || - (lowerName.find("moss") != std::string::npos) || - (lowerName.find("ivy") != std::string::npos) || - (lowerName.find("seaweed") != std::string::npos) || - (lowerName.find("kelp") != std::string::npos) || - (lowerName.find("cattail") != std::string::npos) || - (lowerName.find("reed") != std::string::npos) || - (lowerName.find("palm") != std::string::npos) || - (lowerName.find("bamboo") != std::string::npos) || - (lowerName.find("banana") != std::string::npos) || - (lowerName.find("coconut") != std::string::npos) || - (lowerName.find("watermelon") != std::string::npos) || - (lowerName.find("melon") != std::string::npos) || - (lowerName.find("squash") != std::string::npos) || - (lowerName.find("gourd") != std::string::npos) || - (lowerName.find("canopy") != std::string::npos) || - (lowerName.find("hedge") != std::string::npos) || - (lowerName.find("cactus") != std::string::npos) || - (lowerName.find("leaf") != std::string::npos) || - (lowerName.find("leaves") != std::string::npos) || - (lowerName.find("stalk") != std::string::npos) || - (lowerName.find("corn") != std::string::npos) || - (lowerName.find("crop") != std::string::npos) || - (lowerName.find("hay") != std::string::npos) || - (lowerName.find("frond") != std::string::npos) || - (lowerName.find("algae") != std::string::npos) || - (lowerName.find("coral") != std::string::npos); - bool treeLike = (lowerName.find("tree") != std::string::npos); - foliageOrTreeLike = (foliageName || treeLike); - groundDetailModel = - (lowerName.find("\\nodxt\\detail\\") != std::string::npos) || - (lowerName.find("\\detail\\") != std::string::npos); - bool hardTreePart = - (lowerName.find("trunk") != std::string::npos) || - (lowerName.find("stump") != std::string::npos) || - (lowerName.find("log") != std::string::npos); - // Trees with visible trunks get collision. Threshold: canopy wider than 6 - // model units AND taller than 4 units (filters out small bushes/saplings). - bool treeWithTrunk = treeLike && !hardTreePart && !foliageName && horiz > 6.0f && vert > 4.0f; - bool softTree = treeLike && !hardTreePart && !treeWithTrunk; - bool forceSolidCurb = gpuModel.collisionSteppedLowPlatform || knownStormwindPlanter || likelyCurbName || gpuModel.collisionPlanter; - bool narrowVerticalName = - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("post") != std::string::npos) || - (lowerName.find("pole") != std::string::npos); - bool narrowVerticalShape = - (horiz > 0.12f && horiz < 2.0f && vert > 2.2f && vert > horiz * 1.8f); - gpuModel.collisionTreeTrunk = treeWithTrunk; - gpuModel.collisionNarrowVerticalProp = - !gpuModel.collisionSteppedFountain && - !gpuModel.collisionSteppedLowPlatform && - (narrowVerticalName || narrowVerticalShape); - bool genericSolidPropShape = - (horiz > 0.6f && horiz < 6.0f && vert > 0.30f && vert < 4.0f && vert > horiz * 0.16f) || - statueName; - bool curbLikeName = - (lowerName.find("curb") != std::string::npos) || - (lowerName.find("planter") != std::string::npos) || - (lowerName.find("ring") != std::string::npos) || - (lowerName.find("well") != std::string::npos) || - (lowerName.find("base") != std::string::npos); - bool lowPlatformLikeShape = lowWideShape || lowPlatformShape; - bool carpetOrRug = - (lowerName.find("carpet") != std::string::npos) || - (lowerName.find("rug") != std::string::npos); - gpuModel.collisionSmallSolidProp = - !gpuModel.collisionSteppedFountain && - !gpuModel.collisionSteppedLowPlatform && - !gpuModel.collisionNarrowVerticalProp && - !gpuModel.collisionTreeTrunk && - !curbLikeName && - !lowPlatformLikeShape && - (smallSolidPropName || (genericSolidPropShape && !foliageName && !softTree)); - // Disable collision for foliage, soft trees, and decorative carpets/rugs - gpuModel.collisionNoBlock = ((foliageName || softTree || carpetOrRug) && - !forceSolidCurb); + // Classify model from name and geometry — pure function, no GPU dependencies. + auto cls = classifyM2Model(model.name, tightMin, tightMax, + model.vertices.size(), + model.particleEmitters.size()); + const bool isInvisibleTrap = cls.isInvisibleTrap; + const bool groundDetailModel = cls.isGroundDetail; + if (isInvisibleTrap) { + LOG_INFO("Loading InvisibleTrap model: ", model.name, " (will be invisible, no collision)"); } + + gpuModel.isInvisibleTrap = cls.isInvisibleTrap; + gpuModel.collisionSteppedFountain = cls.collisionSteppedFountain; + gpuModel.collisionSteppedLowPlatform = cls.collisionSteppedLowPlatform; + gpuModel.collisionBridge = cls.collisionBridge; + gpuModel.collisionPlanter = cls.collisionPlanter; + gpuModel.collisionStatue = cls.collisionStatue; + gpuModel.collisionTreeTrunk = cls.collisionTreeTrunk; + gpuModel.collisionNarrowVerticalProp = cls.collisionNarrowVerticalProp; + gpuModel.collisionSmallSolidProp = cls.collisionSmallSolidProp; + gpuModel.collisionNoBlock = cls.collisionNoBlock; + gpuModel.isGroundDetail = cls.isGroundDetail; + gpuModel.isFoliageLike = cls.isFoliageLike; + gpuModel.disableAnimation = cls.disableAnimation; + gpuModel.shadowWindFoliage = cls.shadowWindFoliage; + gpuModel.isFireflyEffect = cls.isFireflyEffect; + gpuModel.isSmoke = cls.isSmoke; + gpuModel.isSpellEffect = cls.isSpellEffect; + gpuModel.isLavaModel = cls.isLavaModel; + gpuModel.isInstancePortal = cls.isInstancePortal; + gpuModel.isWaterVegetation = cls.isWaterVegetation; + gpuModel.isElvenLike = cls.isElvenLike; + gpuModel.isLanternLike = cls.isLanternLike; + gpuModel.isKoboldFlame = cls.isKoboldFlame; gpuModel.boundMin = tightMin; gpuModel.boundMax = tightMax; gpuModel.boundRadius = model.boundRadius; @@ -1087,79 +1068,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { break; } } - bool ambientCreature = - (lowerName.find("firefly") != std::string::npos) || - (lowerName.find("fireflies") != std::string::npos) || - (lowerName.find("fireflys") != std::string::npos) || - (lowerName.find("dragonfly") != std::string::npos) || - (lowerName.find("dragonflies") != std::string::npos) || - (lowerName.find("butterfly") != std::string::npos) || - (lowerName.find("moth") != std::string::npos); - gpuModel.disableAnimation = (foliageOrTreeLike && !ambientCreature) || chestName; - gpuModel.shadowWindFoliage = foliageOrTreeLike && !ambientCreature; - gpuModel.isFoliageLike = foliageOrTreeLike && !ambientCreature; - gpuModel.isElvenLike = - (lowerName.find("elf") != std::string::npos) || - (lowerName.find("elven") != std::string::npos) || - (lowerName.find("quel") != std::string::npos); - gpuModel.isLanternLike = - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("light") != std::string::npos); - gpuModel.isKoboldFlame = - (lowerName.find("kobold") != std::string::npos) && - ((lowerName.find("candle") != std::string::npos) || - (lowerName.find("torch") != std::string::npos) || - (lowerName.find("mine") != std::string::npos)); - gpuModel.isGroundDetail = groundDetailModel; - if (groundDetailModel) { - // Ground clutter (grass/pebbles/detail cards) should never block camera/movement. - gpuModel.collisionNoBlock = true; - } - // Spell effect / pure-visual models: particle-dominated with minimal geometry, - // or named effect models (light shafts, portals, emitters, spotlights) - bool effectByName = - (lowerName.find("lightshaft") != std::string::npos) || - (lowerName.find("volumetriclight") != std::string::npos) || - (lowerName.find("instanceportal") != std::string::npos) || - (lowerName.find("instancenewportal") != std::string::npos) || - (lowerName.find("mageportal") != std::string::npos) || - (lowerName.find("worldtreeportal") != std::string::npos) || - (lowerName.find("particleemitter") != std::string::npos) || - (lowerName.find("bubbles") != std::string::npos) || - (lowerName.find("spotlight") != std::string::npos) || - (lowerName.find("hazardlight") != std::string::npos) || - (lowerName.find("lavasplash") != std::string::npos) || - (lowerName.find("lavabubble") != std::string::npos) || - (lowerName.find("lavasteam") != std::string::npos) || - (lowerName.find("wisps") != std::string::npos) || - (lowerName.find("levelup") != std::string::npos); - gpuModel.isSpellEffect = effectByName || - (hasParticles && model.vertices.size() <= 200 && - model.particleEmitters.size() >= 3); - gpuModel.isLavaModel = - (lowerName.find("forgelava") != std::string::npos) || - (lowerName.find("lavapot") != std::string::npos) || - (lowerName.find("lavaflow") != std::string::npos); - gpuModel.isInstancePortal = - (lowerName.find("instanceportal") != std::string::npos) || - (lowerName.find("instancenewportal") != std::string::npos) || - (lowerName.find("portalfx") != std::string::npos) || - (lowerName.find("spellportal") != std::string::npos); - // Instance portals are spell effects too (additive blend, no collision) - if (gpuModel.isInstancePortal) { - gpuModel.isSpellEffect = true; - } - // Water vegetation: cattails, reeds, bulrushes, kelp, seaweed, lilypad near water - gpuModel.isWaterVegetation = - (lowerName.find("cattail") != std::string::npos) || - (lowerName.find("reed") != std::string::npos) || - (lowerName.find("bulrush") != std::string::npos) || - (lowerName.find("seaweed") != std::string::npos) || - (lowerName.find("kelp") != std::string::npos) || - (lowerName.find("lilypad") != std::string::npos); - // Ambient creature effects: particle-based glow (exempt from particle dampeners) - gpuModel.isFireflyEffect = ambientCreature; + // Build collision mesh + spatial grid from M2 bounding geometry gpuModel.collision.vertices = model.collisionVertices; @@ -1170,14 +1079,6 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { " tris, grid ", gpuModel.collision.gridCellsX, "x", gpuModel.collision.gridCellsY); } - // Flag smoke models for UV scroll animation (in addition to particle emitters) - { - std::string smokeName = model.name; - std::transform(smokeName.begin(), smokeName.end(), smokeName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - gpuModel.isSmoke = (smokeName.find("smoke") != std::string::npos); - } - // Identify idle variation sequences (animation ID 0 = Stand) for (int i = 0; i < static_cast(model.sequences.size()); i++) { if (model.sequences[i].id == 0 && model.sequences[i].duration > 0) { @@ -1238,6 +1139,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. @@ -1294,14 +1199,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { static const bool kGlowDiag = envFlagEnabled("WOWEE_M2_GLOW_DIAG", false); if (kGlowDiag) { - std::string lowerName = model.name; - std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - const bool lanternLike = - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("light") != std::string::npos); - if (lanternLike) { + if (gpuModel.isLanternLike) { for (size_t ti = 0; ti < model.textures.size(); ++ti) { const std::string key = (ti < textureKeysLower.size()) ? textureKeysLower[ti] : std::string(); LOG_DEBUG("M2 GLOW TEX '", model.name, "' tex[", ti, "]='", key, "' flags=0x", @@ -1345,6 +1243,43 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } + // Copy ribbon emitter data and resolve textures + gpuModel.ribbonEmitters = model.ribbonEmitters; + if (!model.ribbonEmitters.empty()) { + VkDevice device = vkCtx_->getDevice(); + gpuModel.ribbonTextures.resize(model.ribbonEmitters.size(), whiteTexture_.get()); + gpuModel.ribbonTexSets.resize(model.ribbonEmitters.size(), VK_NULL_HANDLE); + for (size_t ri = 0; ri < model.ribbonEmitters.size(); ri++) { + // Resolve texture via textureLookup table + uint16_t texLookupIdx = model.ribbonEmitters[ri].textureIndex; + uint32_t texIdx = (texLookupIdx < model.textureLookup.size()) + ? model.textureLookup[texLookupIdx] : UINT32_MAX; + if (texIdx < allTextures.size() && allTextures[texIdx] != nullptr) { + gpuModel.ribbonTextures[ri] = allTextures[texIdx]; + } + // Allocate descriptor set (reuse particleTexLayout_ = single sampler) + if (particleTexLayout_ && materialDescPool_) { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &particleTexLayout_; + if (vkAllocateDescriptorSets(device, &ai, &gpuModel.ribbonTexSets[ri]) == VK_SUCCESS) { + VkTexture* tex = gpuModel.ribbonTextures[ri]; + VkDescriptorImageInfo imgInfo = tex->descriptorInfo(); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = gpuModel.ribbonTexSets[ri]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + } + } + LOG_DEBUG(" Ribbon emitters loaded: ", model.ribbonEmitters.size()); + } + // Copy texture transform data for UV animation gpuModel.textureTransforms = model.textureTransforms; gpuModel.textureTransformLookup = model.textureTransformLookup; @@ -1406,60 +1341,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } bgpu.texture = tex; - const bool exactLanternGlowTexture = - (batchTexKeyLower == "world\\expansion06\\doodads\\nightelf\\7ne_druid_streetlamp01_light.blp") || - (batchTexKeyLower == "world\\generic\\nightelf\\passive doodads\\lamps\\glowblue32.blp") || - (batchTexKeyLower == "world\\generic\\human\\passive doodads\\stormwind\\t_vfx_glow01_64.blp") || - (batchTexKeyLower == "world\\azeroth\\karazahn\\passivedoodads\\bonfire\\flamelicksmallblue.blp") || - (batchTexKeyLower == "world\\generic\\nightelf\\passive doodads\\magicalimplements\\glow.blp"); - const bool texHasGlowToken = - (batchTexKeyLower.find("glow") != std::string::npos) || - (batchTexKeyLower.find("flare") != std::string::npos) || - (batchTexKeyLower.find("halo") != std::string::npos) || - (batchTexKeyLower.find("light") != std::string::npos); - const bool texHasFlameToken = - (batchTexKeyLower.find("flame") != std::string::npos) || - (batchTexKeyLower.find("fire") != std::string::npos) || - (batchTexKeyLower.find("flamelick") != std::string::npos) || - (batchTexKeyLower.find("ember") != std::string::npos); - const bool texGlowCardToken = - (batchTexKeyLower.find("glow") != std::string::npos) || - (batchTexKeyLower.find("flamelick") != std::string::npos) || - (batchTexKeyLower.find("lensflare") != std::string::npos) || - (batchTexKeyLower.find("t_vfx") != std::string::npos) || - (batchTexKeyLower.find("lightbeam") != std::string::npos) || - (batchTexKeyLower.find("glowball") != std::string::npos) || - (batchTexKeyLower.find("genericglow") != std::string::npos); - const bool texLikelyFlame = - (batchTexKeyLower.find("fire") != std::string::npos) || - (batchTexKeyLower.find("flame") != std::string::npos) || - (batchTexKeyLower.find("torch") != std::string::npos); - const bool texLanternFamily = - (batchTexKeyLower.find("lantern") != std::string::npos) || - (batchTexKeyLower.find("lamp") != std::string::npos) || - (batchTexKeyLower.find("elf") != std::string::npos) || - (batchTexKeyLower.find("silvermoon") != std::string::npos) || - (batchTexKeyLower.find("quel") != std::string::npos) || - (batchTexKeyLower.find("thalas") != std::string::npos); - const bool modelLanternFamily = - (lowerName.find("lantern") != std::string::npos) || - (lowerName.find("lamp") != std::string::npos) || - (lowerName.find("light") != std::string::npos); + const auto tcls = classifyBatchTexture(batchTexKeyLower); + const bool modelLanternFamily = gpuModel.isLanternLike; bgpu.lanternGlowHint = - exactLanternGlowTexture || - ((texHasGlowToken || (modelLanternFamily && texHasFlameToken)) && - (texLanternFamily || modelLanternFamily) && - (!texLikelyFlame || modelLanternFamily)); - bgpu.glowCardLike = bgpu.lanternGlowHint && texGlowCardToken; - const bool texCoolTint = - (batchTexKeyLower.find("blue") != std::string::npos) || - (batchTexKeyLower.find("nightelf") != std::string::npos) || - (batchTexKeyLower.find("arcane") != std::string::npos); - const bool texRedTint = - (batchTexKeyLower.find("red") != std::string::npos) || - (batchTexKeyLower.find("scarlet") != std::string::npos) || - (batchTexKeyLower.find("ruby") != std::string::npos); - bgpu.glowTint = texCoolTint ? 1 : (texRedTint ? 2 : 0); + tcls.exactLanternGlowTex || + ((tcls.hasGlowToken || (modelLanternFamily && tcls.hasFlameToken)) && + (tcls.lanternFamily || modelLanternFamily) && + (!tcls.likelyFlame || modelLanternFamily)); + bgpu.glowCardLike = bgpu.lanternGlowHint && tcls.hasGlowCardToken; + bgpu.glowTint = tcls.glowTint; bool texHasAlpha = false; if (tex != nullptr && tex != whiteTexture_.get()) { auto ait = textureHasAlphaByPtr_.find(tex); @@ -1477,12 +1367,26 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { // since we don't have the full combo table — dual-UV effects are rare edge cases. bgpu.textureUnit = 0; - // Batch is hidden only when its named texture failed to load (avoids white shell artifacts). - // Do NOT bake transparency/color animation tracks here — they animate over time and - // baking the first keyframe value causes legitimate meshes to become invisible. - // Keep terrain clutter visible even when source texture paths are malformed. + // Start at full opacity; hide only if texture failed to load. bgpu.batchOpacity = (texFailed && !groundDetailModel) ? 0.0f : 1.0f; + // Apply at-rest transparency and color alpha from the M2 animation tracks. + // These provide per-batch opacity for ghosts, ethereal effects, fading doodads, etc. + // Skip zero values: some animated tracks start at 0 and animate up, and baking + // that first keyframe would make the entire batch permanently invisible. + if (bgpu.batchOpacity > 0.0f) { + float animAlpha = 1.0f; + if (batch.colorIndex < model.colorAlphas.size()) { + float ca = model.colorAlphas[batch.colorIndex]; + if (ca > 0.001f) animAlpha *= ca; + } + if (batch.transparencyIndex < model.textureWeights.size()) { + float tw = model.textureWeights[batch.transparencyIndex]; + if (tw > 0.001f) animAlpha *= tw; + } + bgpu.batchOpacity *= animAlpha; + } + // Compute batch center and radius for glow sprite positioning if ((bgpu.blendMode >= 3 || bgpu.colorKeyBlack) && batch.indexCount > 0) { glm::vec3 sum(0.0f); @@ -1513,10 +1417,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } // Optional diagnostics for glow/light batches (disabled by default). - if (kGlowDiag && - (lowerName.find("light") != std::string::npos || - lowerName.find("lamp") != std::string::npos || - lowerName.find("lantern") != std::string::npos)) { + if (kGlowDiag && gpuModel.isLanternLike) { LOG_DEBUG("M2 GLOW DIAG '", model.name, "' batch ", gpuModel.batches.size(), ": blend=", bgpu.blendMode, " matFlags=0x", std::hex, bgpu.materialFlags, std::dec, @@ -1635,6 +1536,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)"); @@ -1651,6 +1553,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, return 0; } const auto& mdlRef = modelIt->second; + modelUnusedSince_.erase(modelId); // Deduplicate: skip if same model already at nearly the same position. // Uses hash map for O(1) lookup instead of O(N) scan. @@ -1762,6 +1665,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& LOG_WARNING("Cannot create instance: model ", modelId, " not loaded"); return 0; } + modelUnusedSince_.erase(modelId); // Deduplicate: O(1) hash lookup { @@ -2241,6 +2145,9 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: if (!instance.cachedModel) continue; emitParticles(instance, *instance.cachedModel, deltaTime); updateParticles(instance, deltaTime); + if (!instance.cachedModel->ribbonEmitters.empty()) { + updateRibbons(instance, *instance.cachedModel, deltaTime); + } } } @@ -2383,6 +2290,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; @@ -2398,6 +2306,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); @@ -2407,6 +2321,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]; @@ -2414,14 +2335,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; @@ -2551,8 +2475,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const (batch.blendMode >= 3) || batch.colorKeyBlack || ((batch.materialFlags & 0x01) != 0); - if ((batch.glowCardLike && lanternLikeModel) || - (cardLikeSkipMesh && !lanternLikeModel)) { + const bool lanternGlowCardSkip = + lanternLikeModel && + batch.lanternGlowHint && + smallCardLikeBatch && + cardLikeSkipMesh; + if (lanternGlowCardSkip || (cardLikeSkipMesh && !lanternLikeModel)) { continue; } } @@ -2572,10 +2500,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); } @@ -2660,7 +2588,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++; } @@ -2674,6 +2601,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_; @@ -2692,14 +2620,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; @@ -2748,16 +2679,25 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // Skip glow sprites (handled after loop) const bool batchUnlit = (batch.materialFlags & 0x01) != 0; + const bool koboldFlameCard = batch.colorKeyBlack && model.isKoboldFlame; + const bool smallCardLikeBatch = + (batch.glowSize <= 1.35f) || + (batch.lanternGlowHint && batch.glowSize <= 6.0f); const bool shouldUseGlowSprite = - !batch.colorKeyBlack && + !koboldFlameCard && (model.isElvenLike || model.isLanternLike) && !model.isSpellEffect && - (batch.glowSize <= 1.35f || (batch.lanternGlowHint && batch.glowSize <= 6.0f)) && + smallCardLikeBatch && (batch.lanternGlowHint || (batch.blendMode >= 3) || (batch.colorKeyBlack && batchUnlit && batch.blendMode >= 1)); if (shouldUseGlowSprite) { const bool cardLikeSkipMesh = (batch.blendMode >= 3) || batch.colorKeyBlack || batchUnlit; - if ((batch.glowCardLike && model.isLanternLike) || (cardLikeSkipMesh && !model.isLanternLike)) + const bool lanternGlowCardSkip = + model.isLanternLike && + batch.lanternGlowHint && + smallCardLikeBatch && + cardLikeSkipMesh; + if (lanternGlowCardSkip || (cardLikeSkipMesh && !model.isLanternLike)) continue; } @@ -2776,8 +2716,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); } @@ -3021,7 +2960,7 @@ bool M2Renderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -3375,6 +3314,214 @@ void M2Renderer::updateParticles(M2Instance& inst, float dt) { } } +// --------------------------------------------------------------------------- +// Ribbon emitter simulation +// --------------------------------------------------------------------------- +void M2Renderer::updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt) { + const auto& emitters = gpu.ribbonEmitters; + if (emitters.empty()) return; + + // Grow per-instance state arrays if needed + if (inst.ribbonEdges.size() != emitters.size()) { + inst.ribbonEdges.resize(emitters.size()); + } + if (inst.ribbonEdgeAccumulators.size() != emitters.size()) { + inst.ribbonEdgeAccumulators.resize(emitters.size(), 0.0f); + } + + for (size_t ri = 0; ri < emitters.size(); ri++) { + const auto& em = emitters[ri]; + auto& edges = inst.ribbonEdges[ri]; + auto& accum = inst.ribbonEdgeAccumulators[ri]; + + // Determine bone world position for spine + glm::vec3 spineWorld = inst.position; + if (em.bone < inst.boneMatrices.size()) { + glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f); + spineWorld = glm::vec3(inst.modelMatrix * inst.boneMatrices[em.bone] * local); + } else { + glm::vec4 local(em.position.x, em.position.y, em.position.z, 1.0f); + spineWorld = glm::vec3(inst.modelMatrix * local); + } + + // Evaluate animated tracks (use first available sequence key, or fallback value) + auto getFloatVal = [&](const pipeline::M2AnimationTrack& track, float fallback) -> float { + for (const auto& seq : track.sequences) { + if (!seq.floatValues.empty()) return seq.floatValues[0]; + } + return fallback; + }; + auto getVec3Val = [&](const pipeline::M2AnimationTrack& track, glm::vec3 fallback) -> glm::vec3 { + for (const auto& seq : track.sequences) { + if (!seq.vec3Values.empty()) return seq.vec3Values[0]; + } + return fallback; + }; + + float visibility = getFloatVal(em.visibilityTrack, 1.0f); + float heightAbove = getFloatVal(em.heightAboveTrack, 0.5f); + float heightBelow = getFloatVal(em.heightBelowTrack, 0.5f); + glm::vec3 color = getVec3Val(em.colorTrack, glm::vec3(1.0f)); + float alpha = getFloatVal(em.alphaTrack, 1.0f); + + // Age existing edges and remove expired ones + for (auto& e : edges) { + e.age += dt; + // Apply gravity + if (em.gravity != 0.0f) { + e.worldPos.z -= em.gravity * dt * dt * 0.5f; + } + } + while (!edges.empty() && edges.front().age >= em.edgeLifetime) { + edges.pop_front(); + } + + // Emit new edges based on edgesPerSecond + if (visibility > 0.5f) { + accum += em.edgesPerSecond * dt; + while (accum >= 1.0f) { + accum -= 1.0f; + M2Instance::RibbonEdge e; + e.worldPos = spineWorld; + e.color = color; + e.alpha = alpha; + e.heightAbove = heightAbove; + e.heightBelow = heightBelow; + e.age = 0.0f; + edges.push_back(e); + // Cap trail length + if (edges.size() > 128) edges.pop_front(); + } + } else { + accum = 0.0f; + } + } +} + +// --------------------------------------------------------------------------- +// Ribbon rendering +// --------------------------------------------------------------------------- +void M2Renderer::renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { + if (!ribbonPipeline_ || !ribbonAdditivePipeline_ || !ribbonVB_ || !ribbonVBMapped_) return; + + // Build camera right vector for billboard orientation + // For ribbons we orient the quad strip along the spine with screen-space up. + // Simple approach: use world-space Z=up for the ribbon cross direction. + const glm::vec3 upWorld(0.0f, 0.0f, 1.0f); + + float* dst = static_cast(ribbonVBMapped_); + size_t written = 0; + + struct DrawCall { + VkDescriptorSet texSet; + VkPipeline pipeline; + uint32_t firstVertex; + uint32_t vertexCount; + }; + std::vector draws; + + for (const auto& inst : instances) { + if (!inst.cachedModel) continue; + const auto& gpu = *inst.cachedModel; + if (gpu.ribbonEmitters.empty()) continue; + + for (size_t ri = 0; ri < gpu.ribbonEmitters.size(); ri++) { + if (ri >= inst.ribbonEdges.size()) continue; + const auto& edges = inst.ribbonEdges[ri]; + if (edges.size() < 2) continue; + + const auto& em = gpu.ribbonEmitters[ri]; + + // Select blend pipeline based on material blend mode + bool additive = false; + if (em.materialIndex < gpu.batches.size()) { + additive = (gpu.batches[em.materialIndex].blendMode >= 3); + } + VkPipeline pipe = additive ? ribbonAdditivePipeline_ : ribbonPipeline_; + + // Descriptor set for texture + VkDescriptorSet texSet = (ri < gpu.ribbonTexSets.size()) + ? gpu.ribbonTexSets[ri] : VK_NULL_HANDLE; + if (!texSet) continue; + + uint32_t firstVert = static_cast(written); + + // Emit triangle strip: 2 verts per edge (top + bottom) + for (size_t ei = 0; ei < edges.size(); ei++) { + if (written + 2 > MAX_RIBBON_VERTS) break; + const auto& e = edges[ei]; + float t = (em.edgeLifetime > 0.0f) + ? 1.0f - (e.age / em.edgeLifetime) : 1.0f; + float a = e.alpha * t; + float u = static_cast(ei) / static_cast(edges.size() - 1); + + // Top vertex (above spine along upWorld) + glm::vec3 top = e.worldPos + upWorld * e.heightAbove; + dst[written * 9 + 0] = top.x; + dst[written * 9 + 1] = top.y; + dst[written * 9 + 2] = top.z; + dst[written * 9 + 3] = e.color.r; + dst[written * 9 + 4] = e.color.g; + dst[written * 9 + 5] = e.color.b; + dst[written * 9 + 6] = a; + dst[written * 9 + 7] = u; + dst[written * 9 + 8] = 0.0f; // v = top + written++; + + // Bottom vertex (below spine) + glm::vec3 bot = e.worldPos - upWorld * e.heightBelow; + dst[written * 9 + 0] = bot.x; + dst[written * 9 + 1] = bot.y; + dst[written * 9 + 2] = bot.z; + dst[written * 9 + 3] = e.color.r; + dst[written * 9 + 4] = e.color.g; + dst[written * 9 + 5] = e.color.b; + dst[written * 9 + 6] = a; + dst[written * 9 + 7] = u; + dst[written * 9 + 8] = 1.0f; // v = bottom + written++; + } + + uint32_t vertCount = static_cast(written) - firstVert; + if (vertCount >= 4) { + draws.push_back({texSet, pipe, firstVert, vertCount}); + } else { + // Rollback if too few verts + written = firstVert; + } + } + } + + if (draws.empty() || written == 0) return; + + VkExtent2D ext = vkCtx_->getSwapchainExtent(); + VkViewport vp{}; + vp.x = 0; vp.y = 0; + vp.width = static_cast(ext.width); + vp.height = static_cast(ext.height); + vp.minDepth = 0.0f; vp.maxDepth = 1.0f; + VkRect2D sc{}; + sc.offset = {0, 0}; + sc.extent = ext; + vkCmdSetViewport(cmd, 0, 1, &vp); + vkCmdSetScissor(cmd, 0, 1, &sc); + + VkPipeline lastPipe = VK_NULL_HANDLE; + for (const auto& dc : draws) { + if (dc.pipeline != lastPipe) { + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, dc.pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + ribbonPipelineLayout_, 0, 1, &perFrameSet, 0, nullptr); + lastPipe = dc.pipeline; + } + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + ribbonPipelineLayout_, 1, 1, &dc.texSet, 0, nullptr); + VkDeviceSize offset = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &ribbonVB_, &offset); + vkCmdDraw(cmd, dc.vertexCount, 1, dc.firstVertex, 0); + } +} + void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { if (!particlePipeline_ || !m2ParticleVB_) return; @@ -3628,6 +3775,18 @@ void M2Renderer::setInstanceAnimationFrozen(uint32_t instanceId, bool frozen) { } } +float M2Renderer::getInstanceAnimDuration(uint32_t instanceId) const { + auto idxIt = instanceIndexById.find(instanceId); + if (idxIt == instanceIndexById.end()) return 0.0f; + const auto& inst = instances[idxIt->second]; + if (!inst.cachedModel) return 0.0f; + const auto& seqs = inst.cachedModel->sequences; + if (seqs.empty()) return 0.0f; + int seqIdx = inst.currentSequenceIndex; + if (seqIdx < 0 || seqIdx >= static_cast(seqs.size())) seqIdx = 0; + return seqs[seqIdx].duration; // in milliseconds +} + void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& transform) { auto idxIt = instanceIndexById.find(instanceId); if (idxIt == instanceIndexById.end()) return; @@ -3682,14 +3841,67 @@ void M2Renderer::setInstanceTransform(uint32_t instanceId, const glm::mat4& tran } void M2Renderer::removeInstance(uint32_t instanceId) { - for (auto it = instances.begin(); it != instances.end(); ++it) { - if (it->id == instanceId) { - destroyInstanceBones(*it); - instances.erase(it); - rebuildSpatialIndex(); - return; + auto idxIt = instanceIndexById.find(instanceId); + if (idxIt == instanceIndexById.end()) return; + size_t idx = idxIt->second; + if (idx >= instances.size()) return; + + auto& inst = instances[idx]; + + // Remove from spatial grid incrementally (same pattern as the move-update path) + GridCell minCell = toCell(inst.worldBoundsMin); + GridCell maxCell = toCell(inst.worldBoundsMax); + for (int z = minCell.z; z <= maxCell.z; z++) { + for (int y = minCell.y; y <= maxCell.y; y++) { + for (int x = minCell.x; x <= maxCell.x; x++) { + auto gIt = spatialGrid.find(GridCell{x, y, z}); + if (gIt != spatialGrid.end()) { + auto& vec = gIt->second; + vec.erase(std::remove(vec.begin(), vec.end(), instanceId), vec.end()); + } + } } } + + // Remove from dedup map + if (!inst.cachedIsGroundDetail) { + DedupKey dk{inst.modelId, + static_cast(std::round(inst.position.x * 10.0f)), + static_cast(std::round(inst.position.y * 10.0f)), + static_cast(std::round(inst.position.z * 10.0f))}; + instanceDedupMap_.erase(dk); + } + + destroyInstanceBones(inst); + + // Swap-remove: move last element to the hole and pop_back to avoid O(n) shift + instanceIndexById.erase(instanceId); + if (idx < instances.size() - 1) { + uint32_t movedId = instances.back().id; + instances[idx] = std::move(instances.back()); + instances.pop_back(); + instanceIndexById[movedId] = idx; + } else { + instances.pop_back(); + } + + // Rebuild the lightweight auxiliary index vectors (smoke, portal, etc.) + // These are small vectors of indices that are rebuilt cheaply. + smokeInstanceIndices_.clear(); + portalInstanceIndices_.clear(); + animatedInstanceIndices_.clear(); + particleOnlyInstanceIndices_.clear(); + particleInstanceIndices_.clear(); + for (size_t i = 0; i < instances.size(); i++) { + auto& ri = instances[i]; + if (ri.cachedIsSmoke) smokeInstanceIndices_.push_back(i); + if (ri.cachedIsInstancePortal) portalInstanceIndices_.push_back(i); + if (ri.cachedHasParticleEmitters) particleInstanceIndices_.push_back(i); + if (ri.cachedHasAnimation && !ri.cachedDisableAnimation) + animatedInstanceIndices_.push_back(i); + else if (ri.cachedHasParticleEmitters) + particleOnlyInstanceIndices_.push_back(i); + } } void M2Renderer::setSkipCollision(uint32_t instanceId, bool skip) { @@ -3761,6 +3973,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(); @@ -3897,15 +4124,39 @@ void M2Renderer::cleanupUnusedModels() { usedModelIds.insert(instance.modelId); } - // Find and remove models with no instances + const auto now = std::chrono::steady_clock::now(); + constexpr auto kGracePeriod = std::chrono::seconds(60); + + // Find models with no instances that have exceeded the grace period. + // Models that just lost their last instance get tracked but not evicted + // immediately — this prevents thrashing when GO models are briefly + // instance-free between despawn and respawn cycles. std::vector toRemove; for (const auto& [id, model] : models) { - if (usedModelIds.find(id) == usedModelIds.end()) { + if (usedModelIds.find(id) != usedModelIds.end()) { + // Model still in use — clear any pending unused timestamp + modelUnusedSince_.erase(id); + continue; + } + auto unusedIt = modelUnusedSince_.find(id); + if (unusedIt == modelUnusedSince_.end()) { + // First cycle with no instances — start the grace timer + modelUnusedSince_[id] = now; + } else if (now - unusedIt->second >= kGracePeriod) { + // Grace period expired — mark for removal toRemove.push_back(id); + modelUnusedSince_.erase(unusedIt); } } - // Delete GPU resources and remove from map + // Delete GPU resources and remove from map. + // Wait for the GPU to finish all in-flight frames before destroying any + // buffers — the previous frame's command buffer may still be referencing + // vertex/index buffers that are about to be freed. Without this wait, + // the GPU reads freed memory, which can cause VK_ERROR_DEVICE_LOST. + if (!toRemove.empty() && vkCtx_) { + vkDeviceWaitIdle(vkCtx_->getDevice()); + } for (uint32_t id : toRemove) { auto it = models.find(id); if (it != models.end()) { @@ -3920,6 +4171,7 @@ void M2Renderer::cleanupUnusedModels() { } VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { + constexpr uint64_t kFailedTextureRetryLookups = 512; auto normalizeKey = [](std::string key) { std::replace(key.begin(), key.end(), '/', '\\'); std::transform(key.begin(), key.end(), key.begin(), @@ -3927,6 +4179,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { return key; }; std::string key = normalizeKey(path); + const uint64_t lookupSerial = ++textureLookupSerial_; // Check cache auto it = textureCache.find(key); @@ -3934,7 +4187,10 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { it->second.lastUse = ++textureCacheCounter_; return it->second.texture.get(); } - // No negative cache check — allow retries for transiently missing textures + auto failIt = failedTextureRetryAt_.find(key); + if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) { + return whiteTexture_.get(); + } auto containsToken = [](const std::string& haystack, const char* token) { return haystack.find(token) != std::string::npos; @@ -3965,8 +4221,9 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { blp = assetManager->loadTexture(key); } if (!blp.isValid()) { - // Return white fallback but don't cache the failure — MPQ reads can - // fail transiently during streaming; allow retry on next model load. + // Cache misses briefly to avoid repeated expensive MPQ/disk probes. + failedTextureCache_.insert(key); + failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups; if (loggedTextureLoadFails_.insert(key).second) { LOG_WARNING("M2: Failed to load texture: ", path); } @@ -3981,6 +4238,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { // Cache budget-rejected keys too; without this we repeatedly decode/load // the same textures every frame once budget is saturated. failedTextureCache_.insert(key); + failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups; } if (textureBudgetRejectWarnings_ < 3) { LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024), @@ -4019,6 +4277,8 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { e.lastUse = ++textureCacheCounter_; textureCacheBytes_ += e.approxBytes; textureCache[key] = std::move(e); + failedTextureCache_.erase(key); + failedTextureRetryAt_.erase(key); textureHasAlphaByPtr_[texPtr] = hasAlpha; textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint; LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); @@ -4498,6 +4758,8 @@ void M2Renderer::recreatePipelines() { if (particlePipeline_) { vkDestroyPipeline(device, particlePipeline_, nullptr); particlePipeline_ = VK_NULL_HANDLE; } if (particleAdditivePipeline_) { vkDestroyPipeline(device, particleAdditivePipeline_, nullptr); particleAdditivePipeline_ = VK_NULL_HANDLE; } if (smokePipeline_) { vkDestroyPipeline(device, smokePipeline_, nullptr); smokePipeline_ = VK_NULL_HANDLE; } + if (ribbonPipeline_) { vkDestroyPipeline(device, ribbonPipeline_, nullptr); ribbonPipeline_ = VK_NULL_HANDLE; } + if (ribbonAdditivePipeline_) { vkDestroyPipeline(device, ribbonAdditivePipeline_, nullptr); ribbonAdditivePipeline_ = VK_NULL_HANDLE; } // --- Load shaders --- rendering::VkShaderModule m2Vert, m2Frag; @@ -4546,7 +4808,7 @@ void M2Renderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; opaquePipeline_ = buildM2Pipeline(PipelineBuilder::blendDisabled(), true); @@ -4581,7 +4843,7 @@ void M2Renderer::recreatePipelines() { .setLayout(particlePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; particlePipeline_ = buildParticlePipeline(PipelineBuilder::blendAlpha()); @@ -4614,7 +4876,47 @@ void M2Renderer::recreatePipelines() { .setLayout(smokePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); + } + + // --- Ribbon pipelines --- + { + rendering::VkShaderModule ribVert, ribFrag; + ribVert.loadFromFile(device, "assets/shaders/m2_ribbon.vert.spv"); + ribFrag.loadFromFile(device, "assets/shaders/m2_ribbon.frag.spv"); + if (ribVert.isValid() && ribFrag.isValid()) { + VkVertexInputBindingDescription rBind{}; + rBind.binding = 0; + rBind.stride = 9 * sizeof(float); + rBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + std::vector rAttrs = { + {0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, + {1, 0, VK_FORMAT_R32G32B32_SFLOAT, 3 * sizeof(float)}, + {2, 0, VK_FORMAT_R32_SFLOAT, 6 * sizeof(float)}, + {3, 0, VK_FORMAT_R32G32_SFLOAT, 7 * sizeof(float)}, + }; + + auto buildRibbonPipeline = [&](VkPipelineColorBlendAttachmentState blend) -> VkPipeline { + return PipelineBuilder() + .setShaders(ribVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + ribFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({rBind}, rAttrs) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setDepthTest(true, false, VK_COMPARE_OP_LESS_OR_EQUAL) + .setColorBlendAttachment(blend) + .setMultisample(vkCtx_->getMsaaSamples()) + .setLayout(ribbonPipelineLayout_) + .setRenderPass(mainPass) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device, vkCtx_->getPipelineCache()); + }; + + ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha()); + ribbonAdditivePipeline_ = buildRibbonPipeline(PipelineBuilder::blendAdditive()); + } + ribVert.destroy(); ribFrag.destroy(); } m2Vert.destroy(); m2Frag.destroy(); diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index 0f44869b..7cccca2b 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -165,7 +165,7 @@ bool Minimap::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout* .setLayout(tilePipelineLayout) .setRenderPass(compositeTarget->getRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); @@ -192,7 +192,7 @@ bool Minimap::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout* .setLayout(displayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); @@ -228,6 +228,7 @@ void Minimap::shutdown() { if (tex) tex->destroy(device, alloc); } tileTextureCache.clear(); + tileInsertionOrder.clear(); if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); } if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); } @@ -269,7 +270,7 @@ void Minimap::recreatePipelines() { .setLayout(displayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); @@ -362,6 +363,15 @@ VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) { VkTexture* ptr = tex.get(); tileTextureCache[hash] = std::move(tex); + tileInsertionOrder.push_back(hash); + + // Evict oldest tiles when cache grows too large to bound GPU memory usage. + while (tileInsertionOrder.size() > MAX_TILE_CACHE) { + const std::string& oldest = tileInsertionOrder.front(); + tileTextureCache.erase(oldest); + tileInsertionOrder.pop_front(); + } + return ptr; } @@ -513,14 +523,15 @@ void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera, float arrowRotation = 0.0f; if (!rotateWithCamera) { - // Prefer authoritative orientation if provided. This value is expected - // to already match minimap shader rotation convention. if (hasPlayerOrientation) { arrowRotation = playerOrientation; } else { glm::vec3 fwd = playerCamera.getForward(); - arrowRotation = std::atan2(-fwd.x, fwd.y); + arrowRotation = -std::atan2(-fwd.x, fwd.y); } + } else if (hasPlayerOrientation) { + // Show character facing relative to the rotated map + arrowRotation = playerOrientation + rotation; } MinimapDisplayPush push{}; diff --git a/src/rendering/mount_dust.cpp b/src/rendering/mount_dust.cpp index 5678f31c..560e8a42 100644 --- a/src/rendering/mount_dust.cpp +++ b/src/rendering/mount_dust.cpp @@ -92,7 +92,7 @@ bool MountDust::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -199,7 +199,7 @@ void MountDust::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/performance_hud.cpp b/src/rendering/performance_hud.cpp index 09430dce..67f9f7fa 100644 --- a/src/rendering/performance_hud.cpp +++ b/src/rendering/performance_hud.cpp @@ -10,6 +10,7 @@ #include "rendering/clouds.hpp" #include "rendering/lens_flare.hpp" #include "rendering/weather.hpp" +#include "rendering/lightning.hpp" #include "rendering/character_renderer.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" @@ -22,6 +23,12 @@ namespace wowee { namespace rendering { +namespace { + constexpr ImVec4 kHelpText = {0.6f, 0.6f, 0.6f, 1.0f}; + constexpr ImVec4 kSectionHeader = {0.8f, 0.8f, 0.5f, 1.0f}; + constexpr ImVec4 kTitle = {0.7f, 0.7f, 0.7f, 1.0f}; +} // namespace + PerformanceHUD::PerformanceHUD() { } @@ -219,6 +226,13 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) { ImGui::Text(" Upscale Dispatches: %zu", renderer->getAmdFsr3UpscaleDispatchCount()); ImGui::Text(" FG Fallbacks: %zu", renderer->getAmdFsr3FallbackCount()); } + if (renderer->isFXAAEnabled()) { + if (renderer->isFSR2Enabled()) { + ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.8f, 1.0f), "FXAA: ON (FSR3+FXAA combined)"); + } else { + ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.6f, 1.0f), "FXAA: ON"); + } + } ImGui::Spacing(); } @@ -362,6 +376,11 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) { ImGui::Text("Intensity: %.0f%%", weather->getIntensity() * 100.0f); } + auto* lightning = renderer->getLightning(); + if (lightning && lightning->isEnabled()) { + ImGui::Text("Lightning: active (%.0f%%)", lightning->getIntensity() * 100.0f); + } + ImGui::Spacing(); } } @@ -443,39 +462,39 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) { // Controls help if (showControls) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "CONTROLS"); + ImGui::TextColored(kTitle, "CONTROLS"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Movement"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "WASD: Move/Strafe"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Q/E: Turn left/right"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Space: Jump"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "X: Sit/Stand"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "~: Auto-run"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Z: Sheathe weapons"); + ImGui::TextColored(kSectionHeader, "Movement"); + ImGui::TextColored(kHelpText, "WASD: Move/Strafe"); + ImGui::TextColored(kHelpText, "Q/E: Strafe left/right"); + ImGui::TextColored(kHelpText, "Space: Jump"); + ImGui::TextColored(kHelpText, "X: Sit/Stand"); + ImGui::TextColored(kHelpText, "~: Auto-run"); + ImGui::TextColored(kHelpText, "Z: Sheathe weapons"); ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "UI Panels"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "B: Bags/Inventory"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Character sheet"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Quest log"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "N: Talents"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Spellbook"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: World map"); + ImGui::TextColored(kSectionHeader, "UI Panels"); + ImGui::TextColored(kHelpText, "B: Bags/Inventory"); + ImGui::TextColored(kHelpText, "C: Character sheet"); + ImGui::TextColored(kHelpText, "L: Quest log"); + ImGui::TextColored(kHelpText, "N: Talents"); + ImGui::TextColored(kHelpText, "P: Spellbook"); + ImGui::TextColored(kHelpText, "M: World map"); ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Combat & Chat"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "1-0,-,=: Action bar"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Tab: Target cycle"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Enter: Chat"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "/: Chat command"); + ImGui::TextColored(kSectionHeader, "Combat & Chat"); + ImGui::TextColored(kHelpText, "1-0,-,=: Action bar"); + ImGui::TextColored(kHelpText, "Tab: Target cycle"); + ImGui::TextColored(kHelpText, "Enter: Chat"); + ImGui::TextColored(kHelpText, "/: Chat command"); ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Debug"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle this HUD"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Toggle shadows"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F7: Level-up FX"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Esc: Settings/Close"); + ImGui::TextColored(kSectionHeader, "Debug"); + ImGui::TextColored(kHelpText, "F1: Toggle this HUD"); + ImGui::TextColored(kHelpText, "F4: Toggle shadows"); + ImGui::TextColored(kHelpText, "F7: Level-up FX"); + ImGui::TextColored(kHelpText, "Esc: Settings/Close"); } ImGui::End(); diff --git a/src/rendering/quest_marker_renderer.cpp b/src/rendering/quest_marker_renderer.cpp index b274a880..07498285 100644 --- a/src/rendering/quest_marker_renderer.cpp +++ b/src/rendering/quest_marker_renderer.cpp @@ -114,7 +114,7 @@ bool QuestMarkerRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFr .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -233,7 +233,7 @@ void QuestMarkerRenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(vkCtx_->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 7618a345..36e404cb 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -12,6 +12,7 @@ #include "rendering/clouds.hpp" #include "rendering/lens_flare.hpp" #include "rendering/weather.hpp" +#include "rendering/lightning.hpp" #include "rendering/lighting_manager.hpp" #include "rendering/sky_system.hpp" #include "rendering/swim_effects.hpp" @@ -32,7 +33,6 @@ #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "pipeline/dbc_layout.hpp" -#include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" #include "pipeline/adt_loader.hpp" #include "pipeline/terrain_mesh.hpp" @@ -66,6 +66,10 @@ #include #include #include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" #include #include #include @@ -338,7 +342,8 @@ bool Renderer::createPerFrameResources() { sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; sampCI.compareEnable = VK_TRUE; sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; - if (vkCreateSampler(device, &sampCI, nullptr, &shadowSampler) != VK_SUCCESS) { + shadowSampler = vkCtx->getOrCreateSampler(sampCI); + if (shadowSampler == VK_NULL_HANDLE) { LOG_ERROR("Failed to create shadow sampler"); return false; } @@ -592,7 +597,7 @@ void Renderer::destroyPerFrameResources() { shadowDepthLayout_[i] = VK_IMAGE_LAYOUT_UNDEFINED; } if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; } - if (shadowSampler) { vkDestroySampler(device, shadowSampler, nullptr); shadowSampler = VK_NULL_HANDLE; } + shadowSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache } void Renderer::updatePerFrameUBO() { @@ -699,6 +704,9 @@ bool Renderer::initialize(core::Window* win) { weather = std::make_unique(); weather->initialize(vkCtx, perFrameSetLayout); + lightning = std::make_unique(); + lightning->initialize(vkCtx, perFrameSetLayout); + swimEffects = std::make_unique(); swimEffects->initialize(vkCtx, perFrameSetLayout); @@ -802,6 +810,11 @@ void Renderer::shutdown() { weather.reset(); } + if (lightning) { + lightning->shutdown(); + lightning.reset(); + } + if (swimEffects) { swimEffects->shutdown(); swimEffects.reset(); @@ -858,6 +871,7 @@ void Renderer::shutdown() { destroyFSRResources(); destroyFSR2Resources(); + destroyFXAAResources(); destroyPerFrameResources(); zoneManager.reset(); @@ -941,6 +955,7 @@ void Renderer::applyMsaaChange() { if (characterRenderer) characterRenderer->recreatePipelines(); if (questMarkerRenderer) questMarkerRenderer->recreatePipelines(); if (weather) weather->recreatePipelines(); + if (lightning) lightning->recreatePipelines(); if (swimEffects) swimEffects->recreatePipelines(); if (mountDust) mountDust->recreatePipelines(); if (chargeEffect) chargeEffect->recreatePipelines(); @@ -960,8 +975,9 @@ void Renderer::applyMsaaChange() { VkDevice device = vkCtx->getDevice(); if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; } if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; } - if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame() + if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame() if (fsr2_.sceneFramebuffer) destroyFSR2Resources(); + if (fxaa_.sceneFramebuffer) destroyFXAAResources(); // Will be lazily recreated in beginFrame() // Reinitialize ImGui Vulkan backend with new MSAA sample count ImGui_ImplVulkan_Shutdown(); @@ -1017,6 +1033,22 @@ void Renderer::beginFrame() { } } + // FXAA resource management — FXAA can coexist with FSR1 and FSR3. + // When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass. + // Do not force this pass for ghost mode; keep AA quality strictly user-controlled. + const bool useFXAAPostPass = fxaa_.enabled; + if ((fxaa_.needsRecreate || !useFXAAPostPass) && fxaa_.sceneFramebuffer) { + destroyFXAAResources(); + fxaa_.needsRecreate = false; + if (!useFXAAPostPass) LOG_INFO("FXAA: disabled"); + } + if (useFXAAPostPass && !fxaa_.sceneFramebuffer) { + if (!initFXAAResources()) { + LOG_ERROR("FXAA: initialization failed, disabling"); + fxaa_.enabled = false; + } + } + // Handle swapchain recreation if needed if (vkCtx->isSwapchainDirty()) { vkCtx->recreateSwapchain(window->getWidth(), window->getHeight()); @@ -1033,6 +1065,11 @@ void Renderer::beginFrame() { destroyFSR2Resources(); initFSR2Resources(); } + // Recreate FXAA resources for new swapchain dimensions. + if (useFXAAPostPass) { + destroyFXAAResources(); + initFXAAResources(); + } } // Acquire swapchain image and begin command buffer @@ -1119,6 +1156,11 @@ void Renderer::beginFrame() { if (fsr2_.enabled && fsr2_.sceneFramebuffer) { rpInfo.framebuffer = fsr2_.sceneFramebuffer; renderExtent = { fsr2_.internalWidth, fsr2_.internalHeight }; + } else if (useFXAAPostPass && fxaa_.sceneFramebuffer) { + // FXAA takes priority over FSR1: renders at native res with AA post-process. + // When both FSR1 and FXAA are enabled, FXAA wins (native res, no downscale). + rpInfo.framebuffer = fxaa_.sceneFramebuffer; + renderExtent = vkCtx->getSwapchainExtent(); // native resolution — no downscaling } else if (fsr_.enabled && fsr_.sceneFramebuffer) { rpInfo.framebuffer = fsr_.sceneFramebuffer; renderExtent = { fsr_.internalWidth, fsr_.internalHeight }; @@ -1172,6 +1214,11 @@ void Renderer::beginFrame() { void Renderer::endFrame() { if (!vkCtx || currentCmd == VK_NULL_HANDLE) return; + // Track whether a post-processing path switched to an INLINE render pass. + // beginFrame() may have started the scene pass with SECONDARY_COMMAND_BUFFERS; + // post-proc paths end it and begin a new INLINE pass for the swapchain output. + endFrameInlineMode_ = false; + if (fsr2_.enabled && fsr2_.sceneFramebuffer) { // End the off-screen scene render pass vkCmdEndRenderPass(currentCmd); @@ -1208,6 +1255,35 @@ void Renderer::endFrame() { VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); } + // FSR3+FXAA combined: re-point FXAA's descriptor to the FSR3 temporal output + // so renderFXAAPass() applies spatial AA on the temporally-stabilized frame. + // This must happen outside the render pass (descriptor updates are CPU-side). + if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler) { + VkImageView fsr3OutputView = VK_NULL_HANDLE; + if (fsr2_.useAmdBackend) { + if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image) + fsr3OutputView = fsr2_.framegenOutput.imageView; + else if (fsr2_.history[fsr2_.currentHistory].image) + fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView; + } else if (fsr2_.history[fsr2_.currentHistory].image) { + fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView; + } + if (fsr3OutputView) { + VkDescriptorImageInfo imgInfo{}; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imgInfo.imageView = fsr3OutputView; + imgInfo.sampler = fxaa_.sceneSampler; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = fxaa_.descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); + } + } + // Begin swapchain render pass at full resolution for sharpening + ImGui VkRenderPassBeginInfo rpInfo{}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; @@ -1225,7 +1301,7 @@ void Renderer::endFrame() { rpInfo.clearValueCount = msaaOn ? (vkCtx->getDepthResolveImageView() ? 4u : 3u) : 2u; rpInfo.pClearValues = clearValues; - vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + endFrameInlineMode_ = true; vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); VkExtent2D ext = vkCtx->getSwapchainExtent(); VkViewport vp{}; @@ -1237,8 +1313,33 @@ void Renderer::endFrame() { sc.extent = ext; vkCmdSetScissor(currentCmd, 0, 1, &sc); - // Draw RCAS sharpening from accumulated history buffer - renderFSR2Sharpen(); + // When FXAA is also enabled: apply FXAA on the FSR3 temporal output instead + // of RCAS sharpening. FXAA descriptor is temporarily pointed to the FSR3 + // history buffer (which is already in SHADER_READ_ONLY_OPTIMAL). This gives + // FSR3 temporal stability + FXAA spatial edge smoothing ("ultra quality native"). + if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet) { + renderFXAAPass(); + } else { + // Draw RCAS sharpening from accumulated history buffer + renderFSR2Sharpen(); + } + + // Restore FXAA descriptor to its normal scene color source so standalone + // FXAA frames are not affected by the FSR3 history pointer set above. + if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) { + VkDescriptorImageInfo restoreInfo{}; + restoreInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + restoreInfo.imageView = fxaa_.sceneColor.imageView; + restoreInfo.sampler = fxaa_.sceneSampler; + VkWriteDescriptorSet restoreWrite{}; + restoreWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + restoreWrite.dstSet = fxaa_.descSet; + restoreWrite.dstBinding = 0; + restoreWrite.descriptorCount = 1; + restoreWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + restoreWrite.pImageInfo = &restoreInfo; + vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &restoreWrite, 0, nullptr); + } // Maintain frame bookkeeping fsr2_.prevViewProjection = camera->getViewProjectionMatrix(); @@ -1249,43 +1350,33 @@ void Renderer::endFrame() { } fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed - } else if (fsr_.enabled && fsr_.sceneFramebuffer) { + } else if (fxaa_.enabled && fxaa_.sceneFramebuffer) { // End the off-screen scene render pass vkCmdEndRenderPass(currentCmd); - // Transition scene color (1x resolve/color target): PRESENT_SRC_KHR → SHADER_READ_ONLY - // The render pass finalLayout puts the resolve/color attachment in PRESENT_SRC_KHR - transitionImageLayout(currentCmd, fsr_.sceneColor.image, + // Transition resolved scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY + transitionImageLayout(currentCmd, fxaa_.sceneColor.image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - // Begin swapchain render pass at full resolution + // Begin swapchain render pass (1x — no MSAA on the output pass) VkRenderPassBeginInfo rpInfo{}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rpInfo.renderPass = vkCtx->getImGuiRenderPass(); rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; rpInfo.renderArea.offset = {0, 0}; rpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); - - // Clear values must match the render pass attachment count - bool msaaOn = (vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT); - VkClearValue clearValues[4]{}; - clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - clearValues[1].depthStencil = {1.0f, 0}; - clearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - clearValues[3].depthStencil = {1.0f, 0}; - if (msaaOn) { - bool depthRes = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); - rpInfo.clearValueCount = depthRes ? 4 : 3; - } else { - rpInfo.clearValueCount = 2; - } - rpInfo.pClearValues = clearValues; + // The swapchain render pass always has 2 attachments when MSAA is off; + // FXAA output goes to the non-MSAA swapchain directly. + VkClearValue fxaaClear[2]{}; + fxaaClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fxaaClear[1].depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = 2; + rpInfo.pClearValues = fxaaClear; vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); - // Set full-resolution viewport and scissor VkExtent2D ext = vkCtx->getSwapchainExtent(); VkViewport vp{}; vp.width = static_cast(ext.width); @@ -1296,21 +1387,73 @@ void Renderer::endFrame() { sc.extent = ext; vkCmdSetScissor(currentCmd, 0, 1, &sc); - // Draw FSR upscale fullscreen quad + // Draw FXAA pass + renderFXAAPass(); + + } else if (fsr_.enabled && fsr_.sceneFramebuffer) { + // FSR1 upscale path — only runs when FXAA is not active. + // When both FSR1 and FXAA are enabled, FXAA took priority above. + vkCmdEndRenderPass(currentCmd); + + // Transition scene color (1x resolve/color target): PRESENT_SRC_KHR → SHADER_READ_ONLY + transitionImageLayout(currentCmd, fsr_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + // Begin swapchain render pass at full resolution + VkRenderPassBeginInfo fsrRpInfo{}; + fsrRpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + fsrRpInfo.renderPass = vkCtx->getImGuiRenderPass(); + fsrRpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; + fsrRpInfo.renderArea.offset = {0, 0}; + fsrRpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); + + bool fsrMsaaOn = (vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT); + VkClearValue fsrClearValues[4]{}; + fsrClearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fsrClearValues[1].depthStencil = {1.0f, 0}; + fsrClearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fsrClearValues[3].depthStencil = {1.0f, 0}; + if (fsrMsaaOn) { + bool depthRes = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); + fsrRpInfo.clearValueCount = depthRes ? 4 : 3; + } else { + fsrRpInfo.clearValueCount = 2; + } + fsrRpInfo.pClearValues = fsrClearValues; + + vkCmdBeginRenderPass(currentCmd, &fsrRpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkExtent2D fsrExt = vkCtx->getSwapchainExtent(); + VkViewport fsrVp{}; + fsrVp.width = static_cast(fsrExt.width); + fsrVp.height = static_cast(fsrExt.height); + fsrVp.maxDepth = 1.0f; + vkCmdSetViewport(currentCmd, 0, 1, &fsrVp); + VkRect2D fsrSc{}; + fsrSc.extent = fsrExt; + vkCmdSetScissor(currentCmd, 0, 1, &fsrSc); + renderFSRUpscale(); } - // ImGui rendering — must respect subpass contents mode - if (!fsr_.enabled && !fsr2_.enabled && parallelRecordingEnabled_) { - // Scene pass was begun with VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS, - // so ImGui must be recorded into a secondary command buffer. + // ImGui rendering — must respect the subpass contents mode of the + // CURRENT render pass. Post-processing paths (FSR/FXAA) end the scene + // pass and begin a new INLINE pass; if none ran, we're still inside the + // scene pass which may be SECONDARY_COMMAND_BUFFERS when parallel recording + // is active. Track this via endFrameInlineMode_ (set true by any post-proc + // path that started an INLINE render pass). + if (parallelRecordingEnabled_ && !endFrameInlineMode_) { + // Still in the scene pass with SECONDARY_COMMAND_BUFFERS — record + // ImGui into a secondary command buffer. VkCommandBuffer imguiCmd = beginSecondary(SEC_IMGUI); setSecondaryViewportScissor(imguiCmd); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), imguiCmd); vkEndCommandBuffer(imguiCmd); vkCmdExecuteCommands(currentCmd, 1, &imguiCmd); } else { - // FSR swapchain pass uses INLINE mode; non-parallel also uses INLINE. + // INLINE render pass (post-process pass or non-parallel mode). ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), currentCmd); } @@ -1440,7 +1583,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h int bestScore = -999; for (uint32_t id : loops) { int sc = 0; - sc += scoreNear((int)id, 38); // classic hint + sc += scoreNear(static_cast(id), 38); // classic hint const auto* s = findSeqById(id); if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0; if (sc > bestScore) { @@ -1464,10 +1607,10 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h // Start window if (seq.duration >= 450 && seq.duration <= 1100) { int sc = 0; - if (loop) sc += scoreNear((int)seq.id, (int)loop); + if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); // Chain bonus: if this start points at loop or near it - if (loop && (seq.nextAnimation == (int16_t)loop || seq.aliasNext == loop)) sc += 30; - if (loop && scoreNear(seq.nextAnimation, (int)loop) > 0) sc += 10; + if (loop && (seq.nextAnimation == static_cast(loop) || seq.aliasNext == loop)) sc += 30; + if (loop && scoreNear(seq.nextAnimation, static_cast(loop)) > 0) sc += 10; // Penalize "stop/brake-ish": very long blendTime can be a stop transition if (seq.blendTime > 400) sc -= 5; @@ -1480,9 +1623,9 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h // End window if (seq.duration >= 650 && seq.duration <= 1600) { int sc = 0; - if (loop) sc += scoreNear((int)seq.id, (int)loop); + if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); // Chain bonus: end often points to run/stand or has no next - if (seq.nextAnimation == (int16_t)runId || seq.nextAnimation == (int16_t)standId) sc += 10; + if (seq.nextAnimation == static_cast(runId) || seq.nextAnimation == static_cast(standId)) sc += 10; if (seq.nextAnimation < 0) sc += 5; // no chain sometimes = terminal if (sc > bestEnd) { bestEnd = sc; @@ -1555,7 +1698,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration && !isDeathOrWound && !isAttackOrCombat && !isSpecial) { // Bonus: chains back to stand (indicates idle behavior) - bool chainsToStand = (seq.nextAnimation == (int16_t)mountAnims_.stand) || + bool chainsToStand = (seq.nextAnimation == static_cast(mountAnims_.stand)) || (seq.aliasNext == mountAnims_.stand) || (seq.nextAnimation == -1); @@ -1726,7 +1869,18 @@ void Renderer::updateCharacterAnimation() { CharAnimState newState = charAnimState; - bool moving = cameraController->isMoving(); + const bool rawMoving = cameraController->isMoving(); + const bool rawSprinting = cameraController->isSprinting(); + constexpr float kLocomotionStopGraceSec = 0.12f; + if (rawMoving) { + locomotionStopGraceTimer_ = kLocomotionStopGraceSec; + locomotionWasSprinting_ = rawSprinting; + } else { + locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_); + } + // Debounce brief input/state dropouts (notably during both-mouse steering) so + // locomotion clips do not restart every few frames. + bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f; bool movingForward = cameraController->isMovingForward(); bool movingBackward = cameraController->isMovingBackward(); bool autoRunning = cameraController->isAutoRunning(); @@ -1739,7 +1893,7 @@ void Renderer::updateCharacterAnimation() { bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; bool grounded = cameraController->isGrounded(); bool jumping = cameraController->isJumping(); - bool sprinting = cameraController->isSprinting(); + bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_); bool sitting = cameraController->isSitting(); bool swim = cameraController->isSwimming(); bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim; @@ -2399,8 +2553,14 @@ void Renderer::updateCharacterAnimation() { float currentAnimTimeMs = 0.0f; float currentAnimDurationMs = 0.0f; bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - if (!haveState || currentAnimId != animId) { + // Some frames may transiently fail getAnimationState() while resources/instance state churn. + // Avoid reissuing the same clip on those frames, which restarts locomotion and causes hitches. + const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); + const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); + if (shouldPlay) { characterRenderer->playAnimation(characterInstanceId, animId, loop); + lastPlayerAnimRequest_ = animId; + lastPlayerAnimLoopRequest_ = loop; } } @@ -2427,6 +2587,101 @@ void Renderer::cancelEmote() { emoteLoop = false; } +bool Renderer::captureScreenshot(const std::string& outputPath) { + if (!vkCtx) return false; + + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + VkExtent2D extent = vkCtx->getSwapchainExtent(); + const auto& images = vkCtx->getSwapchainImages(); + + if (images.empty() || currentImageIndex >= images.size()) return false; + + VkImage srcImage = images[currentImageIndex]; + uint32_t w = extent.width; + uint32_t h = extent.height; + VkDeviceSize bufSize = static_cast(w) * h * 4; + + // Stall GPU so the swapchain image is idle + vkDeviceWaitIdle(device); + + // Create staging buffer + VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufInfo.size = bufSize; + bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + + VmaAllocationCreateInfo allocCI{}; + allocCI.usage = VMA_MEMORY_USAGE_CPU_ONLY; + + VkBuffer stagingBuf = VK_NULL_HANDLE; + VmaAllocation stagingAlloc = VK_NULL_HANDLE; + if (vmaCreateBuffer(alloc, &bufInfo, &allocCI, &stagingBuf, &stagingAlloc, nullptr) != VK_SUCCESS) { + LOG_WARNING("Screenshot: failed to create staging buffer"); + return false; + } + + // Record copy commands + VkCommandBuffer cmd = vkCtx->beginSingleTimeCommands(); + + // Transition swapchain image: PRESENT_SRC → TRANSFER_SRC + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toTransfer.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toTransfer.image = srcImage; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toTransfer); + + // Copy image to buffer + VkBufferImageCopy region{}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {w, h, 1}; + vkCmdCopyImageToBuffer(cmd, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + stagingBuf, 1, ®ion); + + // Transition back: TRANSFER_SRC → PRESENT_SRC + VkImageMemoryBarrier toPresent = toTransfer; + toPresent.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toPresent.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + toPresent.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toPresent.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, nullptr, 0, nullptr, 1, &toPresent); + + vkCtx->endSingleTimeCommands(cmd); + + // Map and convert BGRA → RGBA + void* mapped = nullptr; + vmaMapMemory(alloc, stagingAlloc, &mapped); + auto* pixels = static_cast(mapped); + for (uint32_t i = 0; i < w * h; ++i) { + std::swap(pixels[i * 4 + 0], pixels[i * 4 + 2]); // B ↔ R + } + + // Ensure output directory exists + std::filesystem::path outPath(outputPath); + if (outPath.has_parent_path()) + std::filesystem::create_directories(outPath.parent_path()); + + int ok = stbi_write_png(outputPath.c_str(), + static_cast(w), static_cast(h), + 4, pixels, static_cast(w * 4)); + + vmaUnmapMemory(alloc, stagingAlloc); + vmaDestroyBuffer(alloc, stagingBuf, stagingAlloc); + + if (ok) { + LOG_INFO("Screenshot saved: ", outputPath); + } else { + LOG_WARNING("Screenshot: stbi_write_png failed for ", outputPath); + } + return ok != 0; +} + void Renderer::triggerLevelUpEffect(const glm::vec3& position) { if (!levelUpEffect) return; @@ -2480,6 +2735,201 @@ void Renderer::stopChargeEffect() { } } +// ─── Spell Visual Effects ──────────────────────────────────────────────────── + +void Renderer::loadSpellVisualDbc() { + if (spellVisualDbcLoaded_) return; + spellVisualDbcLoaded_ = true; // Set early to prevent re-entry on failure + + if (!cachedAssetManager) { + cachedAssetManager = core::Application::getInstance().getAssetManager(); + } + if (!cachedAssetManager) return; + + auto* layout = pipeline::getActiveDBCLayout(); + const pipeline::DBCFieldMap* svLayout = layout ? layout->getLayout("SpellVisual") : nullptr; + const pipeline::DBCFieldMap* kitLayout = layout ? layout->getLayout("SpellVisualKit") : nullptr; + const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr; + + uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2; + uint32_t svImpactKitField = svLayout ? (*svLayout)["ImpactKit"] : 3; + uint32_t svMissileField = svLayout ? (*svLayout)["MissileModel"] : 8; + uint32_t kitSpecial0Field = kitLayout ? (*kitLayout)["SpecialEffect0"] : 11; + uint32_t kitBaseField = kitLayout ? (*kitLayout)["BaseEffect"] : 5; + uint32_t fxFilePathField = fxLayout ? (*fxLayout)["FilePath"] : 2; + + // Helper to look up effectName path from a kit ID + // Load SpellVisualEffectName.dbc — ID → M2 path + auto fxDbc = cachedAssetManager->loadDBC("SpellVisualEffectName.dbc"); + if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) { + LOG_DEBUG("SpellVisual: SpellVisualEffectName.dbc unavailable (fc=", + fxDbc ? fxDbc->getFieldCount() : 0, ")"); + return; + } + std::unordered_map effectPaths; // effectNameId → path + for (uint32_t i = 0; i < fxDbc->getRecordCount(); ++i) { + uint32_t id = fxDbc->getUInt32(i, 0); + std::string p = fxDbc->getString(i, fxFilePathField); + if (id && !p.empty()) effectPaths[id] = p; + } + + // Load SpellVisualKit.dbc — kitId → best SpellVisualEffectName ID + auto kitDbc = cachedAssetManager->loadDBC("SpellVisualKit.dbc"); + std::unordered_map kitToEffectName; // kitId → effectNameId + if (kitDbc && kitDbc->isLoaded()) { + uint32_t fc = kitDbc->getFieldCount(); + for (uint32_t i = 0; i < kitDbc->getRecordCount(); ++i) { + uint32_t kitId = kitDbc->getUInt32(i, 0); + if (!kitId) continue; + // Prefer SpecialEffect0, fall back to BaseEffect + uint32_t eff = 0; + if (kitSpecial0Field < fc) eff = kitDbc->getUInt32(i, kitSpecial0Field); + if (!eff && kitBaseField < fc) eff = kitDbc->getUInt32(i, kitBaseField); + if (eff) kitToEffectName[kitId] = eff; + } + } + + // Helper: resolve path for a given kit ID + auto kitPath = [&](uint32_t kitId) -> std::string { + if (!kitId) return {}; + auto kitIt = kitToEffectName.find(kitId); + if (kitIt == kitToEffectName.end()) return {}; + auto fxIt = effectPaths.find(kitIt->second); + return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; + }; + auto missilePath = [&](uint32_t effId) -> std::string { + if (!effId) return {}; + auto fxIt = effectPaths.find(effId); + return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; + }; + + // Load SpellVisual.dbc — visualId → cast/impact M2 paths via kit chain + auto svDbc = cachedAssetManager->loadDBC("SpellVisual.dbc"); + if (!svDbc || !svDbc->isLoaded()) { + LOG_DEBUG("SpellVisual: SpellVisual.dbc unavailable"); + return; + } + uint32_t svFc = svDbc->getFieldCount(); + uint32_t loadedCast = 0, loadedImpact = 0; + for (uint32_t i = 0; i < svDbc->getRecordCount(); ++i) { + uint32_t vid = svDbc->getUInt32(i, 0); + if (!vid) continue; + + // Cast path: CastKit → SpecialEffect0/BaseEffect, fallback to MissileModel + { + std::string path; + if (svCastKitField < svFc) + path = kitPath(svDbc->getUInt32(i, svCastKitField)); + if (path.empty() && svMissileField < svFc) + path = missilePath(svDbc->getUInt32(i, svMissileField)); + if (!path.empty()) { spellVisualCastPath_[vid] = path; ++loadedCast; } + } + // Impact path: ImpactKit → SpecialEffect0/BaseEffect, fallback to MissileModel + { + std::string path; + if (svImpactKitField < svFc) + path = kitPath(svDbc->getUInt32(i, svImpactKitField)); + if (path.empty() && svMissileField < svFc) + path = missilePath(svDbc->getUInt32(i, svMissileField)); + if (!path.empty()) { spellVisualImpactPath_[vid] = path; ++loadedImpact; } + } + } + LOG_INFO("SpellVisual: loaded cast=", loadedCast, " impact=", loadedImpact, + " visual→M2 mappings (of ", svDbc->getRecordCount(), " records)"); +} + +void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, + bool useImpactKit) { + if (!m2Renderer || visualId == 0) return; + + if (!cachedAssetManager) + cachedAssetManager = core::Application::getInstance().getAssetManager(); + if (!cachedAssetManager) return; + + if (!spellVisualDbcLoaded_) loadSpellVisualDbc(); + + // Select cast or impact path map + auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_; + auto pathIt = pathMap.find(visualId); + if (pathIt == pathMap.end()) return; // No model for this visual + + const std::string& modelPath = pathIt->second; + + // Get or assign a model ID for this path + auto midIt = spellVisualModelIds_.find(modelPath); + uint32_t modelId = 0; + if (midIt != spellVisualModelIds_.end()) { + modelId = midIt->second; + } else { + if (nextSpellVisualModelId_ >= 999800) { + LOG_WARNING("SpellVisual: model ID pool exhausted"); + return; + } + modelId = nextSpellVisualModelId_++; + spellVisualModelIds_[modelPath] = modelId; + } + + // Skip models that have previously failed to load (avoid repeated I/O) + if (spellVisualFailedModels_.count(modelId)) return; + + // Load the M2 model if not already loaded + if (!m2Renderer->hasModel(modelId)) { + auto m2Data = cachedAssetManager->readFile(modelPath); + if (m2Data.empty()) { + LOG_DEBUG("SpellVisual: could not read model: ", modelPath); + spellVisualFailedModels_.insert(modelId); + return; + } + pipeline::M2Model model = pipeline::M2Loader::load(m2Data); + if (model.vertices.empty() && model.particleEmitters.empty()) { + LOG_DEBUG("SpellVisual: empty model: ", modelPath); + spellVisualFailedModels_.insert(modelId); + return; + } + // Load skin file for WotLK-format M2s + if (model.version >= 264) { + std::string skinPath = modelPath.substr(0, modelPath.rfind('.')) + "00.skin"; + auto skinData = cachedAssetManager->readFile(skinPath); + if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model); + } + if (!m2Renderer->loadModel(model, modelId)) { + LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath); + spellVisualFailedModels_.insert(modelId); + return; + } + LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath); + } + + // Spawn instance at world position + uint32_t instanceId = m2Renderer->createInstance(modelId, worldPosition, + glm::vec3(0.0f), 1.0f); + if (instanceId == 0) { + LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId); + return; + } + // Determine lifetime from M2 animation duration (clamp to reasonable range) + float animDurMs = m2Renderer->getInstanceAnimDuration(instanceId); + float duration = (animDurMs > 100.0f) + ? std::clamp(animDurMs / 1000.0f, 0.5f, SPELL_VISUAL_MAX_DURATION) + : SPELL_VISUAL_DEFAULT_DURATION; + activeSpellVisuals_.push_back({instanceId, 0.0f, duration}); + LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId, + " duration=", duration, "s model=", modelPath); +} + +void Renderer::updateSpellVisuals(float deltaTime) { + if (activeSpellVisuals_.empty() || !m2Renderer) return; + for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) { + it->elapsed += deltaTime; + if (it->elapsed >= it->duration) { + m2Renderer->removeInstance(it->instanceId); + it = activeSpellVisuals_.erase(it); + } else { + ++it; + } + } +} + void Renderer::triggerMeleeSwing() { if (!characterRenderer || characterInstanceId == 0) return; if (meleeSwingCooldown > 0.0f) return; @@ -2571,6 +3021,21 @@ void Renderer::setTargetPosition(const glm::vec3* pos) { targetPosition = pos; } +void Renderer::resetCombatVisualState() { + inCombat_ = false; + targetPosition = nullptr; + meleeSwingTimer = 0.0f; + meleeSwingCooldown = 0.0f; + // Clear lingering spell visual instances from the previous map/combat session. + // Without this, old effects could remain visible after teleport or map change. + for (auto& sv : activeSpellVisuals_) { + if (m2Renderer) m2Renderer->removeInstance(sv.instanceId); + } + activeSpellVisuals_.clear(); + // Reset the negative cache so models that failed during asset loading can retry. + spellVisualFailedModels_.clear(); +} + bool Renderer::isMoving() const { return cameraController && cameraController->isMoving(); } @@ -2736,6 +3201,7 @@ void Renderer::update(float deltaTime) { // Server-driven weather (SMSG_WEATHER) — authoritative if (wType == 1) weather->setWeatherType(Weather::Type::RAIN); else if (wType == 2) weather->setWeatherType(Weather::Type::SNOW); + else if (wType == 3) weather->setWeatherType(Weather::Type::STORM); else weather->setWeatherType(Weather::Type::NONE); weather->setIntensity(wInt); } else { @@ -2743,6 +3209,11 @@ void Renderer::update(float deltaTime) { weather->updateZoneWeather(currentZoneId, deltaTime); } weather->setEnabled(true); + + // Lightning flash disabled + if (lightning) { + lightning->setEnabled(false); + } } else if (weather) { // No game handler (single-player without network) — zone weather only weather->updateZoneWeather(currentZoneId, deltaTime); @@ -2812,6 +3283,11 @@ void Renderer::update(float deltaTime) { weather->update(*camera, deltaTime); } + // Update lightning (storm / heavy rain) + if (lightning && camera && lightning->isEnabled()) { + lightning->update(deltaTime, *camera); + } + // Update swim effects if (swimEffects && camera && cameraController && waterRenderer) { swimEffects->update(*camera, *cameraController, *waterRenderer, deltaTime); @@ -2847,6 +3323,8 @@ void Renderer::update(float deltaTime) { if (chargeEffect) { chargeEffect->update(deltaTime); } + // Update transient spell visual instances + updateSpellVisuals(deltaTime); // Launch M2 doodad animation on background thread (overlaps with character animation + audio) @@ -3015,6 +3493,7 @@ void Renderer::update(float deltaTime) { uint32_t insideWmoId = 0; const bool insideWmo = canQueryWmo && wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &insideWmoId); + playerIndoors_ = insideWmo; // Ambient environmental sounds: fireplaces, water, birds, etc. if (ambientSoundManager && camera && wmoRenderer && cameraController) { @@ -3350,7 +3829,7 @@ void Renderer::initSelectionCircle() { .setLayout(selCirclePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -3462,7 +3941,7 @@ void Renderer::initOverlayPipeline() { .setLayout(overlayPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -3587,7 +4066,8 @@ bool Renderer::initFSRResources() { samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - if (vkCreateSampler(device, &samplerInfo, nullptr, &fsr_.sceneSampler) != VK_SUCCESS) { + fsr_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo); + if (fsr_.sceneSampler == VK_NULL_HANDLE) { LOG_ERROR("FSR: failed to create sampler"); destroyFSRResources(); return false; @@ -3674,7 +4154,7 @@ bool Renderer::initFSRResources() { .setLayout(fsr_.pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -3701,7 +4181,7 @@ void Renderer::destroyFSRResources() { if (fsr_.descPool) { vkDestroyDescriptorPool(device, fsr_.descPool, nullptr); fsr_.descPool = VK_NULL_HANDLE; fsr_.descSet = VK_NULL_HANDLE; } if (fsr_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fsr_.descSetLayout, nullptr); fsr_.descSetLayout = VK_NULL_HANDLE; } if (fsr_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr_.sceneFramebuffer, nullptr); fsr_.sceneFramebuffer = VK_NULL_HANDLE; } - if (fsr_.sceneSampler) { vkDestroySampler(device, fsr_.sceneSampler, nullptr); fsr_.sceneSampler = VK_NULL_HANDLE; } + fsr_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache destroyImage(device, alloc, fsr_.sceneDepthResolve); destroyImage(device, alloc, fsr_.sceneMsaaColor); destroyImage(device, alloc, fsr_.sceneDepth); @@ -3745,7 +4225,13 @@ void Renderer::setFSREnabled(bool enabled) { if (fsr_.enabled == enabled) return; fsr_.enabled = enabled; - if (!enabled) { + if (enabled) { + // FSR1 upscaling renders its own AA — disable MSAA to avoid redundant work + if (vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT; + msaaChangePending_ = true; + } + } else { // Defer destruction to next beginFrame() — can't destroy mid-render fsr_.needsRecreate = true; } @@ -3874,11 +4360,11 @@ bool Renderer::initFSR2Resources() { samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.linearSampler); + fsr2_.linearSampler = vkCtx->getOrCreateSampler(samplerInfo); samplerInfo.minFilter = VK_FILTER_NEAREST; samplerInfo.magFilter = VK_FILTER_NEAREST; - vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.nearestSampler); + fsr2_.nearestSampler = vkCtx->getOrCreateSampler(samplerInfo); #if WOWEE_HAS_AMD_FSR2 // Initialize AMD FSR2 context; fall back to internal path on any failure. @@ -3916,7 +4402,11 @@ bool Renderer::initFSR2Resources() { fsr2_.useAmdBackend = true; LOG_INFO("FSR2 AMD: context created successfully."); #if WOWEE_HAS_AMD_FSR3_FRAMEGEN - if (fsr2_.amdFsr3FramegenEnabled) { + // FSR3 frame generation runtime uses AMD FidelityFX SDK which can + // corrupt Vulkan driver state on NVIDIA GPUs when context creation + // fails, causing subsequent vkCmdBeginRenderPass to crash. + // Skip FSR3 frame gen entirely on non-AMD GPUs. + if (fsr2_.amdFsr3FramegenEnabled && vkCtx->isAmdGpu()) { fsr2_.amdFsr3FramegenRuntimeActive = false; if (!fsr2_.amdFsr3Runtime) fsr2_.amdFsr3Runtime = std::make_unique(); AmdFsr3RuntimeInitDesc fgInit{}; @@ -4192,7 +4682,7 @@ bool Renderer::initFSR2Resources() { .setLayout(fsr2_.sharpenPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -4273,8 +4763,8 @@ void Renderer::destroyFSR2Resources() { if (fsr2_.motionVecDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.motionVecDescSetLayout, nullptr); fsr2_.motionVecDescSetLayout = VK_NULL_HANDLE; } if (fsr2_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr2_.sceneFramebuffer, nullptr); fsr2_.sceneFramebuffer = VK_NULL_HANDLE; } - if (fsr2_.linearSampler) { vkDestroySampler(device, fsr2_.linearSampler, nullptr); fsr2_.linearSampler = VK_NULL_HANDLE; } - if (fsr2_.nearestSampler) { vkDestroySampler(device, fsr2_.nearestSampler, nullptr); fsr2_.nearestSampler = VK_NULL_HANDLE; } + fsr2_.linearSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + fsr2_.nearestSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache destroyImage(device, alloc, fsr2_.motionVectors); for (int i = 0; i < 2; i++) destroyImage(device, alloc, fsr2_.history[i]); @@ -4698,6 +5188,260 @@ void Renderer::setAmdFsr3FramegenEnabled(bool enabled) { // ========================= End FSR 2.2 ========================= +// ========================= FXAA Post-Process ========================= + +bool Renderer::initFXAAResources() { + if (!vkCtx) return false; + + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + VkExtent2D ext = vkCtx->getSwapchainExtent(); + VkSampleCountFlagBits msaa = vkCtx->getMsaaSamples(); + bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT); + bool useDepthResolve = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); + + LOG_INFO("FXAA: initializing at ", ext.width, "x", ext.height, + " (MSAA=", static_cast(msaa), "x)"); + + VkFormat colorFmt = vkCtx->getSwapchainFormat(); + VkFormat depthFmt = vkCtx->getDepthFormat(); + + // sceneColor: 1x resolved color target — FXAA reads from here + fxaa_.sceneColor = createImage(device, alloc, ext.width, ext.height, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fxaa_.sceneColor.image) { + LOG_ERROR("FXAA: failed to create scene color image"); + return false; + } + + // sceneDepth: depth buffer at current MSAA sample count + fxaa_.sceneDepth = createImage(device, alloc, ext.width, ext.height, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa); + if (!fxaa_.sceneDepth.image) { + LOG_ERROR("FXAA: failed to create scene depth image"); + destroyFXAAResources(); + return false; + } + + if (useMsaa) { + fxaa_.sceneMsaaColor = createImage(device, alloc, ext.width, ext.height, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa); + if (!fxaa_.sceneMsaaColor.image) { + LOG_ERROR("FXAA: failed to create MSAA color image"); + destroyFXAAResources(); + return false; + } + if (useDepthResolve) { + fxaa_.sceneDepthResolve = createImage(device, alloc, ext.width, ext.height, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + if (!fxaa_.sceneDepthResolve.image) { + LOG_ERROR("FXAA: failed to create depth resolve image"); + destroyFXAAResources(); + return false; + } + } + } + + // Framebuffer — same attachment layout as main render pass + VkImageView fbAttachments[4]{}; + uint32_t fbCount; + if (useMsaa) { + fbAttachments[0] = fxaa_.sceneMsaaColor.imageView; + fbAttachments[1] = fxaa_.sceneDepth.imageView; + fbAttachments[2] = fxaa_.sceneColor.imageView; // resolve target + fbCount = 3; + if (useDepthResolve) { + fbAttachments[3] = fxaa_.sceneDepthResolve.imageView; + fbCount = 4; + } + } else { + fbAttachments[0] = fxaa_.sceneColor.imageView; + fbAttachments[1] = fxaa_.sceneDepth.imageView; + fbCount = 2; + } + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = vkCtx->getImGuiRenderPass(); + fbInfo.attachmentCount = fbCount; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = ext.width; + fbInfo.height = ext.height; + fbInfo.layers = 1; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fxaa_.sceneFramebuffer) != VK_SUCCESS) { + LOG_ERROR("FXAA: failed to create scene framebuffer"); + destroyFXAAResources(); + return false; + } + + // Sampler + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + fxaa_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo); + if (fxaa_.sceneSampler == VK_NULL_HANDLE) { + LOG_ERROR("FXAA: failed to create sampler"); + destroyFXAAResources(); + return false; + } + + // Descriptor set layout: binding 0 = combined image sampler + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout); + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = 1; + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool); + + VkDescriptorSetAllocateInfo dsAllocInfo{}; + dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + dsAllocInfo.descriptorPool = fxaa_.descPool; + dsAllocInfo.descriptorSetCount = 1; + dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout; + vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet); + + // Bind the resolved 1x sceneColor + VkDescriptorImageInfo imgInfo{}; + imgInfo.sampler = fxaa_.sceneSampler; + imgInfo.imageView = fxaa_.sceneColor.imageView; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = fxaa_.descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + + // Pipeline layout — push constant holds vec4(rcpFrame.xy, sharpness, pad) + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pc.offset = 0; + pc.size = 16; // vec4 + VkPipelineLayoutCreateInfo plCI{}; + plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fxaa_.descSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fxaa_.pipelineLayout); + + // FXAA pipeline — fullscreen triangle into the swapchain render pass + // Uses VK_SAMPLE_COUNT_1_BIT: it always runs after MSAA resolve. + VkShaderModule vertMod, fragMod; + if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || + !fragMod.loadFromFile(device, "assets/shaders/fxaa.frag.spv")) { + LOG_ERROR("FXAA: failed to load shaders"); + destroyFXAAResources(); + return false; + } + + fxaa_.pipeline = PipelineBuilder() + .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({}, {}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(VK_SAMPLE_COUNT_1_BIT) // swapchain pass is always 1x + .setLayout(fxaa_.pipelineLayout) + .setRenderPass(vkCtx->getImGuiRenderPass()) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device, vkCtx->getPipelineCache()); + + vertMod.destroy(); + fragMod.destroy(); + + if (!fxaa_.pipeline) { + LOG_ERROR("FXAA: failed to create pipeline"); + destroyFXAAResources(); + return false; + } + + LOG_INFO("FXAA: initialized successfully"); + return true; +} + +void Renderer::destroyFXAAResources() { + if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); + VmaAllocator alloc = vkCtx->getAllocator(); + vkDeviceWaitIdle(device); + + if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; } + if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; } + if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; } + if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; } + if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; } + fxaa_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + destroyImage(device, alloc, fxaa_.sceneDepthResolve); + destroyImage(device, alloc, fxaa_.sceneMsaaColor); + destroyImage(device, alloc, fxaa_.sceneDepth); + destroyImage(device, alloc, fxaa_.sceneColor); +} + +void Renderer::renderFXAAPass() { + if (!fxaa_.pipeline || currentCmd == VK_NULL_HANDLE) return; + VkExtent2D ext = vkCtx->getSwapchainExtent(); + + vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline); + vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr); + + // Pass rcpFrame + sharpness + effect flag (vec4, 16 bytes). + // When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the + // post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes. + float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f; + float pc[4] = { + 1.0f / static_cast(ext.width), + 1.0f / static_cast(ext.height), + sharpness, + 0.0f + }; + vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc); + + vkCmdDraw(currentCmd, 3, 1, 0, 0); // fullscreen triangle +} + +void Renderer::setFXAAEnabled(bool enabled) { + if (fxaa_.enabled == enabled) return; + // FXAA is a post-process AA pass intended to supplement FSR temporal output. + // It conflicts with MSAA (which resolves AA during the scene render pass), so + // refuse to enable FXAA when hardware MSAA is active. + if (enabled && vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + LOG_INFO("FXAA: blocked while MSAA is active — disable MSAA first"); + return; + } + fxaa_.enabled = enabled; + if (!enabled) { + fxaa_.needsRecreate = true; // defer destruction to next beginFrame() + } +} + +// ========================= End FXAA ========================= + void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { (void)world; @@ -4713,6 +5457,9 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { lastWMORenderMs = 0.0; lastM2RenderMs = 0.0; + // Cache ghost state for use in overlay and FXAA passes this frame. + ghostMode_ = (gameHandler && gameHandler->isPlayerGhost()); + uint32_t frameIdx = vkCtx->getCurrentFrame(); VkDescriptorSet perFrameSet = perFrameDescSets[frameIdx]; const glm::mat4& view = camera ? camera->getViewMatrix() : glm::mat4(1.0f); @@ -4777,6 +5524,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { m2Renderer->render(cmd, perFrameSet, *camera); m2Renderer->renderSmokeParticles(cmd, perFrameSet); m2Renderer->renderM2Particles(cmd, perFrameSet); + m2Renderer->renderM2Ribbons(cmd, perFrameSet); vkEndCommandBuffer(cmd); return std::chrono::duration( std::chrono::steady_clock::now() - t0).count(); @@ -4834,6 +5582,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { if (waterRenderer && camera) waterRenderer->render(cmd, perFrameSet, *camera, globalTime, false, frameIdx); if (weather && camera) weather->render(cmd, perFrameSet); + if (lightning && camera && lightning->isEnabled()) lightning->render(cmd, perFrameSet); if (swimEffects && camera) swimEffects->render(cmd, perFrameSet); if (mountDust && camera) mountDust->render(cmd, perFrameSet); if (chargeEffect && camera) chargeEffect->render(cmd, perFrameSet); @@ -4858,6 +5607,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(tint, cmd); } } + // Ghost mode desaturation: cold blue-grey overlay when dead/ghost + if (ghostMode_) { + renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd); + } + // Brightness overlay (applied before minimap so it doesn't affect UI) + if (brightness_ < 0.99f) { + renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_), cmd); + } else if (brightness_ > 1.01f) { + float alpha = (brightness_ - 1.0f) / 1.0f; // maps 1.0-2.0 → 0.0-1.0 + renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd); + } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); if (cameraController && cameraController->isThirdPerson()) @@ -4867,14 +5627,14 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { if (cameraController) { float facingRad = glm::radians(characterYaw); glm::vec3 facingFwd(std::cos(facingRad), std::sin(facingRad), 0.0f); - minimapPlayerOrientation = std::atan2(-facingFwd.x, facingFwd.y); + // atan2(-x,y) = canonical yaw (0=North); negate for shader convention. + minimapPlayerOrientation = -std::atan2(-facingFwd.x, facingFwd.y); hasMinimapPlayerOrientation = true; } else if (gameHandler) { - // Server orientation is in WoW space: π/2 = North, 0 = East. - // Minimap arrow expects render space: 0 = North, π/2 = East. - // Convert: minimap_angle = server_orientation - π/2 - minimapPlayerOrientation = gameHandler->getMovementInfo().orientation - - static_cast(M_PI_2); + // movementInfo.orientation is canonical yaw: 0=North, π/2=East. + // Minimap shader: arrowRotation=0 points up (North), positive rotates CW + // (π/2=West, -π/2=East). Correct mapping: arrowRotation = -canonical_yaw. + minimapPlayerOrientation = -gameHandler->getMovementInfo().orientation; hasMinimapPlayerOrientation = true; } minimap->render(cmd, *camera, minimapCenter, @@ -4956,6 +5716,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { m2Renderer->render(currentCmd, perFrameSet, *camera); m2Renderer->renderSmokeParticles(currentCmd, perFrameSet); m2Renderer->renderM2Particles(currentCmd, perFrameSet); + m2Renderer->renderM2Ribbons(currentCmd, perFrameSet); lastM2RenderMs = std::chrono::duration( std::chrono::steady_clock::now() - m2Start).count(); } @@ -4963,6 +5724,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { if (waterRenderer && camera) waterRenderer->render(currentCmd, perFrameSet, *camera, globalTime, false, frameIdx); if (weather && camera) weather->render(currentCmd, perFrameSet); + if (lightning && camera && lightning->isEnabled()) lightning->render(currentCmd, perFrameSet); if (swimEffects && camera) swimEffects->render(currentCmd, perFrameSet); if (mountDust && camera) mountDust->render(currentCmd, perFrameSet); if (chargeEffect && camera) chargeEffect->render(currentCmd, perFrameSet); @@ -4990,6 +5752,17 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(tint); } } + // Ghost mode desaturation: cold blue-grey overlay when dead/ghost + if (ghostMode_) { + renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f)); + } + // Brightness overlay (applied before minimap so it doesn't affect UI) + if (brightness_ < 0.99f) { + renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_)); + } else if (brightness_ > 1.01f) { + float alpha = (brightness_ - 1.0f) / 1.0f; + renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha)); + } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); if (cameraController && cameraController->isThirdPerson()) @@ -4999,14 +5772,14 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { if (cameraController) { float facingRad = glm::radians(characterYaw); glm::vec3 facingFwd(std::cos(facingRad), std::sin(facingRad), 0.0f); - minimapPlayerOrientation = std::atan2(-facingFwd.x, facingFwd.y); + // atan2(-x,y) = canonical yaw (0=North); negate for shader convention. + minimapPlayerOrientation = -std::atan2(-facingFwd.x, facingFwd.y); hasMinimapPlayerOrientation = true; } else if (gameHandler) { - // Server orientation is in WoW space: π/2 = North, 0 = East. - // Minimap arrow expects render space: 0 = North, π/2 = East. - // Convert: minimap_angle = server_orientation - π/2 - minimapPlayerOrientation = gameHandler->getMovementInfo().orientation - - static_cast(M_PI_2); + // movementInfo.orientation is canonical yaw: 0=North, π/2=East. + // Minimap shader: arrowRotation=0 points up (North), positive rotates CW + // (π/2=West, -π/2=East). Correct mapping: arrowRotation = -canonical_yaw. + minimapPlayerOrientation = -gameHandler->getMovementInfo().orientation; hasMinimapPlayerOrientation = true; } minimap->render(currentCmd, *camera, minimapCenter, diff --git a/src/rendering/sky_system.cpp b/src/rendering/sky_system.cpp index 9509cdc2..98e27621 100644 --- a/src/rendering/sky_system.cpp +++ b/src/rendering/sky_system.cpp @@ -135,6 +135,14 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, // --- Clouds (DBC-driven colors + sun lighting) --- if (clouds_) { + // Sync cloud density with weather/DBC-driven cloud coverage. + // Active weather (rain/snow/storm) increases cloud density for visual consistency. + float effectiveDensity = params.cloudDensity; + if (params.weatherIntensity > 0.05f) { + float weatherBoost = params.weatherIntensity * 0.4f; // storms add up to 0.4 density + effectiveDensity = glm::min(1.0f, effectiveDensity + weatherBoost); + } + clouds_->setDensity(effectiveDensity); clouds_->render(cmd, perFrameSet, params); } diff --git a/src/rendering/skybox.cpp b/src/rendering/skybox.cpp index 3e0e7de6..1e08ac4f 100644 --- a/src/rendering/skybox.cpp +++ b/src/rendering/skybox.cpp @@ -81,7 +81,7 @@ bool Skybox::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); // Shader modules can be freed after pipeline creation vertModule.destroy(); @@ -133,7 +133,7 @@ void Skybox::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/starfield.cpp b/src/rendering/starfield.cpp index e472bc8d..b51d419b 100644 --- a/src/rendering/starfield.cpp +++ b/src/rendering/starfield.cpp @@ -91,7 +91,7 @@ bool StarField::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -164,7 +164,7 @@ void StarField::recreatePipelines() { .setMultisample(vkCtx->getMsaaSamples()) .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/swim_effects.cpp b/src/rendering/swim_effects.cpp index 9a7ad119..9bc4885a 100644 --- a/src/rendering/swim_effects.cpp +++ b/src/rendering/swim_effects.cpp @@ -98,7 +98,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -142,7 +142,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -186,7 +186,7 @@ bool SwimEffects::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(insectPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -366,7 +366,7 @@ void SwimEffects::recreatePipelines() { .setLayout(ripplePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -393,7 +393,7 @@ void SwimEffects::recreatePipelines() { .setLayout(bubblePipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -420,7 +420,7 @@ void SwimEffects::recreatePipelines() { .setLayout(insectPipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 579a909a..50a12d0d 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -54,9 +54,11 @@ int computeTerrainWorkerCount() { unsigned hc = std::thread::hardware_concurrency(); if (hc > 0) { - // Use most cores for loading — leave 1-2 for render/update threads. - const unsigned reserved = (hc >= 8u) ? 2u : 1u; - const unsigned targetWorkers = std::max(4u, hc - reserved); + // Keep terrain workers conservative by default. Over-subscribing loader + // threads can starve main-thread networking/render updates on large-core CPUs. + const unsigned reserved = (hc >= 16u) ? 4u : ((hc >= 8u) ? 2u : 1u); + const unsigned maxDefaultWorkers = 8u; + const unsigned targetWorkers = std::max(4u, std::min(maxDefaultWorkers, hc - reserved)); return static_cast(targetWorkers); } return 4; // Fallback @@ -177,7 +179,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) { } // Always process ready tiles each frame (GPU uploads from background thread) - // Time budget prevents frame spikes from heavy tiles + // Time-budgeted internally to prevent frame spikes. processReadyTiles(); timeSinceLastUpdate += deltaTime; @@ -560,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)); @@ -573,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++) { @@ -597,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; @@ -896,6 +910,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) { continue; } + if (!m2Renderer->hasModel(p.modelId)) { + continue; + } uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale); if (instId) { ft.m2InstanceIds.push_back(instId); @@ -961,6 +978,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { continue; } + if (!wmoRenderer->isModelLoaded(wmoReady.modelId)) { + continue; + } uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); if (wmoInstId) { ft.wmoInstanceIds.push_back(wmoInstId); @@ -1203,18 +1223,25 @@ void TerrainManager::processReadyTiles() { // Async upload batch: record GPU copies into a command buffer, submit with // a fence, but DON'T wait. The fence is polled on subsequent frames. // This eliminates the main-thread stall from vkWaitForFences entirely. - const int maxSteps = taxiStreamingMode_ ? 4 : 1; - int steps = 0; + // + // Time-budgeted: yield after 8ms to prevent main-loop stalls. Each + // advanceFinalization step is designed to be small, but texture uploads + // and M2 model loads can occasionally spike. The budget ensures we + // spread heavy tiles across multiple frames instead of blocking. + const auto budgetStart = std::chrono::steady_clock::now(); + const float budgetMs = taxiStreamingMode_ ? 16.0f : 8.0f; if (vkCtx) vkCtx->beginUploadBatch(); - while (!finalizingTiles_.empty() && steps < maxSteps) { + while (!finalizingTiles_.empty()) { auto& ft = finalizingTiles_.front(); bool done = advanceFinalization(ft); if (done) { finalizingTiles_.pop_front(); } - steps++; + float elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - budgetStart).count(); + if (elapsed >= budgetMs) break; } if (vkCtx) vkCtx->endUploadBatch(); // Async — submits but doesn't wait @@ -1377,6 +1404,10 @@ void TerrainManager::unloadTile(int x, int y) { // Water may have already been loaded in TERRAIN phase, so clean it up. for (auto fit = finalizingTiles_.begin(); fit != finalizingTiles_.end(); ++fit) { if (fit->pending && fit->pending->coord == coord) { + // If terrain chunks were already uploaded, free their descriptor sets + if (fit->terrainMeshDone && terrainRenderer) { + terrainRenderer->removeTile(x, y); + } // If past TERRAIN phase, water was already loaded — remove it if (fit->phase != FinalizationPhase::TERRAIN && waterRenderer) { waterRenderer->removeTile(x, y); @@ -1392,7 +1423,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; } @@ -1413,6 +1448,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 @@ -1497,6 +1534,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(); @@ -1549,6 +1590,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. @@ -2280,17 +2325,6 @@ void TerrainManager::streamTiles() { } if (!tilesToUnload.empty()) { - // Don't clean up models during streaming - keep them in VRAM for performance - // Modern GPUs have 8-16GB VRAM, models are only ~hundreds of MB - // Cleanup can be done manually when memory pressure is detected - // NOTE: Disabled permanent model cleanup to leverage modern VRAM capacity - // if (m2Renderer) { - // m2Renderer->cleanupUnusedModels(); - // } - // if (wmoRenderer) { - // wmoRenderer->cleanupUnusedModels(); - // } - LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ", loadedTiles.size(), " remain (models kept in VRAM)"); } diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 75ca41c9..7543e639 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -143,7 +143,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!pipeline) { LOG_ERROR("TerrainRenderer: failed to create fill pipeline"); @@ -165,7 +165,7 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!wireframePipeline) { LOG_WARNING("TerrainRenderer: wireframe pipeline not available"); @@ -245,7 +245,7 @@ void TerrainRenderer::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!pipeline) { LOG_ERROR("TerrainRenderer::recreatePipelines: failed to create fill pipeline"); @@ -264,7 +264,7 @@ void TerrainRenderer::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); if (!wireframePipeline) { LOG_WARNING("TerrainRenderer::recreatePipelines: wireframe pipeline not available"); @@ -396,9 +396,13 @@ bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh, // Allocate and write material descriptor set gpuChunk.materialSet = allocateMaterialSet(); - if (gpuChunk.materialSet) { - writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); + if (!gpuChunk.materialSet) { + // Pool exhaustion can happen transiently while tile churn is high. + // Drop this chunk instead of retaining non-renderable GPU resources. + destroyChunkGPU(gpuChunk); + continue; } + writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); chunks.push_back(std::move(gpuChunk)); } @@ -487,9 +491,12 @@ bool TerrainRenderer::loadTerrainIncremental(const pipeline::TerrainMesh& mesh, } gpuChunk.materialSet = allocateMaterialSet(); - if (gpuChunk.materialSet) { - writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); + if (!gpuChunk.materialSet) { + // Keep memory/work bounded under descriptor pool pressure. + destroyChunkGPU(gpuChunk); + continue; } + writeMaterialDescriptors(gpuChunk.materialSet, gpuChunk); chunks.push_back(std::move(gpuChunk)); uploaded++; @@ -653,7 +660,11 @@ VkDescriptorSet TerrainRenderer::allocateMaterialSet() { VkDescriptorSet set = VK_NULL_HANDLE; if (vkAllocateDescriptorSets(vkCtx->getDevice(), &allocInfo, &set) != VK_SUCCESS) { - LOG_WARNING("TerrainRenderer: failed to allocate material descriptor set"); + static uint64_t failCount = 0; + ++failCount; + if (failCount <= 8 || (failCount % 256) == 0) { + LOG_WARNING("TerrainRenderer: failed to allocate material descriptor set (count=", failCount, ")"); + } return VK_NULL_HANDLE; } return set; @@ -921,7 +932,7 @@ bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -990,42 +1001,64 @@ void TerrainRenderer::clear() { } chunks.clear(); renderedChunks = 0; - - if (materialDescPool) { - vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); - } } void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) { + if (!vkCtx) return; + + VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); - if (chunk.vertexBuffer) { - AllocatedBuffer ab{}; ab.buffer = chunk.vertexBuffer; ab.allocation = chunk.vertexAlloc; - destroyBuffer(allocator, ab); - chunk.vertexBuffer = VK_NULL_HANDLE; - } - if (chunk.indexBuffer) { - AllocatedBuffer ab{}; ab.buffer = chunk.indexBuffer; ab.allocation = chunk.indexAlloc; - destroyBuffer(allocator, ab); - chunk.indexBuffer = VK_NULL_HANDLE; - } - if (chunk.paramsUBO) { - AllocatedBuffer ab{}; ab.buffer = chunk.paramsUBO; ab.allocation = chunk.paramsAlloc; - destroyBuffer(allocator, ab); - chunk.paramsUBO = VK_NULL_HANDLE; - } - // Return material descriptor set to the pool so it can be reused by new chunks - if (chunk.materialSet && materialDescPool) { - vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &chunk.materialSet); - } - chunk.materialSet = VK_NULL_HANDLE; + // These resources may still be referenced by in-flight command buffers from + // previous frames. Defer actual destruction until this frame slot is safe. + ::VkBuffer vertexBuffer = chunk.vertexBuffer; + VmaAllocation vertexAlloc = chunk.vertexAlloc; + ::VkBuffer indexBuffer = chunk.indexBuffer; + VmaAllocation indexAlloc = chunk.indexAlloc; + ::VkBuffer paramsUBO = chunk.paramsUBO; + VmaAllocation paramsAlloc = chunk.paramsAlloc; + VkDescriptorPool pool = materialDescPool; + VkDescriptorSet materialSet = chunk.materialSet; - // Destroy owned alpha textures (VkTexture::~VkTexture is a no-op, must call destroy() explicitly) - VkDevice device = vkCtx->getDevice(); + std::vector alphaTextures; + alphaTextures.reserve(chunk.ownedAlphaTextures.size()); for (auto& tex : chunk.ownedAlphaTextures) { - if (tex) tex->destroy(device, allocator); + alphaTextures.push_back(tex.release()); } + + chunk.vertexBuffer = VK_NULL_HANDLE; + chunk.vertexAlloc = VK_NULL_HANDLE; + chunk.indexBuffer = VK_NULL_HANDLE; + chunk.indexAlloc = VK_NULL_HANDLE; + chunk.paramsUBO = VK_NULL_HANDLE; + chunk.paramsAlloc = VK_NULL_HANDLE; + chunk.materialSet = VK_NULL_HANDLE; chunk.ownedAlphaTextures.clear(); + + vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc, + paramsUBO, paramsAlloc, pool, materialSet, alphaTextures]() { + if (vertexBuffer) { + AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc; + destroyBuffer(allocator, ab); + } + if (indexBuffer) { + AllocatedBuffer ab{}; ab.buffer = indexBuffer; ab.allocation = indexAlloc; + destroyBuffer(allocator, ab); + } + if (paramsUBO) { + AllocatedBuffer ab{}; ab.buffer = paramsUBO; ab.allocation = paramsAlloc; + destroyBuffer(allocator, ab); + } + if (materialSet && pool) { + VkDescriptorSet set = materialSet; + vkFreeDescriptorSets(device, pool, 1, &set); + } + for (VkTexture* tex : alphaTextures) { + if (!tex) continue; + tex->destroy(device, allocator); + delete tex; + } + }); } int TerrainRenderer::getTriangleCount() const { diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index fdd07d8e..b21838ee 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -6,10 +6,51 @@ #include #include #include +#include +#include +#include namespace wowee { namespace rendering { +VkContext* VkContext::sInstance_ = nullptr; + +// Hash a VkSamplerCreateInfo into a 64-bit key for the sampler cache. +static uint64_t hashSamplerCreateInfo(const VkSamplerCreateInfo& s) { + // Pack the relevant fields into a deterministic hash. + // FNV-1a 64-bit on the raw config values. + uint64_t h = 14695981039346656037ULL; + auto mix = [&](uint64_t v) { + h ^= v; + h *= 1099511628211ULL; + }; + mix(static_cast(s.minFilter)); + mix(static_cast(s.magFilter)); + mix(static_cast(s.mipmapMode)); + mix(static_cast(s.addressModeU)); + mix(static_cast(s.addressModeV)); + mix(static_cast(s.addressModeW)); + mix(static_cast(s.anisotropyEnable)); + // Bit-cast floats to uint32_t for hashing + uint32_t aniso; + std::memcpy(&aniso, &s.maxAnisotropy, sizeof(aniso)); + mix(static_cast(aniso)); + uint32_t maxLodBits; + std::memcpy(&maxLodBits, &s.maxLod, sizeof(maxLodBits)); + mix(static_cast(maxLodBits)); + uint32_t minLodBits; + std::memcpy(&minLodBits, &s.minLod, sizeof(minLodBits)); + mix(static_cast(minLodBits)); + mix(static_cast(s.compareEnable)); + mix(static_cast(s.compareOp)); + mix(static_cast(s.borderColor)); + uint32_t biasBits; + std::memcpy(&biasBits, &s.mipLodBias, sizeof(biasBits)); + mix(static_cast(biasBits)); + mix(static_cast(s.unnormalizedCoordinates)); + return h; +} + static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT severity, [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type, @@ -37,6 +78,10 @@ bool VkContext::initialize(SDL_Window* window) { if (!createLogicalDevice()) return false; if (!createAllocator()) return false; + // Pipeline cache: try to load from disk, fall back to empty cache. + // Not fatal — if it fails we just skip caching. + createPipelineCache(); + int w, h; SDL_Vulkan_GetDrawableSize(window, &w, &h); if (!createSwapchain(w, h)) return false; @@ -45,6 +90,14 @@ bool VkContext::initialize(SDL_Window* window) { if (!createSyncObjects()) return false; if (!createImGuiResources()) return false; + // Query anisotropy support from the physical device. + VkPhysicalDeviceFeatures supportedFeatures{}; + vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures); + samplerAnisotropySupported_ = (supportedFeatures.samplerAnisotropy == VK_TRUE); + LOG_INFO("Sampler anisotropy supported: ", samplerAnisotropySupported_ ? "YES" : "NO"); + + sInstance_ = this; + LOG_INFO("Vulkan context initialized successfully"); return true; } @@ -55,6 +108,11 @@ void VkContext::shutdown() { vkDeviceWaitIdle(device); } + // With the device idle, it is safe to run any deferred per-frame cleanup. + for (uint32_t fi = 0; fi < MAX_FRAMES_IN_FLIGHT; fi++) { + runDeferredCleanup(fi); + } + LOG_WARNING("VkContext::shutdown - destroyImGuiResources..."); destroyImGuiResources(); @@ -77,6 +135,23 @@ void VkContext::shutdown() { if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; } if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; } + if (transferCommandPool_) { vkDestroyCommandPool(device, transferCommandPool_, nullptr); transferCommandPool_ = VK_NULL_HANDLE; } + + // Persist pipeline cache to disk before tearing down the device. + savePipelineCache(); + if (pipelineCache_) { + vkDestroyPipelineCache(device, pipelineCache_, nullptr); + pipelineCache_ = VK_NULL_HANDLE; + } + + // Destroy all cached samplers. + for (auto& [key, sampler] : samplerCache_) { + if (sampler) vkDestroySampler(device, sampler, nullptr); + } + samplerCache_.clear(); + LOG_INFO("Sampler cache cleared"); + + sInstance_ = nullptr; LOG_WARNING("VkContext::shutdown - destroySwapchain..."); destroySwapchain(); @@ -103,6 +178,59 @@ void VkContext::shutdown() { LOG_WARNING("Vulkan context shutdown complete"); } +void VkContext::deferAfterFrameFence(std::function&& fn) { + deferredCleanup_[currentFrame].push_back(std::move(fn)); +} + +void VkContext::runDeferredCleanup(uint32_t frameIndex) { + auto& q = deferredCleanup_[frameIndex]; + if (q.empty()) return; + for (auto& fn : q) { + if (fn) fn(); + } + q.clear(); +} + +VkSampler VkContext::getOrCreateSampler(const VkSamplerCreateInfo& info) { + // Clamp anisotropy if the device doesn't support the feature. + VkSamplerCreateInfo adjusted = info; + if (!samplerAnisotropySupported_) { + adjusted.anisotropyEnable = VK_FALSE; + adjusted.maxAnisotropy = 1.0f; + } + + uint64_t key = hashSamplerCreateInfo(adjusted); + + { + std::lock_guard lock(samplerCacheMutex_); + auto it = samplerCache_.find(key); + if (it != samplerCache_.end()) { + return it->second; + } + } + + // Create a new sampler outside the lock (vkCreateSampler is thread-safe + // for distinct create infos, but we re-lock to insert). + VkSampler sampler = VK_NULL_HANDLE; + if (vkCreateSampler(device, &adjusted, nullptr, &sampler) != VK_SUCCESS) { + LOG_ERROR("getOrCreateSampler: vkCreateSampler failed"); + return VK_NULL_HANDLE; + } + + { + std::lock_guard lock(samplerCacheMutex_); + // Double-check: another thread may have inserted while we were creating. + auto [it, inserted] = samplerCache_.emplace(key, sampler); + if (!inserted) { + // Another thread won the race — destroy our duplicate and use theirs. + vkDestroySampler(device, sampler, nullptr); + return it->second; + } + } + + return sampler; +} + bool VkContext::createInstance(SDL_Window* window) { // Get required SDL extensions unsigned int sdlExtCount = 0; @@ -164,6 +292,10 @@ bool VkContext::selectPhysicalDevice() { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(physicalDevice, &props); uint32_t apiVersion = props.apiVersion; + gpuVendorId_ = props.vendorID; + std::strncpy(gpuName_, props.deviceName, sizeof(gpuName_) - 1); + gpuName_[sizeof(gpuName_) - 1] = '\0'; + LOG_INFO("GPU: ", gpuName_, " (vendor 0x", std::hex, gpuVendorId_, std::dec, ")"); VkPhysicalDeviceDepthStencilResolveProperties dsResolveProps{}; dsResolveProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_STENCIL_RESOLVE_PROPERTIES; @@ -197,11 +329,52 @@ bool VkContext::selectPhysicalDevice() { VK_VERSION_MINOR(props.apiVersion), ".", VK_VERSION_PATCH(props.apiVersion)); LOG_INFO("Depth resolve support: ", depthResolveSupported_ ? "YES" : "NO"); + // Probe queue families to see if the graphics family supports multiple queues + // (used in createLogicalDevice to request a second queue for parallel uploads). + auto queueFamilies = vkbPhysicalDevice_.get_queue_families(); + for (uint32_t i = 0; i < static_cast(queueFamilies.size()); i++) { + if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphicsQueueFamilyQueueCount_ = queueFamilies[i].queueCount; + LOG_INFO("Graphics queue family ", i, " supports ", graphicsQueueFamilyQueueCount_, " queue(s)"); + break; + } + } + return true; } bool VkContext::createLogicalDevice() { vkb::DeviceBuilder deviceBuilder{vkbPhysicalDevice_}; + + // If the graphics queue family supports >= 2 queues, request a second one + // for parallel texture/buffer uploads. Both queues share the same family + // so no queue-ownership-transfer barriers are needed. + const bool requestTransferQueue = (graphicsQueueFamilyQueueCount_ >= 2); + + if (requestTransferQueue) { + // Build a custom queue description list: 2 queues from the graphics + // family, 1 queue from every other family (so present etc. still work). + auto families = vkbPhysicalDevice_.get_queue_families(); + uint32_t gfxFamily = UINT32_MAX; + for (uint32_t i = 0; i < static_cast(families.size()); i++) { + if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + gfxFamily = i; + break; + } + } + + std::vector queueDescs; + for (uint32_t i = 0; i < static_cast(families.size()); i++) { + if (i == gfxFamily) { + // Request 2 queues: [0] graphics, [1] transfer uploads + queueDescs.emplace_back(i, std::vector{1.0f, 1.0f}); + } else { + queueDescs.emplace_back(i, std::vector{1.0f}); + } + } + deviceBuilder.custom_queue_setup(queueDescs); + } + auto devRet = deviceBuilder.build(); if (!devRet) { LOG_ERROR("Failed to create Vulkan logical device: ", devRet.error().message()); @@ -211,22 +384,45 @@ bool VkContext::createLogicalDevice() { auto vkbDevice = devRet.value(); device = vkbDevice.device; - auto gqRet = vkbDevice.get_queue(vkb::QueueType::graphics); - if (!gqRet) { - LOG_ERROR("Failed to get graphics queue"); - return false; - } - graphicsQueue = gqRet.value(); - graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value(); + if (requestTransferQueue) { + // With custom_queue_setup, we must retrieve queues manually. + auto families = vkbPhysicalDevice_.get_queue_families(); + uint32_t gfxFamily = UINT32_MAX; + for (uint32_t i = 0; i < static_cast(families.size()); i++) { + if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + gfxFamily = i; + break; + } + } + graphicsQueueFamily = gfxFamily; + vkGetDeviceQueue(device, gfxFamily, 0, &graphicsQueue); + vkGetDeviceQueue(device, gfxFamily, 1, &transferQueue_); + hasDedicatedTransfer_ = true; - auto pqRet = vkbDevice.get_queue(vkb::QueueType::present); - if (!pqRet) { - // Fall back to graphics queue for presentation + // Present queue: try the graphics family first (most common), otherwise + // find a family that supports presentation. presentQueue = graphicsQueue; - presentQueueFamily = graphicsQueueFamily; + presentQueueFamily = gfxFamily; + + LOG_INFO("Dedicated transfer queue enabled (family ", gfxFamily, ", queue index 1)"); } else { - presentQueue = pqRet.value(); - presentQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::present).value(); + // Standard path — let vkb resolve queues. + auto gqRet = vkbDevice.get_queue(vkb::QueueType::graphics); + if (!gqRet) { + LOG_ERROR("Failed to get graphics queue"); + return false; + } + graphicsQueue = gqRet.value(); + graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value(); + + auto pqRet = vkbDevice.get_queue(vkb::QueueType::present); + if (!pqRet) { + presentQueue = graphicsQueue; + presentQueueFamily = graphicsQueueFamily; + } else { + presentQueue = pqRet.value(); + presentQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::present).value(); + } } LOG_INFO("Vulkan logical device created"); @@ -249,6 +445,114 @@ bool VkContext::createAllocator() { return true; } +// --------------------------------------------------------------------------- +// Pipeline cache persistence +// --------------------------------------------------------------------------- + +static std::string getPipelineCachePath() { +#ifdef _WIN32 + if (const char* appdata = std::getenv("APPDATA")) + return std::string(appdata) + "\\wowee\\pipeline_cache.bin"; + return ".\\pipeline_cache.bin"; +#elif defined(__APPLE__) + if (const char* home = std::getenv("HOME")) + return std::string(home) + "/Library/Caches/wowee/pipeline_cache.bin"; + return "./pipeline_cache.bin"; +#else + if (const char* home = std::getenv("HOME")) + return std::string(home) + "/.local/share/wowee/pipeline_cache.bin"; + return "./pipeline_cache.bin"; +#endif +} + +bool VkContext::createPipelineCache() { + // NVIDIA drivers have their own built-in pipeline/shader disk cache. + // Using VkPipelineCache on NVIDIA 590.x causes vkCmdBeginRenderPass to + // SIGSEGV inside libnvidia-glcore — skip entirely on NVIDIA GPUs. + if (gpuVendorId_ == 0x10DE) { + LOG_INFO("Pipeline cache: skipped (NVIDIA driver provides built-in caching)"); + return true; + } + + std::string path = getPipelineCachePath(); + + // Try to load existing cache data from disk. + std::vector cacheData; + { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (file.is_open()) { + auto size = file.tellg(); + if (size > 0) { + cacheData.resize(static_cast(size)); + file.seekg(0); + file.read(cacheData.data(), size); + if (!file) { + LOG_WARNING("Pipeline cache file read failed, starting with empty cache"); + cacheData.clear(); + } + } + } + } + + VkPipelineCacheCreateInfo cacheCI{}; + cacheCI.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + cacheCI.initialDataSize = cacheData.size(); + cacheCI.pInitialData = cacheData.empty() ? nullptr : cacheData.data(); + + VkResult result = vkCreatePipelineCache(device, &cacheCI, nullptr, &pipelineCache_); + if (result != VK_SUCCESS) { + // If loading stale/corrupt data caused failure, retry with empty cache. + if (!cacheData.empty()) { + LOG_WARNING("Pipeline cache creation failed with saved data, retrying empty"); + cacheCI.initialDataSize = 0; + cacheCI.pInitialData = nullptr; + result = vkCreatePipelineCache(device, &cacheCI, nullptr, &pipelineCache_); + } + if (result != VK_SUCCESS) { + LOG_WARNING("Pipeline cache creation failed — pipelines will not be cached"); + pipelineCache_ = VK_NULL_HANDLE; + return false; + } + } + + if (!cacheData.empty()) { + LOG_INFO("Pipeline cache loaded from disk (", cacheData.size(), " bytes)"); + } else { + LOG_INFO("Pipeline cache created (empty)"); + } + return true; +} + +void VkContext::savePipelineCache() { + if (!pipelineCache_ || !device) return; + + size_t dataSize = 0; + if (vkGetPipelineCacheData(device, pipelineCache_, &dataSize, nullptr) != VK_SUCCESS || dataSize == 0) { + LOG_WARNING("Failed to query pipeline cache size"); + return; + } + + std::vector data(dataSize); + if (vkGetPipelineCacheData(device, pipelineCache_, &dataSize, data.data()) != VK_SUCCESS) { + LOG_WARNING("Failed to retrieve pipeline cache data"); + return; + } + + std::string path = getPipelineCachePath(); + std::filesystem::create_directories(std::filesystem::path(path).parent_path()); + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + LOG_WARNING("Failed to open pipeline cache file for writing: ", path); + return; + } + + file.write(data.data(), static_cast(dataSize)); + file.close(); + + LOG_INFO("Pipeline cache saved to disk (", dataSize, " bytes)"); +} + bool VkContext::createSwapchain(int width, int height) { vkb::SwapchainBuilder swapchainBuilder{physicalDevice, device, surface}; @@ -349,6 +653,19 @@ bool VkContext::createCommandPools() { return false; } + // Separate command pool for the transfer queue (same family, different queue) + if (hasDedicatedTransfer_) { + VkCommandPoolCreateInfo transferPoolInfo{}; + transferPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + transferPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + transferPoolInfo.queueFamilyIndex = graphicsQueueFamily; + + if (vkCreateCommandPool(device, &transferPoolInfo, nullptr, &transferCommandPool_) != VK_SUCCESS) { + LOG_ERROR("Failed to create transfer command pool"); + return false; + } + } + return true; } @@ -836,10 +1153,7 @@ void VkContext::destroyImGuiResources() { if (tex.memory) vkFreeMemory(device, tex.memory, nullptr); } uiTextures_.clear(); - if (uiTextureSampler_) { - vkDestroySampler(device, uiTextureSampler_, nullptr); - uiTextureSampler_ = VK_NULL_HANDLE; - } + uiTextureSampler_ = VK_NULL_HANDLE; // Owned by sampler cache if (imguiDescriptorPool) { vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr); @@ -871,7 +1185,7 @@ VkDescriptorSet VkContext::uploadImGuiTexture(const uint8_t* rgba, int width, in VkDeviceSize imageSize = static_cast(width) * height * 4; - // Create shared sampler on first call + // Create shared sampler on first call (via sampler cache) if (!uiTextureSampler_) { VkSamplerCreateInfo si{}; si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; @@ -880,7 +1194,8 @@ VkDescriptorSet VkContext::uploadImGuiTexture(const uint8_t* rgba, int width, in si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; si.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - if (vkCreateSampler(device, &si, nullptr, &uiTextureSampler_) != VK_SUCCESS) { + uiTextureSampler_ = getOrCreateSampler(si); + if (!uiTextureSampler_) { LOG_ERROR("Failed to create UI texture sampler"); return VK_NULL_HANDLE; } @@ -1342,13 +1657,16 @@ VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) { return VK_NULL_HANDLE; } if (fenceResult != VK_SUCCESS) { - LOG_ERROR("beginFrame[", beginFrameCounter, "] fence wait failed: ", (int)fenceResult); + LOG_ERROR("beginFrame[", beginFrameCounter, "] fence wait failed: ", static_cast(fenceResult)); if (fenceResult == VK_ERROR_DEVICE_LOST) { deviceLost_ = true; } return VK_NULL_HANDLE; } + // Any work queued for this frame slot is now guaranteed to be unused by the GPU. + runDeferredCleanup(currentFrame); + // Acquire next swapchain image VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); @@ -1380,7 +1698,7 @@ void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) { VkResult endResult = vkEndCommandBuffer(cmd); if (endResult != VK_SUCCESS) { - LOG_ERROR("endFrame[", endFrameCounter, "] vkEndCommandBuffer FAILED: ", (int)endResult); + LOG_ERROR("endFrame[", endFrameCounter, "] vkEndCommandBuffer FAILED: ", static_cast(endResult)); } auto& frame = frames[currentFrame]; @@ -1399,7 +1717,7 @@ void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) { VkResult submitResult = vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence); if (submitResult != VK_SUCCESS) { - LOG_ERROR("endFrame[", endFrameCounter, "] vkQueueSubmit FAILED: ", (int)submitResult); + LOG_ERROR("endFrame[", endFrameCounter, "] vkQueueSubmit FAILED: ", static_cast(submitResult)); if (submitResult == VK_ERROR_DEVICE_LOST) { deviceLost_ = true; } @@ -1469,7 +1787,21 @@ void VkContext::beginUploadBatch() { uploadBatchDepth_++; if (inUploadBatch_) return; // already in a batch (nested call) inUploadBatch_ = true; - batchCmd_ = beginSingleTimeCommands(); + + // Allocate from transfer pool if available, otherwise from immCommandPool. + VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool; + + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = pool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + vkAllocateCommandBuffers(device, &allocInfo, &batchCmd_); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(batchCmd_, &beginInfo); } void VkContext::endUploadBatch() { @@ -1479,10 +1811,12 @@ void VkContext::endUploadBatch() { inUploadBatch_ = false; + VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool; + if (batchStagingBuffers_.empty()) { // No GPU copies were recorded — skip the submit entirely. vkEndCommandBuffer(batchCmd_); - vkFreeCommandBuffers(device, immCommandPool, 1, &batchCmd_); + vkFreeCommandBuffers(device, pool, 1, &batchCmd_); batchCmd_ = VK_NULL_HANDLE; return; } @@ -1499,7 +1833,10 @@ void VkContext::endUploadBatch() { submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &batchCmd_; - vkQueueSubmit(graphicsQueue, 1, &submitInfo, fence); + + // Submit to the dedicated transfer queue if available, otherwise graphics. + VkQueue targetQueue = hasDedicatedTransfer_ ? transferQueue_ : graphicsQueue; + vkQueueSubmit(targetQueue, 1, &submitInfo, fence); // Stash everything for later cleanup when fence signals InFlightBatch batch; @@ -1519,15 +1856,30 @@ void VkContext::endUploadBatchSync() { inUploadBatch_ = false; + VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool; + if (batchStagingBuffers_.empty()) { vkEndCommandBuffer(batchCmd_); - vkFreeCommandBuffers(device, immCommandPool, 1, &batchCmd_); + vkFreeCommandBuffers(device, pool, 1, &batchCmd_); batchCmd_ = VK_NULL_HANDLE; return; } - // Synchronous path for load screens — submit and wait - endSingleTimeCommands(batchCmd_); + // Synchronous path for load screens — submit and wait on the target queue. + VkQueue targetQueue = hasDedicatedTransfer_ ? transferQueue_ : graphicsQueue; + + vkEndCommandBuffer(batchCmd_); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &batchCmd_; + + vkQueueSubmit(targetQueue, 1, &submitInfo, immFence); + vkWaitForFences(device, 1, &immFence, VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &immFence); + + vkFreeCommandBuffers(device, pool, 1, &batchCmd_); batchCmd_ = VK_NULL_HANDLE; for (auto& staging : batchStagingBuffers_) { @@ -1539,6 +1891,8 @@ void VkContext::endUploadBatchSync() { void VkContext::pollUploadBatches() { if (inFlightBatches_.empty()) return; + VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool; + for (auto it = inFlightBatches_.begin(); it != inFlightBatches_.end(); ) { VkResult result = vkGetFenceStatus(device, it->fence); if (result == VK_SUCCESS) { @@ -1546,7 +1900,7 @@ void VkContext::pollUploadBatches() { for (auto& staging : it->stagingBuffers) { destroyBuffer(allocator, staging); } - vkFreeCommandBuffers(device, immCommandPool, 1, &it->cmd); + vkFreeCommandBuffers(device, pool, 1, &it->cmd); vkDestroyFence(device, it->fence, nullptr); it = inFlightBatches_.erase(it); } else { @@ -1556,12 +1910,14 @@ void VkContext::pollUploadBatches() { } void VkContext::waitAllUploads() { + VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool; + for (auto& batch : inFlightBatches_) { vkWaitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX); for (auto& staging : batch.stagingBuffers) { destroyBuffer(allocator, staging); } - vkFreeCommandBuffers(device, immCommandPool, 1, &batch.cmd); + vkFreeCommandBuffers(device, pool, 1, &batch.cmd); vkDestroyFence(device, batch.fence, nullptr); } inFlightBatches_.clear(); diff --git a/src/rendering/vk_pipeline.cpp b/src/rendering/vk_pipeline.cpp index 4e565b07..4119d8c8 100644 --- a/src/rendering/vk_pipeline.cpp +++ b/src/rendering/vk_pipeline.cpp @@ -111,7 +111,7 @@ PipelineBuilder& PipelineBuilder::setDynamicStates(const std::vector(mipLevels_); + // Use sampler cache if VkContext is available. + auto* ctx = VkContext::globalInstance(); + if (ctx) { + sampler_ = ctx->getOrCreateSampler(samplerInfo); + ownsSampler_ = false; + return sampler_ != VK_NULL_HANDLE; + } + + // Fallback: no VkContext (shouldn't happen in normal use). if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { LOG_ERROR("Failed to create texture sampler"); return false; } - + ownsSampler_ = true; return true; } @@ -246,11 +259,20 @@ bool VkTexture::createSampler(VkDevice device, samplerInfo.minLod = 0.0f; samplerInfo.maxLod = static_cast(mipLevels_); + // Use sampler cache if VkContext is available. + auto* ctx = VkContext::globalInstance(); + if (ctx) { + sampler_ = ctx->getOrCreateSampler(samplerInfo); + ownsSampler_ = false; + return sampler_ != VK_NULL_HANDLE; + } + + // Fallback: no VkContext (shouldn't happen in normal use). if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { LOG_ERROR("Failed to create texture sampler"); return false; } - + ownsSampler_ = true; return true; } @@ -269,19 +291,29 @@ bool VkTexture::createShadowSampler(VkDevice device) { samplerInfo.minLod = 0.0f; samplerInfo.maxLod = 1.0f; + // Use sampler cache if VkContext is available. + auto* ctx = VkContext::globalInstance(); + if (ctx) { + sampler_ = ctx->getOrCreateSampler(samplerInfo); + ownsSampler_ = false; + return sampler_ != VK_NULL_HANDLE; + } + + // Fallback: no VkContext (shouldn't happen in normal use). if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) { LOG_ERROR("Failed to create shadow sampler"); return false; } - + ownsSampler_ = true; return true; } void VkTexture::destroy(VkDevice device, VmaAllocator allocator) { - if (sampler_ != VK_NULL_HANDLE) { + if (sampler_ != VK_NULL_HANDLE && ownsSampler_) { vkDestroySampler(device, sampler_, nullptr); - sampler_ = VK_NULL_HANDLE; } + sampler_ = VK_NULL_HANDLE; + ownsSampler_ = true; destroyImage(device, allocator, image_); } diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index d79e53f7..b8d3d33b 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -193,7 +193,7 @@ bool WaterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLay .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -257,7 +257,7 @@ void WaterRenderer::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -352,8 +352,8 @@ void WaterRenderer::destroySceneHistoryResources() { if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; } sh.sceneSet = VK_NULL_HANDLE; } - if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; } - if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; } + sceneColorSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + sceneDepthSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache sceneHistoryExtent = {0, 0}; sceneHistoryReady = false; } @@ -374,13 +374,15 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - if (vkCreateSampler(device, &sampCI, nullptr, &sceneColorSampler) != VK_SUCCESS) { + sceneColorSampler = vkCtx->getOrCreateSampler(sampCI); + if (sceneColorSampler == VK_NULL_HANDLE) { LOG_ERROR("WaterRenderer: failed to create scene color sampler"); return; } sampCI.magFilter = VK_FILTER_NEAREST; sampCI.minFilter = VK_FILTER_NEAREST; - if (vkCreateSampler(device, &sampCI, nullptr, &sceneDepthSampler) != VK_SUCCESS) { + sceneDepthSampler = vkCtx->getOrCreateSampler(sampCI); + if (sceneDepthSampler == VK_NULL_HANDLE) { LOG_ERROR("WaterRenderer: failed to create scene depth sampler"); return; } @@ -1000,7 +1002,7 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu } } LOG_DEBUG("WMO water: origin=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z, - ") tiles=", (int)surface.width, "x", (int)surface.height, + ") tiles=", static_cast(surface.width), "x", static_cast(surface.height), " active=", activeTiles, "/", tileCount, " wmoId=", wmoId, " indexCount=", surface.indexCount, " bounds x=[", minWX, "..", maxWX, "] y=[", minWY, "..", maxWY, "]"); @@ -1029,10 +1031,6 @@ void WaterRenderer::clear() { destroyWaterMesh(surface); } surfaces.clear(); - - if (vkCtx && materialDescPool) { - vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); - } } // ============================================================== @@ -1146,10 +1144,14 @@ void WaterRenderer::captureSceneHistory(VkCommandBuffer cmd, }; // Color source: final render pass layout is PRESENT_SRC. + // srcAccessMask must be COLOR_ATTACHMENT_WRITE (not 0) so that GPU cache flushes + // happen before the transfer read. Using srcAccessMask=0 with BOTTOM_OF_PIPE + // causes VK_ERROR_DEVICE_LOST on strict drivers (AMD/Mali) because color writes + // are not made visible to the transfer unit before the copy begins. barrier2(srcColorImage, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - 0, VK_ACCESS_TRANSFER_READ_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); barrier2(sh.colorImage, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, @@ -1358,27 +1360,45 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { void WaterRenderer::destroyWaterMesh(WaterSurface& surface) { if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); - if (surface.vertexBuffer) { - AllocatedBuffer ab{}; ab.buffer = surface.vertexBuffer; ab.allocation = surface.vertexAlloc; - destroyBuffer(allocator, ab); - surface.vertexBuffer = VK_NULL_HANDLE; - } - if (surface.indexBuffer) { - AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc; - destroyBuffer(allocator, ab); - surface.indexBuffer = VK_NULL_HANDLE; - } - if (surface.materialUBO) { - AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc; - destroyBuffer(allocator, ab); - surface.materialUBO = VK_NULL_HANDLE; - } - if (surface.materialSet && materialDescPool) { - vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet); - } + ::VkBuffer vertexBuffer = surface.vertexBuffer; + VmaAllocation vertexAlloc = surface.vertexAlloc; + ::VkBuffer indexBuffer = surface.indexBuffer; + VmaAllocation indexAlloc = surface.indexAlloc; + ::VkBuffer materialUBO = surface.materialUBO; + VmaAllocation materialAlloc = surface.materialAlloc; + VkDescriptorPool pool = materialDescPool; + VkDescriptorSet materialSet = surface.materialSet; + + surface.vertexBuffer = VK_NULL_HANDLE; + surface.vertexAlloc = VK_NULL_HANDLE; + surface.indexBuffer = VK_NULL_HANDLE; + surface.indexAlloc = VK_NULL_HANDLE; + surface.materialUBO = VK_NULL_HANDLE; + surface.materialAlloc = VK_NULL_HANDLE; surface.materialSet = VK_NULL_HANDLE; + + vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc, + materialUBO, materialAlloc, pool, materialSet]() { + if (vertexBuffer) { + AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc; + destroyBuffer(allocator, ab); + } + if (indexBuffer) { + AllocatedBuffer ab{}; ab.buffer = indexBuffer; ab.allocation = indexAlloc; + destroyBuffer(allocator, ab); + } + if (materialUBO) { + AllocatedBuffer ab{}; ab.buffer = materialUBO; ab.allocation = materialAlloc; + destroyBuffer(allocator, ab); + } + if (materialSet && pool) { + VkDescriptorSet set = materialSet; + vkFreeDescriptorSets(device, pool, 1, &set); + } + }); } // ============================================================== @@ -1700,7 +1720,8 @@ void WaterRenderer::createReflectionResources() { sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - if (vkCreateSampler(device, &sampCI, nullptr, &reflectionSampler) != VK_SUCCESS) { + reflectionSampler = vkCtx->getOrCreateSampler(sampCI); + if (reflectionSampler == VK_NULL_HANDLE) { LOG_ERROR("WaterRenderer: failed to create reflection sampler"); return; } @@ -1830,7 +1851,7 @@ void WaterRenderer::destroyReflectionResources() { if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; } if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; } if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; } - if (reflectionSampler) { vkDestroySampler(device, reflectionSampler, nullptr); reflectionSampler = VK_NULL_HANDLE; } + reflectionSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache if (reflectionUBO) { AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc; destroyBuffer(allocator, ab); @@ -2074,7 +2095,7 @@ bool WaterRenderer::createWater1xPass(VkFormat colorFormat, VkFormat depthFormat .setLayout(pipelineLayout) .setRenderPass(water1xRenderPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); diff --git a/src/rendering/weather.cpp b/src/rendering/weather.cpp index fed604dc..6f81aae0 100644 --- a/src/rendering/weather.cpp +++ b/src/rendering/weather.cpp @@ -85,7 +85,7 @@ bool Weather::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout) { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -165,7 +165,7 @@ void Weather::recreatePipelines() { .setLayout(pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates(dynamicStates) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertModule.destroy(); fragModule.destroy(); @@ -198,6 +198,10 @@ void Weather::update(const Camera& camera, float deltaTime) { if (weatherType == Type::RAIN) { p.velocity = glm::vec3(0.0f, -50.0f, 0.0f); // Fast downward p.maxLifetime = 5.0f; + } else if (weatherType == Type::STORM) { + // Storm: faster, angled rain with wind + p.velocity = glm::vec3(15.0f, -70.0f, 8.0f); + p.maxLifetime = 3.5f; } else { // SNOW p.velocity = glm::vec3(0.0f, -5.0f, 0.0f); // Slow downward p.maxLifetime = 10.0f; @@ -245,6 +249,12 @@ void Weather::updateParticle(Particle& particle, const Camera& camera, float del particle.velocity.x = windX; particle.velocity.z = windZ; } + // Storm: gusty, turbulent wind with varying direction + if (weatherType == Type::STORM) { + float gust = std::sin(particle.lifetime * 1.5f + particle.position.x * 0.1f) * 5.0f; + particle.velocity.x = 15.0f + gust; + particle.velocity.z = 8.0f + std::cos(particle.lifetime * 2.0f) * 3.0f; + } // Update position particle.position += particle.velocity * deltaTime; @@ -275,6 +285,9 @@ void Weather::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { if (weatherType == Type::RAIN) { push.particleSize = 3.0f; push.particleColor = glm::vec4(0.7f, 0.8f, 0.9f, 0.6f); + } else if (weatherType == Type::STORM) { + push.particleSize = 3.5f; + push.particleColor = glm::vec4(0.6f, 0.65f, 0.75f, 0.7f); // Darker, more opaque } else { // SNOW push.particleSize = 8.0f; push.particleColor = glm::vec4(1.0f, 1.0f, 1.0f, 0.9f); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index c2a81301..e031bce6 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -183,7 +183,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); if (!opaquePipeline_) { core::Logger::getInstance().error("WMORenderer: failed to create opaque pipeline"); @@ -205,7 +205,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); if (!transparentPipeline_) { core::Logger::getInstance().warning("WMORenderer: transparent pipeline not available"); @@ -224,7 +224,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); // --- Build wireframe pipeline --- wireframePipeline_ = PipelineBuilder() @@ -239,7 +239,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); if (!wireframePipeline_) { core::Logger::getInstance().warning("WMORenderer: wireframe pipeline not available"); @@ -307,7 +307,9 @@ void WMORenderer::shutdown() { textureCacheBytes_ = 0; textureCacheCounter_ = 0; failedTextureCache_.clear(); + failedTextureRetryAt_.clear(); loggedTextureLoadFails_.clear(); + textureLookupSerial_ = 0; textureBudgetRejectWarnings_ = 0; // Free white texture and flat normal texture @@ -805,6 +807,10 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { return true; } +bool WMORenderer::isModelLoaded(uint32_t id) const { + return loadedModels.find(id) != loadedModels.end(); +} + void WMORenderer::unloadModel(uint32_t id) { auto it = loadedModels.find(id); if (it == loadedModels.end()) { @@ -835,7 +841,12 @@ void WMORenderer::cleanupUnusedModels() { } } - // Delete GPU resources and remove from map + // Delete GPU resources and remove from map. + // Ensure all in-flight frames are complete before freeing vertex/index buffers — + // the GPU may still be reading them from the previous frame's command buffer. + if (!toRemove.empty() && vkCtx_) { + vkDeviceWaitIdle(vkCtx_->getDevice()); + } for (uint32_t id : toRemove) { unloadModel(id); } @@ -1078,7 +1089,9 @@ void WMORenderer::clearAll() { textureCacheBytes_ = 0; textureCacheCounter_ = 0; failedTextureCache_.clear(); + failedTextureRetryAt_.clear(); loggedTextureLoadFails_.clear(); + textureLookupSerial_ = 0; textureBudgetRejectWarnings_ = 0; precomputedFloorGrid.clear(); @@ -1666,7 +1679,7 @@ bool WMORenderer::initializeShadow(VkRenderPass shadowRenderPass) { .setLayout(shadowPipelineLayout_) .setRenderPass(shadowRenderPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); @@ -2045,6 +2058,9 @@ void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model, const Frustum& frustum, const glm::mat4& modelMatrix, std::unordered_set& outVisibleGroups) const { + constexpr uint32_t WMO_GROUP_FLAG_OUTDOOR = 0x8; + constexpr uint32_t WMO_GROUP_FLAG_INDOOR = 0x2000; + // Find camera's containing group int cameraGroup = findContainingGroup(model, cameraLocalPos); @@ -2058,6 +2074,33 @@ void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model, return; } + // Outdoor city WMOs (e.g. Stormwind) often have portal graphs that are valid for + // indoor visibility but too aggressive outdoors, causing direction-dependent popout. + // Only trust portal traversal when the camera is in an interior-only group. + if (cameraGroup < static_cast(model.groups.size())) { + const uint32_t gFlags = model.groups[cameraGroup].groupFlags; + const bool isIndoor = (gFlags & WMO_GROUP_FLAG_INDOOR) != 0; + const bool isOutdoor = (gFlags & WMO_GROUP_FLAG_OUTDOOR) != 0; + if (!isIndoor || isOutdoor) { + for (size_t gi = 0; gi < model.groups.size(); gi++) { + outVisibleGroups.insert(static_cast(gi)); + } + return; + } + } + + // If the camera group has no portal refs, it's a dead-end group (utility/transition group). + // Fall back to showing all groups to avoid the rest of the WMO going invisible. + if (cameraGroup < static_cast(model.groupPortalRefs.size())) { + auto [portalStart, portalCount] = model.groupPortalRefs[cameraGroup]; + if (portalCount == 0) { + for (size_t gi = 0; gi < model.groups.size(); gi++) { + outVisibleGroups.insert(static_cast(gi)); + } + return; + } + } + // BFS through portals from camera's group std::vector visited(model.groups.size(), false); std::vector queue; @@ -2134,8 +2177,8 @@ std::unique_ptr WMORenderer::generateNormalHeightMap( // Step 1.5: Box blur the height map to reduce noise from diffuse textures auto wrapSample = [&](const std::vector& map, int x, int y) -> float { - x = ((x % (int)width) + (int)width) % (int)width; - y = ((y % (int)height) + (int)height) % (int)height; + x = ((x % static_cast(width)) + static_cast(width)) % static_cast(width); + y = ((y % static_cast(height)) + static_cast(height)) % static_cast(height); return map[y * width + x]; }; @@ -2157,8 +2200,8 @@ std::unique_ptr WMORenderer::generateNormalHeightMap( std::vector output(totalPixels * 4); auto sampleH = [&](int x, int y) -> float { - x = ((x % (int)width) + (int)width) % (int)width; - y = ((y % (int)height) + (int)height) % (int)height; + x = ((x % static_cast(width)) + static_cast(width)) % static_cast(width); + y = ((y % static_cast(height)) + static_cast(height)) % static_cast(height); return heightMap[y * width + x]; }; @@ -2198,6 +2241,7 @@ std::unique_ptr WMORenderer::generateNormalHeightMap( } VkTexture* WMORenderer::loadTexture(const std::string& path) { + constexpr uint64_t kFailedTextureRetryLookups = 512; if (!assetManager || !vkCtx_) { return whiteTexture_.get(); } @@ -2273,7 +2317,19 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) { } } - const auto& attemptedCandidates = uniqueCandidates; + const uint64_t lookupSerial = ++textureLookupSerial_; + std::vector attemptedCandidates; + attemptedCandidates.reserve(uniqueCandidates.size()); + for (const auto& c : uniqueCandidates) { + auto fit = failedTextureRetryAt_.find(c); + if (fit != failedTextureRetryAt_.end() && lookupSerial < fit->second) { + continue; + } + attemptedCandidates.push_back(c); + } + if (attemptedCandidates.empty()) { + return whiteTexture_.get(); + } // Try loading all candidates until one succeeds // Check pre-decoded BLP cache first (populated by background worker threads) @@ -2300,6 +2356,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) { } } if (!blp.isValid()) { + for (const auto& c : attemptedCandidates) { + failedTextureCache_.insert(c); + failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups; + } if (loggedTextureLoadFails_.insert(key).second) { core::Logger::getInstance().warning("WMO: Failed to load texture: ", path); } @@ -2314,6 +2374,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) { size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; size_t approxBytes = base + (base / 3); if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) { + for (const auto& c : attemptedCandidates) { + failedTextureCache_.insert(c); + failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups; + } if (textureBudgetRejectWarnings_ < 3) { core::Logger::getInstance().warning( "WMO texture cache full (", textureCacheBytes_ / (1024 * 1024), @@ -2355,8 +2419,12 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) { textureCacheBytes_ += e.approxBytes; if (!resolvedKey.empty()) { textureCache[resolvedKey] = std::move(e); + failedTextureCache_.erase(resolvedKey); + failedTextureRetryAt_.erase(resolvedKey); } else { textureCache[key] = std::move(e); + failedTextureCache_.erase(key); + failedTextureRetryAt_.erase(key); } core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); @@ -2609,10 +2677,11 @@ void WMORenderer::GroupResources::buildCollisionGrid() { triNormals[i / 3] = normal; // Classify floor vs wall by normal. - // Wall threshold matches the runtime skip in checkWallCollision (absNz >= 0.35). + // Wall threshold matches MAX_WALK_SLOPE (cos 50° ≈ 0.6428): surfaces steeper + // than 50° from horizontal are walls. Must match checkWallCollision runtime skip. float absNz = std::abs(normal.z); - bool isFloor = (absNz >= 0.35f); // ~70° max slope (relaxed for steep stairs) - bool isWall = (absNz < 0.35f); // Matches checkWallCollision skip threshold + bool isFloor = (absNz >= 0.65f); + bool isWall = (absNz < 0.65f); int cellMinX = std::max(0, static_cast((triMinX - gridOrigin.x) * invCellW)); int cellMinY = std::max(0, static_cast((triMinY - gridOrigin.y) * invCellH)); @@ -3205,9 +3274,11 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, float horizDist = glm::length(glm::vec2(delta.x, delta.y)); if (horizDist <= PLAYER_RADIUS) { - // Skip floor-like surfaces — grounding handles them, not wall collision + // Skip floor-like surfaces — grounding handles them, not wall collision. + // Threshold matches MAX_WALK_SLOPE (cos 50° ≈ 0.6428): surfaces steeper + // than 50° from horizontal must be tested as walls to prevent clip-through. float absNz = std::abs(normal.z); - if (absNz >= 0.35f) continue; + if (absNz >= 0.65f) continue; const float SKIN = 0.005f; // small separation so we don't re-collide immediately // Push must cover full penetration to prevent gradual clip-through @@ -3510,7 +3581,7 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3 const glm::vec3& v2 = verts[indices[triStart + 2]]; glm::vec3 triNormal = group.triNormals[triStart / 3]; if (glm::dot(triNormal, triNormal) < 0.5f) continue; // degenerate - // Wall list pre-filters at 0.35; apply stricter camera threshold + // Wall list pre-filters at 0.65; apply stricter camera threshold if (std::abs(triNormal.z) > MAX_WALKABLE_ABS_NORMAL_Z) { continue; } @@ -3610,7 +3681,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); transparentPipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -3624,7 +3695,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); glassPipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -3638,7 +3709,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); wireframePipeline_ = PipelineBuilder() .setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), @@ -3652,7 +3723,7 @@ void WMORenderer::recreatePipelines() { .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx_->getPipelineCache()); vertShader.destroy(); fragShader.destroy(); diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 9c30a3b5..03da7972 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -12,6 +12,7 @@ #include "core/logger.hpp" #include #include +#include #include #include @@ -164,7 +165,7 @@ bool WorldMap::initialize(VkContext* ctx, pipeline::AssetManager* am) { .setLayout(tilePipelineLayout) .setRenderPass(compositeTarget->getRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) - .build(device); + .build(device, vkCtx->getPipelineCache()); vs.destroy(); fs.destroy(); @@ -362,14 +363,15 @@ void WorldMap::loadZonesFromDBC() { cont.locTop = z.locTop; cont.locBottom = z.locBottom; first = false; } else { - cont.locLeft = std::max(cont.locLeft, z.locLeft); - cont.locRight = std::min(cont.locRight, z.locRight); - cont.locTop = std::max(cont.locTop, z.locTop); - cont.locBottom = std::min(cont.locBottom, z.locBottom); + cont.locLeft = std::min(cont.locLeft, z.locLeft); + cont.locRight = std::max(cont.locRight, z.locRight); + cont.locTop = std::min(cont.locTop, z.locTop); + cont.locBottom = std::max(cont.locBottom, z.locBottom); } } } + currentMapId_ = mapID; LOG_INFO("WorldMap: loaded ", zones.size(), " zones for mapID=", mapID, ", continentIdx=", continentIdx); } @@ -834,63 +836,66 @@ void WorldMap::zoomOut() { // Main render (input + ImGui overlay) // -------------------------------------------------------- -void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { +void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, + float playerYawDeg) { if (!initialized || !assetManager) return; auto& input = core::Input::getInstance(); if (!zones.empty()) updateExploration(playerRenderPos); - if (open) { - if (input.isKeyJustPressed(SDL_SCANCODE_M) || - input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) { - open = false; - return; + // game_screen owns the open/close toggle (via showWorldMap_ + TOGGLE_WORLD_MAP keybinding). + // render() is only called when showWorldMap_ is true, so treat each call as "should be open". + if (!open) { + // First time shown: load zones and navigate to player's location. + open = true; + if (zones.empty()) loadZonesFromDBC(); + + int bestContinent = findBestContinentForPlayer(playerRenderPos); + if (bestContinent >= 0 && bestContinent != continentIdx) { + continentIdx = bestContinent; + compositedIdx = -1; } + int playerZone = findZoneForPlayer(playerRenderPos); + if (playerZone >= 0 && continentIdx >= 0 && + zoneBelongsToContinent(playerZone, continentIdx)) { + loadZoneTextures(playerZone); + requestComposite(playerZone); + currentIdx = playerZone; + viewLevel = ViewLevel::ZONE; + } else if (continentIdx >= 0) { + loadZoneTextures(continentIdx); + requestComposite(continentIdx); + currentIdx = continentIdx; + viewLevel = ViewLevel::CONTINENT; + } + } + + // ESC closes the map; game_screen will sync showWorldMap_ via wm->isOpen() next frame. + if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) { + open = false; + return; + } + + { auto& io = ImGui::GetIO(); float wheelDelta = io.MouseWheel; if (std::abs(wheelDelta) < 0.001f) wheelDelta = input.getMouseWheelDelta(); if (wheelDelta > 0.0f) zoomIn(playerRenderPos); else if (wheelDelta < 0.0f) zoomOut(); - } else { - auto& io = ImGui::GetIO(); - if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_M)) { - open = true; - if (zones.empty()) loadZonesFromDBC(); - - int bestContinent = findBestContinentForPlayer(playerRenderPos); - if (bestContinent >= 0 && bestContinent != continentIdx) { - continentIdx = bestContinent; - compositedIdx = -1; - } - - int playerZone = findZoneForPlayer(playerRenderPos); - if (playerZone >= 0 && continentIdx >= 0 && - zoneBelongsToContinent(playerZone, continentIdx)) { - loadZoneTextures(playerZone); - requestComposite(playerZone); - currentIdx = playerZone; - viewLevel = ViewLevel::ZONE; - } else if (continentIdx >= 0) { - loadZoneTextures(continentIdx); - requestComposite(continentIdx); - currentIdx = continentIdx; - viewLevel = ViewLevel::CONTINENT; - } - } } if (!open) return; - renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight); + renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight, playerYawDeg); } // -------------------------------------------------------- // ImGui overlay // -------------------------------------------------------- -void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { +void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight, float playerYawDeg) { float sw = static_cast(screenWidth); float sh = static_cast(screenHeight); @@ -1011,8 +1016,179 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi playerUV.y >= 0.0f && playerUV.y <= 1.0f) { float px = imgMin.x + playerUV.x * displayW; float py = imgMin.y + playerUV.y * displayH; - drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255)); - drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f); + // Directional arrow: render-space (cos,sin) maps to screen (-dx,-dy) + // because render+X=west=left and render+Y=north=up (screen Y is down). + float yawRad = glm::radians(playerYawDeg); + float adx = -std::cos(yawRad); // screen-space arrow X + float ady = -std::sin(yawRad); // screen-space arrow Y + float apx = -ady, apy = adx; // perpendicular (left/right of arrow) + constexpr float TIP = 9.0f; // tip distance from center + constexpr float TAIL = 4.0f; // tail distance from center + constexpr float HALF = 5.0f; // half base width + ImVec2 tip(px + adx * TIP, py + ady * TIP); + ImVec2 bl (px - adx * TAIL + apx * HALF, py - ady * TAIL + apy * HALF); + ImVec2 br (px - adx * TAIL - apx * HALF, py - ady * TAIL - apy * HALF); + drawList->AddTriangleFilled(tip, bl, br, IM_COL32(255, 40, 40, 255)); + drawList->AddTriangle(tip, bl, br, IM_COL32(0, 0, 0, 200), 1.5f); + } + } + + // Party member dots + if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) { + ImFont* font = ImGui::GetFont(); + for (const auto& dot : partyDots_) { + glm::vec2 uv = renderPosToMapUV(dot.renderPos, currentIdx); + if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue; + float px = imgMin.x + uv.x * displayW; + float py = imgMin.y + uv.y * displayH; + drawList->AddCircleFilled(ImVec2(px, py), 5.0f, dot.color); + drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(0, 0, 0, 200), 0, 1.5f); + // Name tooltip on hover + if (!dot.name.empty()) { + ImVec2 mp = ImGui::GetMousePos(); + float dx = mp.x - px, dy = mp.y - py; + if (dx * dx + dy * dy <= 49.0f) { // radius 7 px hit area + ImGui::SetTooltip("%s", dot.name.c_str()); + } + // Draw name label above the dot + ImVec2 nameSz = font->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f, dot.name.c_str()); + float tx = px - nameSz.x * 0.5f; + float ty = py - nameSz.y - 7.0f; + drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), dot.name.c_str()); + drawList->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 220), dot.name.c_str()); + } + } + } + + // Taxi node markers — flight master icons on the map + if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD && !taxiNodes_.empty()) { + ImVec2 mp = ImGui::GetMousePos(); + for (const auto& node : taxiNodes_) { + if (!node.known) continue; + if (static_cast(node.mapId) != currentMapId_) continue; + + glm::vec3 rPos = core::coords::canonicalToRender( + glm::vec3(node.wowX, node.wowY, node.wowZ)); + glm::vec2 uv = renderPosToMapUV(rPos, currentIdx); + if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue; + + float px = imgMin.x + uv.x * displayW; + float py = imgMin.y + uv.y * displayH; + + // Flight-master icon: yellow diamond with dark border + constexpr float H = 5.0f; // half-size of diamond + ImVec2 top2(px, py - H); + ImVec2 right2(px + H, py ); + ImVec2 bot2(px, py + H); + ImVec2 left2(px - H, py ); + drawList->AddQuadFilled(top2, right2, bot2, left2, + IM_COL32(255, 215, 0, 230)); + drawList->AddQuad(top2, right2, bot2, left2, + IM_COL32(80, 50, 0, 200), 1.2f); + + // Tooltip on hover + if (!node.name.empty()) { + float mdx = mp.x - px, mdy = mp.y - py; + if (mdx * mdx + mdy * mdy < 49.0f) { + ImGui::SetTooltip("%s\n(Flight Master)", node.name.c_str()); + } + } + } + } + + // Quest POI markers — golden exclamation marks / question marks + if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD && !questPois_.empty()) { + ImVec2 mp = ImGui::GetMousePos(); + ImFont* qFont = ImGui::GetFont(); + for (const auto& qp : questPois_) { + glm::vec3 rPos = core::coords::canonicalToRender( + glm::vec3(qp.wowX, qp.wowY, 0.0f)); + glm::vec2 uv = renderPosToMapUV(rPos, currentIdx); + if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue; + + float px = imgMin.x + uv.x * displayW; + float py = imgMin.y + uv.y * displayH; + + // Cyan circle with golden ring (matches minimap POI style) + drawList->AddCircleFilled(ImVec2(px, py), 5.0f, IM_COL32(0, 210, 255, 220)); + drawList->AddCircle(ImVec2(px, py), 5.0f, IM_COL32(255, 215, 0, 220), 0, 1.5f); + + // Quest name label + if (!qp.name.empty()) { + ImVec2 nameSz = qFont->CalcTextSizeA(ImGui::GetFontSize() * 0.85f, FLT_MAX, 0.0f, qp.name.c_str()); + float tx = px - nameSz.x * 0.5f; + float ty = py - nameSz.y - 7.0f; + drawList->AddText(qFont, ImGui::GetFontSize() * 0.85f, + ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), qp.name.c_str()); + drawList->AddText(qFont, ImGui::GetFontSize() * 0.85f, + ImVec2(tx, ty), IM_COL32(255, 230, 100, 230), qp.name.c_str()); + } + // Tooltip on hover + float mdx = mp.x - px, mdy = mp.y - py; + if (mdx * mdx + mdy * mdy < 49.0f && !qp.name.empty()) { + ImGui::SetTooltip("%s\n(Quest Objective)", qp.name.c_str()); + } + } + } + + // Corpse marker — skull X shown when player is a ghost with unclaimed corpse + if (hasCorpse_ && currentIdx >= 0 && viewLevel != ViewLevel::WORLD) { + glm::vec2 uv = renderPosToMapUV(corpseRenderPos_, currentIdx); + if (uv.x >= 0.0f && uv.x <= 1.0f && uv.y >= 0.0f && uv.y <= 1.0f) { + float cx = imgMin.x + uv.x * displayW; + float cy = imgMin.y + uv.y * displayH; + constexpr float R = 5.0f; // cross arm half-length + constexpr float T = 1.8f; // line thickness + // Dark outline + drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R), + IM_COL32(0, 0, 0, 220), T + 1.5f); + drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R), + IM_COL32(0, 0, 0, 220), T + 1.5f); + // Bone-white X + drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R), + IM_COL32(230, 220, 200, 240), T); + drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R), + IM_COL32(230, 220, 200, 240), T); + // Tooltip on hover + ImVec2 mp = ImGui::GetMousePos(); + float dx = mp.x - cx, dy = mp.y - cy; + if (dx * dx + dy * dy < 64.0f) { + ImGui::SetTooltip("Your corpse"); + } + } + } + + // Hover coordinate display — show WoW coordinates under cursor + if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) { + auto& io = ImGui::GetIO(); + ImVec2 mp = io.MousePos; + if (mp.x >= imgMin.x && mp.x <= imgMin.x + displayW && + mp.y >= imgMin.y && mp.y <= imgMin.y + displayH) { + float mu = (mp.x - imgMin.x) / displayW; + float mv = (mp.y - imgMin.y) / displayH; + + const auto& zone = zones[currentIdx]; + float left = zone.locLeft, right = zone.locRight; + float top = zone.locTop, bottom = zone.locBottom; + if (zone.areaID == 0) { + float l, r, t, b; + getContinentProjectionBounds(currentIdx, l, r, t, b); + left = l; right = r; top = t; bottom = b; + // Undo the kVOffset applied during renderPosToMapUV for continent + constexpr float kVOffset = -0.15f; + mv -= kVOffset; + } + + float hWowX = left - mu * (left - right); + float hWowY = top - mv * (top - bottom); + + char coordBuf[32]; + snprintf(coordBuf, sizeof(coordBuf), "%.0f, %.0f", hWowX, hWowY); + ImVec2 coordSz = ImGui::CalcTextSize(coordBuf); + float cx = imgMin.x + displayW - coordSz.x - 8.0f; + float cy = imgMin.y + displayH - coordSz.y - 8.0f; + drawList->AddText(ImVec2(cx + 1.0f, cy + 1.0f), IM_COL32(0, 0, 0, 180), coordBuf); + drawList->AddText(ImVec2(cx, cy), IM_COL32(220, 210, 150, 230), coordBuf); } } @@ -1080,6 +1256,23 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f); } + + // Zone name label — only if the rect is large enough to fit it + if (!z.areaName.empty()) { + ImVec2 textSz = ImGui::CalcTextSize(z.areaName.c_str()); + float rectW = sx1 - sx0; + float rectH = sy1 - sy0; + if (rectW > textSz.x + 4.0f && rectH > textSz.y + 2.0f) { + float tx = (sx0 + sx1) * 0.5f - textSz.x * 0.5f; + float ty = (sy0 + sy1) * 0.5f - textSz.y * 0.5f; + ImU32 labelCol = explored + ? IM_COL32(255, 230, 150, 210) + : IM_COL32(160, 160, 160, 80); + drawList->AddText(ImVec2(tx + 1.0f, ty + 1.0f), + IM_COL32(0, 0, 0, 130), z.areaName.c_str()); + drawList->AddText(ImVec2(tx, ty), labelCol, z.areaName.c_str()); + } + } } } diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 2f4b83cc..710d45d5 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -1,4 +1,5 @@ #include "ui/auth_screen.hpp" +#include "ui/ui_colors.hpp" #include "auth/crypto.hpp" #include "core/application.hpp" #include "core/logger.hpp" @@ -37,7 +38,7 @@ static std::string trimAscii(std::string s) { static std::string hexEncode(const std::vector& data) { std::ostringstream ss; for (uint8_t b : data) - ss << std::hex << std::setfill('0') << std::setw(2) << (int)b; + ss << std::hex << std::setfill('0') << std::setw(2) << static_cast(b); return ss.str(); } @@ -206,8 +207,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { } } } - // Login screen music disabled - if (false && renderer) { + // Login screen music + if (renderer) { auto* music = renderer->getMusicManager(); if (music) { if (!loginMusicVolumeAdjusted_) { @@ -393,9 +394,9 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { // Connection status if (!statusMessage.empty()) { if (statusIsError) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ui::colors::kRed); } else { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ui::colors::kBrightGreen); } ImGui::TextWrapped("%s", statusMessage.c_str()); ImGui::PopStyleColor(); @@ -915,7 +916,7 @@ bool AuthScreen::loadBackgroundImage() { samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - vkCreateSampler(device, &samplerInfo, nullptr, &bgSampler); + bgSampler = bgVkCtx->getOrCreateSampler(samplerInfo); } bgDescriptorSet = ImGui_ImplVulkan_AddTexture(bgSampler, bgImageView, @@ -930,7 +931,7 @@ void AuthScreen::destroyBackgroundImage() { VkDevice device = bgVkCtx->getDevice(); vkDeviceWaitIdle(device); if (bgDescriptorSet) { ImGui_ImplVulkan_RemoveTexture(bgDescriptorSet); bgDescriptorSet = VK_NULL_HANDLE; } - if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; } + bgSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; } if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; } if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; } diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index fa81756f..4a9cda9e 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -1,4 +1,5 @@ #include "ui/character_create_screen.hpp" +#include "ui/ui_colors.hpp" #include "rendering/character_preview.hpp" #include "rendering/renderer.hpp" #include "core/application.hpp" @@ -249,16 +250,17 @@ void CharacterCreateScreen::updateAppearanceRanges() { uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u; const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + auto csF = pipeline::detectCharSectionsFields(dbc.get(), csL); int skinMax = -1; int hairStyleMax = -1; for (uint32_t r = 0; r < dbc->getRecordCount(); r++) { - uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + uint32_t raceId = dbc->getUInt32(r, csF.raceId); + uint32_t sexId = dbc->getUInt32(r, csF.sexId); if (raceId != targetRaceId || sexId != targetSexId) continue; - uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t baseSection = dbc->getUInt32(r, csF.baseSection); + uint32_t variationIndex = dbc->getUInt32(r, csF.variationIndex); + uint32_t colorIndex = dbc->getUInt32(r, csF.colorIndex); if (baseSection == 0 && variationIndex == 0) { skinMax = std::max(skinMax, static_cast(colorIndex)); @@ -279,13 +281,13 @@ void CharacterCreateScreen::updateAppearanceRanges() { int faceMax = -1; std::vector hairColorIds; for (uint32_t r = 0; r < dbc->getRecordCount(); r++) { - uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); - uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + uint32_t raceId = dbc->getUInt32(r, csF.raceId); + uint32_t sexId = dbc->getUInt32(r, csF.sexId); if (raceId != targetRaceId || sexId != targetSexId) continue; - uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); - uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); - uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t baseSection = dbc->getUInt32(r, csF.baseSection); + uint32_t variationIndex = dbc->getUInt32(r, csF.variationIndex); + uint32_t colorIndex = dbc->getUInt32(r, csF.colorIndex); if (baseSection == 1 && colorIndex == static_cast(skin)) { faceMax = std::max(faceMax, static_cast(variationIndex)); @@ -381,7 +383,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { preview_->rotate(deltaX * 0.2f); } - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Drag to rotate"); + ImGui::TextColored(ui::colors::kDarkGray, "Drag to rotate"); } ImGui::EndChild(); @@ -423,7 +425,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { } } if (allianceRaceCount_ < raceCount) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Horde:"); + ImGui::TextColored(ui::colors::kRed, "Horde:"); ImGui::SameLine(); for (int i = allianceRaceCount_; i < raceCount; ++i) { if (i > allianceRaceCount_) ImGui::SameLine(); @@ -516,7 +518,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { if (!statusMessage.empty()) { ImGui::Separator(); ImGui::Spacing(); - ImVec4 color = statusIsError ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + ImVec4 color = statusIsError ? ui::colors::kRed : ui::colors::kBrightGreen; ImGui::TextColored(color, "%s", statusMessage.c_str()); } diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index 406164ac..67ada3f0 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -1,4 +1,5 @@ #include "ui/character_screen.hpp" +#include "ui/ui_colors.hpp" #include "rendering/character_preview.hpp" #include "rendering/renderer.hpp" #include "pipeline/asset_manager.hpp" @@ -37,6 +38,22 @@ static uint64_t hashEquipment(const std::vector& eq) { return h; } +static ImVec4 classColor(uint8_t classId) { + switch (classId) { + case 1: return ImVec4(0.78f, 0.61f, 0.43f, 1.0f); // Warrior #C79C6E + case 2: return ImVec4(0.96f, 0.55f, 0.73f, 1.0f); // Paladin #F58CBA + case 3: return ImVec4(0.67f, 0.83f, 0.45f, 1.0f); // Hunter #ABD473 + case 4: return ImVec4(1.00f, 0.96f, 0.41f, 1.0f); // Rogue #FFF569 + case 5: return ImVec4(1.00f, 1.00f, 1.00f, 1.0f); // Priest #FFFFFF + case 6: return ImVec4(0.77f, 0.12f, 0.23f, 1.0f); // DeathKnight #C41F3B + case 7: return ImVec4(0.00f, 0.44f, 0.87f, 1.0f); // Shaman #0070DE + case 8: return ImVec4(0.41f, 0.80f, 0.94f, 1.0f); // Mage #69CCF0 + case 9: return ImVec4(0.58f, 0.51f, 0.79f, 1.0f); // Warlock #9482C9 + case 11: return ImVec4(1.00f, 0.49f, 0.04f, 1.0f); // Druid #FF7D0A + default: return ImVec4(0.85f, 0.85f, 0.85f, 1.0f); + } +} + void CharacterScreen::render(game::GameHandler& gameHandler) { ImGuiViewport* vp = ImGui::GetMainViewport(); const ImVec2 pad(24.0f, 24.0f); @@ -157,7 +174,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { // Status message if (!statusMessage.empty()) { - ImVec4 color = statusIsError ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + ImVec4 color = statusIsError ? ui::colors::kRed : ui::colors::kBrightGreen; ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", statusMessage.c_str()); ImGui::PopStyleColor(); @@ -184,7 +201,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 45.0f); ImGui::TableSetupColumn("Race", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 1.2f); - ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 1.5f); ImGui::TableSetupScrollFreeze(0, 1); ImGui::TableHeadersRow(); @@ -224,10 +241,16 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::Text("%s", game::getRaceName(character.race)); ImGui::TableSetColumnIndex(3); - ImGui::Text("%s", game::getClassName(character.characterClass)); + ImGui::TextColored(classColor(static_cast(character.characterClass)), "%s", game::getClassName(character.characterClass)); ImGui::TableSetColumnIndex(4); - ImGui::Text("%d", character.zoneId); + { + std::string zoneName = gameHandler.getWhoAreaName(character.zoneId); + if (!zoneName.empty()) + ImGui::TextUnformatted(zoneName.c_str()); + else + ImGui::Text("%u", character.zoneId); + } } ImGui::EndTable(); @@ -325,10 +348,21 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::Text("Level %d", character.level); ImGui::Text("%s", game::getRaceName(character.race)); - ImGui::Text("%s", game::getClassName(character.characterClass)); + ImGui::TextColored(classColor(static_cast(character.characterClass)), "%s", game::getClassName(character.characterClass)); ImGui::Text("%s", game::getGenderName(character.gender)); ImGui::Spacing(); - ImGui::Text("Map %d, Zone %d", character.mapId, character.zoneId); + { + std::string mapName = gameHandler.getMapName(character.mapId); + std::string zoneName = gameHandler.getWhoAreaName(character.zoneId); + if (!mapName.empty() && !zoneName.empty()) + ImGui::Text("%s — %s", mapName.c_str(), zoneName.c_str()); + else if (!mapName.empty()) + ImGui::Text("%s (Zone %u)", mapName.c_str(), character.zoneId); + else if (!zoneName.empty()) + ImGui::Text("Map %u — %s", character.mapId, zoneName.c_str()); + else + ImGui::Text("Map %u, Zone %u", character.mapId, character.zoneId); + } if (character.hasGuild()) { ImGui::Text("Guild ID: %d", character.guildId); @@ -429,7 +463,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { if (ImGui::BeginPopupModal("DeleteConfirm2", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { const auto& ch = characters[selectedCharacterIndex]; - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ui::colors::kRed); ImGui::Text("THIS CANNOT BE UNDONE!"); ImGui::PopStyleColor(); ImGui::Spacing(); @@ -485,7 +519,7 @@ ImVec4 CharacterScreen::getFactionColor(game::Race race) const { race == game::Race::TAUREN || race == game::Race::TROLL || race == game::Race::BLOOD_ELF) { - return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); + return ui::colors::kRed; } return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 563a1225..6cafa6ed 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1,9 +1,9 @@ #include "ui/game_screen.hpp" -#include "rendering/character_preview.hpp" +#include "ui/ui_colors.hpp" #include "rendering/vk_context.hpp" #include "core/application.hpp" +#include "addons/addon_manager.hpp" #include "core/coordinates.hpp" -#include "core/spawn_presets.hpp" #include "core/input.hpp" #include "rendering/renderer.hpp" #include "rendering/wmo_renderer.hpp" @@ -27,16 +27,17 @@ #include "audio/movement_sound_manager.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" -#include "pipeline/blp_loader.hpp" #include "pipeline/dbc_layout.hpp" #include "game/expansion_profile.hpp" +#include "game/character.hpp" #include "core/logger.hpp" #include #include #include #include #include +#include #include #include #include @@ -46,11 +47,23 @@ #include namespace { + // Common ImGui colors (aliases into local namespace for brevity) + using namespace wowee::ui::colors; + constexpr auto& kColorRed = kRed; + constexpr auto& kColorGreen = kGreen; + constexpr auto& kColorBrightGreen= kBrightGreen; + constexpr auto& kColorYellow = kYellow; + constexpr auto& kColorGray = kGray; + constexpr auto& kColorDarkGray = kDarkGray; + + // Common ImGui window flags for popup dialogs + const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + // Build a WoW-format item link string for chat insertion. // Format: |cff|Hitem::0:0:0:0:0:0:0:0|h[]|h|r std::string buildItemChatLink(uint32_t itemId, uint8_t quality, const std::string& name) { - static const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000"}; - uint8_t qi = quality < 6 ? quality : 1; + static const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"}; + uint8_t qi = quality < 8 ? quality : 1; char buf[512]; snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r", kQualHex[qi], itemId, name.c_str()); @@ -71,6 +84,52 @@ namespace { return s; } + // Render gold/silver/copper amounts in WoW-canonical colors on the current ImGui line. + // Skips zero-value denominations (except copper, which is always shown when gold=silver=0). + // Return the canonical Blizzard class color as ImVec4. + // classId is byte 1 of UNIT_FIELD_BYTES_0 (or CharacterData::classId). + // Returns a neutral light-gray for unknown / class 0. + ImVec4 classColorVec4(uint8_t classId) { + switch (classId) { + case 1: return ImVec4(0.78f, 0.61f, 0.43f, 1.0f); // Warrior #C79C6E + case 2: return ImVec4(0.96f, 0.55f, 0.73f, 1.0f); // Paladin #F58CBA + case 3: return ImVec4(0.67f, 0.83f, 0.45f, 1.0f); // Hunter #ABD473 + case 4: return ImVec4(1.00f, 0.96f, 0.41f, 1.0f); // Rogue #FFF569 + case 5: return ImVec4(1.00f, 1.00f, 1.00f, 1.0f); // Priest #FFFFFF + case 6: return ImVec4(0.77f, 0.12f, 0.23f, 1.0f); // DeathKnight #C41F3B + case 7: return ImVec4(0.00f, 0.44f, 0.87f, 1.0f); // Shaman #0070DE + case 8: return ImVec4(0.41f, 0.80f, 0.94f, 1.0f); // Mage #69CCF0 + case 9: return ImVec4(0.58f, 0.51f, 0.79f, 1.0f); // Warlock #9482C9 + case 11: return ImVec4(1.00f, 0.49f, 0.04f, 1.0f); // Druid #FF7D0A + default: return ImVec4(0.85f, 0.85f, 0.85f, 1.0f); // unknown + } + } + + // ImU32 variant with alpha in [0,255]. + ImU32 classColorU32(uint8_t classId, int alpha = 255) { + ImVec4 c = classColorVec4(classId); + return IM_COL32(static_cast(c.x * 255), static_cast(c.y * 255), + static_cast(c.z * 255), alpha); + } + + // Extract class id from a unit's UNIT_FIELD_BYTES_0 update field. + // Returns 0 if the entity pointer is null or field is unset. + uint8_t entityClassId(const wowee::game::Entity* entity) { + if (!entity) return 0; + using UF = wowee::game::UF; + uint32_t bytes0 = entity->getField(wowee::game::fieldIndex(UF::UNIT_FIELD_BYTES_0)); + return static_cast((bytes0 >> 8) & 0xFF); + } + + // Return the English class name for a class ID (1-11), or "Unknown". + const char* classNameStr(uint8_t classId) { + static const char* kNames[] = { + "Unknown","Warrior","Paladin","Hunter","Rogue","Priest", + "Death Knight","Shaman","Mage","Warlock","","Druid" + }; + return (classId < 12) ? kNames[classId] : "Unknown"; + } + bool isPortBotTarget(const std::string& target) { std::string t = toLower(trim(target)); return t == "portbot" || t == "gmbot" || t == "telebot"; @@ -143,32 +202,43 @@ GameScreen::GameScreen() { void GameScreen::initChatTabs() { chatTabs_.clear(); // General tab: shows everything - chatTabs_.push_back({"General", 0xFFFFFFFF}); + chatTabs_.push_back({"General", ~0ULL}); // Combat tab: system, loot, skills, achievements, and NPC speech/emotes - chatTabs_.push_back({"Combat", (1u << static_cast(game::ChatType::SYSTEM)) | - (1u << static_cast(game::ChatType::LOOT)) | - (1u << static_cast(game::ChatType::SKILL)) | - (1u << static_cast(game::ChatType::ACHIEVEMENT)) | - (1u << static_cast(game::ChatType::GUILD_ACHIEVEMENT)) | - (1u << static_cast(game::ChatType::MONSTER_SAY)) | - (1u << static_cast(game::ChatType::MONSTER_YELL)) | - (1u << static_cast(game::ChatType::MONSTER_EMOTE))}); + chatTabs_.push_back({"Combat", (1ULL << static_cast(game::ChatType::SYSTEM)) | + (1ULL << static_cast(game::ChatType::LOOT)) | + (1ULL << static_cast(game::ChatType::SKILL)) | + (1ULL << static_cast(game::ChatType::ACHIEVEMENT)) | + (1ULL << static_cast(game::ChatType::GUILD_ACHIEVEMENT)) | + (1ULL << static_cast(game::ChatType::MONSTER_SAY)) | + (1ULL << static_cast(game::ChatType::MONSTER_YELL)) | + (1ULL << static_cast(game::ChatType::MONSTER_EMOTE)) | + (1ULL << static_cast(game::ChatType::MONSTER_WHISPER)) | + (1ULL << static_cast(game::ChatType::MONSTER_PARTY)) | + (1ULL << static_cast(game::ChatType::RAID_BOSS_WHISPER)) | + (1ULL << static_cast(game::ChatType::RAID_BOSS_EMOTE))}); // Whispers tab - chatTabs_.push_back({"Whispers", (1u << static_cast(game::ChatType::WHISPER)) | - (1u << static_cast(game::ChatType::WHISPER_INFORM))}); + chatTabs_.push_back({"Whispers", (1ULL << static_cast(game::ChatType::WHISPER)) | + (1ULL << static_cast(game::ChatType::WHISPER_INFORM))}); + // Guild tab: guild and officer chat + chatTabs_.push_back({"Guild", (1ULL << static_cast(game::ChatType::GUILD)) | + (1ULL << static_cast(game::ChatType::OFFICER)) | + (1ULL << static_cast(game::ChatType::GUILD_ACHIEVEMENT))}); // Trade/LFG tab: channel messages - chatTabs_.push_back({"Trade/LFG", (1u << static_cast(game::ChatType::CHANNEL))}); + chatTabs_.push_back({"Trade/LFG", (1ULL << static_cast(game::ChatType::CHANNEL))}); + // Reset unread counts to match new tab list + chatTabUnread_.assign(chatTabs_.size(), 0); + chatTabSeenCount_ = 0; } bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const { if (tabIndex < 0 || tabIndex >= static_cast(chatTabs_.size())) return true; const auto& tab = chatTabs_[tabIndex]; - if (tab.typeMask == 0xFFFFFFFF) return true; // General tab shows all + if (tab.typeMask == ~0ULL) return true; // General tab shows all - uint32_t typeBit = 1u << static_cast(msg.type); + uint64_t typeBit = 1ULL << static_cast(msg.type); - // For Trade/LFG tab, also filter by channel name - if (tabIndex == 3 && msg.type == game::ChatType::CHANNEL) { + // For Trade/LFG tab (now index 4), also filter by channel name + if (tabIndex == 4 && msg.type == game::ChatType::CHANNEL) { const std::string& ch = msg.channelName; if (ch.find("Trade") == std::string::npos && ch.find("General") == std::string::npos && @@ -182,7 +252,18 @@ bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabInde return (tab.typeMask & typeBit) != 0; } +// Forward declaration — defined near sendChatMessage below +static std::string firstMacroCommand(const std::string& macroText); +static std::vector allMacroCommands(const std::string& macroText); +static std::string evaluateMacroConditionals(const std::string& rawArg, + game::GameHandler& gameHandler, + uint64_t& targetOverride); +// Returns the spell/item name from #showtooltip [Name], or "__auto__" for bare +// #showtooltip (use first /cast target), or "" if no directive is present. +static std::string getMacroShowtooltipArg(const std::string& macroText); + void GameScreen::render(game::GameHandler& gameHandler) { + cachedGameHandler_ = &gameHandler; // Set up chat bubble callback (once) if (!chatBubbleCallbackSet_) { gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) { @@ -211,10 +292,11 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Set up level-up callback (once) if (!levelUpCallbackSet_) { - gameHandler.setLevelUpCallback([this](uint32_t newLevel) { + gameHandler.setLevelUpCallback([this, &gameHandler](uint32_t newLevel) { levelUpFlashAlpha_ = 1.0f; levelUpDisplayLevel_ = newLevel; - triggerDing(newLevel); + const auto& d = gameHandler.getLastLevelUpDeltas(); + triggerDing(newLevel, d.hp, d.mana, d.str, d.agi, d.sta, d.intel, d.spi); }); levelUpCallbackSet_ = true; } @@ -227,15 +309,126 @@ void GameScreen::render(game::GameHandler& gameHandler) { achievementCallbackSet_ = true; } + // Set up area discovery toast callback (once) + if (!areaDiscoveryCallbackSet_) { + gameHandler.setAreaDiscoveryCallback([this](const std::string& areaName, uint32_t xpGained) { + discoveryToastName_ = areaName.empty() ? "New Area" : areaName; + discoveryToastXP_ = xpGained; + discoveryToastTimer_ = DISCOVERY_TOAST_DURATION; + }); + areaDiscoveryCallbackSet_ = true; + } + + // Set up quest objective progress toast callback (once) + if (!questProgressCallbackSet_) { + gameHandler.setQuestProgressCallback([this](const std::string& questTitle, + const std::string& objectiveName, + uint32_t current, uint32_t required) { + // Coalesce: if the same objective already has a toast, just update counts + for (auto& t : questToasts_) { + if (t.questTitle == questTitle && t.objectiveName == objectiveName) { + t.current = current; + t.required = required; + t.age = 0.0f; // restart lifetime + return; + } + } + if (questToasts_.size() >= 4) questToasts_.erase(questToasts_.begin()); + questToasts_.push_back({questTitle, objectiveName, current, required, 0.0f}); + }); + questProgressCallbackSet_ = true; + } + + // Set up other-player level-up toast callback (once) + if (!otherPlayerLevelUpCallbackSet_) { + gameHandler.setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) { + // Coalesce: update existing toast for same player + for (auto& t : playerLevelUpToasts_) { + if (t.guid == guid) { + t.newLevel = newLevel; + t.age = 0.0f; + return; + } + } + if (playerLevelUpToasts_.size() >= 3) + playerLevelUpToasts_.erase(playerLevelUpToasts_.begin()); + playerLevelUpToasts_.push_back({guid, "", newLevel, 0.0f}); + }); + otherPlayerLevelUpCallbackSet_ = true; + } + + // Set up PvP honor credit toast callback (once) + if (!pvpHonorCallbackSet_) { + gameHandler.setPvpHonorCallback([this](uint32_t honor, uint64_t /*victimGuid*/, uint32_t rank) { + if (honor == 0) return; + pvpHonorToasts_.push_back({honor, rank, 0.0f}); + if (pvpHonorToasts_.size() > 4) + pvpHonorToasts_.erase(pvpHonorToasts_.begin()); + }); + pvpHonorCallbackSet_ = true; + } + + // Set up item loot toast callback (once) + if (!itemLootCallbackSet_) { + gameHandler.setItemLootCallback([this](uint32_t itemId, uint32_t count, + uint32_t quality, const std::string& name) { + // Coalesce: if same item already in queue, bump count and reset age + for (auto& t : itemLootToasts_) { + if (t.itemId == itemId) { + t.count += count; + t.age = 0.0f; + return; + } + } + if (itemLootToasts_.size() >= 5) + itemLootToasts_.erase(itemLootToasts_.begin()); + itemLootToasts_.push_back({itemId, count, quality, name, 0.0f}); + }); + itemLootCallbackSet_ = true; + } + + // Set up ghost-state callback to flash "You have been resurrected!" on revival (once) + if (!ghostStateCallbackSet_) { + gameHandler.setGhostStateCallback([this](bool isGhost) { + if (!isGhost) { + // Transitioning ghost→alive: trigger the resurrection flash + resurrectFlashTimer_ = kResurrectFlashDuration; + } + }); + ghostStateCallbackSet_ = true; + } + + // Set up appearance-changed callback to refresh inventory preview (barber shop, etc.) + if (!appearanceCallbackSet_) { + gameHandler.setAppearanceChangedCallback([this]() { + inventoryScreenCharGuid_ = 0; // force preview re-sync on next frame + }); + appearanceCallbackSet_ = true; + } + // Set up UI error frame callback (once) if (!uiErrorCallbackSet_) { gameHandler.setUIErrorCallback([this](const std::string& msg) { uiErrors_.push_back({msg, 0.0f}); if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin()); + // Play error sound for each new error (rate-limited by deque cap of 5) + if (auto* r = core::Application::getInstance().getRenderer()) { + if (auto* sfx = r->getUiSoundManager()) sfx->playError(); + } }); uiErrorCallbackSet_ = true; } + // Flash the action bar button whose spell just failed (0.5 s red overlay). + if (!castFailedCallbackSet_) { + gameHandler.setSpellCastFailedCallback([this](uint32_t spellId) { + if (spellId == 0) return; + float now = static_cast(ImGui::GetTime()); + actionFlashEndTimes_[spellId] = now + kActionFlashDuration; + }); + castFailedCallbackSet_ = true; + } + // Set up reputation change toast callback (once) if (!repChangeCallbackSet_) { gameHandler.setRepChangeCallback([this](const std::string& name, int32_t delta, int32_t standing) { @@ -245,6 +438,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { repChangeCallbackSet_ = true; } + // Set up quest completion toast callback (once) + if (!questCompleteCallbackSet_) { + gameHandler.setQuestCompleteCallback([this](uint32_t id, const std::string& title) { + questCompleteToasts_.push_back({id, title, 0.0f}); + if (questCompleteToasts_.size() > 3) questCompleteToasts_.erase(questCompleteToasts_.begin()); + }); + questCompleteCallbackSet_ = true; + } + // Apply UI transparency setting float prevAlpha = ImGui::GetStyle().Alpha; ImGui::GetStyle().Alpha = uiOpacity_; @@ -290,38 +492,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (!volumeSettingsApplied_) { auto* renderer = core::Application::getInstance().getRenderer(); if (renderer && renderer->getUiSoundManager()) { - float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; - audio::AudioEngine::instance().setMasterVolume(masterScale); - if (auto* music = renderer->getMusicManager()) { - music->setVolume(pendingMusicVolume); - } - if (auto* ambient = renderer->getAmbientSoundManager()) { - ambient->setVolumeScale(pendingAmbientVolume / 100.0f); - } - if (auto* ui = renderer->getUiSoundManager()) { - ui->setVolumeScale(pendingUiVolume / 100.0f); - } - if (auto* combat = renderer->getCombatSoundManager()) { - combat->setVolumeScale(pendingCombatVolume / 100.0f); - } - if (auto* spell = renderer->getSpellSoundManager()) { - spell->setVolumeScale(pendingSpellVolume / 100.0f); - } - if (auto* movement = renderer->getMovementSoundManager()) { - movement->setVolumeScale(pendingMovementVolume / 100.0f); - } - if (auto* footstep = renderer->getFootstepManager()) { - footstep->setVolumeScale(pendingFootstepVolume / 100.0f); - } - if (auto* npcVoice = renderer->getNpcVoiceManager()) { - npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f); - } - if (auto* mount = renderer->getMountSoundManager()) { - mount->setVolumeScale(pendingMountVolume / 100.0f); - } - if (auto* activity = renderer->getActivitySoundManager()) { - activity->setVolumeScale(pendingActivityVolume / 100.0f); - } + applyAudioVolumes(renderer); volumeSettingsApplied_ = true; } } @@ -341,6 +512,15 @@ void GameScreen::render(game::GameHandler& gameHandler) { msaaSettingsApplied_ = true; } + // Apply saved FXAA setting once when renderer is available + if (!fxaaSettingsApplied_) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) { + renderer->setFXAAEnabled(pendingFXAA); + fxaaSettingsApplied_ = true; + } + } + // Apply saved water refraction setting once when renderer is available if (!waterRefractionApplied_) { auto* renderer = core::Application::getInstance().getRenderer(); @@ -380,22 +560,10 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderer->setFSRSharpness(pendingFSRSharpness); renderer->setFSR2DebugTuning(pendingFSR2JitterSign, pendingFSR2MotionVecScaleX, pendingFSR2MotionVecScaleY); renderer->setAmdFsr3FramegenEnabled(pendingAMDFramegen); - // Safety fallback: persisted FSR2 can still hang on some systems during startup. - // Require explicit opt-in for startup FSR2; otherwise fall back to FSR1. - const bool allowStartupFsr2 = (std::getenv("WOWEE_ALLOW_STARTUP_FSR2") != nullptr); int effectiveMode = pendingUpscalingMode; - if (effectiveMode == 2 && !allowStartupFsr2) { - static bool warnedStartupFsr2Fallback = false; - if (!warnedStartupFsr2Fallback) { - LOG_WARNING("Startup FSR2 is disabled by default for stability; falling back to FSR1. Set WOWEE_ALLOW_STARTUP_FSR2=1 to override."); - warnedStartupFsr2Fallback = true; - } - effectiveMode = 1; - pendingUpscalingMode = 1; - pendingFSR = true; - } - // If explicitly enabled, still defer FSR2 until fully in-world. + // Defer FSR2/FSR3 activation until fully in-world to avoid + // init issues during login/character selection screens. if (effectiveMode == 2 && gameHandler.getState() != game::WorldState::IN_WORLD) { renderer->setFSREnabled(false); renderer->setFSR2Enabled(false); @@ -407,8 +575,24 @@ void GameScreen::render(game::GameHandler& gameHandler) { } } - // Apply auto-loot setting to GameHandler every frame (cheap bool sync) + // Apply auto-loot / auto-sell settings to GameHandler every frame (cheap bool sync) gameHandler.setAutoLoot(pendingAutoLoot); + gameHandler.setAutoSellGrey(pendingAutoSellGrey); + gameHandler.setAutoRepair(pendingAutoRepair); + + // Zone entry detection — fire a toast when the renderer's zone name changes + if (auto* rend = core::Application::getInstance().getRenderer()) { + const std::string& curZone = rend->getCurrentZoneName(); + if (!curZone.empty() && curZone != lastKnownZone_) { + if (!lastKnownZone_.empty()) { + // Genuine zone change (not first entry) + zoneToasts_.push_back({curZone, 0.0f}); + if (zoneToasts_.size() > 3) + zoneToasts_.erase(zoneToasts_.begin()); + } + lastKnownZone_ = curZone; + } + } // Sync chat auto-join settings to GameHandler gameHandler.chatAutoJoin.general = chatAutoJoinGeneral_; @@ -428,6 +612,17 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderPetFrame(gameHandler); } + // Auto-open pet rename modal when server signals the pet is renameable (first tame) + if (gameHandler.consumePetRenameablePending()) { + petRenameOpen_ = true; + petRenameBuf_[0] = '\0'; + } + + // Totem frame (Shaman only, when any totem is active) + if (gameHandler.getPlayerClass() == 7) { + renderTotemFrame(gameHandler); + } + // Target frame (only when we have a target) if (gameHandler.hasTarget()) { renderTargetFrame(gameHandler); @@ -453,22 +648,32 @@ void GameScreen::render(game::GameHandler& gameHandler) { // ---- New UI elements ---- renderActionBar(gameHandler); + renderStanceBar(gameHandler); renderBagBar(gameHandler); renderXpBar(gameHandler); + renderRepBar(gameHandler); renderCastBar(gameHandler); renderMirrorTimers(gameHandler); + renderCooldownTracker(gameHandler); renderQuestObjectiveTracker(gameHandler); renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_ renderBattlegroundScore(gameHandler); + renderRaidWarningOverlay(gameHandler); renderCombatText(gameHandler); + renderDPSMeter(gameHandler); + renderDurabilityWarning(gameHandler); renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime); renderRepToasts(ImGui::GetIO().DeltaTime); + renderQuestCompleteToasts(ImGui::GetIO().DeltaTime); + renderZoneToasts(ImGui::GetIO().DeltaTime); + renderAreaTriggerToasts(ImGui::GetIO().DeltaTime, gameHandler); if (showRaidFrames_) { renderPartyFrames(gameHandler); } renderBossFrames(gameHandler); renderGroupInvitePopup(gameHandler); renderDuelRequestPopup(gameHandler); + renderDuelCountdown(gameHandler); renderLootRollPopup(gameHandler); renderTradeRequestPopup(gameHandler); renderTradeWindow(gameHandler); @@ -478,7 +683,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderGuildInvitePopup(gameHandler); renderReadyCheckPopup(gameHandler); renderBgInvitePopup(gameHandler); + renderBfMgrInvitePopup(gameHandler); renderLfgProposalPopup(gameHandler); + renderLfgRoleCheckPopup(gameHandler); renderGuildRoster(gameHandler); renderSocialFrame(gameHandler); renderBuffBar(gameHandler); @@ -489,6 +696,8 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderQuestOfferRewardWindow(gameHandler); renderVendorWindow(gameHandler); renderTrainerWindow(gameHandler); + renderBarberShopWindow(gameHandler); + renderStableWindow(gameHandler); renderTaxiWindow(gameHandler); renderMailWindow(gameHandler); renderMailComposeWindow(gameHandler); @@ -497,25 +706,41 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderAuctionHouseWindow(gameHandler); renderDungeonFinderWindow(gameHandler); renderInstanceLockouts(gameHandler); + renderWhoWindow(gameHandler); + renderCombatLog(gameHandler); renderAchievementWindow(gameHandler); + renderSkillsWindow(gameHandler); + renderTitlesWindow(gameHandler); + renderEquipSetWindow(gameHandler); renderGmTicketWindow(gameHandler); renderInspectWindow(gameHandler); + renderBookWindow(gameHandler); renderThreatWindow(gameHandler); - renderObjectiveTracker(gameHandler); + renderBgScoreboard(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now if (showMinimap_) { renderMinimapMarkers(gameHandler); } + renderLogoutCountdown(gameHandler); renderDeathScreen(gameHandler); renderReclaimCorpseButton(gameHandler); renderResurrectDialog(gameHandler); renderTalentWipeConfirmDialog(gameHandler); + renderPetUnlearnConfirmDialog(gameHandler); renderChatBubbles(gameHandler); renderEscapeMenu(); renderSettingsWindow(); renderDingEffect(); renderAchievementToast(); - renderZoneText(); + renderDiscoveryToast(); + renderWhisperToasts(); + renderQuestProgressToasts(); + renderPlayerLevelUpToasts(gameHandler); + renderPvpHonorToasts(); + renderItemLootToasts(); + renderResurrectFlash(); + renderZoneText(gameHandler); + renderWeatherOverlay(gameHandler); // World map (M key toggle handled inside) renderWorldMap(gameHandler); @@ -617,7 +842,23 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Update renderer face-target position and selection circle auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { - renderer->setInCombat(gameHandler.isInCombat()); + renderer->setInCombat(gameHandler.isInCombat() && + !gameHandler.isPlayerDead() && + !gameHandler.isPlayerGhost()); + if (auto* cr = renderer->getCharacterRenderer()) { + uint32_t charInstId = renderer->getCharacterInstanceId(); + if (charInstId != 0) { + const bool isGhost = gameHandler.isPlayerGhost(); + if (!ghostOpacityStateKnown_ || + ghostOpacityLastState_ != isGhost || + ghostOpacityLastInstanceId_ != charInstId) { + cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f); + ghostOpacityStateKnown_ = true; + ghostOpacityLastState_ = isGhost; + ghostOpacityLastInstanceId_ = charInstId; + } + } + } static glm::vec3 targetGLPos; if (gameHandler.hasTarget()) { auto target = gameHandler.getTarget(); @@ -744,6 +985,45 @@ void GameScreen::render(game::GameHandler& gameHandler) { } } + // Persistent low-health vignette — pulsing red edges when HP < 20% + { + auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); + bool isDead = gameHandler.isPlayerDead(); + float hpPct = 1.0f; + if (!isDead && playerEntity && + (playerEntity->getType() == game::ObjectType::PLAYER || + playerEntity->getType() == game::ObjectType::UNIT)) { + auto unit = std::static_pointer_cast(playerEntity); + if (unit->getMaxHealth() > 0) + hpPct = static_cast(unit->getHealth()) / static_cast(unit->getMaxHealth()); + } + + // Only show when alive and below 20% HP; intensity increases as HP drops + if (lowHealthVignetteEnabled_ && !isDead && hpPct < 0.20f && hpPct > 0.0f) { + // Base intensity from HP deficit (0 at 20%, 1 at 0%); pulse at ~1.5 Hz + float danger = (0.20f - hpPct) / 0.20f; + float pulse = 0.55f + 0.45f * std::sin(static_cast(ImGui::GetTime()) * 9.4f); + int alpha = static_cast(danger * pulse * 90.0f); // max ~90 alpha, subtle + if (alpha > 0) { + ImDrawList* fg = ImGui::GetForegroundDrawList(); + ImGuiIO& io = ImGui::GetIO(); + const float W = io.DisplaySize.x; + const float H = io.DisplaySize.y; + const float thickness = std::min(W, H) * 0.15f; + const ImU32 edgeCol = IM_COL32(200, 0, 0, alpha); + const ImU32 fadeCol = IM_COL32(200, 0, 0, 0); + fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness), + edgeCol, edgeCol, fadeCol, fadeCol); + fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H), + fadeCol, fadeCol, edgeCol, edgeCol); + fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H), + edgeCol, fadeCol, fadeCol, edgeCol); + fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H), + fadeCol, edgeCol, edgeCol, fadeCol); + } + } + } + // Level-up golden burst overlay if (levelUpFlashAlpha_ > 0.0f) { levelUpFlashAlpha_ -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second @@ -821,16 +1101,16 @@ void GameScreen::renderPlayerInfo(game::GameHandler& gameHandler) { auto state = gameHandler.getState(); switch (state) { case game::WorldState::IN_WORLD: - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "In World"); + ImGui::TextColored(kColorBrightGreen, "In World"); break; case game::WorldState::AUTHENTICATED: - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Authenticated"); + ImGui::TextColored(kColorYellow, "Authenticated"); break; case game::WorldState::ENTERING_WORLD: - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Entering World..."); + ImGui::TextColored(kColorYellow, "Entering World..."); break; default: - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "State: %d", static_cast(state)); + ImGui::TextColored(kColorRed, "State: %d", static_cast(state)); break; } ImGui::Unindent(); @@ -880,10 +1160,10 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(1); switch (entity->getType()) { case game::ObjectType::PLAYER: - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Player"); + ImGui::TextColored(kColorBrightGreen, "Player"); break; case game::ObjectType::UNIT: - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Unit"); + ImGui::TextColored(kColorYellow, "Unit"); break; case game::ObjectType::GAMEOBJECT: ImGui::TextColored(ImVec4(0.3f, 0.8f, 1.0f, 1.0f), "GameObject"); @@ -931,6 +1211,7 @@ void GameScreen::renderEntityList(game::GameHandler& gameHandler) { void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { auto* window = core::Application::getInstance().getWindow(); + auto* assetMgr = core::Application::getInstance().getAssetManager(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; float chatW = std::min(500.0f, screenW * 0.4f); @@ -950,7 +1231,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::SetNextWindowSize(ImVec2(chatW, chatH), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(chatWindowPos_, ImGuiCond_FirstUseEver); } - ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + ImGuiWindowFlags flags = kDialogFlags; if (chatWindowLocked) { flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar; } @@ -960,13 +1241,51 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { chatWindowPos_ = ImGui::GetWindowPos(); } + // Update unread counts: scan any new messages since last frame + { + const auto& history = gameHandler.getChatHistory(); + // Ensure unread array is sized correctly (guards against late init) + if (chatTabUnread_.size() != chatTabs_.size()) + chatTabUnread_.assign(chatTabs_.size(), 0); + // If history shrank (e.g. cleared), reset + if (chatTabSeenCount_ > history.size()) chatTabSeenCount_ = 0; + for (size_t mi = chatTabSeenCount_; mi < history.size(); ++mi) { + const auto& msg = history[mi]; + // For each non-General (non-0) tab that isn't currently active, check visibility + for (int ti = 1; ti < static_cast(chatTabs_.size()); ++ti) { + if (ti == activeChatTab_) continue; + if (shouldShowMessage(msg, ti)) { + chatTabUnread_[ti]++; + } + } + } + chatTabSeenCount_ = history.size(); + } + // Chat tabs if (ImGui::BeginTabBar("ChatTabs")) { for (int i = 0; i < static_cast(chatTabs_.size()); ++i) { - if (ImGui::BeginTabItem(chatTabs_[i].name.c_str())) { - activeChatTab_ = i; + // Build label with unread count suffix for non-General tabs + std::string tabLabel = chatTabs_[i].name; + if (i > 0 && i < static_cast(chatTabUnread_.size()) && chatTabUnread_[i] > 0) { + tabLabel += " (" + std::to_string(chatTabUnread_[i]) + ")"; + } + // Flash tab text color when unread messages exist + bool hasUnread = (i > 0 && i < static_cast(chatTabUnread_.size()) && chatTabUnread_[i] > 0); + if (hasUnread) { + float pulse = 0.6f + 0.4f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f * pulse, 0.2f * pulse, 1.0f)); + } + if (ImGui::BeginTabItem(tabLabel.c_str())) { + if (activeChatTab_ != i) { + activeChatTab_ = i; + // Clear unread count when tab becomes active + if (i < static_cast(chatTabUnread_.size())) + chatTabUnread_[i] = 0; + } ImGui::EndTabItem(); } + if (hasUnread) ImGui::PopStyleColor(); } ImGui::EndTabBar(); } @@ -1053,17 +1372,28 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::BeginTooltip(); // Quality color for name - ImVec4 qColor(1, 1, 1, 1); - switch (info->quality) { - case 0: qColor = ImVec4(0.62f, 0.62f, 0.62f, 1.0f); break; // Poor - case 1: qColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // Common - case 2: qColor = ImVec4(0.12f, 1.0f, 0.0f, 1.0f); break; // Uncommon - case 3: qColor = ImVec4(0.0f, 0.44f, 0.87f, 1.0f); break; // Rare - case 4: qColor = ImVec4(0.64f, 0.21f, 0.93f, 1.0f); break; // Epic - case 5: qColor = ImVec4(1.0f, 0.50f, 0.0f, 1.0f); break; // Legendary - } + auto qColor = ui::getQualityColor(static_cast(info->quality)); ImGui::TextColored(qColor, "%s", info->name.c_str()); + // Heroic indicator (green, matches WoW tooltip style) + constexpr uint32_t kFlagHeroic = 0x8; + constexpr uint32_t kFlagUniqueEquipped = 0x1000000; + if (info->itemFlags & kFlagHeroic) + ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic"); + + // Bind type (appears right under name in WoW) + switch (info->bindType) { + case 1: ImGui::TextDisabled("Binds when picked up"); break; + case 2: ImGui::TextDisabled("Binds when equipped"); break; + case 3: ImGui::TextDisabled("Binds when used"); break; + case 4: ImGui::TextDisabled("Quest Item"); break; + } + // Unique / Unique-Equipped + if (info->maxCount == 1) + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique"); + else if (info->itemFlags & kFlagUniqueEquipped) + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped"); + // Slot type if (info->inventoryType > 0) { const char* slotName = ""; @@ -1096,9 +1426,9 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { } if (slotName[0]) { if (!info->subclassName.empty()) - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, info->subclassName.c_str()); + ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, info->subclassName.c_str()); else - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName); + ImGui::TextColored(ui::colors::kLightGray, "%s", slotName); } } auto isWeaponInventoryType = [](uint32_t invType) { @@ -1116,10 +1446,23 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { }; const bool isWeapon = isWeaponInventoryType(info->inventoryType); + // Item level (after slot/subclass) + if (info->itemLevel > 0) + ImGui::TextDisabled("Item Level %u", info->itemLevel); + if (isWeapon && info->damageMax > 0.0f && info->delayMs > 0) { float speed = static_cast(info->delayMs) / 1000.0f; float dps = ((info->damageMin + info->damageMax) * 0.5f) / speed; - ImGui::Text("%.1f DPS", dps); + // WoW-style: "22 - 41 Damage" with speed right-aligned on same row + char dmgBuf[64], spdBuf[32]; + std::snprintf(dmgBuf, sizeof(dmgBuf), "%d - %d Damage", + static_cast(info->damageMin), static_cast(info->damageMax)); + std::snprintf(spdBuf, sizeof(spdBuf), "Speed %.2f", speed); + float spdW = ImGui::CalcTextSize(spdBuf).x; + ImGui::Text("%s", dmgBuf); + ImGui::SameLine(ImGui::GetWindowWidth() - spdW - 16.0f); + ImGui::Text("%s", spdBuf); + ImGui::TextDisabled("(%.1f damage per second)", dps); } ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f); auto appendBonus = [](std::string& out, int32_t val, const char* shortName) { @@ -1140,11 +1483,334 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { if (info->armor > 0) { ImGui::Text("%d Armor", info->armor); } + // Elemental resistances (fire resist gear, nature resist gear, etc.) + { + const int32_t resVals[6] = { + info->holyRes, info->fireRes, info->natureRes, + info->frostRes, info->shadowRes, info->arcaneRes + }; + static const char* resLabels[6] = { + "Holy Resistance", "Fire Resistance", "Nature Resistance", + "Frost Resistance", "Shadow Resistance", "Arcane Resistance" + }; + for (int ri = 0; ri < 6; ++ri) + if (resVals[ri] > 0) ImGui::Text("+%d %s", resVals[ri], resLabels[ri]); + } + // Extra stats (hit/crit/haste/sp/ap/expertise/resilience/etc.) + if (!info->extraStats.empty()) { + auto statName = [](uint32_t t) -> const char* { + switch (t) { + case 12: return "Defense Rating"; + case 13: return "Dodge Rating"; + case 14: return "Parry Rating"; + case 15: return "Block Rating"; + case 16: case 17: case 18: case 31: return "Hit Rating"; + case 19: case 20: case 21: case 32: return "Critical Strike Rating"; + case 28: case 29: case 30: case 35: return "Haste Rating"; + case 34: return "Resilience Rating"; + case 36: return "Expertise Rating"; + case 37: return "Attack Power"; + case 38: return "Ranged Attack Power"; + case 45: return "Spell Power"; + case 46: return "Healing Power"; + case 47: return "Spell Damage"; + case 49: return "Mana per 5 sec."; + case 43: return "Spell Penetration"; + case 44: return "Block Value"; + default: return nullptr; + } + }; + for (const auto& es : info->extraStats) { + const char* nm = statName(es.statType); + if (nm && es.statValue > 0) + ImGui::TextColored(green, "+%d %s", es.statValue, nm); + } + } + // Gem sockets (WotLK only — socketColor != 0 means socket present) + // socketColor bitmask: 1=Meta, 2=Red, 4=Yellow, 8=Blue + { + static const struct { uint32_t mask; const char* label; ImVec4 col; } kSocketTypes[] = { + { 1, "Meta Socket", { 0.7f, 0.7f, 0.9f, 1.0f } }, + { 2, "Red Socket", { 1.0f, 0.3f, 0.3f, 1.0f } }, + { 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } }, + { 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } }, + }; + bool hasSocket = false; + for (int s = 0; s < 3; ++s) { + if (info->socketColor[s] == 0) continue; + if (!hasSocket) { ImGui::Spacing(); hasSocket = true; } + for (const auto& st : kSocketTypes) { + if (info->socketColor[s] & st.mask) { + ImGui::TextColored(st.col, "%s", st.label); + break; + } + } + } + if (hasSocket && info->socketBonus != 0) { + // Socket bonus ID maps to SpellItemEnchantment.dbc — lazy-load names + static std::unordered_map s_enchantNames; + static bool s_enchantNamesLoaded = false; + if (!s_enchantNamesLoaded && assetMgr) { + s_enchantNamesLoaded = true; + auto dbc = assetMgr->loadDBC("SpellItemEnchantment.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* lay = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr; + uint32_t nameField = lay ? lay->field("Name") : 8u; + if (nameField == 0xFFFFFFFF) nameField = 8; + uint32_t fc = dbc->getFieldCount(); + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t eid = dbc->getUInt32(r, 0); + if (eid == 0 || nameField >= fc) continue; + std::string ename = dbc->getString(r, nameField); + if (!ename.empty()) s_enchantNames[eid] = std::move(ename); + } + } + } + auto enchIt = s_enchantNames.find(info->socketBonus); + if (enchIt != s_enchantNames.end()) + ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str()); + else + ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", info->socketBonus); + } + } + // Item set membership + if (info->itemSetId != 0) { + struct SetEntry { + std::string name; + std::array itemIds{}; + std::array spellIds{}; + std::array thresholds{}; + }; + static std::unordered_map s_setData; + static bool s_setDataLoaded = false; + if (!s_setDataLoaded && assetMgr) { + s_setDataLoaded = true; + auto dbc = assetMgr->loadDBC("ItemSet.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr; + auto lf = [&](const char* k, uint32_t def) -> uint32_t { + return layout ? (*layout)[k] : def; + }; + uint32_t idF = lf("ID", 0), nameF = lf("Name", 1); + static const char* itemKeys[10] = {"Item0","Item1","Item2","Item3","Item4","Item5","Item6","Item7","Item8","Item9"}; + static const char* spellKeys[10] = {"Spell0","Spell1","Spell2","Spell3","Spell4","Spell5","Spell6","Spell7","Spell8","Spell9"}; + static const char* thrKeys[10] = {"Threshold0","Threshold1","Threshold2","Threshold3","Threshold4","Threshold5","Threshold6","Threshold7","Threshold8","Threshold9"}; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t id = dbc->getUInt32(r, idF); + if (!id) continue; + SetEntry e; + e.name = dbc->getString(r, nameF); + for (int i = 0; i < 10; ++i) { + e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : uint32_t(18 + i)); + e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : uint32_t(28 + i)); + e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : uint32_t(38 + i)); + } + s_setData[id] = std::move(e); + } + } + } + ImGui::Spacing(); + const auto& inv = gameHandler.getInventory(); + auto setIt = s_setData.find(info->itemSetId); + if (setIt != s_setData.end()) { + const SetEntry& se = setIt->second; + int equipped = 0, total = 0; + for (int i = 0; i < 10; ++i) { + if (se.itemIds[i] == 0) continue; + ++total; + for (int sl = 0; sl < game::Inventory::NUM_EQUIP_SLOTS; sl++) { + const auto& eq = inv.getEquipSlot(static_cast(sl)); + if (!eq.empty() && eq.item.itemId == se.itemIds[i]) { ++equipped; break; } + } + } + if (total > 0) + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), + "%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total); + else if (!se.name.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "%s", se.name.c_str()); + for (int i = 0; i < 10; ++i) { + if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue; + const std::string& bname = gameHandler.getSpellName(se.spellIds[i]); + bool active = (equipped >= static_cast(se.thresholds[i])); + ImVec4 col = active ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f) : ImVec4(0.55f, 0.55f, 0.55f, 1.0f); + if (!bname.empty()) + ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str()); + else + ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]); + } + } else { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Set (id %u)", info->itemSetId); + } + } + // Item spell effects (Use / Equip / Chance on Hit / Teaches) + for (const auto& sp : info->spells) { + if (sp.spellId == 0) continue; + const char* triggerLabel = nullptr; + switch (sp.spellTrigger) { + case 0: triggerLabel = "Use"; break; + case 1: triggerLabel = "Equip"; break; + case 2: triggerLabel = "Chance on Hit"; break; + case 5: triggerLabel = "Teaches"; break; + } + if (!triggerLabel) continue; + // Use full spell description if available (matches inventory tooltip style) + const std::string& spDesc = gameHandler.getSpellDescription(sp.spellId); + const std::string& spText = !spDesc.empty() ? spDesc + : gameHandler.getSpellName(sp.spellId); + if (!spText.empty()) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f); + ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), + "%s: %s", triggerLabel, spText.c_str()); + ImGui::PopTextWrapPos(); + } + } + // Required level + if (info->requiredLevel > 1) + ImGui::TextDisabled("Requires Level %u", info->requiredLevel); + // Required skill (e.g. "Requires Blacksmithing (300)") + if (info->requiredSkill != 0 && info->requiredSkillRank > 0) { + static std::unordered_map s_skillNames; + static bool s_skillNamesLoaded = false; + if (!s_skillNamesLoaded && assetMgr) { + s_skillNamesLoaded = true; + auto dbc = assetMgr->loadDBC("SkillLine.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0u; + uint32_t nameF = layout ? (*layout)["Name"] : 2u; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t sid = dbc->getUInt32(r, idF); + if (!sid) continue; + std::string sname = dbc->getString(r, nameF); + if (!sname.empty()) s_skillNames[sid] = std::move(sname); + } + } + } + uint32_t playerSkillVal = 0; + const auto& skills = gameHandler.getPlayerSkills(); + auto skPit = skills.find(info->requiredSkill); + if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue(); + bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info->requiredSkillRank); + ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + auto skIt = s_skillNames.find(info->requiredSkill); + if (skIt != s_skillNames.end()) + ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info->requiredSkillRank); + else + ImGui::TextColored(skColor, "Requires Skill %u (%u)", info->requiredSkill, info->requiredSkillRank); + } + // Required reputation (e.g. "Requires Exalted with Argent Dawn") + if (info->requiredReputationFaction != 0 && info->requiredReputationRank > 0) { + static std::unordered_map s_factionNames; + static bool s_factionNamesLoaded = false; + if (!s_factionNamesLoaded && assetMgr) { + s_factionNamesLoaded = true; + auto dbc = assetMgr->loadDBC("Faction.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0u; + uint32_t nameF = layout ? (*layout)["Name"] : 20u; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t fid = dbc->getUInt32(r, idF); + if (!fid) continue; + std::string fname = dbc->getString(r, nameF); + if (!fname.empty()) s_factionNames[fid] = std::move(fname); + } + } + } + static const char* kRepRankNames[] = { + "Hated", "Hostile", "Unfriendly", "Neutral", + "Friendly", "Honored", "Revered", "Exalted" + }; + const char* rankName = (info->requiredReputationRank < 8) + ? kRepRankNames[info->requiredReputationRank] : "Unknown"; + auto fIt = s_factionNames.find(info->requiredReputationFaction); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s", + rankName, + fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction"); + } + // Class restriction (e.g. "Classes: Paladin, Warrior") + if (info->allowableClass != 0) { + static const struct { uint32_t mask; const char* name; } kClasses[] = { + { 1, "Warrior" }, + { 2, "Paladin" }, + { 4, "Hunter" }, + { 8, "Rogue" }, + { 16, "Priest" }, + { 32, "Death Knight" }, + { 64, "Shaman" }, + { 128, "Mage" }, + { 256, "Warlock" }, + { 1024, "Druid" }, + }; + int matchCount = 0; + for (const auto& kc : kClasses) + if (info->allowableClass & kc.mask) ++matchCount; + if (matchCount > 0 && matchCount < 10) { + char classBuf[128] = "Classes: "; + bool first = true; + for (const auto& kc : kClasses) { + if (!(info->allowableClass & kc.mask)) continue; + if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1); + strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1); + first = false; + } + uint8_t pc = gameHandler.getPlayerClass(); + uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0u; + bool playerAllowed = (pmask == 0 || (info->allowableClass & pmask)); + ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + ImGui::TextColored(clColor, "%s", classBuf); + } + } + // Race restriction (e.g. "Races: Night Elf, Human") + if (info->allowableRace != 0) { + static const struct { uint32_t mask; const char* name; } kRaces[] = { + { 1, "Human" }, + { 2, "Orc" }, + { 4, "Dwarf" }, + { 8, "Night Elf" }, + { 16, "Undead" }, + { 32, "Tauren" }, + { 64, "Gnome" }, + { 128, "Troll" }, + { 512, "Blood Elf" }, + { 1024, "Draenei" }, + }; + constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024; + if ((info->allowableRace & kAllPlayable) != kAllPlayable) { + int matchCount = 0; + for (const auto& kr : kRaces) + if (info->allowableRace & kr.mask) ++matchCount; + if (matchCount > 0) { + char raceBuf[160] = "Races: "; + bool first = true; + for (const auto& kr : kRaces) { + if (!(info->allowableRace & kr.mask)) continue; + if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1); + strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1); + first = false; + } + uint8_t pr = gameHandler.getPlayerRace(); + uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0u; + bool playerAllowed = (pmask == 0 || (info->allowableRace & pmask)); + ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + ImGui::TextColored(rColor, "%s", raceBuf); + } + } + } + // Flavor / lore text (shown in gold italic in WoW, use a yellow-ish dim color here) + if (!info->description.empty()) { + ImGui::Spacing(); + ImGui::PushTextWrapPos(300.0f); + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 0.85f), "\"%s\"", info->description.c_str()); + ImGui::PopTextWrapPos(); + } if (info->sellPrice > 0) { - uint32_t g = info->sellPrice / 10000; - uint32_t s = (info->sellPrice / 100) % 100; - uint32_t c = info->sellPrice % 100; - ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); + ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(info->sellPrice); } if (ImGui::GetIO().KeyShift && info->inventoryType > 0) { @@ -1161,7 +1827,15 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { eq->item.damageMax > 0.0f && eq->item.delayMs > 0) { float speed = static_cast(eq->item.delayMs) / 1000.0f; float dps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / speed; - ImGui::Text("%.1f DPS", dps); + char eqDmg[64], eqSpd[32]; + std::snprintf(eqDmg, sizeof(eqDmg), "%d - %d Damage", + static_cast(eq->item.damageMin), static_cast(eq->item.damageMax)); + std::snprintf(eqSpd, sizeof(eqSpd), "Speed %.2f", speed); + float eqSpdW = ImGui::CalcTextSize(eqSpd).x; + ImGui::Text("%s", eqDmg); + ImGui::SameLine(ImGui::GetWindowWidth() - eqSpdW - 16.0f); + ImGui::Text("%s", eqSpd); + ImGui::TextDisabled("(%.1f damage per second)", dps); } if (eq->item.armor > 0) { ImGui::Text("%d Armor", eq->item.armor); @@ -1175,6 +1849,28 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { if (!eqBonusLine.empty()) { ImGui::TextColored(green, "%s", eqBonusLine.c_str()); } + // Extra stats for the equipped item + for (const auto& es : eq->item.extraStats) { + const char* nm = nullptr; + switch (es.statType) { + case 12: nm = "Defense Rating"; break; + case 13: nm = "Dodge Rating"; break; + case 14: nm = "Parry Rating"; break; + case 16: case 17: case 18: case 31: nm = "Hit Rating"; break; + case 19: case 20: case 21: case 32: nm = "Critical Strike Rating"; break; + case 28: case 29: case 30: case 35: nm = "Haste Rating"; break; + case 34: nm = "Resilience Rating"; break; + case 36: nm = "Expertise Rating"; break; + case 37: nm = "Attack Power"; break; + case 38: nm = "Ranged Attack Power"; break; + case 45: nm = "Spell Power"; break; + case 46: nm = "Healing Power"; break; + case 49: nm = "Mana per 5 sec."; break; + default: break; + } + if (nm && es.statValue > 0) + ImGui::TextColored(green, "+%d %s", es.statValue, nm); + } } } ImGui::EndTooltip(); @@ -1187,10 +1883,13 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { // Find next special element: URL or WoW link size_t urlStart = text.find("https://", pos); - // Find next WoW item link: |cXXXXXXXX|Hitem:ENTRY:...|h[Name]|h|r + // Find next WoW link (may be colored with |c prefix or bare |H) size_t linkStart = text.find("|c", pos); - // Also handle bare |Hitem: without color prefix - size_t bareLinkStart = text.find("|Hitem:", pos); + // Also handle bare |H links without color prefix + size_t bareItem = text.find("|Hitem:", pos); + size_t bareSpell = text.find("|Hspell:", pos); + size_t bareQuest = text.find("|Hquest:", pos); + size_t bareLinkStart = std::min({bareItem, bareSpell, bareQuest}); // Determine which comes first size_t nextSpecial = std::min({urlStart, linkStart, bareLinkStart}); @@ -1223,18 +1922,30 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { if (nextSpecial == linkStart && text.size() > linkStart + 10) { // Parse |cAARRGGBB color linkColor = parseWowColor(text, linkStart); - hStart = text.find("|Hitem:", linkStart + 10); + // Find the nearest |H link of any supported type + size_t hItem = text.find("|Hitem:", linkStart + 10); + size_t hSpell = text.find("|Hspell:", linkStart + 10); + size_t hQuest = text.find("|Hquest:", linkStart + 10); + size_t hAch = text.find("|Hachievement:", linkStart + 10); + hStart = std::min({hItem, hSpell, hQuest, hAch}); } else if (nextSpecial == bareLinkStart) { hStart = bareLinkStart; } if (hStart != std::string::npos) { - // Parse item entry: |Hitem:ENTRY:... - size_t entryStart = hStart + 7; // skip "|Hitem:" + // Determine link type + const bool isSpellLink = (text.compare(hStart, 8, "|Hspell:") == 0); + const bool isQuestLink = (text.compare(hStart, 8, "|Hquest:") == 0); + const bool isAchievLink = (text.compare(hStart, 14, "|Hachievement:") == 0); + // Default: item link + + // Parse the first numeric ID after |Htype: + size_t idOffset = isSpellLink ? 8 : (isQuestLink ? 8 : (isAchievLink ? 14 : 7)); + size_t entryStart = hStart + idOffset; size_t entryEnd = text.find(':', entryStart); - uint32_t itemEntry = 0; + uint32_t linkId = 0; if (entryEnd != std::string::npos) { - itemEntry = static_cast(strtoul( + linkId = static_cast(strtoul( text.substr(entryStart, entryEnd - entryStart).c_str(), nullptr, 10)); } @@ -1243,53 +1954,122 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { size_t nameTagEnd = (nameTagStart != std::string::npos) ? text.find("]|h", nameTagStart + 3) : std::string::npos; - std::string itemName = "Unknown Item"; + std::string linkName = isSpellLink ? "Unknown Spell" + : isQuestLink ? "Unknown Quest" + : isAchievLink ? "Unknown Achievement" + : "Unknown Item"; if (nameTagStart != std::string::npos && nameTagEnd != std::string::npos) { - itemName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3); + linkName = text.substr(nameTagStart + 3, nameTagEnd - nameTagStart - 3); } // Find end of entire link sequence (|r or after ]|h) - size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + 7; + size_t linkEnd = (nameTagEnd != std::string::npos) ? nameTagEnd + 3 : hStart + idOffset; size_t resetPos = text.find("|r", linkEnd); if (resetPos != std::string::npos && resetPos <= linkEnd + 2) { linkEnd = resetPos + 2; } - // Ensure item info is cached (trigger query if needed) - if (itemEntry > 0) { - gameHandler.ensureItemInfo(itemEntry); - } + if (!isSpellLink && !isQuestLink && !isAchievLink) { + // --- Item link --- + uint32_t itemEntry = linkId; + if (itemEntry > 0) { + gameHandler.ensureItemInfo(itemEntry); + } - // Show small icon before item link if available - if (itemEntry > 0) { - const auto* chatInfo = gameHandler.getItemInfo(itemEntry); - if (chatInfo && chatInfo->valid && chatInfo->displayInfoId != 0) { - VkDescriptorSet chatIcon = inventoryScreen.getItemIcon(chatInfo->displayInfoId); - if (chatIcon) { - ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12)); - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - renderItemLinkTooltip(itemEntry); + // Show small icon before item link if available + if (itemEntry > 0) { + const auto* chatInfo = gameHandler.getItemInfo(itemEntry); + if (chatInfo && chatInfo->valid && chatInfo->displayInfoId != 0) { + VkDescriptorSet chatIcon = inventoryScreen.getItemIcon(chatInfo->displayInfoId); + if (chatIcon) { + ImGui::Image((ImTextureID)(uintptr_t)chatIcon, ImVec2(12, 12)); + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + renderItemLinkTooltip(itemEntry); + } + ImGui::SameLine(0, 2); } - ImGui::SameLine(0, 2); } } - } - // Render bracketed item name in quality color - std::string display = "[" + itemName + "]"; - ImGui::PushStyleColor(ImGuiCol_Text, linkColor); - ImGui::TextWrapped("%s", display.c_str()); - ImGui::PopStyleColor(); + // Render bracketed item name in quality color + std::string display = "[" + linkName + "]"; + ImGui::PushStyleColor(ImGuiCol_Text, linkColor); + ImGui::TextWrapped("%s", display.c_str()); + ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (itemEntry > 0) { - renderItemLinkTooltip(itemEntry); + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (itemEntry > 0) { + renderItemLinkTooltip(itemEntry); + } + } + } else if (isSpellLink) { + // --- Spell link: |Hspell:SPELLID:RANK|h[Name]|h --- + // Small icon (use spell icon cache if available) + VkDescriptorSet spellIcon = (linkId > 0) ? getSpellIcon(linkId, assetMgr) : VK_NULL_HANDLE; + if (spellIcon) { + ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(12, 12)); + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + spellbookScreen.renderSpellInfoTooltip(linkId, gameHandler, assetMgr); + } + ImGui::SameLine(0, 2); + } + + std::string display = "[" + linkName + "]"; + ImGui::PushStyleColor(ImGuiCol_Text, linkColor); + ImGui::TextWrapped("%s", display.c_str()); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (linkId > 0) { + spellbookScreen.renderSpellInfoTooltip(linkId, gameHandler, assetMgr); + } + } + } else if (isQuestLink) { + // --- Quest link: |Hquest:QUESTID:QUESTLEVEL|h[Name]|h --- + std::string display = "[" + linkName + "]"; + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.84f, 0.0f, 1.0f)); // gold + ImGui::TextWrapped("%s", display.c_str()); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::BeginTooltip(); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%s", linkName.c_str()); + // Parse quest level (second field after questId) + if (entryEnd != std::string::npos) { + size_t lvlEnd = text.find(':', entryEnd + 1); + if (lvlEnd == std::string::npos) lvlEnd = text.find('|', entryEnd + 1); + if (lvlEnd != std::string::npos) { + uint32_t qLvl = static_cast(strtoul( + text.substr(entryEnd + 1, lvlEnd - entryEnd - 1).c_str(), nullptr, 10)); + if (qLvl > 0) ImGui::TextDisabled("Level %u Quest", qLvl); + } + } + ImGui::TextDisabled("Click quest log to view details"); + ImGui::EndTooltip(); + } + // Click: open quest log and select this quest if we have it + if (ImGui::IsItemClicked() && linkId > 0) { + questLogScreen.openAndSelectQuest(linkId); + } + } else { + // --- Achievement link --- + std::string display = "[" + linkName + "]"; + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); // gold + ImGui::TextWrapped("%s", display.c_str()); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("Achievement: %s", linkName.c_str()); } } - // Shift-click: insert item link into chat input + // Shift-click: insert entire link back into chat input if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) { std::string linkText = text.substr(nextSpecial, linkEnd - nextSpecial); size_t curLen = strlen(chatInputBuffer); @@ -1375,6 +2155,65 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { } }; + // Determine local player name for mention detection (case-insensitive) + std::string selfNameLower; + { + const auto* ch = gameHandler.getActiveCharacter(); + if (ch && !ch->name.empty()) { + selfNameLower = ch->name; + for (auto& c : selfNameLower) c = static_cast(std::tolower(static_cast(c))); + } + } + + // Scan NEW messages (beyond chatMentionSeenCount_) for mentions and play notification sound + if (!selfNameLower.empty() && chatHistory.size() > chatMentionSeenCount_) { + for (size_t mi = chatMentionSeenCount_; mi < chatHistory.size(); ++mi) { + const auto& mMsg = chatHistory[mi]; + // Skip outgoing whispers, system, and monster messages + if (mMsg.type == game::ChatType::WHISPER_INFORM || + mMsg.type == game::ChatType::SYSTEM) continue; + // Case-insensitive search in message body + std::string bodyLower = mMsg.message; + for (auto& c : bodyLower) c = static_cast(std::tolower(static_cast(c))); + if (bodyLower.find(selfNameLower) != std::string::npos) { + if (auto* renderer = core::Application::getInstance().getRenderer()) { + if (auto* ui = renderer->getUiSoundManager()) + ui->playWhisperReceived(); + } + break; // play at most once per scan pass + } + } + chatMentionSeenCount_ = chatHistory.size(); + } else if (chatHistory.size() <= chatMentionSeenCount_) { + chatMentionSeenCount_ = chatHistory.size(); // reset if history was cleared + } + + // Scan NEW messages for incoming whispers and push a toast notification + { + size_t histSize = chatHistory.size(); + if (histSize < whisperSeenCount_) whisperSeenCount_ = histSize; // cleared + for (size_t wi = whisperSeenCount_; wi < histSize; ++wi) { + const auto& wMsg = chatHistory[wi]; + if (wMsg.type == game::ChatType::WHISPER || + wMsg.type == game::ChatType::RAID_BOSS_WHISPER) { + WhisperToastEntry toast; + toast.sender = wMsg.senderName; + if (toast.sender.empty() && wMsg.senderGuid != 0) + toast.sender = gameHandler.lookupName(wMsg.senderGuid); + if (toast.sender.empty()) toast.sender = "Unknown"; + // Truncate preview to 60 chars + toast.preview = wMsg.message.size() > 60 + ? wMsg.message.substr(0, 57) + "..." + : wMsg.message; + toast.age = 0.0f; + // Keep at most 3 stacked toasts + if (whisperToasts_.size() >= 3) whisperToasts_.erase(whisperToasts_.begin()); + whisperToasts_.push_back(std::move(toast)); + } + } + whisperSeenCount_ = histSize; + } + int chatMsgIdx = 0; for (const auto& msg : chatHistory) { if (!shouldShowMessage(msg, activeChatTab_)) continue; @@ -1458,10 +2297,30 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { } } + // Detect mention: does this message contain the local player's name? + bool isMention = false; + if (!selfNameLower.empty() && + msg.type != game::ChatType::WHISPER_INFORM && + msg.type != game::ChatType::SYSTEM) { + std::string msgLower = fullMsg; + for (auto& c : msgLower) c = static_cast(std::tolower(static_cast(c))); + isMention = (msgLower.find(selfNameLower) != std::string::npos); + } + // Render message in a group so we can attach a right-click context menu ImGui::PushID(chatMsgIdx++); + if (isMention) { + // Golden highlight strip behind the text + ImVec2 groupMin = ImGui::GetCursorScreenPos(); + float availW = ImGui::GetContentRegionAvail().x; + float lineH = ImGui::GetTextLineHeightWithSpacing(); + ImGui::GetWindowDrawList()->AddRectFilled( + groupMin, + ImVec2(groupMin.x + availW, groupMin.y + lineH), + IM_COL32(255, 200, 50, 45)); // soft golden tint + } ImGui::BeginGroup(); - renderTextWithLinks(fullMsg, color); + renderTextWithLinks(fullMsg, isMention ? ImVec4(1.0f, 0.9f, 0.35f, 1.0f) : color); ImGui::EndGroup(); // Right-click context menu (only for player messages with a sender) @@ -1542,8 +2401,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::Text("Type:"); ImGui::SameLine(); ImGui::SetNextItemWidth(100); - const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE" }; - ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 10); + const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD", "WHISPER", "RAID", "OFFICER", "BATTLEGROUND", "RAID WARNING", "INSTANCE", "CHANNEL" }; + ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 11); // Auto-fill whisper target when switching to WHISPER mode if (selectedChatType == 4 && lastChatType != 4) { @@ -1570,6 +2429,27 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::InputText("##WhisperTarget", whisperTargetBuffer, sizeof(whisperTargetBuffer)); } + // Show channel picker if CHANNEL is selected + if (selectedChatType == 10) { + const auto& channels = gameHandler.getJoinedChannels(); + if (channels.empty()) { + ImGui::SameLine(); + ImGui::TextDisabled("(no channels joined)"); + } else { + ImGui::SameLine(); + if (selectedChannelIdx >= static_cast(channels.size())) selectedChannelIdx = 0; + ImGui::SetNextItemWidth(140); + if (ImGui::BeginCombo("##ChannelPicker", channels[selectedChannelIdx].c_str())) { + for (int ci = 0; ci < static_cast(channels.size()); ++ci) { + bool selected = (ci == selectedChannelIdx); + if (ImGui::Selectable(channels[ci].c_str(), selected)) selectedChannelIdx = ci; + if (selected) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + } + ImGui::SameLine(); ImGui::Text("Message:"); ImGui::SameLine(); @@ -1590,22 +2470,41 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { std::string cmd = buf.substr(1, sp - 1); for (char& c : cmd) c = std::tolower(c); int detected = -1; + bool isReply = false; if (cmd == "s" || cmd == "say") detected = 0; else if (cmd == "y" || cmd == "yell" || cmd == "shout") detected = 1; else if (cmd == "p" || cmd == "party") detected = 2; else if (cmd == "g" || cmd == "guild") detected = 3; else if (cmd == "w" || cmd == "whisper" || cmd == "tell" || cmd == "t") detected = 4; + else if (cmd == "r" || cmd == "reply") { detected = 4; isReply = true; } else if (cmd == "raid" || cmd == "rsay" || cmd == "ra") detected = 5; else if (cmd == "o" || cmd == "officer" || cmd == "osay") detected = 6; else if (cmd == "bg" || cmd == "battleground") detected = 7; else if (cmd == "rw" || cmd == "raidwarning") detected = 8; else if (cmd == "i" || cmd == "instance") detected = 9; - if (detected >= 0 && selectedChatType != detected) { + else if (cmd.size() == 1 && cmd[0] >= '1' && cmd[0] <= '9') detected = 10; // /1, /2 etc. + if (detected >= 0 && (selectedChatType != detected || detected == 10 || isReply)) { + // For channel shortcuts, also update selectedChannelIdx + if (detected == 10) { + int chanIdx = cmd[0] - '1'; // /1 -> index 0, /2 -> index 1, etc. + const auto& chans = gameHandler.getJoinedChannels(); + if (chanIdx >= 0 && chanIdx < static_cast(chans.size())) { + selectedChannelIdx = chanIdx; + } + } selectedChatType = detected; // Strip the prefix, keep only the message part std::string remaining = buf.substr(sp + 1); - // For whisper, first word after /w is the target - if (detected == 4) { + // /r reply: pre-fill whisper target from last whisper sender + if (detected == 4 && isReply) { + std::string lastSender = gameHandler.getLastWhisperSender(); + if (!lastSender.empty()) { + strncpy(whisperTargetBuffer, lastSender.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + } + // remaining is the message — don't extract a target from it + } else if (detected == 4) { + // For whisper, first word after /w is the target size_t msgStart = remaining.find(' '); if (msgStart != std::string::npos) { std::string wTarget = remaining.substr(0, msgStart); @@ -1630,15 +2529,16 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { // Color the input text based on current chat type ImVec4 inputColor; switch (selectedChatType) { - case 1: inputColor = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); break; // YELL - red + case 1: inputColor = kColorRed; break; // YELL - red case 2: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // PARTY - blue - case 3: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // GUILD - green + case 3: inputColor = kColorBrightGreen; break; // GUILD - green case 4: inputColor = ImVec4(1.0f, 0.5f, 1.0f, 1.0f); break; // WHISPER - pink case 5: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // RAID - orange - case 6: inputColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); break; // OFFICER - green + case 6: inputColor = kColorBrightGreen; break; // OFFICER - green case 7: inputColor = ImVec4(1.0f, 0.5f, 0.0f, 1.0f); break; // BG - orange case 8: inputColor = ImVec4(1.0f, 0.3f, 0.0f, 1.0f); break; // RAID WARNING - red-orange - case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue + case 9: inputColor = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; // INSTANCE - blue + case 10: inputColor = ImVec4(0.3f, 0.9f, 0.9f, 1.0f); break; // CHANNEL - cyan default: inputColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); break; // SAY - white } ImGui::PushStyleColor(ImGuiCol_Text, inputColor); @@ -1656,8 +2556,189 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { self->chatInputMoveCursorToEnd = false; } + // Tab: slash-command autocomplete + if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) { + if (data->BufTextLen > 0 && data->Buf[0] == '/') { + // Split buffer into command word and trailing args + std::string fullBuf(data->Buf, data->BufTextLen); + size_t spacePos = fullBuf.find(' '); + std::string word = (spacePos != std::string::npos) ? fullBuf.substr(0, spacePos) : fullBuf; + std::string rest = (spacePos != std::string::npos) ? fullBuf.substr(spacePos) : ""; + + // Normalize to lowercase for matching + std::string lowerWord = word; + for (auto& ch : lowerWord) ch = static_cast(std::tolower(static_cast(ch))); + + static const std::vector kCmds = { + "/afk", "/assist", "/away", + "/cancelaura", "/cancelform", "/cancellogout", "/cancelshapeshift", + "/cast", "/castsequence", "/chathelp", "/clear", "/clearfocus", + "/clearmainassist", "/clearmaintank", "/cleartarget", "/cloak", + "/combatlog", "/dance", "/dismount", "/dnd", "/do", "/duel", "/dump", + "/e", "/emote", "/equip", "/equipset", + "/focus", "/follow", "/forfeit", "/friend", + "/g", "/gdemote", "/ginvite", "/gkick", "/gleader", "/gmotd", + "/gmticket", "/gpromote", "/gquit", "/grouploot", "/groster", + "/guild", "/guildinfo", + "/helm", "/help", + "/i", "/ignore", "/inspect", "/instance", "/invite", + "/j", "/join", "/kick", "/kneel", + "/l", "/leave", "/leaveparty", "/loc", "/local", "/logout", + "/macrohelp", "/mainassist", "/maintank", "/mark", "/me", + "/notready", + "/p", "/party", "/petaggressive", "/petattack", "/petdefensive", + "/petdismiss", "/petfollow", "/pethalt", "/petpassive", "/petstay", + "/played", "/pvp", + "/r", "/raid", "/raidinfo", "/raidwarning", "/random", "/ready", + "/readycheck", "/reload", "/reloadui", "/removefriend", + "/reply", "/rl", "/roll", "/run", + "/s", "/say", "/score", "/screenshot", "/script", "/setloot", + "/shout", "/sit", "/stand", + "/startattack", "/stopattack", "/stopcasting", "/stopfollow", "/stopmacro", + "/t", "/target", "/targetenemy", "/targetfriend", "/targetlast", + "/threat", "/ticket", "/time", "/trade", + "/unignore", "/uninvite", "/unstuck", "/use", + "/w", "/whisper", "/who", "/wts", "/wtb", + "/y", "/yell", "/zone" + }; + + // New session if prefix changed + if (self->chatTabMatchIdx_ < 0 || self->chatTabPrefix_ != lowerWord) { + self->chatTabPrefix_ = lowerWord; + self->chatTabMatches_.clear(); + for (const auto& cmd : kCmds) { + if (cmd.size() >= lowerWord.size() && + cmd.compare(0, lowerWord.size(), lowerWord) == 0) + self->chatTabMatches_.push_back(cmd); + } + self->chatTabMatchIdx_ = 0; + } else { + // Cycle forward through matches + ++self->chatTabMatchIdx_; + if (self->chatTabMatchIdx_ >= static_cast(self->chatTabMatches_.size())) + self->chatTabMatchIdx_ = 0; + } + + if (!self->chatTabMatches_.empty()) { + std::string match = self->chatTabMatches_[self->chatTabMatchIdx_]; + // Append trailing space when match is unambiguous + if (self->chatTabMatches_.size() == 1 && rest.empty()) + match += ' '; + std::string newBuf = match + rest; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, newBuf.c_str()); + } + } else if (data->BufTextLen > 0) { + // Player name tab-completion for commands like /w, /whisper, /invite, /trade, /duel + // Also works for plain text (completes nearby player names) + std::string fullBuf(data->Buf, data->BufTextLen); + size_t spacePos = fullBuf.find(' '); + bool isNameCommand = false; + std::string namePrefix; + size_t replaceStart = 0; + + if (fullBuf[0] == '/' && spacePos != std::string::npos) { + std::string cmd = fullBuf.substr(0, spacePos); + for (char& c : cmd) c = static_cast(std::tolower(static_cast(c))); + // Commands that take a player name as the first argument after the command + if (cmd == "/w" || cmd == "/whisper" || cmd == "/invite" || + cmd == "/trade" || cmd == "/duel" || cmd == "/follow" || + cmd == "/inspect" || cmd == "/friend" || cmd == "/removefriend" || + cmd == "/ignore" || cmd == "/unignore" || cmd == "/who" || + cmd == "/t" || cmd == "/target" || cmd == "/kick" || + cmd == "/uninvite" || cmd == "/ginvite" || cmd == "/gkick") { + // Extract the partial name after the space + namePrefix = fullBuf.substr(spacePos + 1); + // Only complete the first word after the command + size_t nameSpace = namePrefix.find(' '); + if (nameSpace == std::string::npos) { + isNameCommand = true; + replaceStart = spacePos + 1; + } + } + } + + if (isNameCommand && !namePrefix.empty()) { + std::string lowerPrefix = namePrefix; + for (char& c : lowerPrefix) c = static_cast(std::tolower(static_cast(c))); + + if (self->chatTabMatchIdx_ < 0 || self->chatTabPrefix_ != lowerPrefix) { + self->chatTabPrefix_ = lowerPrefix; + self->chatTabMatches_.clear(); + // Search player name cache and nearby entities + auto* gh = self->cachedGameHandler_; + // Party/raid members + for (const auto& m : gh->getPartyData().members) { + if (m.name.empty()) continue; + std::string lname = m.name; + for (char& c : lname) c = static_cast(std::tolower(static_cast(c))); + if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) + self->chatTabMatches_.push_back(m.name); + } + // Friends + for (const auto& c : gh->getContacts()) { + if (!c.isFriend() || c.name.empty()) continue; + std::string lname = c.name; + for (char& cc : lname) cc = static_cast(std::tolower(static_cast(cc))); + if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) { + // Avoid duplicates from party + bool dup = false; + for (const auto& em : self->chatTabMatches_) + if (em == c.name) { dup = true; break; } + if (!dup) self->chatTabMatches_.push_back(c.name); + } + } + // Nearby visible players + for (const auto& [guid, entity] : gh->getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::PLAYER) continue; + auto player = std::static_pointer_cast(entity); + if (player->getName().empty()) continue; + std::string lname = player->getName(); + for (char& cc : lname) cc = static_cast(std::tolower(static_cast(cc))); + if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) { + bool dup = false; + for (const auto& em : self->chatTabMatches_) + if (em == player->getName()) { dup = true; break; } + if (!dup) self->chatTabMatches_.push_back(player->getName()); + } + } + // Last whisper sender + if (!gh->getLastWhisperSender().empty()) { + std::string lname = gh->getLastWhisperSender(); + for (char& cc : lname) cc = static_cast(std::tolower(static_cast(cc))); + if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) { + bool dup = false; + for (const auto& em : self->chatTabMatches_) + if (em == gh->getLastWhisperSender()) { dup = true; break; } + if (!dup) self->chatTabMatches_.insert(self->chatTabMatches_.begin(), gh->getLastWhisperSender()); + } + } + self->chatTabMatchIdx_ = 0; + } else { + ++self->chatTabMatchIdx_; + if (self->chatTabMatchIdx_ >= static_cast(self->chatTabMatches_.size())) + self->chatTabMatchIdx_ = 0; + } + + if (!self->chatTabMatches_.empty()) { + std::string match = self->chatTabMatches_[self->chatTabMatchIdx_]; + std::string prefix = fullBuf.substr(0, replaceStart); + std::string newBuf = prefix + match; + if (self->chatTabMatches_.size() == 1) newBuf += ' '; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, newBuf.c_str()); + } + } + } + return 0; + } + // Up/Down arrow: cycle through sent message history if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { + // Any history navigation resets autocomplete + self->chatTabMatchIdx_ = -1; + self->chatTabMatches_.clear(); + const int histSize = static_cast(self->chatSentHistory_.size()); if (histSize == 0) return 0; @@ -1688,7 +2769,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways | - ImGuiInputTextFlags_CallbackHistory; + ImGuiInputTextFlags_CallbackHistory | + ImGuiInputTextFlags_CallbackCompletion; if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), inputFlags, inputCallback, this)) { sendChatMessage(gameHandler); // Close chat input on send so movement keys work immediately. @@ -1717,16 +2799,31 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { auto& io = ImGui::GetIO(); auto& input = core::Input::getInstance(); + // If the user is typing (or about to focus chat this frame), do not allow + // A-Z or 1-0 shortcuts to fire. + if (!io.WantTextInput && !chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) { + refocusChatInput = true; + chatInputBuffer[0] = '/'; + chatInputBuffer[1] = '\0'; + chatInputMoveCursorToEnd = true; + } + if (!io.WantTextInput && !chatInputActive && + KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) { + refocusChatInput = true; + } + + const bool textFocus = chatInputActive || refocusChatInput || io.WantTextInput; + // Tab targeting (when keyboard not captured by UI) if (!io.WantCaptureKeyboard) { - if (input.isKeyJustPressed(SDL_SCANCODE_TAB)) { + // When typing in chat (or any text input), never treat keys as gameplay/UI shortcuts. + if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) { const auto& movement = gameHandler.getMovementInfo(); gameHandler.tabTarget(movement.x, movement.y, movement.z); } if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) { if (showSettingsWindow) { - // Close settings window if open showSettingsWindow = false; } else if (showEscapeMenu) { showEscapeMenu = false; @@ -1737,76 +2834,159 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { gameHandler.closeLoot(); } else if (gameHandler.isGossipWindowOpen()) { gameHandler.closeGossip(); + } else if (gameHandler.isVendorWindowOpen()) { + gameHandler.closeVendor(); + } else if (gameHandler.isBarberShopOpen()) { + gameHandler.closeBarberShop(); + } else if (gameHandler.isBankOpen()) { + gameHandler.closeBank(); + } else if (gameHandler.isTrainerWindowOpen()) { + gameHandler.closeTrainer(); + } else if (gameHandler.isMailboxOpen()) { + gameHandler.closeMailbox(); + } else if (gameHandler.isAuctionHouseOpen()) { + gameHandler.closeAuctionHouse(); + } else if (gameHandler.isQuestDetailsOpen()) { + gameHandler.declineQuest(); + } else if (gameHandler.isQuestOfferRewardOpen()) { + gameHandler.closeQuestOfferReward(); + } else if (gameHandler.isQuestRequestItemsOpen()) { + gameHandler.closeQuestRequestItems(); + } else if (gameHandler.isTradeOpen()) { + gameHandler.cancelTrade(); + } else if (showWhoWindow_) { + showWhoWindow_ = false; + } else if (showCombatLog_) { + showCombatLog_ = false; + } else if (showSocialFrame_) { + showSocialFrame_ = false; + } else if (talentScreen.isOpen()) { + talentScreen.setOpen(false); + } else if (spellbookScreen.isOpen()) { + spellbookScreen.setOpen(false); + } else if (questLogScreen.isOpen()) { + questLogScreen.setOpen(false); + } else if (inventoryScreen.isCharacterOpen()) { + inventoryScreen.toggleCharacter(); + } else if (inventoryScreen.isOpen()) { + inventoryScreen.setOpen(false); + } else if (showWorldMap_) { + showWorldMap_ = false; } else { showEscapeMenu = true; } } - // Toggle nameplates (customizable keybinding, default V) - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) { - inventoryScreen.toggle(); - } + if (!textFocus) { + // Toggle character screen (C) and inventory/bags (I) + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) { + const bool wasOpen = inventoryScreen.isCharacterOpen(); + inventoryScreen.toggleCharacter(); + if (!wasOpen && gameHandler.isConnected()) { + gameHandler.requestPlayedTime(); + } + } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) { - showNameplates_ = !showNameplates_; - } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) { + inventoryScreen.toggle(); + } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) { - showWorldMap_ = !showWorldMap_; - } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) { + if (ImGui::GetIO().KeyShift) + showFriendlyNameplates_ = !showFriendlyNameplates_; + else + showNameplates_ = !showNameplates_; + } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) { - showMinimap_ = !showMinimap_; - } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) { + showWorldMap_ = !showWorldMap_; + } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) { - showRaidFrames_ = !showRaidFrames_; - } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) { + showMinimap_ = !showMinimap_; + } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_QUEST_LOG)) { - questLogScreen.toggle(); - } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) { + showRaidFrames_ = !showRaidFrames_; + } - if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) { - showAchievementWindow_ = !showAchievementWindow_; - } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) { + showAchievementWindow_ = !showAchievementWindow_; + } + if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SKILLS)) { + showSkillsWindow_ = !showSkillsWindow_; + } - // Action bar keys (1-9, 0, -, =) - static const SDL_Scancode actionBarKeys[] = { - SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, - SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8, - SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS - }; - const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT); - const auto& bar = gameHandler.getActionBar(); - for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) { - if (input.isKeyJustPressed(actionBarKeys[i])) { - int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i; - if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) { - uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; - gameHandler.castSpell(bar[slotIdx].id, target); - } else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) { - gameHandler.useItemById(bar[slotIdx].id); + // Toggle Titles window with H (hero/title screen — no conflicting keybinding) + if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) { + showTitlesWindow_ = !showTitlesWindow_; + } + + // Screenshot (PrintScreen key) + if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) { + takeScreenshot(gameHandler); + } + + // Action bar keys (1-9, 0, -, =) + static const SDL_Scancode actionBarKeys[] = { + SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, + SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8, + SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS + }; + const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT); + const bool ctrlDown = input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL); + const auto& bar = gameHandler.getActionBar(); + + // Ctrl+1..Ctrl+8 → switch stance/form/presence (WoW default bindings). + // Only fires for classes that use a stance bar; same slot ordering as + // renderStanceBar: Warrior, DK, Druid, Rogue, Priest. + if (ctrlDown) { + static const uint32_t warriorStances[] = { 2457, 71, 2458 }; + static const uint32_t dkPresences[] = { 48266, 48263, 48265 }; + static const uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 }; + static const uint32_t rogueForms[] = { 1784 }; + static const uint32_t priestForms[] = { 15473 }; + const uint32_t* stArr = nullptr; int stCnt = 0; + switch (gameHandler.getPlayerClass()) { + case 1: stArr = warriorStances; stCnt = 3; break; + case 6: stArr = dkPresences; stCnt = 3; break; + case 11: stArr = druidForms; stCnt = 9; break; + case 4: stArr = rogueForms; stCnt = 1; break; + case 5: stArr = priestForms; stCnt = 1; break; + } + if (stArr) { + const auto& known = gameHandler.getKnownSpells(); + // Build available list (same order as UI) + std::vector avail; + avail.reserve(stCnt); + for (int i = 0; i < stCnt; ++i) + if (known.count(stArr[i])) avail.push_back(stArr[i]); + // Ctrl+1 = first stance, Ctrl+2 = second, … + for (int i = 0; i < static_cast(avail.size()) && i < 8; ++i) { + if (input.isKeyJustPressed(actionBarKeys[i])) + gameHandler.castSpell(avail[i]); + } + } + } + + for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) { + if (!ctrlDown && input.isKeyJustPressed(actionBarKeys[i])) { + int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i; + if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) { + uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; + gameHandler.castSpell(bar[slotIdx].id, target); + } else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) { + gameHandler.useItemById(bar[slotIdx].id); + } else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) { + executeMacroText(gameHandler, gameHandler.getMacroText(bar[slotIdx].id)); + } } } } } - // Slash key: focus chat input — always works unless already typing in chat - if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) { - refocusChatInput = true; - chatInputBuffer[0] = '/'; - chatInputBuffer[1] = '\0'; - chatInputMoveCursorToEnd = true; - } - - // Enter key: focus chat input (empty) — always works unless already typing - if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) { - refocusChatInput = true; - } - - // Cursor affordance: show hand cursor over interactable game objects. + // Cursor affordance: show hand cursor over interactable entities. if (!io.WantCaptureMouse) { auto* renderer = core::Application::getInstance().getRenderer(); auto* camera = renderer ? renderer->getCamera() : nullptr; @@ -1817,17 +2997,21 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { float screenH = static_cast(window->getHeight()); rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH); float closestT = 1e30f; - bool hoverInteractableGo = false; + bool hoverInteractable = false; for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { - if (entity->getType() != game::ObjectType::GAMEOBJECT) continue; + bool isGo = (entity->getType() == game::ObjectType::GAMEOBJECT); + bool isUnit = (entity->getType() == game::ObjectType::UNIT); + bool isPlayer = (entity->getType() == game::ObjectType::PLAYER); + if (!isGo && !isUnit && !isPlayer) continue; + if (guid == gameHandler.getPlayerGuid()) continue; // skip self glm::vec3 hitCenter; float hitRadius = 0.0f; bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius); if (!hasBounds) { - hitRadius = 2.5f; + hitRadius = isGo ? 2.5f : 1.8f; hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); - hitCenter.z += 1.2f; + hitCenter.z += isGo ? 1.2f : 1.0f; } else { hitRadius = std::max(hitRadius * 1.1f, 0.8f); } @@ -1835,10 +3019,10 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { float hitT; if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) { closestT = hitT; - hoverInteractableGo = true; + hoverInteractable = true; } } - if (hoverInteractableGo) { + if (hoverInteractable) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } } @@ -1958,6 +3142,25 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { auto* camera = renderer ? renderer->getCamera() : nullptr; auto* window = core::Application::getInstance().getWindow(); if (camera && window) { + // If a quest objective gameobject is under the cursor, prefer it over + // hostile units so quest pickups (e.g. "Bundle of Wood") are reliable. + std::unordered_set questObjectiveGoEntries; + { + const auto& ql = gameHandler.getQuestLog(); + questObjectiveGoEntries.reserve(32); + for (const auto& q : ql) { + if (q.complete) continue; + for (const auto& obj : q.killObjectives) { + if (obj.npcOrGoId >= 0 || obj.required == 0) continue; + uint32_t entry = static_cast(-obj.npcOrGoId); + uint32_t cur = 0; + auto it = q.killCounts.find(entry); + if (it != q.killCounts.end()) cur = it->second.first; + if (cur < obj.required) questObjectiveGoEntries.insert(entry); + } + } + } + glm::vec2 mousePos = input.getMousePosition(); float screenW = static_cast(window->getWidth()); float screenH = static_cast(window->getHeight()); @@ -1967,13 +3170,19 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { game::ObjectType closestType = game::ObjectType::OBJECT; float closestHostileUnitT = 1e30f; uint64_t closestHostileUnitGuid = 0; + float closestQuestGoT = 1e30f; + uint64_t closestQuestGoGuid = 0; + float closestGoT = 1e30f; + uint64_t closestGoGuid = 0; const uint64_t myGuid = gameHandler.getPlayerGuid(); for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { auto t = entity->getType(); if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER && - t != game::ObjectType::GAMEOBJECT) continue; + t != game::ObjectType::GAMEOBJECT) + continue; if (guid == myGuid) continue; + glm::vec3 hitCenter; float hitRadius = 0.0f; bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius); @@ -1987,11 +3196,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { heightOffset = 0.3f; } } else if (t == game::ObjectType::GAMEOBJECT) { - // For GOs with no renderer instance yet, use a tight fallback - // sphere (not 2.5f) so invisible/unloaded GOs (chairs, doodads) - // are not accidentally clicked during camera right-drag. - hitRadius = 1.2f; - heightOffset = 1.0f; + hitRadius = 2.5f; + heightOffset = 1.2f; } hitCenter = core::coords::canonicalToRender( glm::vec3(entity->getX(), entity->getY(), entity->getZ())); @@ -1999,6 +3205,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } else { hitRadius = std::max(hitRadius * 1.1f, 0.6f); } + float hitT; if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) { if (t == game::ObjectType::UNIT) { @@ -2009,6 +3216,21 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { closestHostileUnitGuid = guid; } } + if (t == game::ObjectType::GAMEOBJECT) { + if (hitT < closestGoT) { + closestGoT = hitT; + closestGoGuid = guid; + } + if (!questObjectiveGoEntries.empty()) { + auto go = std::static_pointer_cast(entity); + if (questObjectiveGoEntries.count(go->getEntry())) { + if (hitT < closestQuestGoT) { + closestQuestGoT = hitT; + closestQuestGoGuid = guid; + } + } + } + } if (hitT < closestT) { closestT = hitT; closestGuid = guid; @@ -2016,11 +3238,28 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } } } - // Prefer hostile monsters over nearby gameobjects/others when right-click picking. - if (closestHostileUnitGuid != 0) { + + // Priority: quest GO > closer of (GO, hostile unit) > closest anything. + if (closestQuestGoGuid != 0) { + closestGuid = closestQuestGoGuid; + closestType = game::ObjectType::GAMEOBJECT; + } else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) { + // Both a GO and hostile unit were hit — prefer whichever is closer. + if (closestGoT <= closestHostileUnitT) { + closestGuid = closestGoGuid; + closestType = game::ObjectType::GAMEOBJECT; + } else { + closestGuid = closestHostileUnitGuid; + closestType = game::ObjectType::UNIT; + } + } else if (closestGoGuid != 0) { + closestGuid = closestGoGuid; + closestType = game::ObjectType::GAMEOBJECT; + } else if (closestHostileUnitGuid != 0) { closestGuid = closestHostileUnitGuid; closestType = game::ObjectType::UNIT; } + if (closestGuid != 0) { if (closestType == game::ObjectType::GAMEOBJECT) { gameHandler.setTarget(closestGuid); @@ -2087,7 +3326,7 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { const bool inCombatConfirmed = gameHandler.isInCombat(); const bool attackIntentOnly = gameHandler.hasAutoAttackIntent() && !inCombatConfirmed; ImVec4 playerBorder = isDead - ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) + ? kColorDarkGray : (inCombatConfirmed ? ImVec4(1.0f, 0.2f, 0.2f, 1.0f) : (attackIntentOnly @@ -2119,8 +3358,13 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { playerHp = playerMaxHp; } - // Name in green (friendly player color) — clickable for self-target, right-click for menu - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); + // Derive class color via shared helper + ImVec4 classColor = activeChar + ? classColorVec4(static_cast(activeChar->characterClass)) + : kColorBrightGreen; + + // Name in class color — clickable for self-target, right-click for menu + ImGui::PushStyleColor(ImGuiCol_Text, classColor); if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) { gameHandler.setTarget(gameHandler.getPlayerGuid()); } @@ -2157,6 +3401,13 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.2f, 0.2f, 1.0f), "DEAD"); } + // Group leader crown on self frame when you lead the party/raid + if (gameHandler.isInGroup() && + gameHandler.getPartyData().leaderGuid == gameHandler.getPlayerGuid()) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), "\xe2\x99\x9b"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are the group leader"); + } if (gameHandler.isAfk()) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), ""); @@ -2166,6 +3417,33 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), ""); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Do not disturb — /dnd to cancel"); } + if (auto* ren = core::Application::getInstance().getRenderer()) { + if (auto* cam = ren->getCameraController()) { + if (cam->isAutoRunning()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "[Auto-Run]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Auto-running — press ` or NumLock to stop"); + } + } + } + if (inCombatConfirmed && !isDead) { + float combatPulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.2f * combatPulse, 0.2f * combatPulse, 1.0f), "[Combat]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are in combat"); + } + + // Active title — shown in gold below the name/level line + { + int32_t titleBit = gameHandler.getChosenTitleBit(); + if (titleBit >= 0) { + const std::string titleText = gameHandler.getFormattedTitle( + static_cast(titleBit)); + if (!titleText.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 0.9f), "%s", titleText.c_str()); + } + } + } // Try to get real HP/mana from the player entity auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); @@ -2177,9 +3455,21 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { } } - // Health bar + // Health bar — color transitions green→yellow→red as HP drops float pct = static_cast(playerHp) / static_cast(playerMaxHp); - ImVec4 hpColor = isDead ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) : ImVec4(0.2f, 0.8f, 0.2f, 1.0f); + ImVec4 hpColor; + if (isDead) { + hpColor = kColorDarkGray; + } else if (pct > 0.5f) { + hpColor = ImVec4(0.2f, 0.8f, 0.2f, 1.0f); // green + } else if (pct > 0.2f) { + float t = (pct - 0.2f) / 0.3f; // 0 at 20%, 1 at 50% + hpColor = ImVec4(0.9f - 0.7f * t, 0.4f + 0.4f * t, 0.0f, 1.0f); // orange→yellow + } else { + // Critical — pulse red when < 20% + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 3.5f); + hpColor = ImVec4(0.9f * pulse, 0.05f, 0.05f, 1.0f); // pulsing red + } ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpColor); char overlay[64]; snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp); @@ -2199,7 +3489,16 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { float mpPct = static_cast(power) / static_cast(maxPower); ImVec4 powerColor; switch (powerType) { - case 0: powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); break; // Mana (blue) + case 0: { + // Mana: pulse desaturated blue when critically low (< 20%) + if (mpPct < 0.2f) { + float pulse = 0.6f + 0.4f * std::sin(static_cast(ImGui::GetTime()) * 3.0f); + powerColor = ImVec4(0.1f, 0.1f, 0.8f * pulse, 1.0f); + } else { + powerColor = ImVec4(0.2f, 0.2f, 0.9f, 1.0f); + } + break; + } case 1: powerColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage (red) case 2: powerColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus (orange) case 3: powerColor = ImVec4(0.9f, 0.9f, 0.2f, 1.0f); break; // Energy (yellow) @@ -2296,7 +3595,127 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { } } } + + // Shaman totem bar (class 7) — 4 slots: Earth, Fire, Water, Air + if (gameHandler.getPlayerClass() == 7) { + static const ImVec4 kTotemColors[] = { + ImVec4(0.80f, 0.55f, 0.25f, 1.0f), // Earth — brown + ImVec4(1.00f, 0.35f, 0.10f, 1.0f), // Fire — orange-red + ImVec4(0.20f, 0.55f, 0.90f, 1.0f), // Water — blue + ImVec4(0.70f, 0.90f, 1.00f, 1.0f), // Air — pale sky + }; + static const char* kTotemNames[] = { "Earth", "Fire", "Water", "Air" }; + + ImGui::Spacing(); + ImVec2 cursor = ImGui::GetCursorScreenPos(); + float totalW = ImGui::GetContentRegionAvail().x; + float spacing = 3.0f; + float slotW = (totalW - spacing * 3.0f) / 4.0f; + float slotH = 14.0f; + ImDrawList* tdl = ImGui::GetWindowDrawList(); + + for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; i++) { + const auto& ts = gameHandler.getTotemSlot(i); + float x0 = cursor.x + i * (slotW + spacing); + float y0 = cursor.y; + float x1 = x0 + slotW; + float y1 = y0 + slotH; + + // Background + tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(20, 20, 20, 200), 2.0f); + + if (ts.active()) { + float rem = ts.remainingMs(); + float frac = rem / static_cast(ts.durationMs); + float fillX = x0 + (x1 - x0) * frac; + tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1), + ImGui::ColorConvertFloat4ToU32(kTotemColors[i]), 2.0f); + // Remaining seconds label + char secBuf[8]; + snprintf(secBuf, sizeof(secBuf), "%.0f", rem / 1000.0f); + ImVec2 tsz = ImGui::CalcTextSize(secBuf); + float lx = x0 + (slotW - tsz.x) * 0.5f; + float ly = y0 + (slotH - tsz.y) * 0.5f; + tdl->AddText(ImVec2(lx + 1, ly + 1), IM_COL32(0, 0, 0, 180), secBuf); + tdl->AddText(ImVec2(lx, ly), IM_COL32(255, 255, 255, 230), secBuf); + } else { + // Inactive — show element letter + const char* letter = kTotemNames[i]; + char single[2] = { letter[0], '\0' }; + ImVec2 tsz = ImGui::CalcTextSize(single); + float lx = x0 + (slotW - tsz.x) * 0.5f; + float ly = y0 + (slotH - tsz.y) * 0.5f; + tdl->AddText(ImVec2(lx, ly), IM_COL32(80, 80, 80, 200), single); + } + + // Border + ImU32 borderCol = ts.active() + ? ImGui::ColorConvertFloat4ToU32(kTotemColors[i]) + : IM_COL32(60, 60, 60, 160); + tdl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f); + + // Tooltip on hover + ImGui::SetCursorScreenPos(ImVec2(x0, y0)); + ImGui::InvisibleButton(("##totem" + std::to_string(i)).c_str(), ImVec2(slotW, slotH)); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + if (ts.active()) { + const std::string& spellNm = gameHandler.getSpellName(ts.spellId); + ImGui::TextColored(ImVec4(kTotemColors[i].x, kTotemColors[i].y, + kTotemColors[i].z, 1.0f), + "%s Totem", kTotemNames[i]); + if (!spellNm.empty()) ImGui::Text("%s", spellNm.c_str()); + ImGui::Text("%.1fs remaining", ts.remainingMs() / 1000.0f); + } else { + ImGui::TextDisabled("%s Totem (empty)", kTotemNames[i]); + } + ImGui::EndTooltip(); + } + } + ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f)); + } } + + // Melee swing timer — shown when player is auto-attacking + if (gameHandler.isAutoAttacking()) { + const uint64_t lastSwingMs = gameHandler.getLastMeleeSwingMs(); + if (lastSwingMs > 0) { + // Determine weapon speed from the equipped main-hand weapon + uint32_t weaponDelayMs = 2000; // Default: 2.0s unarmed + const auto& mainSlot = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND); + if (!mainSlot.empty() && mainSlot.item.itemId != 0) { + const auto* info = gameHandler.getItemInfo(mainSlot.item.itemId); + if (info && info->delayMs > 0) { + weaponDelayMs = info->delayMs; + } + } + + // Compute elapsed since last swing + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + uint64_t elapsedMs = (nowMs >= lastSwingMs) ? (nowMs - lastSwingMs) : 0; + + // Clamp to weapon delay (cap at 1.0 so the bar fills but doesn't exceed) + float pct = std::min(static_cast(elapsedMs) / static_cast(weaponDelayMs), 1.0f); + + // Light silver-orange color indicating auto-attack readiness + ImVec4 swingColor = (pct >= 0.95f) + ? ImVec4(1.0f, 0.75f, 0.15f, 1.0f) // gold when ready to swing + : ImVec4(0.65f, 0.55f, 0.40f, 1.0f); // muted brown-orange while filling + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, swingColor); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.12f, 0.08f, 0.8f)); + char swingLabel[24]; + float remainSec = std::max(0.0f, (weaponDelayMs - static_cast(elapsedMs)) / 1000.0f); + if (pct >= 0.98f) + snprintf(swingLabel, sizeof(swingLabel), "Swing!"); + else + snprintf(swingLabel, sizeof(swingLabel), "%.1fs", remainSec); + ImGui::ProgressBar(pct, ImVec2(-1.0f, 8.0f), swingLabel); + ImGui::PopStyleColor(2); + } + } + ImGui::End(); ImGui::PopStyleColor(2); @@ -2350,11 +3769,41 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { if (ImGui::MenuItem("Target Pet")) { gameHandler.setTarget(petGuid); } + if (ImGui::MenuItem("Rename Pet")) { + ImGui::CloseCurrentPopup(); + petRenameOpen_ = true; + petRenameBuf_[0] = '\0'; + } if (ImGui::MenuItem("Dismiss Pet")) { gameHandler.dismissPet(); } ImGui::EndPopup(); } + // Pet rename modal (opened via context menu) + if (petRenameOpen_) { + ImGui::OpenPopup("Rename Pet###PetRename"); + petRenameOpen_ = false; + } + if (ImGui::BeginPopupModal("Rename Pet###PetRename", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) { + ImGui::Text("Enter new pet name (max 12 characters):"); + ImGui::SetNextItemWidth(180.0f); + bool submitted = ImGui::InputText("##PetRenameInput", petRenameBuf_, sizeof(petRenameBuf_), + ImGuiInputTextFlags_EnterReturnsTrue); + ImGui::SameLine(); + if (ImGui::Button("OK") || submitted) { + std::string newName(petRenameBuf_); + if (!newName.empty() && newName.size() <= 12) { + gameHandler.renamePet(newName); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } ImGui::PopStyleColor(); if (petLevel > 0) { ImGui::SameLine(); @@ -2366,7 +3815,10 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { uint32_t maxHp = petUnit->getMaxHealth(); if (maxHp > 0) { float pct = static_cast(hp) / static_cast(maxHp); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImVec4 petHpColor = pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) + : pct > 0.2f ? ImVec4(0.9f, 0.6f, 0.0f, 1.0f) + : ImVec4(0.9f, 0.15f, 0.15f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, petHpColor); char hpText[32]; snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp); ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText); @@ -2395,10 +3847,94 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } - // Dismiss button (compact, right-aligned) - ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60.0f); - if (ImGui::SmallButton("Dismiss")) { - gameHandler.dismissPet(); + // Happiness bar — hunter pets store happiness as power type 4 + { + uint32_t happiness = petUnit->getPowerByType(4); + uint32_t maxHappiness = petUnit->getMaxPowerByType(4); + if (maxHappiness > 0 && happiness > 0) { + float hapPct = static_cast(happiness) / static_cast(maxHappiness); + // Tier: < 33% = Unhappy (red), < 67% = Content (yellow), >= 67% = Happy (green) + ImVec4 hapColor = hapPct >= 0.667f ? ImVec4(0.2f, 0.85f, 0.2f, 1.0f) + : hapPct >= 0.333f ? ImVec4(0.9f, 0.75f, 0.1f, 1.0f) + : ImVec4(0.85f, 0.2f, 0.2f, 1.0f); + const char* hapLabel = hapPct >= 0.667f ? "Happy" : hapPct >= 0.333f ? "Content" : "Unhappy"; + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hapColor); + ImGui::ProgressBar(hapPct, ImVec2(-1, 8), hapLabel); + ImGui::PopStyleColor(); + } + } + + // Pet cast bar + if (auto* pcs = gameHandler.getUnitCastState(petGuid)) { + float castPct = (pcs->timeTotal > 0.0f) + ? (pcs->timeTotal - pcs->timeRemaining) / pcs->timeTotal : 0.0f; + // Orange color to distinguish from health/power bars + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.85f, 0.55f, 0.1f, 1.0f)); + char petCastLabel[48]; + const std::string& spellNm = gameHandler.getSpellName(pcs->spellId); + if (!spellNm.empty()) + snprintf(petCastLabel, sizeof(petCastLabel), "%s (%.1fs)", spellNm.c_str(), pcs->timeRemaining); + else + snprintf(petCastLabel, sizeof(petCastLabel), "Casting... (%.1fs)", pcs->timeRemaining); + ImGui::ProgressBar(castPct, ImVec2(-1, 10), petCastLabel); + ImGui::PopStyleColor(); + } + + // Stance row: Passive / Defensive / Aggressive — with Dismiss right-aligned + { + static const char* kReactLabels[] = { "Psv", "Def", "Agg" }; + static const char* kReactTooltips[] = { "Passive", "Defensive", "Aggressive" }; + static const ImVec4 kReactColors[] = { + ImVec4(0.4f, 0.6f, 1.0f, 1.0f), // passive — blue + ImVec4(0.3f, 0.85f, 0.3f, 1.0f), // defensive — green + ImVec4(1.0f, 0.35f, 0.35f, 1.0f),// aggressive — red + }; + static const ImVec4 kReactDimColors[] = { + ImVec4(0.15f, 0.2f, 0.4f, 0.8f), + ImVec4(0.1f, 0.3f, 0.1f, 0.8f), + ImVec4(0.4f, 0.1f, 0.1f, 0.8f), + }; + uint8_t curReact = gameHandler.getPetReact(); // 0=passive,1=defensive,2=aggressive + + // Find each react-type slot in the action bar by known built-in IDs: + // 1=Passive, 4=Defensive, 6=Aggressive (WoW wire protocol) + static const uint32_t kReactActionIds[] = { 1u, 4u, 6u }; + uint32_t reactSlotVals[3] = { 0, 0, 0 }; + const int slotTotal = game::GameHandler::PET_ACTION_BAR_SLOTS; + for (int i = 0; i < slotTotal; ++i) { + uint32_t sv = gameHandler.getPetActionSlot(i); + uint32_t aid = sv & 0x00FFFFFFu; + for (int r = 0; r < 3; ++r) { + if (aid == kReactActionIds[r]) { reactSlotVals[r] = sv; break; } + } + } + + for (int r = 0; r < 3; ++r) { + if (r > 0) ImGui::SameLine(0.0f, 3.0f); + bool active = (curReact == static_cast(r)); + ImVec4 btnCol = active ? kReactColors[r] : kReactDimColors[r]; + ImGui::PushID(r + 1000); + ImGui::PushStyleColor(ImGuiCol_Button, btnCol); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kReactColors[r]); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, kReactColors[r]); + if (ImGui::Button(kReactLabels[r], ImVec2(34.0f, 16.0f))) { + // Use server-provided slot value if available; fall back to raw ID + uint32_t action = (reactSlotVals[r] != 0) + ? reactSlotVals[r] + : kReactActionIds[r]; + gameHandler.sendPetAction(action, 0); + } + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", kReactTooltips[r]); + ImGui::PopID(); + } + + // Dismiss button right-aligned on the same row + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 58.0f); + if (ImGui::SmallButton("Dismiss")) { + gameHandler.dismissPet(); + } } // Pet action bar — show up to 10 action slots from SMSG_PET_SPELLS @@ -2422,20 +3958,28 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { // Use the authoritative autocast set from SMSG_PET_SPELLS spell list flags. bool autocastOn = gameHandler.isPetSpellAutocast(actionId); + // Cooldown tracking for pet spells (actionId > 6 are spell IDs) + float petCd = (actionId > 6) ? gameHandler.getSpellCooldown(actionId) : 0.0f; + bool petOnCd = (petCd > 0.0f); + ImGui::PushID(i); if (rendered > 0) ImGui::SameLine(0.0f, spacing); // Try to show spell icon; fall back to abbreviated text label. VkDescriptorSet iconTex = VK_NULL_HANDLE; const char* builtinLabel = nullptr; - if (actionId == 2) builtinLabel = "Fol"; + if (actionId == 1) builtinLabel = "Psv"; + else if (actionId == 2) builtinLabel = "Fol"; else if (actionId == 3) builtinLabel = "Sty"; + else if (actionId == 4) builtinLabel = "Def"; else if (actionId == 5) builtinLabel = "Atk"; + else if (actionId == 6) builtinLabel = "Agg"; else if (assetMgr) iconTex = getSpellIcon(actionId, assetMgr); - // Tint green when autocast is on. - ImVec4 tint = autocastOn ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) - : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + // Dim when on cooldown; tint green when autocast is on + ImVec4 tint = petOnCd + ? ImVec4(0.35f, 0.35f, 0.35f, 0.7f) + : (autocastOn ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); bool clicked = false; if (iconTex) { clicked = ImGui::ImageButton("##pa", @@ -2453,30 +3997,79 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { if (nm.empty()) snprintf(label, sizeof(label), "?%u", actionId % 100); else snprintf(label, sizeof(label), "%.3s", nm.c_str()); } - ImGui::PushStyleColor(ImGuiCol_Button, - autocastOn ? ImVec4(0.2f,0.5f,0.2f,0.9f) - : ImVec4(0.2f,0.2f,0.3f,0.9f)); + ImVec4 btnCol = petOnCd ? ImVec4(0.1f,0.1f,0.15f,0.9f) + : (autocastOn ? ImVec4(0.2f,0.5f,0.2f,0.9f) + : ImVec4(0.2f,0.2f,0.3f,0.9f)); + ImGui::PushStyleColor(ImGuiCol_Button, btnCol); clicked = ImGui::Button(label, ImVec2(iconSz + 4.0f, iconSz)); ImGui::PopStyleColor(); } - if (clicked) { + // Cooldown overlay: dark fill + time text centered on the button + if (petOnCd && !builtinLabel) { + ImVec2 bMin = ImGui::GetItemRectMin(); + ImVec2 bMax = ImGui::GetItemRectMax(); + auto* cdDL = ImGui::GetWindowDrawList(); + cdDL->AddRectFilled(bMin, bMax, IM_COL32(0, 0, 0, 140)); + char cdTxt[8]; + if (petCd >= 60.0f) + snprintf(cdTxt, sizeof(cdTxt), "%dm", static_cast(petCd / 60.0f)); + else if (petCd >= 1.0f) + snprintf(cdTxt, sizeof(cdTxt), "%d", static_cast(petCd)); + else + snprintf(cdTxt, sizeof(cdTxt), "%.1f", petCd); + ImVec2 tsz = ImGui::CalcTextSize(cdTxt); + float cx = (bMin.x + bMax.x) * 0.5f; + float cy = (bMin.y + bMax.y) * 0.5f; + cdDL->AddText(ImVec2(cx - tsz.x * 0.5f, cy - tsz.y * 0.5f), + IM_COL32(255, 255, 255, 230), cdTxt); + } + + if (clicked && !petOnCd) { // Send pet action; use current target for spells. uint64_t targetGuid = (actionId > 5) ? gameHandler.getTargetGuid() : 0u; gameHandler.sendPetAction(slotVal, targetGuid); } + // Right-click toggles autocast for castable pet spells (actionId > 6) + if (actionId > 6 && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + gameHandler.togglePetSpellAutocast(actionId); + } - // Tooltip: show spell name or built-in command name. + // Tooltip: rich spell info for pet spells, simple label for built-in commands if (ImGui::IsItemHovered()) { - const char* tip = builtinLabel - ? (actionId == 5 ? "Attack" : actionId == 2 ? "Follow" : "Stay") - : nullptr; - std::string spellNm; - if (!tip && actionId > 5) { - spellNm = gameHandler.getSpellName(actionId); - if (!spellNm.empty()) tip = spellNm.c_str(); + if (builtinLabel) { + const char* tip = nullptr; + if (actionId == 1) tip = "Passive"; + else if (actionId == 2) tip = "Follow"; + else if (actionId == 3) tip = "Stay"; + else if (actionId == 4) tip = "Defensive"; + else if (actionId == 5) tip = "Attack"; + else if (actionId == 6) tip = "Aggressive"; + if (tip) ImGui::SetTooltip("%s", tip); + } else if (actionId > 6) { + auto* spellAsset = core::Application::getInstance().getAssetManager(); + ImGui::BeginTooltip(); + bool richOk = spellbookScreen.renderSpellInfoTooltip(actionId, gameHandler, spellAsset); + if (!richOk) { + std::string nm = gameHandler.getSpellName(actionId); + if (nm.empty()) nm = "Spell #" + std::to_string(actionId); + ImGui::Text("%s", nm.c_str()); + } + ImGui::TextColored(autocastOn + ? kColorGreen + : kColorGray, + "Autocast: %s (right-click to toggle)", autocastOn ? "On" : "Off"); + if (petOnCd) { + if (petCd >= 60.0f) + ImGui::TextColored(kColorRed, + "Cooldown: %d min %d sec", + static_cast(petCd) / 60, static_cast(petCd) % 60); + else + ImGui::TextColored(kColorRed, + "Cooldown: %.1f sec", petCd); + } + ImGui::EndTooltip(); } - if (tip) ImGui::SetTooltip("%s", tip); } ImGui::PopID(); @@ -2490,6 +4083,87 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +// ============================================================ +// Totem Frame (Shaman — below pet frame / player frame) +// ============================================================ + +void GameScreen::renderTotemFrame(game::GameHandler& gameHandler) { + // Only show if at least one totem is active + bool anyActive = false; + for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) { + if (gameHandler.getTotemSlot(i).active()) { anyActive = true; break; } + } + if (!anyActive) return; + + static const struct { const char* name; ImU32 color; } kTotemInfo[4] = { + { "Earth", IM_COL32(139, 90, 43, 255) }, // brown + { "Fire", IM_COL32(220, 80, 30, 255) }, // red-orange + { "Water", IM_COL32( 30,120, 220, 255) }, // blue + { "Air", IM_COL32(180,220, 255, 255) }, // light blue + }; + + // Position: below pet frame / player frame, left side + // Pet frame is at ~y=200 if active, player frame is at y=20; totem frame near y=300 + // We anchor relative to screen left edge like pet frame + ImGui::SetNextWindowPos(ImVec2(8.0f, 300.0f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(130.0f, 0.0f), ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoTitleBar; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.08f, 0.06f, 0.88f)); + + if (ImGui::Begin("##TotemFrame", nullptr, flags)) { + ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.3f, 1.0f), "Totems"); + ImGui::Separator(); + + for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) { + const auto& slot = gameHandler.getTotemSlot(i); + if (!slot.active()) continue; + + ImGui::PushID(i); + + // Colored element dot + ImVec2 dotPos = ImGui::GetCursorScreenPos(); + dotPos.x += 4.0f; dotPos.y += 6.0f; + ImGui::GetWindowDrawList()->AddCircleFilled( + ImVec2(dotPos.x + 4.0f, dotPos.y + 4.0f), 4.0f, kTotemInfo[i].color); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f); + + // Totem name or spell name + const std::string& spellName = gameHandler.getSpellName(slot.spellId); + const char* displayName = spellName.empty() ? kTotemInfo[i].name : spellName.c_str(); + ImGui::Text("%s", displayName); + + // Duration countdown bar + float remMs = slot.remainingMs(); + float totMs = static_cast(slot.durationMs); + float frac = (totMs > 0.0f) ? std::min(remMs / totMs, 1.0f) : 0.0f; + float remSec = remMs / 1000.0f; + + // Color bar with totem element tint + ImVec4 barCol( + static_cast((kTotemInfo[i].color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f, + static_cast((kTotemInfo[i].color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f, + static_cast((kTotemInfo[i].color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f, + 0.9f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barCol); + char timeBuf[16]; + snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remSec); + ImGui::ProgressBar(frac, ImVec2(-1, 8), timeBuf); + ImGui::PopStyleColor(); + + ImGui::PopID(); + } + } + ImGui::End(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); +} + void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { auto target = gameHandler.getTarget(); if (!target) return; @@ -2510,29 +4184,41 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { // Determine hostility/level color for border and name (WoW-canonical) ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f); if (target->getType() == game::ObjectType::PLAYER) { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + hostileColor = kColorBrightGreen; } else if (target->getType() == game::ObjectType::UNIT) { auto u = std::static_pointer_cast(target); if (u->getHealth() == 0 && u->getMaxHealth() > 0) { - hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + hostileColor = kColorDarkGray; } else if (u->isHostile()) { + // Check tapped-by-other: grey name for mobs tagged by someone else + uint32_t tgtDynFlags = u->getDynamicFlags(); + bool tgtTapped = (tgtDynFlags & 0x0004) != 0 && (tgtDynFlags & 0x0008) == 0; + if (tgtTapped) { + hostileColor = kColorGray; // Grey — tapped by other + } else { // WoW level-based color for hostile mobs uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); - int32_t diff = static_cast(mobLv) - static_cast(playerLv); - if (game::GameHandler::killXp(playerLv, mobLv) == 0) { - hostileColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey - no XP - } else if (diff >= 10) { - hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard - } else if (diff >= 5) { - hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard - } else if (diff >= -2) { - hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even + if (mobLv == 0) { + // Level 0 = unknown/?? (e.g. high-level raid bosses) — always skull red + hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); } else { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy + int32_t diff = static_cast(mobLv) - static_cast(playerLv); + if (game::GameHandler::killXp(playerLv, mobLv) == 0) { + hostileColor = kColorGray; // Grey - no XP + } else if (diff >= 10) { + hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard + } else if (diff >= 5) { + hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard + } else if (diff >= -2) { + hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even + } else { + hostileColor = kColorBrightGreen; // Green - easy + } } + } // end tapped else } else { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly + hostileColor = kColorBrightGreen; // Friendly } } @@ -2577,7 +4263,12 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { // Entity name and type — Selectable so we can attach a right-click context menu std::string name = getEntityName(target); + // Player targets: use class color instead of the generic green ImVec4 nameColor = hostileColor; + if (target->getType() == game::ObjectType::PLAYER) { + uint8_t cid = entityClassId(target.get()); + if (cid != 0) nameColor = classColorVec4(cid); + } ImGui::SameLine(0.0f, 0.0f); ImGui::PushStyleColor(ImGuiCol_Text, nameColor); @@ -2588,6 +4279,91 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0)); ImGui::PopStyleColor(4); + // Right-click context menu on target frame + if (ImGui::BeginPopupContextItem("##TargetFrameCtx")) { + ImGui::TextDisabled("%s", name.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem("Set Focus")) + gameHandler.setFocus(target->getGuid()); + if (target->getType() == game::ObjectType::PLAYER) { + ImGui::Separator(); + if (ImGui::MenuItem("Whisper")) { + selectedChatType = 4; + strncpy(whisperTargetBuffer, name.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + refocusChatInput = true; + } + if (ImGui::MenuItem("Invite to Group")) + gameHandler.inviteToGroup(name); + if (ImGui::MenuItem("Trade")) + gameHandler.initiateTrade(target->getGuid()); + if (ImGui::MenuItem("Duel")) + gameHandler.proposeDuel(target->getGuid()); + if (ImGui::MenuItem("Inspect")) { + gameHandler.inspectTarget(); + showInspectWindow_ = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("Add Friend")) + gameHandler.addFriend(name); + if (ImGui::MenuItem("Ignore")) + gameHandler.addIgnore(name); + } + ImGui::EndPopup(); + } + + // Group leader crown — golden ♛ when the targeted player is the party/raid leader + if (gameHandler.isInGroup() && target->getType() == game::ObjectType::PLAYER) { + if (gameHandler.getPartyData().leaderGuid == target->getGuid()) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), "\xe2\x99\x9b"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Group Leader"); + } + } + + // Quest giver indicator — "!" for available quests, "?" for completable quests + { + using QGS = game::QuestGiverStatus; + QGS qgs = gameHandler.getQuestGiverStatus(target->getGuid()); + if (qgs == QGS::AVAILABLE) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "!"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Has a quest available"); + } else if (qgs == QGS::AVAILABLE_LOW) { + ImGui::SameLine(0, 4); + ImGui::TextColored(kColorGray, "!"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Has a low-level quest available"); + } else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "?"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest ready to turn in"); + } else if (qgs == QGS::INCOMPLETE) { + ImGui::SameLine(0, 4); + ImGui::TextColored(kColorGray, "?"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest incomplete"); + } + } + + // Creature subtitle (e.g. "", "Captain of the Guard") + if (target->getType() == game::ObjectType::UNIT) { + auto unit = std::static_pointer_cast(target); + const std::string sub = gameHandler.getCachedCreatureSubName(unit->getEntry()); + if (!sub.empty()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", sub.c_str()); + } + } + + // Player guild name (e.g. "") — mirrors NPC subtitle styling + if (target->getType() == game::ObjectType::PLAYER) { + uint32_t guildId = gameHandler.getEntityGuildId(target->getGuid()); + if (guildId != 0) { + const std::string& gn = gameHandler.lookupGuildName(guildId); + if (!gn.empty()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", gn.c_str()); + } + } + } + // Right-click context menu on the target name if (ImGui::BeginPopupContextItem("##TargetNameCtx")) { const bool isPlayer = (target->getType() == game::ObjectType::PLAYER); @@ -2659,9 +4435,63 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { // Level color matches the hostility/difficulty color ImVec4 levelColor = hostileColor; if (target->getType() == game::ObjectType::PLAYER) { - levelColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + levelColor = ui::colors::kLightGray; + } + if (unit->getLevel() == 0) + ImGui::TextColored(levelColor, "Lv ??"); + else + ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); + // Classification badge: Elite / Rare Elite / Boss / Rare + if (target->getType() == game::ObjectType::UNIT) { + int rank = gameHandler.getCreatureRank(unit->getEntry()); + if (rank == 1) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "[Elite]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Elite — requires a group"); + } else if (rank == 2) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(0.8f, 0.4f, 1.0f, 1.0f), "[Rare Elite]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Rare Elite — uncommon spawn, group recommended"); + } else if (rank == 3) { + ImGui::SameLine(0, 4); + ImGui::TextColored(kColorRed, "[Boss]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Boss — raid / dungeon boss"); + } else if (rank == 4) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(0.5f, 0.9f, 1.0f, 1.0f), "[Rare]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Rare — uncommon spawn with better loot"); + } + } + // Creature type label (Beast, Humanoid, Demon, etc.) + if (target->getType() == game::ObjectType::UNIT) { + uint32_t ctype = gameHandler.getCreatureType(unit->getEntry()); + const char* ctypeName = nullptr; + switch (ctype) { + case 1: ctypeName = "Beast"; break; + case 2: ctypeName = "Dragonkin"; break; + case 3: ctypeName = "Demon"; break; + case 4: ctypeName = "Elemental"; break; + case 5: ctypeName = "Giant"; break; + case 6: ctypeName = "Undead"; break; + case 7: ctypeName = "Humanoid"; break; + case 8: ctypeName = "Critter"; break; + case 9: ctypeName = "Mechanical"; break; + case 11: ctypeName = "Totem"; break; + case 12: ctypeName = "Non-combat Pet"; break; + case 13: ctypeName = "Gas Cloud"; break; + default: break; + } + if (ctypeName) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 0.9f), "(%s)", ctypeName); + } + } + if (confirmedCombatWithTarget) { + float cPulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.2f * cPulse, 0.2f * cPulse, 1.0f), "[Attacking]"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Engaged in combat with this target"); } - ImGui::TextColored(levelColor, "Lv %u", unit->getLevel()); // Health bar uint32_t hp = unit->getHealth(); @@ -2706,22 +4536,137 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { } } + // Combo points — shown when the player has combo points on this target + { + uint8_t cp = gameHandler.getComboPoints(); + if (cp > 0 && gameHandler.getComboTarget() == target->getGuid()) { + const float dotSize = 12.0f; + const float dotSpacing = 4.0f; + const int maxCP = 5; + float totalW = maxCP * dotSize + (maxCP - 1) * dotSpacing; + float startX = (frameW - totalW) * 0.5f; + ImGui::SetCursorPosX(startX); + ImVec2 cursor = ImGui::GetCursorScreenPos(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + for (int ci = 0; ci < maxCP; ++ci) { + float cx = cursor.x + ci * (dotSize + dotSpacing) + dotSize * 0.5f; + float cy = cursor.y + dotSize * 0.5f; + if (ci < static_cast(cp)) { + // Lit: yellow for 1-4, red glow for 5 + ImU32 col = (cp >= 5) + ? IM_COL32(255, 50, 30, 255) + : IM_COL32(255, 210, 30, 255); + dl->AddCircleFilled(ImVec2(cx, cy), dotSize * 0.45f, col); + // Subtle glow + dl->AddCircle(ImVec2(cx, cy), dotSize * 0.5f, IM_COL32(255, 255, 200, 80), 0, 1.5f); + } else { + // Unlit: dark outline + dl->AddCircle(ImVec2(cx, cy), dotSize * 0.4f, IM_COL32(80, 80, 80, 180), 0, 1.5f); + } + } + ImGui::Dummy(ImVec2(totalW, dotSize + 2.0f)); + } + } + // Target cast bar — shown when the target is casting if (gameHandler.isTargetCasting()) { float castPct = gameHandler.getTargetCastProgress(); float castLeft = gameHandler.getTargetCastTimeRemaining(); uint32_t tspell = gameHandler.getTargetCastSpellId(); + bool interruptible = gameHandler.isTargetCastInterruptible(); const std::string& castName = (tspell != 0) ? gameHandler.getSpellName(tspell) : ""; - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f)); + // Color: interruptible = green (can Kick/CS), not interruptible = red, both pulse when >80% + ImVec4 castBarColor; + if (castPct > 0.8f) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); + if (interruptible) + castBarColor = ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f); // green pulse + else + castBarColor = ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); // red pulse + } else { + castBarColor = interruptible ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) // green = can interrupt + : ImVec4(0.85f, 0.15f, 0.15f, 1.0f); // red = uninterruptible + } + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, castBarColor); char castLabel[72]; if (!castName.empty()) snprintf(castLabel, sizeof(castLabel), "%s (%.1fs)", castName.c_str(), castLeft); else snprintf(castLabel, sizeof(castLabel), "Casting... (%.1fs)", castLeft); - ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel); + { + auto* tcastAsset = core::Application::getInstance().getAssetManager(); + VkDescriptorSet tIcon = (tspell != 0 && tcastAsset) + ? getSpellIcon(tspell, tcastAsset) : VK_NULL_HANDLE; + if (tIcon) { + ImGui::Image((ImTextureID)(uintptr_t)tIcon, ImVec2(14, 14)); + ImGui::SameLine(0, 2); + ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel); + } else { + ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel); + } + } ImGui::PopStyleColor(); } + // Target-of-Target (ToT): show who the current target is targeting + { + uint64_t totGuid = 0; + const auto& tFields = target->getFields(); + auto itLo = tFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (itLo != tFields.end()) { + totGuid = itLo->second; + auto itHi = tFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (itHi != tFields.end()) + totGuid |= (static_cast(itHi->second) << 32); + } + if (totGuid != 0) { + auto totEnt = gameHandler.getEntityManager().getEntity(totGuid); + std::string totName; + ImVec4 totColor(0.7f, 0.7f, 0.7f, 1.0f); + if (totGuid == gameHandler.getPlayerGuid()) { + auto playerEnt = gameHandler.getEntityManager().getEntity(totGuid); + totName = playerEnt ? getEntityName(playerEnt) : "You"; + totColor = kColorBrightGreen; + } else if (totEnt) { + totName = getEntityName(totEnt); + uint8_t cid = entityClassId(totEnt.get()); + if (cid != 0) totColor = classColorVec4(cid); + } + if (!totName.empty()) { + ImGui::TextDisabled("▶"); + ImGui::SameLine(0, 2); + ImGui::TextColored(totColor, "%s", totName.c_str()); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Target's target: %s\nClick to target", totName.c_str()); + } + if (ImGui::IsItemClicked()) { + gameHandler.setTarget(totGuid); + } + + // Compact health bar for the ToT — essential for healers tracking boss target + if (totEnt) { + auto totUnit = std::dynamic_pointer_cast(totEnt); + if (totUnit && totUnit->getMaxHealth() > 0) { + uint32_t totHp = totUnit->getHealth(); + uint32_t totMaxHp = totUnit->getMaxHealth(); + float totPct = static_cast(totHp) / static_cast(totMaxHp); + ImVec4 totBarColor = + totPct > 0.5f ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) : + totPct > 0.2f ? ImVec4(0.75f, 0.75f, 0.2f, 1.0f) : + ImVec4(0.75f, 0.2f, 0.2f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, totBarColor); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); + char totOverlay[32]; + snprintf(totOverlay, sizeof(totOverlay), "%u%%", + static_cast(totPct * 100.0f + 0.5f)); + ImGui::ProgressBar(totPct, ImVec2(-1, 10), totOverlay); + ImGui::PopStyleColor(2); + } + } + } + } + } + // Distance const auto& movement = gameHandler.getMovementInfo(); float dx = target->getX() - movement.x; @@ -2752,8 +4697,31 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGui::Separator(); + // Build sorted index list: debuffs before buffs, shorter duration first + uint64_t tNowSort = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + std::vector sortedIdx; + sortedIdx.reserve(targetAuras.size()); + for (size_t i = 0; i < targetAuras.size(); ++i) + if (!targetAuras[i].isEmpty()) sortedIdx.push_back(i); + std::sort(sortedIdx.begin(), sortedIdx.end(), [&](size_t a, size_t b) { + const auto& aa = targetAuras[a]; const auto& ab = targetAuras[b]; + bool aDebuff = (aa.flags & 0x80) != 0; + bool bDebuff = (ab.flags & 0x80) != 0; + if (aDebuff != bDebuff) return aDebuff > bDebuff; // debuffs first + int32_t ra = aa.getRemainingMs(tNowSort); + int32_t rb = ab.getRemainingMs(tNowSort); + // Permanent (-1) goes last; shorter remaining goes first + if (ra < 0 && rb < 0) return false; + if (ra < 0) return false; + if (rb < 0) return true; + return ra < rb; + }); + int shown = 0; - for (size_t i = 0; i < targetAuras.size() && shown < 16; ++i) { + for (size_t si = 0; si < sortedIdx.size() && shown < 16; ++si) { + size_t i = sortedIdx[si]; const auto& aura = targetAuras[i]; if (aura.isEmpty()) continue; @@ -2762,7 +4730,20 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGui::PushID(static_cast(10000 + i)); bool isBuff = (aura.flags & 0x80) == 0; - ImVec4 auraBorderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); + ImVec4 auraBorderColor; + if (isBuff) { + auraBorderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); + } else { + // Debuff: color by dispel type, matching player buff bar convention + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + switch (dt) { + case 1: auraBorderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue + case 2: auraBorderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple + case 3: auraBorderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown + case 4: auraBorderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green + default: auraBorderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red + } + } VkDescriptorSet iconTex = VK_NULL_HANDLE; if (assetMgr) { @@ -2791,6 +4772,32 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { std::chrono::steady_clock::now().time_since_epoch()).count()); int32_t tRemainMs = aura.getRemainingMs(tNowMs); + // Clock-sweep overlay (elapsed = dark area, WoW style) + if (tRemainMs > 0 && aura.maxDurationMs > 0) { + ImVec2 tIconMin = ImGui::GetItemRectMin(); + ImVec2 tIconMax = ImGui::GetItemRectMax(); + float tcx = (tIconMin.x + tIconMax.x) * 0.5f; + float tcy = (tIconMin.y + tIconMax.y) * 0.5f; + float tR = (tIconMax.x - tIconMin.x) * 0.5f; + float tTot = static_cast(aura.maxDurationMs); + float tFrac = std::clamp( + 1.0f - static_cast(tRemainMs) / tTot, 0.0f, 1.0f); + if (tFrac > 0.005f) { + constexpr int TSEGS = 24; + float tSa = -IM_PI * 0.5f; + float tEa = tSa + tFrac * 2.0f * IM_PI; + ImVec2 tPts[TSEGS + 2]; + tPts[0] = ImVec2(tcx, tcy); + for (int s = 0; s <= TSEGS; ++s) { + float a = tSa + (tEa - tSa) * s / static_cast(TSEGS); + tPts[s + 1] = ImVec2(tcx + std::cos(a) * tR, + tcy + std::sin(a) * tR); + } + ImGui::GetWindowDrawList()->AddConvexPolyFilled( + tPts, TSEGS + 2, IM_COL32(0, 0, 0, 145)); + } + } + // Duration countdown overlay if (tRemainMs > 0) { ImVec2 iconMin = ImGui::GetItemRectMin(); @@ -2806,10 +4813,24 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(timeStr); float cx = iconMin.x + (iconMax.x - iconMin.x - textSize.x) * 0.5f; float cy = iconMax.y - textSize.y - 1.0f; + // Color by urgency (matches player buff bar) + ImU32 tTimerColor; + if (tRemainMs < 10000) { + float pulse = 0.7f + 0.3f * std::sin( + static_cast(ImGui::GetTime()) * 6.0f); + tTimerColor = IM_COL32( + static_cast(255 * pulse), + static_cast(80 * pulse), + static_cast(60 * pulse), 255); + } else if (tRemainMs < 30000) { + tTimerColor = IM_COL32(255, 165, 0, 255); + } else { + tTimerColor = IM_COL32(255, 255, 255, 255); + } ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 200), timeStr); ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), - IM_COL32(255, 255, 255, 255), timeStr); + tTimerColor, timeStr); } // Stack / charge count — upper-left corner @@ -2837,7 +4858,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { char durBuf[32]; if (seconds < 60) snprintf(durBuf, sizeof(durBuf), "Remaining: %ds", seconds); else snprintf(durBuf, sizeof(durBuf), "Remaining: %dm %ds", seconds / 60, seconds % 60); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", durBuf); + ImGui::TextColored(ui::colors::kLightGray, "%s", durBuf); } ImGui::EndTooltip(); } @@ -2883,8 +4904,14 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) { std::string totName = getEntityName(totEntity); + // Class color for players; gray for NPCs + ImVec4 totNameColor = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + if (totEntity->getType() == game::ObjectType::PLAYER) { + uint8_t cid = entityClassId(totEntity.get()); + if (cid != 0) totNameColor = classColorVec4(cid); + } // Selectable so we can attach a right-click context menu - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, totNameColor); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1,1,1,0.08f)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1,1,1,0.12f)); @@ -2923,6 +4950,158 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { ImGui::ProgressBar(pct, ImVec2(-1, 10), ""); ImGui::PopStyleColor(); } + + // ToT cast bar — green if interruptible, red if not; pulses near completion + if (auto* totCs = gameHandler.getUnitCastState(totGuid)) { + float totCastPct = (totCs->timeTotal > 0.0f) + ? (totCs->timeTotal - totCs->timeRemaining) / totCs->timeTotal : 0.0f; + ImVec4 tcColor; + if (totCastPct > 0.8f) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); + tcColor = totCs->interruptible + ? ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f) + : ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); + } else { + tcColor = totCs->interruptible + ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) + : ImVec4(0.85f, 0.15f, 0.15f, 1.0f); + } + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, tcColor); + char tcLabel[48]; + const std::string& tcName = gameHandler.getSpellName(totCs->spellId); + if (!tcName.empty()) + snprintf(tcLabel, sizeof(tcLabel), "%s (%.1fs)", tcName.c_str(), totCs->timeRemaining); + else + snprintf(tcLabel, sizeof(tcLabel), "Casting... (%.1fs)", totCs->timeRemaining); + ImGui::ProgressBar(totCastPct, ImVec2(-1, 8), tcLabel); + ImGui::PopStyleColor(); + } + + // ToT aura row — compact icons, debuffs first + { + const std::vector* totAuras = nullptr; + if (totGuid == gameHandler.getPlayerGuid()) + totAuras = &gameHandler.getPlayerAuras(); + else if (totGuid == gameHandler.getTargetGuid()) + totAuras = &gameHandler.getTargetAuras(); + else + totAuras = gameHandler.getUnitAuras(totGuid); + + if (totAuras) { + int totActive = 0; + for (const auto& a : *totAuras) if (!a.isEmpty()) totActive++; + if (totActive > 0) { + auto* totAsset = core::Application::getInstance().getAssetManager(); + constexpr float TA_ICON = 16.0f; + constexpr int TA_PER_ROW = 8; + + ImGui::Separator(); + + uint64_t taNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + std::vector taIdx; + taIdx.reserve(totAuras->size()); + for (size_t i = 0; i < totAuras->size(); ++i) + if (!(*totAuras)[i].isEmpty()) taIdx.push_back(i); + std::sort(taIdx.begin(), taIdx.end(), [&](size_t a, size_t b) { + bool aD = ((*totAuras)[a].flags & 0x80) != 0; + bool bD = ((*totAuras)[b].flags & 0x80) != 0; + if (aD != bD) return aD > bD; + int32_t ra = (*totAuras)[a].getRemainingMs(taNowMs); + int32_t rb = (*totAuras)[b].getRemainingMs(taNowMs); + if (ra < 0 && rb < 0) return false; + if (ra < 0) return false; + if (rb < 0) return true; + return ra < rb; + }); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); + int taShown = 0; + for (size_t si = 0; si < taIdx.size() && taShown < 16; ++si) { + const auto& aura = (*totAuras)[taIdx[si]]; + bool isBuff = (aura.flags & 0x80) == 0; + + if (taShown > 0 && taShown % TA_PER_ROW != 0) ImGui::SameLine(); + ImGui::PushID(static_cast(taIdx[si]) + 5000); + + ImVec4 borderCol; + if (isBuff) { + borderCol = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); + } else { + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + switch (dt) { + case 1: borderCol = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; + case 2: borderCol = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; + case 3: borderCol = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; + case 4: borderCol = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; + default: borderCol = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; + } + } + + VkDescriptorSet taIcon = (totAsset) + ? getSpellIcon(aura.spellId, totAsset) : VK_NULL_HANDLE; + if (taIcon) { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::ImageButton("##taura", + (ImTextureID)(uintptr_t)taIcon, + ImVec2(TA_ICON - 2, TA_ICON - 2)); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + char lab[8]; + snprintf(lab, sizeof(lab), "%u", aura.spellId % 10000); + ImGui::Button(lab, ImVec2(TA_ICON, TA_ICON)); + ImGui::PopStyleColor(); + } + + // Duration overlay + int32_t taRemain = aura.getRemainingMs(taNowMs); + if (taRemain > 0) { + ImVec2 imin = ImGui::GetItemRectMin(); + ImVec2 imax = ImGui::GetItemRectMax(); + char ts[12]; + int s = (taRemain + 999) / 1000; + if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600); + else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60); + else snprintf(ts, sizeof(ts), "%d", s); + ImVec2 tsz = ImGui::CalcTextSize(ts); + float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f; + float cy = imax.y - tsz.y; + ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts); + ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts); + } + + // Tooltip + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + bool richOk = spellbookScreen.renderSpellInfoTooltip( + aura.spellId, gameHandler, totAsset); + if (!richOk) { + std::string nm = spellbookScreen.lookupSpellName(aura.spellId, totAsset); + if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId); + ImGui::Text("%s", nm.c_str()); + } + if (taRemain > 0) { + int s = taRemain / 1000; + char db[32]; + if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s); + else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60); + ImGui::TextColored(ui::colors::kLightGray, "%s", db); + } + ImGui::EndTooltip(); + } + + ImGui::PopID(); + taShown++; + } + ImGui::PopStyleVar(); + } + } + } } } ImGui::End(); @@ -2954,27 +5133,40 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { // Determine color based on relation (same logic as target frame) ImVec4 focusColor(0.7f, 0.7f, 0.7f, 1.0f); if (focus->getType() == game::ObjectType::PLAYER) { - focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + // Use class color for player focus targets + uint8_t cid = entityClassId(focus.get()); + focusColor = (cid != 0) ? classColorVec4(cid) : kColorBrightGreen; } else if (focus->getType() == game::ObjectType::UNIT) { auto u = std::static_pointer_cast(focus); if (u->getHealth() == 0 && u->getMaxHealth() > 0) { - focusColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + focusColor = kColorDarkGray; } else if (u->isHostile()) { + // Tapped-by-other: grey focus frame name + uint32_t focDynFlags = u->getDynamicFlags(); + bool focTapped = (focDynFlags & 0x0004) != 0 && (focDynFlags & 0x0008) == 0; + if (focTapped) { + focusColor = kColorGray; + } else { uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); - int32_t diff = static_cast(mobLv) - static_cast(playerLv); - if (game::GameHandler::killXp(playerLv, mobLv) == 0) - focusColor = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); - else if (diff >= 10) - focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); - else if (diff >= 5) - focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); - else if (diff >= -2) - focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); - else - focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + if (mobLv == 0) { + focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // ?? level = skull red + } else { + int32_t diff = static_cast(mobLv) - static_cast(playerLv); + if (game::GameHandler::killXp(playerLv, mobLv) == 0) + focusColor = kColorGray; + else if (diff >= 10) + focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); + else if (diff >= 5) + focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); + else if (diff >= -2) + focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); + else + focusColor = kColorBrightGreen; + } + } // end tapped else } else { - focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + focusColor = kColorBrightGreen; } } @@ -2987,6 +5179,27 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImGui::TextDisabled("[Focus]"); ImGui::SameLine(); + // Raid mark icon (star, circle, diamond, …) preceding the name + { + static constexpr struct { const char* sym; ImU32 col; } kFocusMarks[] = { + { "\xe2\x98\x85", IM_COL32(255, 204, 0, 255) }, // 0 Star (yellow) + { "\xe2\x97\x8f", IM_COL32(255, 103, 0, 255) }, // 1 Circle (orange) + { "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, // 2 Diamond (purple) + { "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, // 3 Triangle (green) + { "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, // 4 Moon (blue) + { "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, // 5 Square (teal) + { "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, // 6 Cross (red) + { "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, // 7 Skull (white) + }; + uint8_t fmark = gameHandler.getEntityRaidMark(focus->getGuid()); + if (fmark < game::GameHandler::kRaidMarkCount) { + ImGui::GetWindowDrawList()->AddText( + ImGui::GetCursorScreenPos(), + kFocusMarks[fmark].col, kFocusMarks[fmark].sym); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 18.0f); + } + } + std::string focusName = getEntityName(focus); ImGui::PushStyleColor(ImGuiCol_Text, focusColor); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0)); @@ -2996,6 +5209,117 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImVec2(ImGui::CalcTextSize(focusName.c_str()).x, 0)); ImGui::PopStyleColor(4); + // Right-click context menu on focus frame + if (ImGui::BeginPopupContextItem("##FocusFrameCtx")) { + ImGui::TextDisabled("%s", focusName.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem("Target")) + gameHandler.setTarget(focus->getGuid()); + if (ImGui::MenuItem("Clear Focus")) + gameHandler.clearFocus(); + if (focus->getType() == game::ObjectType::PLAYER) { + ImGui::Separator(); + if (ImGui::MenuItem("Whisper")) { + selectedChatType = 4; + strncpy(whisperTargetBuffer, focusName.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + refocusChatInput = true; + } + if (ImGui::MenuItem("Invite to Group")) + gameHandler.inviteToGroup(focusName); + if (ImGui::MenuItem("Trade")) + gameHandler.initiateTrade(focus->getGuid()); + if (ImGui::MenuItem("Duel")) + gameHandler.proposeDuel(focus->getGuid()); + if (ImGui::MenuItem("Inspect")) { + gameHandler.setTarget(focus->getGuid()); + gameHandler.inspectTarget(); + showInspectWindow_ = true; + } + ImGui::Separator(); + if (ImGui::MenuItem("Add Friend")) + gameHandler.addFriend(focusName); + if (ImGui::MenuItem("Ignore")) + gameHandler.addIgnore(focusName); + } + ImGui::EndPopup(); + } + + // Group leader crown — golden ♛ when the focused player is the party/raid leader + if (gameHandler.isInGroup() && focus->getType() == game::ObjectType::PLAYER) { + if (gameHandler.getPartyData().leaderGuid == focus->getGuid()) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), "\xe2\x99\x9b"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Group Leader"); + } + } + + // Quest giver indicator and classification badge for NPC focus targets + if (focus->getType() == game::ObjectType::UNIT) { + auto focusUnit = std::static_pointer_cast(focus); + + // Quest indicator: ! / ? + { + using QGS = game::QuestGiverStatus; + QGS qgs = gameHandler.getQuestGiverStatus(focus->getGuid()); + if (qgs == QGS::AVAILABLE) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "!"); + } else if (qgs == QGS::AVAILABLE_LOW) { + ImGui::SameLine(0, 4); + ImGui::TextColored(kColorGray, "!"); + } else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "?"); + } else if (qgs == QGS::INCOMPLETE) { + ImGui::SameLine(0, 4); + ImGui::TextColored(kColorGray, "?"); + } + } + + // Classification badge + int fRank = gameHandler.getCreatureRank(focusUnit->getEntry()); + if (fRank == 1) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(1.0f,0.8f,0.2f,1.0f), "[Elite]"); } + else if (fRank == 2) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(0.8f,0.4f,1.0f,1.0f), "[Rare Elite]"); } + else if (fRank == 3) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(1.0f,0.3f,0.3f,1.0f), "[Boss]"); } + else if (fRank == 4) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(0.5f,0.9f,1.0f,1.0f), "[Rare]"); } + + // Creature type + { + uint32_t fctype = gameHandler.getCreatureType(focusUnit->getEntry()); + const char* fctName = nullptr; + switch (fctype) { + case 1: fctName="Beast"; break; case 2: fctName="Dragonkin"; break; + case 3: fctName="Demon"; break; case 4: fctName="Elemental"; break; + case 5: fctName="Giant"; break; case 6: fctName="Undead"; break; + case 7: fctName="Humanoid"; break; case 8: fctName="Critter"; break; + case 9: fctName="Mechanical"; break; case 11: fctName="Totem"; break; + case 12: fctName="Non-combat Pet"; break; case 13: fctName="Gas Cloud"; break; + default: break; + } + if (fctName) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 0.9f), "(%s)", fctName); + } + } + + // Creature subtitle + const std::string fSub = gameHandler.getCachedCreatureSubName(focusUnit->getEntry()); + if (!fSub.empty()) + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", fSub.c_str()); + } + + // Player guild name on focus frame + if (focus->getType() == game::ObjectType::PLAYER) { + uint32_t guildId = gameHandler.getEntityGuildId(focus->getGuid()); + if (guildId != 0) { + const std::string& gn = gameHandler.lookupGuildName(guildId); + if (!gn.empty()) { + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", gn.c_str()); + } + } + } + if (ImGui::BeginPopupContextItem("##FocusNameCtx")) { const bool focusIsPlayer = (focus->getType() == game::ObjectType::PLAYER); const uint64_t fGuid = focus->getGuid(); @@ -3039,7 +5363,10 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { // Level + health on same row ImGui::SameLine(); - ImGui::TextDisabled("Lv %u", unit->getLevel()); + if (unit->getLevel() == 0) + ImGui::TextDisabled("Lv ??"); + else + ImGui::TextDisabled("Lv %u", unit->getLevel()); uint32_t hp = unit->getHealth(); uint32_t maxHp = unit->getMaxHealth(); @@ -3082,17 +5409,236 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { float rem = focusCast->timeRemaining; float prog = std::clamp(1.0f - rem / total, 0.f, 1.f); const std::string& spName = gameHandler.getSpellName(focusCast->spellId); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f)); + // Pulse orange when > 80% complete — interrupt window closing + ImVec4 focusCastColor; + if (prog > 0.8f) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); + focusCastColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f); + } else { + focusCastColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f); + } + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, focusCastColor); char castBuf[64]; if (!spName.empty()) snprintf(castBuf, sizeof(castBuf), "%s (%.1fs)", spName.c_str(), rem); else snprintf(castBuf, sizeof(castBuf), "Casting... (%.1fs)", rem); - ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf); + { + auto* fcAsset = core::Application::getInstance().getAssetManager(); + VkDescriptorSet fcIcon = (focusCast->spellId != 0 && fcAsset) + ? getSpellIcon(focusCast->spellId, fcAsset) : VK_NULL_HANDLE; + if (fcIcon) { + ImGui::Image((ImTextureID)(uintptr_t)fcIcon, ImVec2(12, 12)); + ImGui::SameLine(0, 2); + ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf); + } else { + ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf); + } + } ImGui::PopStyleColor(); } } + // Focus auras — buffs first, then debuffs, up to 8 icons wide + { + const std::vector* focusAuras = + (focus->getGuid() == gameHandler.getTargetGuid()) + ? &gameHandler.getTargetAuras() + : gameHandler.getUnitAuras(focus->getGuid()); + + if (focusAuras) { + int activeCount = 0; + for (const auto& a : *focusAuras) if (!a.isEmpty()) activeCount++; + if (activeCount > 0) { + auto* focusAsset = core::Application::getInstance().getAssetManager(); + constexpr float FA_ICON = 20.0f; + constexpr int FA_PER_ROW = 10; + + ImGui::Separator(); + + uint64_t faNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + // Sort: debuffs first (so hostile-caster info is prominent), then buffs + std::vector faIdx; + faIdx.reserve(focusAuras->size()); + for (size_t i = 0; i < focusAuras->size(); ++i) + if (!(*focusAuras)[i].isEmpty()) faIdx.push_back(i); + std::sort(faIdx.begin(), faIdx.end(), [&](size_t a, size_t b) { + bool aD = ((*focusAuras)[a].flags & 0x80) != 0; + bool bD = ((*focusAuras)[b].flags & 0x80) != 0; + if (aD != bD) return aD > bD; // debuffs first + int32_t ra = (*focusAuras)[a].getRemainingMs(faNowMs); + int32_t rb = (*focusAuras)[b].getRemainingMs(faNowMs); + if (ra < 0 && rb < 0) return false; + if (ra < 0) return false; + if (rb < 0) return true; + return ra < rb; + }); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); + int faShown = 0; + for (size_t si = 0; si < faIdx.size() && faShown < 20; ++si) { + const auto& aura = (*focusAuras)[faIdx[si]]; + bool isBuff = (aura.flags & 0x80) == 0; + + if (faShown > 0 && faShown % FA_PER_ROW != 0) ImGui::SameLine(); + ImGui::PushID(static_cast(faIdx[si]) + 3000); + + ImVec4 borderCol; + if (isBuff) { + borderCol = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); + } else { + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + switch (dt) { + case 1: borderCol = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; + case 2: borderCol = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; + case 3: borderCol = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; + case 4: borderCol = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; + default: borderCol = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; + } + } + + VkDescriptorSet faIcon = (focusAsset) + ? getSpellIcon(aura.spellId, focusAsset) : VK_NULL_HANDLE; + if (faIcon) { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::ImageButton("##faura", + (ImTextureID)(uintptr_t)faIcon, + ImVec2(FA_ICON - 2, FA_ICON - 2)); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + char lab[8]; + snprintf(lab, sizeof(lab), "%u", aura.spellId); + ImGui::Button(lab, ImVec2(FA_ICON, FA_ICON)); + ImGui::PopStyleColor(); + } + + // Duration overlay + int32_t faRemain = aura.getRemainingMs(faNowMs); + if (faRemain > 0) { + ImVec2 imin = ImGui::GetItemRectMin(); + ImVec2 imax = ImGui::GetItemRectMax(); + char ts[12]; + int s = (faRemain + 999) / 1000; + if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600); + else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60); + else snprintf(ts, sizeof(ts), "%d", s); + ImVec2 tsz = ImGui::CalcTextSize(ts); + float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f; + float cy = imax.y - tsz.y - 1.0f; + ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts); + ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts); + } + + // Stack / charge count — upper-left corner (parity with target frame) + if (aura.charges > 1) { + ImVec2 faMin = ImGui::GetItemRectMin(); + char chargeStr[8]; + snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast(aura.charges)); + ImGui::GetWindowDrawList()->AddText(ImVec2(faMin.x + 3, faMin.y + 3), + IM_COL32(0, 0, 0, 200), chargeStr); + ImGui::GetWindowDrawList()->AddText(ImVec2(faMin.x + 2, faMin.y + 2), + IM_COL32(255, 220, 50, 255), chargeStr); + } + + // Tooltip + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + bool richOk = spellbookScreen.renderSpellInfoTooltip( + aura.spellId, gameHandler, focusAsset); + if (!richOk) { + std::string nm = spellbookScreen.lookupSpellName(aura.spellId, focusAsset); + if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId); + ImGui::Text("%s", nm.c_str()); + } + if (faRemain > 0) { + int s = faRemain / 1000; + char db[32]; + if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s); + else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60); + ImGui::TextColored(ui::colors::kLightGray, "%s", db); + } + ImGui::EndTooltip(); + } + + ImGui::PopID(); + faShown++; + } + ImGui::PopStyleVar(); + } + } + } + + // Target-of-Focus: who the focus target is currently targeting + { + uint64_t fofGuid = 0; + const auto& fFields = focus->getFields(); + auto fItLo = fFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (fItLo != fFields.end()) { + fofGuid = fItLo->second; + auto fItHi = fFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (fItHi != fFields.end()) + fofGuid |= (static_cast(fItHi->second) << 32); + } + if (fofGuid != 0) { + auto fofEnt = gameHandler.getEntityManager().getEntity(fofGuid); + std::string fofName; + ImVec4 fofColor(0.7f, 0.7f, 0.7f, 1.0f); + if (fofGuid == gameHandler.getPlayerGuid()) { + fofName = "You"; + fofColor = kColorBrightGreen; + } else if (fofEnt) { + fofName = getEntityName(fofEnt); + uint8_t fcid = entityClassId(fofEnt.get()); + if (fcid != 0) fofColor = classColorVec4(fcid); + } + if (!fofName.empty()) { + ImGui::TextDisabled("▶"); + ImGui::SameLine(0, 2); + ImGui::TextColored(fofColor, "%s", fofName.c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Focus's target: %s\nClick to target", fofName.c_str()); + if (ImGui::IsItemClicked()) + gameHandler.setTarget(fofGuid); + + // Compact health bar for target-of-focus + if (fofEnt) { + auto fofUnit = std::dynamic_pointer_cast(fofEnt); + if (fofUnit && fofUnit->getMaxHealth() > 0) { + float fofPct = static_cast(fofUnit->getHealth()) / + static_cast(fofUnit->getMaxHealth()); + ImVec4 fofBarColor = + fofPct > 0.5f ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) : + fofPct > 0.2f ? ImVec4(0.75f, 0.75f, 0.2f, 1.0f) : + ImVec4(0.75f, 0.2f, 0.2f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, fofBarColor); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); + char fofOverlay[32]; + snprintf(fofOverlay, sizeof(fofOverlay), "%u%%", + static_cast(fofPct * 100.0f + 0.5f)); + ImGui::ProgressBar(fofPct, ImVec2(-1, 10), fofOverlay); + ImGui::PopStyleColor(2); + } + } + } + } + } + + // Distance to focus target + { + const auto& mv = gameHandler.getMovementInfo(); + float fdx = focus->getX() - mv.x; + float fdy = focus->getY() - mv.y; + float fdz = focus->getZ() - mv.z; + float fdist = std::sqrt(fdx * fdx + fdy * fdy + fdz * fdz); + ImGui::TextDisabled("%.1f yd", fdist); + } + // Clicking the focus frame targets it if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0)) { gameHandler.setTarget(focus->getGuid()); @@ -3104,6 +5650,404 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +// Returns the first executable line of a macro text block, skipping blank lines +// and # directive lines (e.g. #showtooltip). Returns empty string if none found. +static std::string firstMacroCommand(const std::string& macroText) { + size_t pos = 0; + while (pos <= macroText.size()) { + size_t nl = macroText.find('\n', pos); + std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t start = line.find_first_not_of(" \t"); + if (start != std::string::npos) line = line.substr(start); + if (!line.empty() && line.front() != '#') + return line; + if (nl == std::string::npos) break; + pos = nl + 1; + } + return {}; +} + +// Collect all non-comment, non-empty lines from a macro body. +static std::vector allMacroCommands(const std::string& macroText) { + std::vector cmds; + size_t pos = 0; + while (pos <= macroText.size()) { + size_t nl = macroText.find('\n', pos); + std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t start = line.find_first_not_of(" \t"); + if (start != std::string::npos) line = line.substr(start); + if (!line.empty() && line.front() != '#') + cmds.push_back(std::move(line)); + if (nl == std::string::npos) break; + pos = nl + 1; + } + return cmds; +} + +// Returns the #showtooltip argument from a macro body: +// "#showtooltip Spell" → "Spell" +// "#showtooltip" → "__auto__" (derive from first /cast) +// (none) → "" +static std::string getMacroShowtooltipArg(const std::string& macroText) { + size_t pos = 0; + while (pos <= macroText.size()) { + size_t nl = macroText.find('\n', pos); + std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos); + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t fs = line.find_first_not_of(" \t"); + if (fs != std::string::npos) line = line.substr(fs); + if (line.rfind("#showtooltip", 0) == 0 || line.rfind("#show", 0) == 0) { + size_t sp = line.find(' '); + if (sp != std::string::npos) { + std::string arg = line.substr(sp + 1); + size_t as = arg.find_first_not_of(" \t"); + if (as != std::string::npos) arg = arg.substr(as); + size_t ae = arg.find_last_not_of(" \t"); + if (ae != std::string::npos) arg.resize(ae + 1); + if (!arg.empty()) return arg; + } + return "__auto__"; + } + if (nl == std::string::npos) break; + pos = nl + 1; + } + return {}; +} + +// --------------------------------------------------------------------------- +// WoW macro conditional evaluator +// Parses: [cond1,cond2] Spell1; [cond3] Spell2; DefaultSpell +// Returns the first matching alternative's argument, or "" if none matches. +// targetOverride is set to a specific GUID if [target=X] was in the conditions, +// or left as UINT64_MAX to mean "use the normal target". +// --------------------------------------------------------------------------- +static std::string evaluateMacroConditionals(const std::string& rawArg, + game::GameHandler& gameHandler, + uint64_t& targetOverride) { + targetOverride = static_cast(-1); + + auto& input = core::Input::getInstance(); + + const bool shiftHeld = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || + input.isKeyPressed(SDL_SCANCODE_RSHIFT); + const bool ctrlHeld = input.isKeyPressed(SDL_SCANCODE_LCTRL) || + input.isKeyPressed(SDL_SCANCODE_RCTRL); + const bool altHeld = input.isKeyPressed(SDL_SCANCODE_LALT) || + input.isKeyPressed(SDL_SCANCODE_RALT); + const bool anyMod = shiftHeld || ctrlHeld || altHeld; + + // Split rawArg on ';' → alternatives + std::vector alts; + { + std::string cur; + for (char c : rawArg) { + if (c == ';') { alts.push_back(cur); cur.clear(); } + else cur += c; + } + alts.push_back(cur); + } + + // Evaluate a single comma-separated condition token. + // tgt is updated if a target= or @ specifier is found. + auto evalCond = [&](const std::string& raw, uint64_t& tgt) -> bool { + std::string c = raw; + // trim + size_t s = c.find_first_not_of(" \t"); if (s) c = (s != std::string::npos) ? c.substr(s) : ""; + size_t e = c.find_last_not_of(" \t"); if (e != std::string::npos) c.resize(e + 1); + if (c.empty()) return true; + + // @target specifiers: @player, @focus, @pet, @mouseover, @target + if (!c.empty() && c[0] == '@') { + std::string spec = c.substr(1); + if (spec == "player") tgt = gameHandler.getPlayerGuid(); + else if (spec == "focus") tgt = gameHandler.getFocusGuid(); + else if (spec == "target") tgt = gameHandler.getTargetGuid(); + else if (spec == "pet") { + uint64_t pg = gameHandler.getPetGuid(); + if (pg != 0) tgt = pg; + else return false; // no pet — skip this alternative + } + else if (spec == "mouseover") { + uint64_t mo = gameHandler.getMouseoverGuid(); + if (mo != 0) tgt = mo; + else return false; // no mouseover — skip this alternative + } + return true; + } + // target=X specifiers + if (c.rfind("target=", 0) == 0) { + std::string spec = c.substr(7); + if (spec == "player") tgt = gameHandler.getPlayerGuid(); + else if (spec == "focus") tgt = gameHandler.getFocusGuid(); + else if (spec == "target") tgt = gameHandler.getTargetGuid(); + else if (spec == "pet") { + uint64_t pg = gameHandler.getPetGuid(); + if (pg != 0) tgt = pg; + else return false; // no pet — skip this alternative + } + else if (spec == "mouseover") { + uint64_t mo = gameHandler.getMouseoverGuid(); + if (mo != 0) tgt = mo; + else return false; // no mouseover — skip this alternative + } + return true; + } + + // mod / nomod + if (c == "nomod" || c == "mod:none") return !anyMod; + if (c.rfind("mod:", 0) == 0) { + std::string mods = c.substr(4); + bool ok = true; + if (mods.find("shift") != std::string::npos && !shiftHeld) ok = false; + if (mods.find("ctrl") != std::string::npos && !ctrlHeld) ok = false; + if (mods.find("alt") != std::string::npos && !altHeld) ok = false; + return ok; + } + + // combat / nocombat + if (c == "combat") return gameHandler.isInCombat(); + if (c == "nocombat") return !gameHandler.isInCombat(); + + // Helper to get the effective target entity + auto effTarget = [&]() -> std::shared_ptr { + if (tgt != static_cast(-1) && tgt != 0) + return gameHandler.getEntityManager().getEntity(tgt); + return gameHandler.getTarget(); + }; + + // exists / noexists + if (c == "exists") return effTarget() != nullptr; + if (c == "noexists") return effTarget() == nullptr; + + // dead / nodead + if (c == "dead") { + auto t = effTarget(); + auto u = t ? std::dynamic_pointer_cast(t) : nullptr; + return u && u->getHealth() == 0; + } + if (c == "nodead") { + auto t = effTarget(); + auto u = t ? std::dynamic_pointer_cast(t) : nullptr; + return u && u->getHealth() > 0; + } + + // help (friendly) / harm (hostile) and their no- variants + auto unitHostile = [&](const std::shared_ptr& t) -> bool { + if (!t) return false; + auto u = std::dynamic_pointer_cast(t); + return u && gameHandler.isHostileFactionPublic(u->getFactionTemplate()); + }; + if (c == "harm" || c == "nohelp") { return unitHostile(effTarget()); } + if (c == "help" || c == "noharm") { return !unitHostile(effTarget()); } + + // mounted / nomounted + if (c == "mounted") return gameHandler.isMounted(); + if (c == "nomounted") return !gameHandler.isMounted(); + + // swimming / noswimming + if (c == "swimming") return gameHandler.isSwimming(); + if (c == "noswimming") return !gameHandler.isSwimming(); + + // flying / noflying (CAN_FLY + FLYING flags active) + if (c == "flying") return gameHandler.isPlayerFlying(); + if (c == "noflying") return !gameHandler.isPlayerFlying(); + + // channeling / nochanneling + if (c == "channeling") return gameHandler.isCasting() && gameHandler.isChanneling(); + if (c == "nochanneling") return !(gameHandler.isCasting() && gameHandler.isChanneling()); + + // stealthed / nostealthed (unit flag 0x02000000 = UNIT_FLAG_SNEAKING) + auto isStealthedFn = [&]() -> bool { + auto pe = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); + if (!pe) return false; + auto pu = std::dynamic_pointer_cast(pe); + return pu && (pu->getUnitFlags() & 0x02000000u) != 0; + }; + if (c == "stealthed") return isStealthedFn(); + if (c == "nostealthed") return !isStealthedFn(); + + // pet / nopet — player has an active pet (hunters, warlocks, DKs) + if (c == "pet") return gameHandler.hasPet(); + if (c == "nopet") return !gameHandler.hasPet(); + + // indoors / outdoors — WMO interior detection (affects mount type selection) + if (c == "indoors" || c == "nooutdoors") { + auto* r = core::Application::getInstance().getRenderer(); + return r && r->isPlayerIndoors(); + } + if (c == "outdoors" || c == "noindoors") { + auto* r = core::Application::getInstance().getRenderer(); + return !r || !r->isPlayerIndoors(); + } + + // group / nogroup — player is in a party or raid + if (c == "group" || c == "party") return gameHandler.isInGroup(); + if (c == "nogroup") return !gameHandler.isInGroup(); + + // raid / noraid — player is in a raid group (groupType == 1) + if (c == "raid") return gameHandler.isInGroup() && gameHandler.getPartyData().groupType == 1; + if (c == "noraid") return !gameHandler.isInGroup() || gameHandler.getPartyData().groupType != 1; + + // spec:N — active talent spec (1-based: spec:1 = primary, spec:2 = secondary) + if (c.rfind("spec:", 0) == 0) { + uint8_t wantSpec = 0; + try { wantSpec = static_cast(std::stoul(c.substr(5))); } catch (...) {} + return wantSpec > 0 && gameHandler.getActiveTalentSpec() == (wantSpec - 1); + } + + // noform / nostance — player is NOT in a shapeshift/stance + if (c == "noform" || c == "nostance") { + for (const auto& a : gameHandler.getPlayerAuras()) + if (!a.isEmpty() && a.maxDurationMs == -1) return false; + return true; + } + // form:0 same as noform + if (c == "form:0" || c == "stance:0") { + for (const auto& a : gameHandler.getPlayerAuras()) + if (!a.isEmpty() && a.maxDurationMs == -1) return false; + return true; + } + + // buff:SpellName / nobuff:SpellName — check if the effective target (or player + // if no target specified) has a buff with the given name. + // debuff:SpellName / nodebuff:SpellName — same for debuffs (harmful auras). + auto checkAuraByName = [&](const std::string& spellName, bool wantDebuff, + bool negate) -> bool { + // Determine which aura list to check: effective target or player + const std::vector* auras = nullptr; + if (tgt != static_cast(-1) && tgt != 0 && tgt != gameHandler.getPlayerGuid()) { + // Check target's auras + auras = &gameHandler.getTargetAuras(); + } else { + auras = &gameHandler.getPlayerAuras(); + } + std::string nameLow = spellName; + for (char& ch : nameLow) ch = static_cast(std::tolower(static_cast(ch))); + for (const auto& a : *auras) { + if (a.isEmpty() || a.spellId == 0) continue; + // Filter: debuffs have the HARMFUL flag (0x80) or spell has a dispel type + bool isDebuff = (a.flags & 0x80) != 0; + if (wantDebuff ? !isDebuff : isDebuff) continue; + std::string sn = gameHandler.getSpellName(a.spellId); + for (char& ch : sn) ch = static_cast(std::tolower(static_cast(ch))); + if (sn == nameLow) return !negate; + } + return negate; + }; + if (c.rfind("buff:", 0) == 0 && c.size() > 5) + return checkAuraByName(c.substr(5), false, false); + if (c.rfind("nobuff:", 0) == 0 && c.size() > 7) + return checkAuraByName(c.substr(7), false, true); + if (c.rfind("debuff:", 0) == 0 && c.size() > 7) + return checkAuraByName(c.substr(7), true, false); + if (c.rfind("nodebuff:", 0) == 0 && c.size() > 9) + return checkAuraByName(c.substr(9), true, true); + + // mounted / nomounted + if (c == "mounted") return gameHandler.isMounted(); + if (c == "nomounted") return !gameHandler.isMounted(); + + // group (any group) / nogroup / raid + if (c == "group") return !gameHandler.getPartyData().isEmpty(); + if (c == "nogroup") return gameHandler.getPartyData().isEmpty(); + if (c == "raid") { + const auto& pd = gameHandler.getPartyData(); + return pd.groupType >= 1; // groupType 1 = raid, 0 = party + } + + // channeling:SpellName — player is currently channeling that spell + if (c.rfind("channeling:", 0) == 0 && c.size() > 11) { + if (!gameHandler.isChanneling()) return false; + std::string want = c.substr(11); + for (char& ch : want) ch = static_cast(std::tolower(static_cast(ch))); + uint32_t castSpellId = gameHandler.getCurrentCastSpellId(); + std::string sn = gameHandler.getSpellName(castSpellId); + for (char& ch : sn) ch = static_cast(std::tolower(static_cast(ch))); + return sn == want; + } + if (c == "channeling") return gameHandler.isChanneling(); + if (c == "nochanneling") return !gameHandler.isChanneling(); + + // casting (any active cast or channel) + if (c == "casting") return gameHandler.isCasting(); + if (c == "nocasting") return !gameHandler.isCasting(); + + // vehicle / novehicle (WotLK) + if (c == "vehicle") return gameHandler.getVehicleId() != 0; + if (c == "novehicle") return gameHandler.getVehicleId() == 0; + + // Unknown → permissive (don't block) + return true; + }; + + for (auto& alt : alts) { + // trim + size_t fs = alt.find_first_not_of(" \t"); + if (fs == std::string::npos) continue; + alt = alt.substr(fs); + size_t ls = alt.find_last_not_of(" \t"); + if (ls != std::string::npos) alt.resize(ls + 1); + + if (!alt.empty() && alt[0] == '[') { + size_t close = alt.find(']'); + if (close == std::string::npos) continue; + std::string condStr = alt.substr(1, close - 1); + std::string argPart = alt.substr(close + 1); + // Trim argPart + size_t as = argPart.find_first_not_of(" \t"); + argPart = (as != std::string::npos) ? argPart.substr(as) : ""; + + // Evaluate comma-separated conditions + uint64_t tgt = static_cast(-1); + bool pass = true; + size_t cp = 0; + while (pass) { + size_t comma = condStr.find(',', cp); + std::string tok = condStr.substr(cp, comma == std::string::npos ? std::string::npos : comma - cp); + if (!evalCond(tok, tgt)) { pass = false; break; } + if (comma == std::string::npos) break; + cp = comma + 1; + } + if (pass) { + if (tgt != static_cast(-1)) targetOverride = tgt; + return argPart; + } + } else { + // No condition block — default fallback always matches + return alt; + } + } + return {}; +} + +// Execute all non-comment lines of a macro body in sequence. +// In WoW, every line executes per click; the server enforces spell-cast limits. +// /stopmacro (with optional conditionals) halts the remaining commands early. +void GameScreen::executeMacroText(game::GameHandler& gameHandler, const std::string& macroText) { + macroStopped_ = false; + for (const auto& cmd : allMacroCommands(macroText)) { + strncpy(chatInputBuffer, cmd.c_str(), sizeof(chatInputBuffer) - 1); + chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0'; + sendChatMessage(gameHandler); + if (macroStopped_) break; + } + macroStopped_ = false; +} + +// /castsequence persistent state — shared across all macros using the same spell list. +// Keyed by the normalized (lowercase, comma-joined) spell sequence string. +namespace { +struct CastSeqState { + size_t index = 0; + float lastPressSec = 0.0f; + uint64_t lastTargetGuid = 0; + bool lastInCombat = false; +}; +std::unordered_map s_castSeqStates; +} // namespace + void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { if (strlen(chatInputBuffer) > 0) { std::string input(chatInputBuffer); @@ -3140,6 +6084,57 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { std::string cmdLower = cmd; for (char& c : cmdLower) c = std::tolower(c); + // /run — execute Lua script via addon system + if ((cmdLower == "run" || cmdLower == "script") && spacePos != std::string::npos) { + std::string luaCode = command.substr(spacePos + 1); + auto* am = core::Application::getInstance().getAddonManager(); + if (am) { + am->runScript(luaCode); + } else { + gameHandler.addUIError("Addon system not initialized."); + } + chatInputBuffer[0] = '\0'; + return; + } + + // /dump — evaluate Lua expression and print result + if ((cmdLower == "dump" || cmdLower == "print") && spacePos != std::string::npos) { + std::string expr = command.substr(spacePos + 1); + auto* am = core::Application::getInstance().getAddonManager(); + if (am && am->isInitialized()) { + // Wrap expression in print(tostring(...)) to display the value + std::string wrapped = "local __v = " + expr + + "; if type(__v) == 'table' then " + " local parts = {} " + " for k,v in pairs(__v) do parts[#parts+1] = tostring(k)..'='..tostring(v) end " + " print('{' .. table.concat(parts, ', ') .. '}') " + "else print(tostring(__v)) end"; + am->runScript(wrapped); + } else { + game::MessageChatData errMsg; + errMsg.type = game::ChatType::SYSTEM; + errMsg.language = game::ChatLanguage::UNIVERSAL; + errMsg.message = "Addon system not initialized."; + gameHandler.addLocalChatMessage(errMsg); + } + chatInputBuffer[0] = '\0'; + return; + } + + // Check addon slash commands (SlashCmdList) before built-in commands + { + auto* am = core::Application::getInstance().getAddonManager(); + if (am && am->isInitialized()) { + std::string slashCmd = "/" + cmdLower; + std::string slashArgs; + if (spacePos != std::string::npos) slashArgs = command.substr(spacePos + 1); + if (am->getLuaEngine()->dispatchSlashCommand(slashCmd, slashArgs)) { + chatInputBuffer[0] = '\0'; + return; + } + } + } + // Special commands if (cmdLower == "logout") { core::Application::getInstance().logoutToLogin(); @@ -3147,6 +6142,59 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + if (cmdLower == "clear") { + gameHandler.clearChatHistory(); + chatInputBuffer[0] = '\0'; + return; + } + + // /reload or /reloadui — reload all addons (save variables, re-init Lua, re-scan .toc files) + if (cmdLower == "reload" || cmdLower == "reloadui" || cmdLower == "rl") { + auto* am = core::Application::getInstance().getAddonManager(); + if (am) { + am->reload(); + am->fireEvent("VARIABLES_LOADED"); + am->fireEvent("PLAYER_LOGIN"); + am->fireEvent("PLAYER_ENTERING_WORLD"); + game::MessageChatData rlMsg; + rlMsg.type = game::ChatType::SYSTEM; + rlMsg.language = game::ChatLanguage::UNIVERSAL; + rlMsg.message = "Interface reloaded."; + gameHandler.addLocalChatMessage(rlMsg); + } else { + game::MessageChatData rlMsg; + rlMsg.type = game::ChatType::SYSTEM; + rlMsg.language = game::ChatLanguage::UNIVERSAL; + rlMsg.message = "Addon system not available."; + gameHandler.addLocalChatMessage(rlMsg); + } + chatInputBuffer[0] = '\0'; + return; + } + + // /stopmacro [conditions] + // Halts execution of the current macro (remaining lines are skipped). + // With a condition block, only stops if the conditions evaluate to true. + // /stopmacro → always stops + // /stopmacro [combat] → stops only while in combat + // /stopmacro [nocombat] → stops only when not in combat + if (cmdLower == "stopmacro") { + bool shouldStop = true; + if (spacePos != std::string::npos) { + std::string condArg = command.substr(spacePos + 1); + while (!condArg.empty() && condArg.front() == ' ') condArg.erase(condArg.begin()); + if (!condArg.empty() && condArg.front() == '[') { + // Append a sentinel action so evaluateMacroConditionals can signal a match. + uint64_t tgtOver = static_cast(-1); + std::string hit = evaluateMacroConditionals(condArg + " __stop__", gameHandler, tgtOver); + shouldStop = !hit.empty(); + } + } + if (shouldStop) macroStopped_ = true; + chatInputBuffer[0] = '\0'; + return; + } + // /invite command if (cmdLower == "invite" && spacePos != std::string::npos) { std::string targetName = command.substr(spacePos + 1); @@ -3170,6 +6218,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /score command — BG scoreboard + if (cmdLower == "score") { + gameHandler.requestPvpLog(); + showBgScoreboard_ = true; + chatInputBuffer[0] = '\0'; + return; + } + // /time command if (cmdLower == "time") { gameHandler.queryServerTime(); @@ -3177,6 +6233,47 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /loc command — print player coordinates and zone name + if (cmdLower == "loc" || cmdLower == "coords" || cmdLower == "whereami") { + const auto& pmi = gameHandler.getMovementInfo(); + std::string zoneName; + if (auto* rend = core::Application::getInstance().getRenderer()) + zoneName = rend->getCurrentZoneName(); + char buf[256]; + snprintf(buf, sizeof(buf), "%.1f, %.1f, %.1f%s%s", + pmi.x, pmi.y, pmi.z, + zoneName.empty() ? "" : " — ", + zoneName.c_str()); + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = buf; + gameHandler.addLocalChatMessage(sysMsg); + chatInputBuffer[0] = '\0'; + return; + } + + // /screenshot command — capture current frame to PNG + if (cmdLower == "screenshot" || cmdLower == "ss") { + takeScreenshot(gameHandler); + chatInputBuffer[0] = '\0'; + return; + } + + // /zone command — print current zone name + if (cmdLower == "zone") { + std::string zoneName; + if (auto* rend = core::Application::getInstance().getRenderer()) + zoneName = rend->getCurrentZoneName(); + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = zoneName.empty() ? "You are not in a known zone." : "You are in: " + zoneName; + gameHandler.addLocalChatMessage(sysMsg); + chatInputBuffer[0] = '\0'; + return; + } + // /played command if (cmdLower == "played") { gameHandler.requestPlayedTime(); @@ -3191,24 +6288,86 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /chathelp command — list chat-channel slash commands + if (cmdLower == "chathelp") { + static const char* kChatHelp[] = { + "--- Chat Channel Commands ---", + "/s [msg] Say to nearby players", + "/y [msg] Yell to a wider area", + "/w [msg] Whisper to player", + "/r [msg] Reply to last whisper", + "/p [msg] Party chat", + "/g [msg] Guild chat", + "/o [msg] Guild officer chat", + "/raid [msg] Raid chat", + "/rw [msg] Raid warning", + "/bg [msg] Battleground chat", + "/1 [msg] General channel", + "/2 [msg] Trade channel (also /wts /wtb)", + "/ [msg] Channel by number", + "/join Join a channel", + "/leave Leave a channel", + "/afk [msg] Set AFK status", + "/dnd [msg] Set Do Not Disturb", + }; + for (const char* line : kChatHelp) { + game::MessageChatData helpMsg; + helpMsg.type = game::ChatType::SYSTEM; + helpMsg.language = game::ChatLanguage::UNIVERSAL; + helpMsg.message = line; + gameHandler.addLocalChatMessage(helpMsg); + } + chatInputBuffer[0] = '\0'; + return; + } + + // /macrohelp command — list available macro conditionals + if (cmdLower == "macrohelp") { + static const char* kMacroHelp[] = { + "--- Macro Conditionals ---", + "Usage: /cast [cond1,cond2] Spell1; [cond3] Spell2; Default", + "State: [combat] [mounted] [swimming] [flying] [stealthed]", + " [channeling] [pet] [group] [raid] [indoors] [outdoors]", + "Spec: [spec:1] [spec:2] (active talent spec, 1-based)", + " (prefix no- to negate any condition)", + "Target: [harm] [help] [exists] [noexists] [dead] [nodead]", + " [target=focus] [target=pet] [target=mouseover] [target=player]", + " (also: @focus, @pet, @mouseover, @player, @target)", + "Form: [noform] [nostance] [form:0]", + "Keys: [mod:shift] [mod:ctrl] [mod:alt]", + "Aura: [buff:Name] [nobuff:Name] [debuff:Name] [nodebuff:Name]", + "Other: #showtooltip, /stopmacro [cond], /castsequence", + }; + for (const char* line : kMacroHelp) { + game::MessageChatData m; + m.type = game::ChatType::SYSTEM; + m.language = game::ChatLanguage::UNIVERSAL; + m.message = line; + gameHandler.addLocalChatMessage(m); + } + chatInputBuffer[0] = '\0'; + return; + } + // /help command — list available slash commands if (cmdLower == "help" || cmdLower == "?") { static const char* kHelpLines[] = { "--- Wowee Slash Commands ---", - "Chat: /s /y /p /g /raid /rw /o /bg /w [msg] /r [msg]", - "Social: /who [filter] /whois /friend add/remove ", - " /ignore /unignore ", - "Party: /invite /uninvite /leave /readycheck", - " /maintank /mainassist /roll [min-max]", + "Chat: /s /y /p /g /raid /rw /o /bg /w /r /join /leave", + "Social: /who /friend add/remove /ignore /unignore", + "Party: /invite /uninvite /leave /readycheck /mark /roll", + " /maintank /mainassist /raidinfo", "Guild: /ginvite /gkick /gquit /gpromote /gdemote /gmotd", " /gleader /groster /ginfo /gcreate /gdisband", - "Combat: /startattack /stopattack /stopcasting /duel /pvp", - " /forfeit /follow /assist", - "Target: /target /cleartarget /focus /clearfocus", + "Combat: /cast /castsequence /use /startattack /stopattack", + " /stopcasting /duel /forfeit /pvp /assist", + " /follow /stopfollow /threat /combatlog", + "Items: /use /equip /equipset [name]", + "Target: /target /cleartarget /focus /clearfocus /inspect", "Movement: /sit /stand /kneel /dismount", - "Misc: /played /time /afk [msg] /dnd [msg] /inspect", - " /helm /cloak /trade /join /leave ", - " /unstuck /logout /ticket /help", + "Misc: /played /time /zone /loc /afk /dnd /helm /cloak", + " /trade /score /unstuck /logout /ticket /screenshot", + " /macrohelp /chathelp /help", }; for (const char* line : kHelpLines) { game::MessageChatData helpMsg; @@ -3257,6 +6416,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } gameHandler.queryWho(query); + showWhoWindow_ = true; + chatInputBuffer[0] = '\0'; + return; + } + + // /combatlog command + if (cmdLower == "combatlog" || cmdLower == "cl") { + showCombatLog_ = !showCombatLog_; chatInputBuffer[0] = '\0'; return; } @@ -3395,6 +6562,96 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // Pet control commands (common macro use) + // Action IDs: 1=passive, 2=follow, 3=stay, 4=defensive, 5=attack, 6=aggressive + if (cmdLower == "petattack") { + uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; + gameHandler.sendPetAction(5, target); + chatInputBuffer[0] = '\0'; + return; + } + if (cmdLower == "petfollow") { + gameHandler.sendPetAction(2, 0); + chatInputBuffer[0] = '\0'; + return; + } + if (cmdLower == "petstay" || cmdLower == "pethalt") { + gameHandler.sendPetAction(3, 0); + chatInputBuffer[0] = '\0'; + return; + } + if (cmdLower == "petpassive") { + gameHandler.sendPetAction(1, 0); + chatInputBuffer[0] = '\0'; + return; + } + if (cmdLower == "petdefensive") { + gameHandler.sendPetAction(4, 0); + chatInputBuffer[0] = '\0'; + return; + } + if (cmdLower == "petaggressive") { + gameHandler.sendPetAction(6, 0); + chatInputBuffer[0] = '\0'; + return; + } + if (cmdLower == "petdismiss") { + gameHandler.dismissPet(); + chatInputBuffer[0] = '\0'; + return; + } + + // /cancelform / /cancelshapeshift — leave current shapeshift/stance + if (cmdLower == "cancelform" || cmdLower == "cancelshapeshift") { + // Cancel the first permanent shapeshift aura the player has + for (const auto& aura : gameHandler.getPlayerAuras()) { + if (aura.spellId == 0) continue; + // Permanent shapeshift auras have the permanent flag (0x20) set + if (aura.flags & 0x20) { + gameHandler.cancelAura(aura.spellId); + break; + } + } + chatInputBuffer[0] = '\0'; + return; + } + + // /cancelaura — cancel a specific buff by name or ID + if (cmdLower == "cancelaura" && spacePos != std::string::npos) { + std::string auraArg = command.substr(spacePos + 1); + while (!auraArg.empty() && auraArg.front() == ' ') auraArg.erase(auraArg.begin()); + while (!auraArg.empty() && auraArg.back() == ' ') auraArg.pop_back(); + // Try numeric ID first + { + std::string numStr = auraArg; + if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin()); + bool isNum = !numStr.empty() && + std::all_of(numStr.begin(), numStr.end(), + [](unsigned char c){ return std::isdigit(c); }); + if (isNum) { + uint32_t spellId = 0; + try { spellId = static_cast(std::stoul(numStr)); } catch (...) {} + if (spellId) gameHandler.cancelAura(spellId); + chatInputBuffer[0] = '\0'; + return; + } + } + // Name match against player auras + std::string argLow = auraArg; + for (char& c : argLow) c = static_cast(std::tolower(static_cast(c))); + for (const auto& aura : gameHandler.getPlayerAuras()) { + if (aura.spellId == 0) continue; + std::string sn = gameHandler.getSpellName(aura.spellId); + for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); + if (sn == argLow) { + gameHandler.cancelAura(aura.spellId); + break; + } + } + chatInputBuffer[0] = '\0'; + return; + } + // /sit command if (cmdLower == "sit") { gameHandler.setStandState(1); // 1 = sit @@ -3451,9 +6708,88 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /stopfollow command + if (cmdLower == "stopfollow") { + gameHandler.cancelFollow(); + chatInputBuffer[0] = '\0'; + return; + } + // /assist command if (cmdLower == "assist") { - gameHandler.assistTarget(); + // /assist → assist current target (use their target) + // /assist PlayerName → find PlayerName, target their target + // /assist [target=X] → evaluate conditional, target that entity's target + auto assistEntityTarget = [&](uint64_t srcGuid) { + auto srcEnt = gameHandler.getEntityManager().getEntity(srcGuid); + if (!srcEnt) { gameHandler.assistTarget(); return; } + uint64_t atkGuid = 0; + const auto& flds = srcEnt->getFields(); + auto iLo = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (iLo != flds.end()) { + atkGuid = iLo->second; + auto iHi = flds.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (iHi != flds.end()) atkGuid |= (static_cast(iHi->second) << 32); + } + if (atkGuid != 0) { + gameHandler.setTarget(atkGuid); + } else { + std::string sn = getEntityName(srcEnt); + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = (sn.empty() ? "Target" : sn) + " has no target."; + gameHandler.addLocalChatMessage(msg); + } + }; + + if (spacePos != std::string::npos) { + std::string assistArg = command.substr(spacePos + 1); + while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin()); + + // Evaluate conditionals if present + uint64_t assistOver = static_cast(-1); + if (!assistArg.empty() && assistArg.front() == '[') { + assistArg = evaluateMacroConditionals(assistArg, gameHandler, assistOver); + if (assistArg.empty() && assistOver == static_cast(-1)) { + chatInputBuffer[0] = '\0'; return; // no condition matched + } + while (!assistArg.empty() && assistArg.front() == ' ') assistArg.erase(assistArg.begin()); + while (!assistArg.empty() && assistArg.back() == ' ') assistArg.pop_back(); + } + + if (assistOver != static_cast(-1) && assistOver != 0) { + assistEntityTarget(assistOver); + } else if (!assistArg.empty()) { + // Name search + std::string argLow = assistArg; + for (char& c : argLow) c = static_cast(std::tolower(static_cast(c))); + uint64_t bestGuid = 0; float bestDist = std::numeric_limits::max(); + const auto& pmi = gameHandler.getMovementInfo(); + for (const auto& [guid, ent] : gameHandler.getEntityManager().getEntities()) { + if (!ent || ent->getType() == game::ObjectType::OBJECT) continue; + std::string nm = getEntityName(ent); + std::string nml = nm; + for (char& c : nml) c = static_cast(std::tolower(static_cast(c))); + if (nml.find(argLow) != 0) continue; + float d2 = (ent->getX()-pmi.x)*(ent->getX()-pmi.x) + + (ent->getY()-pmi.y)*(ent->getY()-pmi.y); + if (d2 < bestDist) { bestDist = d2; bestGuid = guid; } + } + if (bestGuid) assistEntityTarget(bestGuid); + else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "No unit matching '" + assistArg + "' found."; + gameHandler.addLocalChatMessage(msg); + } + } else { + gameHandler.assistTarget(); + } + } else { + gameHandler.assistTarget(); + } chatInputBuffer[0] = '\0'; return; } @@ -3757,6 +7093,57 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /mark [icon] — set or clear a raid target mark on the current target. + // Icon names (case-insensitive): star, circle, diamond, triangle, moon, square, cross, skull + // /mark clear | /mark 0 — remove all marks (sets icon 0xFF = clear) + // /mark — no arg marks with skull (icon 7) + if (cmdLower == "mark" || cmdLower == "marktarget" || cmdLower == "raidtarget") { + if (!gameHandler.hasTarget()) { + game::MessageChatData noTgt; + noTgt.type = game::ChatType::SYSTEM; + noTgt.language = game::ChatLanguage::UNIVERSAL; + noTgt.message = "No target selected."; + gameHandler.addLocalChatMessage(noTgt); + chatInputBuffer[0] = '\0'; + return; + } + static const char* kMarkWords[] = { + "star", "circle", "diamond", "triangle", "moon", "square", "cross", "skull" + }; + uint8_t icon = 7; // default: skull + if (spacePos != std::string::npos) { + std::string arg = command.substr(spacePos + 1); + while (!arg.empty() && arg.front() == ' ') arg.erase(arg.begin()); + std::string argLow = arg; + for (auto& c : argLow) c = static_cast(std::tolower(c)); + if (argLow == "clear" || argLow == "0" || argLow == "none") { + gameHandler.setRaidMark(gameHandler.getTargetGuid(), 0xFF); + chatInputBuffer[0] = '\0'; + return; + } + bool found = false; + for (int mi = 0; mi < 8; ++mi) { + if (argLow == kMarkWords[mi]) { icon = static_cast(mi); found = true; break; } + } + if (!found && !argLow.empty() && argLow[0] >= '1' && argLow[0] <= '8') { + icon = static_cast(argLow[0] - '1'); + found = true; + } + if (!found) { + game::MessageChatData badArg; + badArg.type = game::ChatType::SYSTEM; + badArg.language = game::ChatLanguage::UNIVERSAL; + badArg.message = "Unknown mark. Use: star circle diamond triangle moon square cross skull"; + gameHandler.addLocalChatMessage(badArg); + chatInputBuffer[0] = '\0'; + return; + } + } + gameHandler.setRaidMark(gameHandler.getTargetGuid(), icon); + chatInputBuffer[0] = '\0'; + return; + } + // Combat and Trade commands if (cmdLower == "duel") { if (gameHandler.hasTarget()) { @@ -3794,14 +7181,29 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } if (cmdLower == "startattack") { - if (gameHandler.hasTarget()) { - gameHandler.startAutoAttack(gameHandler.getTargetGuid()); - } else { - game::MessageChatData msg; - msg.type = game::ChatType::SYSTEM; - msg.language = game::ChatLanguage::UNIVERSAL; - msg.message = "You have no target."; - gameHandler.addLocalChatMessage(msg); + // Support macro conditionals: /startattack [harm,nodead] + bool condPass = true; + uint64_t saOverride = static_cast(-1); + if (spacePos != std::string::npos) { + std::string saArg = command.substr(spacePos + 1); + while (!saArg.empty() && saArg.front() == ' ') saArg.erase(saArg.begin()); + if (!saArg.empty() && saArg.front() == '[') { + std::string result = evaluateMacroConditionals(saArg, gameHandler, saOverride); + condPass = !(result.empty() && saOverride == static_cast(-1)); + } + } + if (condPass) { + uint64_t atkTarget = (saOverride != static_cast(-1) && saOverride != 0) + ? saOverride : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); + if (atkTarget != 0) { + gameHandler.startAutoAttack(atkTarget); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "You have no target."; + gameHandler.addLocalChatMessage(msg); + } } chatInputBuffer[0] = '\0'; return; @@ -3819,19 +7221,510 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + if (cmdLower == "cancelqueuedspell" || cmdLower == "stopspellqueue") { + gameHandler.cancelQueuedSpell(); + chatInputBuffer[0] = '\0'; + return; + } + + // /equipset [name] — equip a saved equipment set by name (partial match, case-insensitive) + // /equipset — list available sets in chat + if (cmdLower == "equipset") { + const auto& sets = gameHandler.getEquipmentSets(); + auto sysSay = [&](const std::string& msg) { + game::MessageChatData m; + m.type = game::ChatType::SYSTEM; + m.language = game::ChatLanguage::UNIVERSAL; + m.message = msg; + gameHandler.addLocalChatMessage(m); + }; + if (spacePos == std::string::npos) { + // No argument: list available sets + if (sets.empty()) { + sysSay("[System] No equipment sets saved."); + } else { + sysSay("[System] Equipment sets:"); + for (const auto& es : sets) + sysSay(" " + es.name); + } + } else { + std::string setName = command.substr(spacePos + 1); + while (!setName.empty() && setName.front() == ' ') setName.erase(setName.begin()); + while (!setName.empty() && setName.back() == ' ') setName.pop_back(); + // Case-insensitive prefix match + std::string setLower = setName; + std::transform(setLower.begin(), setLower.end(), setLower.begin(), ::tolower); + const game::GameHandler::EquipmentSetInfo* found = nullptr; + for (const auto& es : sets) { + std::string nameLow = es.name; + std::transform(nameLow.begin(), nameLow.end(), nameLow.begin(), ::tolower); + if (nameLow == setLower || nameLow.find(setLower) == 0) { + found = &es; + break; + } + } + if (found) { + gameHandler.useEquipmentSet(found->setId); + } else { + sysSay("[System] No equipment set matching '" + setName + "'."); + } + } + chatInputBuffer[0] = '\0'; + return; + } + + // /castsequence [conds] [reset=N/target/combat] Spell1, Spell2, ... + // Cycles through the spell list on successive presses; resets per the reset= spec. + if (cmdLower == "castsequence" && spacePos != std::string::npos) { + std::string seqArg = command.substr(spacePos + 1); + while (!seqArg.empty() && seqArg.front() == ' ') seqArg.erase(seqArg.begin()); + + // Macro conditionals + uint64_t seqTgtOver = static_cast(-1); + if (!seqArg.empty() && seqArg.front() == '[') { + seqArg = evaluateMacroConditionals(seqArg, gameHandler, seqTgtOver); + if (seqArg.empty() && seqTgtOver == static_cast(-1)) { + chatInputBuffer[0] = '\0'; return; + } + while (!seqArg.empty() && seqArg.front() == ' ') seqArg.erase(seqArg.begin()); + while (!seqArg.empty() && seqArg.back() == ' ') seqArg.pop_back(); + } + + // Optional reset= spec (may contain slash-separated conditions: reset=5/target) + std::string resetSpec; + if (seqArg.rfind("reset=", 0) == 0) { + size_t spAfter = seqArg.find(' '); + if (spAfter != std::string::npos) { + resetSpec = seqArg.substr(6, spAfter - 6); + seqArg = seqArg.substr(spAfter + 1); + while (!seqArg.empty() && seqArg.front() == ' ') seqArg.erase(seqArg.begin()); + } + } + + // Parse comma-separated spell list + std::vector seqSpells; + { + std::string cur; + for (char c : seqArg) { + if (c == ',') { + while (!cur.empty() && cur.front() == ' ') cur.erase(cur.begin()); + while (!cur.empty() && cur.back() == ' ') cur.pop_back(); + if (!cur.empty()) seqSpells.push_back(cur); + cur.clear(); + } else { cur += c; } + } + while (!cur.empty() && cur.front() == ' ') cur.erase(cur.begin()); + while (!cur.empty() && cur.back() == ' ') cur.pop_back(); + if (!cur.empty()) seqSpells.push_back(cur); + } + if (seqSpells.empty()) { chatInputBuffer[0] = '\0'; return; } + + // Build stable key from lowercase spell list + std::string seqKey; + for (size_t k = 0; k < seqSpells.size(); ++k) { + if (k) seqKey += ','; + std::string sl = seqSpells[k]; + for (char& c : sl) c = static_cast(std::tolower(static_cast(c))); + seqKey += sl; + } + + auto& seqState = s_castSeqStates[seqKey]; + + // Check reset conditions (slash-separated: e.g. "5/target") + float nowSec = static_cast(ImGui::GetTime()); + bool shouldReset = false; + if (!resetSpec.empty()) { + size_t rpos = 0; + while (rpos <= resetSpec.size()) { + size_t slash = resetSpec.find('/', rpos); + std::string part = (slash != std::string::npos) + ? resetSpec.substr(rpos, slash - rpos) + : resetSpec.substr(rpos); + std::string plow = part; + for (char& c : plow) c = static_cast(std::tolower(static_cast(c))); + bool isNum = !plow.empty() && std::all_of(plow.begin(), plow.end(), + [](unsigned char c){ return std::isdigit(c) || c == '.'; }); + if (isNum) { + float rSec = 0.0f; + try { rSec = std::stof(plow); } catch (...) {} + if (rSec > 0.0f && nowSec - seqState.lastPressSec > rSec) shouldReset = true; + } else if (plow == "target") { + if (gameHandler.getTargetGuid() != seqState.lastTargetGuid) shouldReset = true; + } else if (plow == "combat") { + if (gameHandler.isInCombat() != seqState.lastInCombat) shouldReset = true; + } + if (slash == std::string::npos) break; + rpos = slash + 1; + } + } + if (shouldReset || seqState.index >= seqSpells.size()) seqState.index = 0; + + const std::string& seqSpell = seqSpells[seqState.index]; + seqState.index = (seqState.index + 1) % seqSpells.size(); + seqState.lastPressSec = nowSec; + seqState.lastTargetGuid = gameHandler.getTargetGuid(); + seqState.lastInCombat = gameHandler.isInCombat(); + + // Cast the selected spell — mirrors /cast spell lookup + std::string ssLow = seqSpell; + for (char& c : ssLow) c = static_cast(std::tolower(static_cast(c))); + if (!ssLow.empty() && ssLow.front() == '!') ssLow.erase(ssLow.begin()); + + uint64_t seqTargetGuid = (seqTgtOver != static_cast(-1) && seqTgtOver != 0) + ? seqTgtOver : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); + + // Numeric ID + if (!ssLow.empty() && ssLow.front() == '#') ssLow.erase(ssLow.begin()); + bool ssNumeric = !ssLow.empty() && std::all_of(ssLow.begin(), ssLow.end(), + [](unsigned char c){ return std::isdigit(c); }); + if (ssNumeric) { + uint32_t ssId = 0; + try { ssId = static_cast(std::stoul(ssLow)); } catch (...) {} + if (ssId) gameHandler.castSpell(ssId, seqTargetGuid); + } else { + uint32_t ssBest = 0; int ssBestRank = -1; + for (uint32_t sid : gameHandler.getKnownSpells()) { + const std::string& sn = gameHandler.getSpellName(sid); + if (sn.empty()) continue; + std::string snl = sn; + for (char& c : snl) c = static_cast(std::tolower(static_cast(c))); + if (snl != ssLow) continue; + int sRnk = 0; + const std::string& rk = gameHandler.getSpellRank(sid); + if (!rk.empty()) { + std::string rkl = rk; + for (char& c : rkl) c = static_cast(std::tolower(static_cast(c))); + if (rkl.rfind("rank ", 0) == 0) { try { sRnk = std::stoi(rkl.substr(5)); } catch (...) {} } + } + if (sRnk > ssBestRank) { ssBestRank = sRnk; ssBest = sid; } + } + if (ssBest) gameHandler.castSpell(ssBest, seqTargetGuid); + } + chatInputBuffer[0] = '\0'; + return; + } + + if (cmdLower == "cast" && spacePos != std::string::npos) { + std::string spellArg = command.substr(spacePos + 1); + // Trim leading/trailing whitespace + while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin()); + while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back(); + + // Evaluate WoW macro conditionals: /cast [mod:shift] Greater Heal; Flash Heal + uint64_t castTargetOverride = static_cast(-1); + if (!spellArg.empty() && spellArg.front() == '[') { + spellArg = evaluateMacroConditionals(spellArg, gameHandler, castTargetOverride); + if (spellArg.empty()) { + chatInputBuffer[0] = '\0'; + return; // No conditional matched — skip cast + } + while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin()); + while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back(); + } + + // Strip leading '!' (WoW /cast !Spell forces recast without toggling off) + if (!spellArg.empty() && spellArg.front() == '!') spellArg.erase(spellArg.begin()); + + // Support numeric spell ID: /cast 133 or /cast #133 + { + std::string numStr = spellArg; + if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin()); + bool isNumeric = !numStr.empty() && + std::all_of(numStr.begin(), numStr.end(), + [](unsigned char c){ return std::isdigit(c); }); + if (isNumeric) { + uint32_t spellId = 0; + try { spellId = static_cast(std::stoul(numStr)); } catch (...) {} + if (spellId != 0) { + uint64_t targetGuid = (castTargetOverride != static_cast(-1)) + ? castTargetOverride + : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); + gameHandler.castSpell(spellId, targetGuid); + } + chatInputBuffer[0] = '\0'; + return; + } + } + + // Parse optional "(Rank N)" suffix: "Fireball(Rank 3)" or "Fireball (Rank 3)" + int requestedRank = -1; // -1 = highest rank + std::string spellName = spellArg; + { + auto rankPos = spellArg.find('('); + if (rankPos != std::string::npos) { + std::string rankStr = spellArg.substr(rankPos + 1); + // Strip closing paren and whitespace + auto closePos = rankStr.find(')'); + if (closePos != std::string::npos) rankStr = rankStr.substr(0, closePos); + for (char& c : rankStr) c = static_cast(std::tolower(static_cast(c))); + // Expect "rank N" + if (rankStr.rfind("rank ", 0) == 0) { + try { requestedRank = std::stoi(rankStr.substr(5)); } catch (...) {} + } + spellName = spellArg.substr(0, rankPos); + while (!spellName.empty() && spellName.back() == ' ') spellName.pop_back(); + } + } + + std::string spellNameLower = spellName; + for (char& c : spellNameLower) c = static_cast(std::tolower(static_cast(c))); + + // Search known spells for a name match; pick highest rank (or specific rank) + uint32_t bestSpellId = 0; + int bestRank = -1; + for (uint32_t sid : gameHandler.getKnownSpells()) { + const std::string& sName = gameHandler.getSpellName(sid); + if (sName.empty()) continue; + std::string sNameLower = sName; + for (char& c : sNameLower) c = static_cast(std::tolower(static_cast(c))); + if (sNameLower != spellNameLower) continue; + + // Parse numeric rank from rank string ("Rank 3" → 3, "" → 0) + int sRank = 0; + const std::string& rankStr = gameHandler.getSpellRank(sid); + if (!rankStr.empty()) { + std::string rLow = rankStr; + for (char& c : rLow) c = static_cast(std::tolower(static_cast(c))); + if (rLow.rfind("rank ", 0) == 0) { + try { sRank = std::stoi(rLow.substr(5)); } catch (...) {} + } + } + + if (requestedRank >= 0) { + if (sRank == requestedRank) { bestSpellId = sid; break; } + } else { + if (sRank > bestRank) { bestRank = sRank; bestSpellId = sid; } + } + } + + if (bestSpellId) { + uint64_t targetGuid = (castTargetOverride != static_cast(-1)) + ? castTargetOverride + : (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0); + gameHandler.castSpell(bestSpellId, targetGuid); + } else { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = requestedRank >= 0 + ? "You don't know '" + spellName + "' (Rank " + std::to_string(requestedRank) + ")." + : "Unknown spell: '" + spellName + "'."; + gameHandler.addLocalChatMessage(sysMsg); + } + chatInputBuffer[0] = '\0'; + return; + } + + // /use + // Supports: item name, numeric item ID (#N or N), bag/slot (/use 0 1 = backpack slot 1, + // /use 1-4 slot = bag slot), equipment slot number (/use 16 = main hand) + if (cmdLower == "use" && spacePos != std::string::npos) { + std::string useArg = command.substr(spacePos + 1); + while (!useArg.empty() && useArg.front() == ' ') useArg.erase(useArg.begin()); + while (!useArg.empty() && useArg.back() == ' ') useArg.pop_back(); + + // Handle macro conditionals: /use [mod:shift] ItemName; OtherItem + if (!useArg.empty() && useArg.front() == '[') { + uint64_t dummy = static_cast(-1); + useArg = evaluateMacroConditionals(useArg, gameHandler, dummy); + if (useArg.empty()) { chatInputBuffer[0] = '\0'; return; } + while (!useArg.empty() && useArg.front() == ' ') useArg.erase(useArg.begin()); + while (!useArg.empty() && useArg.back() == ' ') useArg.pop_back(); + } + + // Check for bag/slot notation: two numbers separated by whitespace + { + std::istringstream iss(useArg); + int bagNum = -1, slotNum = -1; + iss >> bagNum >> slotNum; + if (!iss.fail() && slotNum >= 1) { + if (bagNum == 0) { + // Backpack: bag=0, slot 1-based → 0-based + gameHandler.useItemBySlot(slotNum - 1); + chatInputBuffer[0] = '\0'; + return; + } else if (bagNum >= 1 && bagNum <= game::Inventory::NUM_BAG_SLOTS) { + // Equip bag: bags are 1-indexed (bag 1 = bagIndex 0) + gameHandler.useItemInBag(bagNum - 1, slotNum - 1); + chatInputBuffer[0] = '\0'; + return; + } + } + } + + // Numeric equip slot: /use 16 = slot 16 (1-based, WoW equip slot enum) + { + std::string numStr = useArg; + if (!numStr.empty() && numStr.front() == '#') numStr.erase(numStr.begin()); + bool isNumeric = !numStr.empty() && + std::all_of(numStr.begin(), numStr.end(), + [](unsigned char c){ return std::isdigit(c); }); + if (isNumeric) { + // Treat as equip slot (1-based, maps to EquipSlot enum 0-based) + int slotNum = 0; + try { slotNum = std::stoi(numStr); } catch (...) {} + if (slotNum >= 1 && slotNum <= static_cast(game::EquipSlot::BAG4) + 1) { + auto eslot = static_cast(slotNum - 1); + const auto& esl = gameHandler.getInventory().getEquipSlot(eslot); + if (!esl.empty()) + gameHandler.useItemById(esl.item.itemId); + } + chatInputBuffer[0] = '\0'; + return; + } + } + + std::string useArgLower = useArg; + for (char& c : useArgLower) c = static_cast(std::tolower(static_cast(c))); + + bool found = false; + const auto& inv = gameHandler.getInventory(); + // Search backpack + for (int s = 0; s < inv.getBackpackSize() && !found; ++s) { + const auto& slot = inv.getBackpackSlot(s); + if (slot.empty()) continue; + const auto* info = gameHandler.getItemInfo(slot.item.itemId); + if (!info) continue; + std::string nameLow = info->name; + for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); + if (nameLow == useArgLower) { + gameHandler.useItemBySlot(s); + found = true; + } + } + // Search bags + for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) { + for (int s = 0; s < inv.getBagSize(b) && !found; ++s) { + const auto& slot = inv.getBagSlot(b, s); + if (slot.empty()) continue; + const auto* info = gameHandler.getItemInfo(slot.item.itemId); + if (!info) continue; + std::string nameLow = info->name; + for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); + if (nameLow == useArgLower) { + gameHandler.useItemInBag(b, s); + found = true; + } + } + } + if (!found) { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = "Item not found: '" + useArg + "'."; + gameHandler.addLocalChatMessage(sysMsg); + } + chatInputBuffer[0] = '\0'; + return; + } + + // /equip — auto-equip an item from backpack/bags by name + if (cmdLower == "equip" && spacePos != std::string::npos) { + std::string equipArg = command.substr(spacePos + 1); + while (!equipArg.empty() && equipArg.front() == ' ') equipArg.erase(equipArg.begin()); + while (!equipArg.empty() && equipArg.back() == ' ') equipArg.pop_back(); + std::string equipArgLower = equipArg; + for (char& c : equipArgLower) c = static_cast(std::tolower(static_cast(c))); + + bool found = false; + const auto& inv = gameHandler.getInventory(); + // Search backpack + for (int s = 0; s < inv.getBackpackSize() && !found; ++s) { + const auto& slot = inv.getBackpackSlot(s); + if (slot.empty()) continue; + const auto* info = gameHandler.getItemInfo(slot.item.itemId); + if (!info) continue; + std::string nameLow = info->name; + for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); + if (nameLow == equipArgLower) { + gameHandler.autoEquipItemBySlot(s); + found = true; + } + } + // Search bags + for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS && !found; ++b) { + for (int s = 0; s < inv.getBagSize(b) && !found; ++s) { + const auto& slot = inv.getBagSlot(b, s); + if (slot.empty()) continue; + const auto* info = gameHandler.getItemInfo(slot.item.itemId); + if (!info) continue; + std::string nameLow = info->name; + for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); + if (nameLow == equipArgLower) { + gameHandler.autoEquipItemInBag(b, s); + found = true; + } + } + } + if (!found) { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = "Item not found: '" + equipArg + "'."; + gameHandler.addLocalChatMessage(sysMsg); + } + chatInputBuffer[0] = '\0'; + return; + } + // Targeting commands if (cmdLower == "cleartarget") { - gameHandler.clearTarget(); + // Support macro conditionals: /cleartarget [dead] clears only if target is dead + bool ctCondPass = true; + if (spacePos != std::string::npos) { + std::string ctArg = command.substr(spacePos + 1); + while (!ctArg.empty() && ctArg.front() == ' ') ctArg.erase(ctArg.begin()); + if (!ctArg.empty() && ctArg.front() == '[') { + uint64_t ctOver = static_cast(-1); + std::string res = evaluateMacroConditionals(ctArg, gameHandler, ctOver); + ctCondPass = !(res.empty() && ctOver == static_cast(-1)); + } + } + if (ctCondPass) gameHandler.clearTarget(); chatInputBuffer[0] = '\0'; return; } if (cmdLower == "target" && spacePos != std::string::npos) { - // Search visible entities for name match (case-insensitive prefix) + // Search visible entities for name match (case-insensitive prefix). + // Among all matches, pick the nearest living unit to the player. + // Supports WoW macro conditionals: /target [target=mouseover]; /target [mod:shift] Boss std::string targetArg = command.substr(spacePos + 1); + + // Evaluate conditionals if present + uint64_t targetCmdOverride = static_cast(-1); + if (!targetArg.empty() && targetArg.front() == '[') { + targetArg = evaluateMacroConditionals(targetArg, gameHandler, targetCmdOverride); + if (targetArg.empty() && targetCmdOverride == static_cast(-1)) { + // No condition matched — silently skip (macro fallthrough) + chatInputBuffer[0] = '\0'; + return; + } + while (!targetArg.empty() && targetArg.front() == ' ') targetArg.erase(targetArg.begin()); + while (!targetArg.empty() && targetArg.back() == ' ') targetArg.pop_back(); + } + + // If conditionals resolved to a specific GUID, target it directly + if (targetCmdOverride != static_cast(-1) && targetCmdOverride != 0) { + gameHandler.setTarget(targetCmdOverride); + chatInputBuffer[0] = '\0'; + return; + } + + // If no name remains (bare conditional like [target=mouseover] with 0 guid), skip silently + if (targetArg.empty()) { + chatInputBuffer[0] = '\0'; + return; + } + std::string targetArgLower = targetArg; for (char& c : targetArgLower) c = static_cast(std::tolower(static_cast(c))); uint64_t bestGuid = 0; + float bestDist = std::numeric_limits::max(); + const auto& pmi = gameHandler.getMovementInfo(); + const float playerX = pmi.x; + const float playerY = pmi.y; + const float playerZ = pmi.z; for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { if (!entity || entity->getType() == game::ObjectType::OBJECT) continue; std::string name; @@ -3844,8 +7737,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { std::string nameLower = name; for (char& c : nameLower) c = static_cast(std::tolower(static_cast(c))); if (nameLower.find(targetArgLower) == 0) { - bestGuid = guid; - if (nameLower == targetArgLower) break; // Exact match wins + float dx = entity->getX() - playerX; + float dy = entity->getY() - playerY; + float dz = entity->getZ() - playerZ; + float dist = dx*dx + dy*dy + dz*dz; + if (dist < bestDist) { + bestDist = dist; + bestGuid = guid; + } } } if (bestGuid) { @@ -3892,7 +7791,64 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } if (cmdLower == "focus") { - if (gameHandler.hasTarget()) { + // /focus → set current target as focus + // /focus PlayerName → search for entity by name and set as focus + // /focus [target=X] Name → macro conditional: set focus to resolved target + if (spacePos != std::string::npos) { + std::string focusArg = command.substr(spacePos + 1); + + // Evaluate conditionals if present + uint64_t focusCmdOverride = static_cast(-1); + if (!focusArg.empty() && focusArg.front() == '[') { + focusArg = evaluateMacroConditionals(focusArg, gameHandler, focusCmdOverride); + if (focusArg.empty() && focusCmdOverride == static_cast(-1)) { + chatInputBuffer[0] = '\0'; + return; + } + while (!focusArg.empty() && focusArg.front() == ' ') focusArg.erase(focusArg.begin()); + while (!focusArg.empty() && focusArg.back() == ' ') focusArg.pop_back(); + } + + if (focusCmdOverride != static_cast(-1) && focusCmdOverride != 0) { + // Conditional resolved to a specific GUID (e.g. [target=mouseover]) + gameHandler.setFocus(focusCmdOverride); + } else if (!focusArg.empty()) { + // Name search — same logic as /target + std::string focusArgLower = focusArg; + for (char& c : focusArgLower) c = static_cast(std::tolower(static_cast(c))); + uint64_t bestGuid = 0; + float bestDist = std::numeric_limits::max(); + const auto& pmi = gameHandler.getMovementInfo(); + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() == game::ObjectType::OBJECT) continue; + std::string name; + if (entity->getType() == game::ObjectType::PLAYER || + entity->getType() == game::ObjectType::UNIT) { + auto unit = std::static_pointer_cast(entity); + name = unit->getName(); + } + if (name.empty()) continue; + std::string nameLower = name; + for (char& c : nameLower) c = static_cast(std::tolower(static_cast(c))); + if (nameLower.find(focusArgLower) == 0) { + float dx = entity->getX() - pmi.x; + float dy = entity->getY() - pmi.y; + float dz = entity->getZ() - pmi.z; + float dist = dx*dx + dy*dy + dz*dz; + if (dist < bestDist) { bestDist = dist; bestGuid = guid; } + } + } + if (bestGuid) { + gameHandler.setFocus(bestGuid); + } else { + game::MessageChatData msg; + msg.type = game::ChatType::SYSTEM; + msg.language = game::ChatLanguage::UNIVERSAL; + msg.message = "No unit matching '" + focusArg + "' found."; + gameHandler.addLocalChatMessage(msg); + } + } + } else if (gameHandler.hasTarget()) { gameHandler.setFocus(gameHandler.getTargetGuid()); } else { game::MessageChatData msg; @@ -4049,6 +8005,31 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { } chatInputBuffer[0] = '\0'; return; + } else if ((cmdLower == "wts" || cmdLower == "wtb") && spacePos != std::string::npos) { + // /wts and /wtb — send to Trade channel + // Prefix with [WTS] / [WTB] and route to the Trade channel + const std::string tag = (cmdLower == "wts") ? "[WTS] " : "[WTB] "; + const std::string body = command.substr(spacePos + 1); + // Find the Trade channel among joined channels (case-insensitive prefix match) + std::string tradeChan; + for (const auto& ch : gameHandler.getJoinedChannels()) { + std::string chLow = ch; + for (char& c : chLow) c = static_cast(std::tolower(static_cast(c))); + if (chLow.rfind("trade", 0) == 0) { tradeChan = ch; break; } + } + if (tradeChan.empty()) { + game::MessageChatData errMsg; + errMsg.type = game::ChatType::SYSTEM; + errMsg.language = game::ChatLanguage::UNIVERSAL; + errMsg.message = "You are not in the Trade channel."; + gameHandler.addLocalChatMessage(errMsg); + chatInputBuffer[0] = '\0'; + return; + } + message = tag + body; + type = game::ChatType::CHANNEL; + target = tradeChan; + isChannelCommand = true; } else if (cmdLower.size() == 1 && cmdLower[0] >= '1' && cmdLower[0] <= '9') { // /1 msg, /2 msg — channel shortcuts int channelIdx = cmdLower[0] - '0'; @@ -4095,6 +8076,28 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { message = ""; isChannelCommand = true; } + } else if (cmdLower == "r" || cmdLower == "reply") { + switchChatType = 4; + std::string lastSender = gameHandler.getLastWhisperSender(); + if (lastSender.empty()) { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = "No one has whispered you yet."; + gameHandler.addLocalChatMessage(sysMsg); + chatInputBuffer[0] = '\0'; + return; + } + target = lastSender; + strncpy(whisperTargetBuffer, target.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + if (spacePos != std::string::npos) { + message = command.substr(spacePos + 1); + type = game::ChatType::WHISPER; + } else { + message = ""; + } + isChannelCommand = true; } // Check for emote commands @@ -4155,6 +8158,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { case 7: type = game::ChatType::BATTLEGROUND; break; case 8: type = game::ChatType::RAID_WARNING; break; case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY + case 10: { // CHANNEL + const auto& chans = gameHandler.getJoinedChannels(); + if (!chans.empty() && selectedChannelIdx < static_cast(chans.size())) { + type = game::ChatType::CHANNEL; + target = chans[selectedChannelIdx]; + } else { type = game::ChatType::SAY; } + break; + } default: type = game::ChatType::SAY; break; } } @@ -4171,6 +8182,14 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { case 7: type = game::ChatType::BATTLEGROUND; break; case 8: type = game::ChatType::RAID_WARNING; break; case 9: type = game::ChatType::PARTY; break; // INSTANCE uses PARTY + case 10: { // CHANNEL + const auto& chans = gameHandler.getJoinedChannels(); + if (!chans.empty() && selectedChannelIdx < static_cast(chans.size())) { + type = game::ChatType::CHANNEL; + target = chans[selectedChannelIdx]; + } else { type = game::ChatType::SAY; } + break; + } default: type = game::ChatType::SAY; break; } } @@ -4257,7 +8276,7 @@ ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const { case game::ChatType::SAY: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White case game::ChatType::YELL: - return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red + return kColorRed; // Red case game::ChatType::EMOTE: return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange case game::ChatType::TEXT_EMOTE: @@ -4265,7 +8284,7 @@ ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const { case game::ChatType::PARTY: return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue case game::ChatType::GUILD: - return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green + return kColorBrightGreen; // Green case game::ChatType::OFFICER: return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green case game::ChatType::RAID: @@ -4283,19 +8302,41 @@ ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const { case game::ChatType::WHISPER_INFORM: return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink case game::ChatType::SYSTEM: - return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow + return kColorYellow; // Yellow case game::ChatType::MONSTER_SAY: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White (same as SAY) case game::ChatType::MONSTER_YELL: - return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red (same as YELL) + return kColorRed; // Red (same as YELL) case game::ChatType::MONSTER_EMOTE: return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE) case game::ChatType::CHANNEL: return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink case game::ChatType::ACHIEVEMENT: return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow + case game::ChatType::GUILD_ACHIEVEMENT: + return ImVec4(1.0f, 0.84f, 0.0f, 1.0f); // Gold + case game::ChatType::SKILL: + return ImVec4(0.0f, 0.8f, 1.0f, 1.0f); // Cyan + case game::ChatType::LOOT: + return ImVec4(0.8f, 0.5f, 1.0f, 1.0f); // Light purple + case game::ChatType::MONSTER_WHISPER: + case game::ChatType::RAID_BOSS_WHISPER: + return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink (same as WHISPER) + case game::ChatType::RAID_BOSS_EMOTE: + return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange (same as EMOTE) + case game::ChatType::MONSTER_PARTY: + return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue (same as PARTY) + case game::ChatType::BG_SYSTEM_NEUTRAL: + return ImVec4(1.0f, 0.84f, 0.0f, 1.0f); // Gold + case game::ChatType::BG_SYSTEM_ALLIANCE: + return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue + case game::ChatType::BG_SYSTEM_HORDE: + return kColorRed; // Red + case game::ChatType::AFK: + case game::ChatType::DND: + return ImVec4(0.85f, 0.85f, 0.85f, 0.8f); // Light gray default: - return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Gray + return ui::colors::kLightGray; // Gray } } @@ -4598,11 +8639,83 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { gameHandler.getPlayerExploredZoneMasks(), gameHandler.hasPlayerExploredZoneMasks()); + // Party member dots on world map + { + std::vector dots; + if (gameHandler.isInGroup()) { + const auto& partyData = gameHandler.getPartyData(); + for (const auto& member : partyData.members) { + if (!member.isOnline || !member.hasPartyStats) continue; + if (member.posX == 0 && member.posY == 0) continue; + // posY → canonical X (north), posX → canonical Y (west) + float wowX = static_cast(member.posY); + float wowY = static_cast(member.posX); + glm::vec3 rpos = core::coords::canonicalToRender(glm::vec3(wowX, wowY, 0.0f)); + auto ent = gameHandler.getEntityManager().getEntity(member.guid); + uint8_t cid = entityClassId(ent.get()); + ImU32 col = (cid != 0) + ? classColorU32(cid, 230) + : (member.guid == partyData.leaderGuid + ? IM_COL32(255, 210, 0, 230) + : IM_COL32(100, 180, 255, 230)); + dots.push_back({ rpos, col, member.name }); + } + } + wm->setPartyDots(std::move(dots)); + } + + // Taxi node markers on world map + { + std::vector taxiNodes; + const auto& nodes = gameHandler.getTaxiNodes(); + taxiNodes.reserve(nodes.size()); + for (const auto& [id, node] : nodes) { + rendering::WorldMapTaxiNode wtn; + wtn.id = node.id; + wtn.mapId = node.mapId; + wtn.wowX = node.x; + wtn.wowY = node.y; + wtn.wowZ = node.z; + wtn.name = node.name; + wtn.known = gameHandler.isKnownTaxiNode(id); + taxiNodes.push_back(std::move(wtn)); + } + wm->setTaxiNodes(std::move(taxiNodes)); + } + + // Quest POI markers on world map (from SMSG_QUEST_POI_QUERY_RESPONSE / gossip POIs) + { + std::vector qpois; + for (const auto& poi : gameHandler.getGossipPois()) { + rendering::WorldMap::QuestPoi qp; + qp.wowX = poi.x; + qp.wowY = poi.y; + qp.name = poi.name; + qpois.push_back(std::move(qp)); + } + wm->setQuestPois(std::move(qpois)); + } + + // Corpse marker: show skull X on world map when ghost with unclaimed corpse + { + float corpseCanX = 0.0f, corpseCanY = 0.0f; + bool ghostWithCorpse = gameHandler.isPlayerGhost() && + gameHandler.getCorpseCanonicalPos(corpseCanX, corpseCanY); + glm::vec3 corpseRender = ghostWithCorpse + ? core::coords::canonicalToRender(glm::vec3(corpseCanX, corpseCanY, 0.0f)) + : glm::vec3{}; + wm->setCorpsePos(ghostWithCorpse, corpseRender); + } + glm::vec3 playerPos = renderer->getCharacterPosition(); + float playerYaw = renderer->getCharacterYaw(); auto* window = app.getWindow(); int screenW = window ? window->getWidth() : 1280; int screenH = window ? window->getHeight() : 720; - wm->render(playerPos, screenW, screenH); + wm->render(playerPos, screenW, screenH, playerYaw); + + // Sync showWorldMap_ if the map closed itself (e.g. ESC key inside the overlay). + if (!wm->isOpen()) showWorldMap_ = false; } // ============================================================ @@ -4651,17 +8764,23 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage } }; - // Always use expansion-aware layout if available - // Field indices vary by expansion: Classic=117, TBC=124, WotLK=133 + // Use expansion-aware layout if available AND the DBC field count + // matches the expansion's expected format. Classic=173, TBC=216, + // WotLK=234 fields. When Classic is active but the base WotLK DBC + // is loaded (234 fields), field 117 is NOT IconID — we must use + // the WotLK field 133 instead. + uint32_t iconField = 133; // WotLK default + uint32_t idField = 0; if (spellL) { - tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]); - } - - // Fallback if expansion layout missing or yielded nothing - // Only use WotLK field 133 as last resort if we have no layout - if (spellIconIds_.empty() && !spellL && fieldCount > 133) { - tryLoadIcons(0, 133); + uint32_t layoutIcon = (*spellL)["IconID"]; + // Only trust the expansion layout if the DBC has a compatible + // field count (within ~20 of the layout's icon field). + if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) { + iconField = layoutIcon; + idField = (*spellL)["ID"]; + } } + tryLoadIcons(idField, iconField); } } @@ -4714,6 +8833,81 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage return ds; } +uint32_t GameScreen::resolveMacroPrimarySpellId(uint32_t macroId, game::GameHandler& gameHandler) { + // Invalidate cache when spell list changes (learning/unlearning spells) + size_t curSpellCount = gameHandler.getKnownSpells().size(); + if (curSpellCount != macroCacheSpellCount_) { + macroPrimarySpellCache_.clear(); + macroCacheSpellCount_ = curSpellCount; + } + auto cacheIt = macroPrimarySpellCache_.find(macroId); + if (cacheIt != macroPrimarySpellCache_.end()) return cacheIt->second; + + const std::string& macroText = gameHandler.getMacroText(macroId); + uint32_t result = 0; + if (!macroText.empty()) { + for (const auto& cmdLine : allMacroCommands(macroText)) { + std::string cl = cmdLine; + for (char& c : cl) c = static_cast(std::tolower(static_cast(c))); + bool isCast = (cl.rfind("/cast ", 0) == 0); + bool isCastSeq = (cl.rfind("/castsequence ", 0) == 0); + bool isUse = (cl.rfind("/use ", 0) == 0); + if (!isCast && !isCastSeq && !isUse) continue; + size_t sp2 = cmdLine.find(' '); + if (sp2 == std::string::npos) continue; + std::string spellArg = cmdLine.substr(sp2 + 1); + // Strip conditionals [...] + if (!spellArg.empty() && spellArg.front() == '[') { + size_t ce = spellArg.find(']'); + if (ce != std::string::npos) spellArg = spellArg.substr(ce + 1); + } + // Strip reset= spec for castsequence + if (isCastSeq) { + std::string tmp = spellArg; + while (!tmp.empty() && tmp.front() == ' ') tmp.erase(tmp.begin()); + if (tmp.rfind("reset=", 0) == 0) { + size_t spAfter = tmp.find(' '); + if (spAfter != std::string::npos) spellArg = tmp.substr(spAfter + 1); + } + } + // Take first alternative before ';' (for /cast) or first spell before ',' (for /castsequence) + size_t semi = spellArg.find(isCastSeq ? ',' : ';'); + if (semi != std::string::npos) spellArg = spellArg.substr(0, semi); + size_t ss = spellArg.find_first_not_of(" \t!"); + if (ss != std::string::npos) spellArg = spellArg.substr(ss); + size_t se = spellArg.find_last_not_of(" \t"); + if (se != std::string::npos) spellArg.resize(se + 1); + if (spellArg.empty()) continue; + std::string spLow = spellArg; + for (char& c : spLow) c = static_cast(std::tolower(static_cast(c))); + if (isUse) { + // /use resolves an item name → find the item's on-use spell ID + for (const auto& [entry, info] : gameHandler.getItemInfoCache()) { + if (!info.valid) continue; + std::string iName = info.name; + for (char& c : iName) c = static_cast(std::tolower(static_cast(c))); + if (iName == spLow) { + for (const auto& sp : info.spells) { + if (sp.spellId != 0 && sp.spellTrigger == 0) { result = sp.spellId; break; } + } + break; + } + } + } else { + // /cast and /castsequence resolve a spell name + for (uint32_t sid : gameHandler.getKnownSpells()) { + std::string sn = gameHandler.getSpellName(sid); + for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); + if (sn == spLow) { result = sid; break; } + } + } + break; + } + } + macroPrimarySpellCache_[macroId] = result; + return result; +} + void GameScreen::renderActionBar(game::GameHandler& gameHandler) { // Use ImGui's display size — always in sync with the current swap-chain/frame, // whereas window->getWidth/Height() can lag by one frame on resize events. @@ -4761,6 +8955,75 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const auto& slot = bar[absSlot]; bool onCooldown = !slot.isReady(); + // Macro cooldown: check the cached primary spell's cooldown. + float macroCooldownRemaining = 0.0f; + float macroCooldownTotal = 0.0f; + if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0 && !onCooldown) { + uint32_t macroSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler); + if (macroSpellId != 0) { + float cd = gameHandler.getSpellCooldown(macroSpellId); + if (cd > 0.0f) { + macroCooldownRemaining = cd; + macroCooldownTotal = cd; + onCooldown = true; + } + } + } + + const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty(); + + // Out-of-range check: red tint when a targeted spell cannot reach the current target. + // Applies to SPELL and MACRO slots with a known max range (>5 yd) and an active target. + // Item range is checked below after barItemDef is populated. + bool outOfRange = false; + { + uint32_t rangeCheckSpellId = 0; + if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) + rangeCheckSpellId = slot.id; + else if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0) + rangeCheckSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler); + if (rangeCheckSpellId != 0 && !onCooldown && gameHandler.hasTarget()) { + uint32_t maxRange = spellbookScreen.getSpellMaxRange(rangeCheckSpellId, assetMgr); + if (maxRange > 5) { + auto& em = gameHandler.getEntityManager(); + auto playerEnt = em.getEntity(gameHandler.getPlayerGuid()); + auto targetEnt = em.getEntity(gameHandler.getTargetGuid()); + if (playerEnt && targetEnt) { + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + if (std::sqrt(dx*dx + dy*dy + dz*dz) > static_cast(maxRange)) + outOfRange = true; + } + } + } + } + + // Insufficient-power check: tint when player doesn't have enough power to cast. + // Applies to SPELL and MACRO slots with a known power cost. + bool insufficientPower = false; + { + uint32_t powerCheckSpellId = 0; + if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) + powerCheckSpellId = slot.id; + else if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0) + powerCheckSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler); + uint32_t spellCost = 0, spellPowerType = 0; + if (powerCheckSpellId != 0 && !onCooldown) + spellbookScreen.getSpellPowerInfo(powerCheckSpellId, assetMgr, spellCost, spellPowerType); + if (spellCost > 0) { + auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); + if (playerEnt && (playerEnt->getType() == game::ObjectType::PLAYER || + playerEnt->getType() == game::ObjectType::UNIT)) { + auto unit = std::static_pointer_cast(playerEnt); + if (unit->getPowerType() == static_cast(spellPowerType)) { + if (unit->getPower() < spellCost) + insufficientPower = true; + } + } + } + } + auto getSpellName = [&](uint32_t spellId) -> std::string { std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); if (!name.empty()) return name; @@ -4807,20 +9070,137 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId); } + // Macro icon: #showtooltip [SpellName] → show that spell's icon on the button + bool macroIsUseCmd = false; // tracks if the macro's primary command is /use (for item icon fallback) + if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0 && !iconTex) { + const std::string& macroText = gameHandler.getMacroText(slot.id); + if (!macroText.empty()) { + std::string showArg = getMacroShowtooltipArg(macroText); + if (showArg.empty() || showArg == "__auto__") { + // No explicit #showtooltip arg — derive spell from first /cast, /castsequence, or /use line + for (const auto& cmdLine : allMacroCommands(macroText)) { + if (cmdLine.size() < 6) continue; + std::string cl = cmdLine; + for (char& c : cl) c = static_cast(std::tolower(static_cast(c))); + bool isCastCmd = (cl.rfind("/cast ", 0) == 0 || cl == "/cast"); + bool isCastSeqCmd = (cl.rfind("/castsequence ", 0) == 0); + bool isUseCmd = (cl.rfind("/use ", 0) == 0); + if (isUseCmd) macroIsUseCmd = true; + if (!isCastCmd && !isCastSeqCmd && !isUseCmd) continue; + size_t sp2 = cmdLine.find(' '); + if (sp2 == std::string::npos) continue; + showArg = cmdLine.substr(sp2 + 1); + // Strip conditionals [...] + if (!showArg.empty() && showArg.front() == '[') { + size_t ce = showArg.find(']'); + if (ce != std::string::npos) showArg = showArg.substr(ce + 1); + } + // Strip reset= spec for castsequence + if (isCastSeqCmd) { + std::string tmp = showArg; + while (!tmp.empty() && tmp.front() == ' ') tmp.erase(tmp.begin()); + if (tmp.rfind("reset=", 0) == 0) { + size_t spA = tmp.find(' '); + if (spA != std::string::npos) showArg = tmp.substr(spA + 1); + } + } + // First alternative: ';' for /cast, ',' for /castsequence + size_t sep = showArg.find(isCastSeqCmd ? ',' : ';'); + if (sep != std::string::npos) showArg = showArg.substr(0, sep); + // Trim and strip '!' + size_t ss = showArg.find_first_not_of(" \t!"); + if (ss != std::string::npos) showArg = showArg.substr(ss); + size_t se = showArg.find_last_not_of(" \t"); + if (se != std::string::npos) showArg.resize(se + 1); + break; + } + } + // Look up the spell icon by name + if (!showArg.empty() && showArg != "__auto__") { + std::string showLower = showArg; + for (char& c : showLower) c = static_cast(std::tolower(static_cast(c))); + // Also strip "(Rank N)" suffix for matching + size_t rankParen = showLower.find('('); + if (rankParen != std::string::npos) showLower.resize(rankParen); + while (!showLower.empty() && showLower.back() == ' ') showLower.pop_back(); + for (uint32_t sid : gameHandler.getKnownSpells()) { + const std::string& sn = gameHandler.getSpellName(sid); + if (sn.empty()) continue; + std::string snl = sn; + for (char& c : snl) c = static_cast(std::tolower(static_cast(c))); + if (snl == showLower) { + iconTex = assetMgr ? getSpellIcon(sid, assetMgr) : VK_NULL_HANDLE; + if (iconTex) break; + } + } + // Fallback for /use macros: if no spell matched, search item cache for the item icon + if (!iconTex && macroIsUseCmd) { + for (const auto& [entry, info] : gameHandler.getItemInfoCache()) { + if (!info.valid) continue; + std::string iName = info.name; + for (char& c : iName) c = static_cast(std::tolower(static_cast(c))); + if (iName == showLower && info.displayInfoId != 0) { + iconTex = inventoryScreen.getItemIcon(info.displayInfoId); + break; + } + } + } + } + } + } + + // Item-missing check: grey out item slots whose item is not in the player's inventory. + const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0 + && barItemDef == nullptr && !onCooldown); + + // Ranged item out-of-range check (runs after barItemDef is populated above). + // invType 15=Ranged (bow/gun/crossbow), 26=Thrown, 28=RangedRight (wand/crossbow). + if (!outOfRange && slot.type == game::ActionBarSlot::ITEM && barItemDef + && !onCooldown && gameHandler.hasTarget()) { + constexpr uint8_t INVTYPE_RANGED = 15; + constexpr uint8_t INVTYPE_THROWN = 26; + constexpr uint8_t INVTYPE_RANGEDRIGHT = 28; + uint32_t itemMaxRange = 0; + if (barItemDef->inventoryType == INVTYPE_RANGED || + barItemDef->inventoryType == INVTYPE_RANGEDRIGHT) + itemMaxRange = 40; + else if (barItemDef->inventoryType == INVTYPE_THROWN) + itemMaxRange = 30; + if (itemMaxRange > 0) { + auto& em = gameHandler.getEntityManager(); + auto playerEnt = em.getEntity(gameHandler.getPlayerGuid()); + auto targetEnt = em.getEntity(gameHandler.getTargetGuid()); + if (playerEnt && targetEnt) { + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + if (std::sqrt(dx*dx + dy*dy + dz*dz) > static_cast(itemMaxRange)) + outOfRange = true; + } + } + } + bool clicked = false; if (iconTex) { ImVec4 tintColor(1, 1, 1, 1); ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f); - if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); } + if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); } + else if (onGCD) { tintColor = ImVec4(0.6f, 0.6f, 0.6f, 0.85f); } + else if (outOfRange) { tintColor = ImVec4(0.85f, 0.35f, 0.35f, 0.9f); } + else if (insufficientPower) { tintColor = ImVec4(0.6f, 0.5f, 0.9f, 0.85f); } + else if (itemMissing) { tintColor = ImVec4(0.35f, 0.35f, 0.35f, 0.7f); } clicked = ImGui::ImageButton("##icon", (ImTextureID)(uintptr_t)iconTex, ImVec2(slotSize, slotSize), ImVec2(0, 0), ImVec2(1, 1), bgColor, tintColor); } else { - if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); - else if (slot.isEmpty())ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); - else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); + if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f)); + else if (outOfRange) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.15f, 0.15f, 0.9f)); + else if (insufficientPower)ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.15f, 0.4f, 0.9f)); + else if (itemMissing) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.12f, 0.12f, 0.7f)); + else if (slot.isEmpty()) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); + else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f)); char label[32]; if (slot.type == game::ActionBarSlot::SPELL) { @@ -4842,6 +9222,31 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } + // Error-flash overlay: red fade on spell cast failure (~0.5 s). + // Check both spell slots directly and macro slots via their primary spell. + { + uint32_t flashSpellId = 0; + if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) + flashSpellId = slot.id; + else if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0) + flashSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler); + auto flashIt = (flashSpellId != 0) ? actionFlashEndTimes_.find(flashSpellId) : actionFlashEndTimes_.end(); + if (flashIt != actionFlashEndTimes_.end()) { + float now = static_cast(ImGui::GetTime()); + float remaining = flashIt->second - now; + if (remaining > 0.0f) { + float alpha = remaining / kActionFlashDuration; // 1→0 + ImVec2 rMin = ImGui::GetItemRectMin(); + ImVec2 rMax = ImGui::GetItemRectMax(); + ImGui::GetWindowDrawList()->AddRectFilled( + rMin, rMax, + ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.1f, 0.1f, 0.55f * alpha))); + } else { + actionFlashEndTimes_.erase(flashIt); + } + } + } + bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseReleased(ImGuiMouseButton_Left); @@ -4867,6 +9272,8 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { gameHandler.castSpell(slot.id, target); } else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { gameHandler.useItemById(slot.id); + } else if (slot.type == game::ActionBarSlot::MACRO) { + executeMacroText(gameHandler, gameHandler.getMacroText(slot.id)); } } @@ -4895,6 +9302,19 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (ImGui::MenuItem("Use")) { gameHandler.useItemById(slot.id); } + } else if (slot.type == game::ActionBarSlot::MACRO) { + ImGui::TextDisabled("Macro #%u", slot.id); + ImGui::Separator(); + if (ImGui::MenuItem("Execute")) { + executeMacroText(gameHandler, gameHandler.getMacroText(slot.id)); + } + if (ImGui::MenuItem("Edit")) { + const std::string& txt = gameHandler.getMacroText(slot.id); + strncpy(macroEditorBuf_, txt.c_str(), sizeof(macroEditorBuf_) - 1); + macroEditorBuf_[sizeof(macroEditorBuf_) - 1] = '\0'; + macroEditorId_ = slot.id; + macroEditorOpen_ = true; + } } ImGui::Separator(); if (ImGui::MenuItem("Clear Slot")) { @@ -4918,40 +9338,118 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (slot.id == 8690) { uint32_t mapId = 0; glm::vec3 pos; if (gameHandler.getHomeBind(mapId, pos)) { - const char* mapName = "Unknown"; - switch (mapId) { - case 0: mapName = "Eastern Kingdoms"; break; - case 1: mapName = "Kalimdor"; break; - case 530: mapName = "Outland"; break; - case 571: mapName = "Northrend"; break; + std::string homeLocation; + // Zone name (from zoneId stored in bind point) + uint32_t zoneId = gameHandler.getHomeBindZoneId(); + if (zoneId != 0) { + homeLocation = gameHandler.getWhoAreaName(zoneId); } - ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName); + // Fall back to continent name if zone unavailable + if (homeLocation.empty()) { + switch (mapId) { + case 0: homeLocation = "Eastern Kingdoms"; break; + case 1: homeLocation = "Kalimdor"; break; + case 530: homeLocation = "Outland"; break; + case 571: homeLocation = "Northrend"; break; + default: homeLocation = "Unknown"; break; + } + } + ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), + "Home: %s", homeLocation.c_str()); } } + if (outOfRange) { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Out of range"); + } + if (insufficientPower) { + ImGui::TextColored(ImVec4(0.75f, 0.55f, 1.0f, 1.0f), "Not enough power"); + } if (onCooldown) { float cd = slot.cooldownRemaining; if (cd >= 60.0f) - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), - "Cooldown: %d min %d sec", (int)cd/60, (int)cd%60); + ImGui::TextColored(kColorRed, + "Cooldown: %d min %d sec", static_cast(cd)/60, static_cast(cd)%60); else - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1f sec", cd); + ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", cd); + } + ImGui::EndTooltip(); + } else if (slot.type == game::ActionBarSlot::MACRO) { + ImGui::BeginTooltip(); + // Show the primary spell's rich tooltip (like WoW does for macro buttons) + uint32_t macroSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler); + bool showedRich = false; + if (macroSpellId != 0) { + showedRich = spellbookScreen.renderSpellInfoTooltip(macroSpellId, gameHandler, assetMgr); + if (onCooldown && macroCooldownRemaining > 0.0f) { + float cd = macroCooldownRemaining; + if (cd >= 60.0f) + ImGui::TextColored(kColorRed, + "Cooldown: %d min %d sec", static_cast(cd)/60, static_cast(cd)%60); + else + ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", cd); + } + } + if (!showedRich) { + // For /use macros: try showing the item tooltip instead + if (macroIsUseCmd) { + const std::string& macroText = gameHandler.getMacroText(slot.id); + // Extract item name from first /use command + for (const auto& cmd : allMacroCommands(macroText)) { + std::string cl = cmd; + for (char& c : cl) c = static_cast(std::tolower(static_cast(c))); + if (cl.rfind("/use ", 0) != 0) continue; + size_t sp = cmd.find(' '); + if (sp == std::string::npos) continue; + std::string itemArg = cmd.substr(sp + 1); + while (!itemArg.empty() && itemArg.front() == ' ') itemArg.erase(itemArg.begin()); + while (!itemArg.empty() && itemArg.back() == ' ') itemArg.pop_back(); + std::string itemLow = itemArg; + for (char& c : itemLow) c = static_cast(std::tolower(static_cast(c))); + for (const auto& [entry, info] : gameHandler.getItemInfoCache()) { + if (!info.valid) continue; + std::string iName = info.name; + for (char& c : iName) c = static_cast(std::tolower(static_cast(c))); + if (iName == itemLow) { + inventoryScreen.renderItemTooltip(info); + showedRich = true; + break; + } + } + break; + } + } + if (!showedRich) { + ImGui::Text("Macro #%u", slot.id); + const std::string& macroText = gameHandler.getMacroText(slot.id); + if (!macroText.empty()) { + ImGui::Separator(); + ImGui::TextUnformatted(macroText.c_str()); + } else { + ImGui::TextDisabled("(no text — right-click to Edit)"); + } + } } ImGui::EndTooltip(); } else if (slot.type == game::ActionBarSlot::ITEM) { ImGui::BeginTooltip(); - if (barItemDef && !barItemDef->name.empty()) + // Prefer full rich tooltip from ItemQueryResponseData (has stats, quality, set info) + const auto* itemQueryInfo = gameHandler.getItemInfo(slot.id); + if (itemQueryInfo && itemQueryInfo->valid) { + inventoryScreen.renderItemTooltip(*itemQueryInfo); + } else if (barItemDef && !barItemDef->name.empty()) { ImGui::Text("%s", barItemDef->name.c_str()); - else if (!itemNameFromQuery.empty()) + } else if (!itemNameFromQuery.empty()) { ImGui::Text("%s", itemNameFromQuery.c_str()); - else + } else { ImGui::Text("Item #%u", slot.id); + } if (onCooldown) { float cd = slot.cooldownRemaining; if (cd >= 60.0f) - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), - "Cooldown: %d min %d sec", (int)cd/60, (int)cd%60); + ImGui::TextColored(kColorRed, + "Cooldown: %d min %d sec", static_cast(cd)/60, static_cast(cd)%60); else - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1f sec", cd); + ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", cd); } ImGui::EndTooltip(); } @@ -4966,8 +9464,11 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { float r = (btnMax.x - btnMin.x) * 0.5f; auto* dl = ImGui::GetWindowDrawList(); - float total = (slot.cooldownTotal > 0.0f) ? slot.cooldownTotal : 1.0f; - float elapsed = total - slot.cooldownRemaining; + // For macros, use the resolved primary spell cooldown instead of the slot's own. + float effCdTotal = (macroCooldownTotal > 0.0f) ? macroCooldownTotal : slot.cooldownTotal; + float effCdRemaining = (macroCooldownRemaining > 0.0f) ? macroCooldownRemaining : slot.cooldownRemaining; + float total = (effCdTotal > 0.0f) ? effCdTotal : 1.0f; + float elapsed = total - effCdRemaining; float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / total)); if (elapsedFrac > 0.005f) { constexpr int N_SEGS = 32; @@ -4984,10 +9485,11 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } char cdText[16]; - float cd = slot.cooldownRemaining; - if (cd >= 3600.0f) snprintf(cdText, sizeof(cdText), "%dh", (int)cd / 3600); - else if (cd >= 60.0f) snprintf(cdText, sizeof(cdText), "%dm%ds", (int)cd / 60, (int)cd % 60); - else snprintf(cdText, sizeof(cdText), "%ds", (int)cd); + float cd = effCdRemaining; + if (cd >= 3600.0f) snprintf(cdText, sizeof(cdText), "%dh", static_cast(cd) / 3600); + else if (cd >= 60.0f) snprintf(cdText, sizeof(cdText), "%dm%ds", static_cast(cd) / 60, static_cast(cd) % 60); + else if (cd >= 5.0f) snprintf(cdText, sizeof(cdText), "%ds", static_cast(cd)); + else snprintf(cdText, sizeof(cdText), "%.1f", cd); ImVec2 textSize = ImGui::CalcTextSize(cdText); float tx = cx - textSize.x * 0.5f; float ty = cy - textSize.y * 0.5f; @@ -4995,6 +9497,113 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { dl->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 255), cdText); } + // GCD overlay — subtle dark fan sweep (thinner/lighter than regular cooldown) + if (onGCD) { + ImVec2 btnMin = ImGui::GetItemRectMin(); + ImVec2 btnMax = ImGui::GetItemRectMax(); + float cx = (btnMin.x + btnMax.x) * 0.5f; + float cy = (btnMin.y + btnMax.y) * 0.5f; + float r = (btnMax.x - btnMin.x) * 0.5f; + auto* dl = ImGui::GetWindowDrawList(); + float gcdRem = gameHandler.getGCDRemaining(); + float gcdTotal = gameHandler.getGCDTotal(); + if (gcdTotal > 0.0f) { + float elapsed = gcdTotal - gcdRem; + float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / gcdTotal)); + if (elapsedFrac > 0.005f) { + constexpr int N_SEGS = 24; + float startAngle = -IM_PI * 0.5f; + float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI; + float fanR = r * 1.4f; + ImVec2 pts[N_SEGS + 2]; + pts[0] = ImVec2(cx, cy); + for (int s = 0; s <= N_SEGS; ++s) { + float a = startAngle + (endAngle - startAngle) * s / static_cast(N_SEGS); + pts[s + 1] = ImVec2(cx + std::cos(a) * fanR, cy + std::sin(a) * fanR); + } + dl->AddConvexPolyFilled(pts, N_SEGS + 2, IM_COL32(0, 0, 0, 110)); + } + } + } + + // Auto-attack active glow — pulsing golden border when slot 6603 (Attack) is toggled on + if (slot.type == game::ActionBarSlot::SPELL && slot.id == 6603 + && gameHandler.isAutoAttacking()) { + ImVec2 bMin = ImGui::GetItemRectMin(); + ImVec2 bMax = ImGui::GetItemRectMax(); + float pulse = 0.55f + 0.45f * std::sin(static_cast(ImGui::GetTime()) * 5.0f); + ImU32 glowCol = IM_COL32( + static_cast(255), + static_cast(200 * pulse), + static_cast(0), + static_cast(200 * pulse)); + ImGui::GetWindowDrawList()->AddRect(bMin, bMax, glowCol, 2.0f, 0, 2.5f); + } + + // Item stack count overlay — bottom-right corner of icon + if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { + // Count total of this item across all inventory slots + auto& inv = gameHandler.getInventory(); + int totalCount = 0; + for (int bi = 0; bi < inv.getBackpackSize(); bi++) { + const auto& bs = inv.getBackpackSlot(bi); + if (!bs.empty() && bs.item.itemId == slot.id) totalCount += bs.item.stackCount; + } + for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) { + for (int si = 0; si < inv.getBagSize(bag); si++) { + const auto& bs = inv.getBagSlot(bag, si); + if (!bs.empty() && bs.item.itemId == slot.id) totalCount += bs.item.stackCount; + } + } + if (totalCount > 0) { + char countStr[16]; + snprintf(countStr, sizeof(countStr), "%d", totalCount); + ImVec2 btnMax = ImGui::GetItemRectMax(); + ImVec2 tsz = ImGui::CalcTextSize(countStr); + float cx2 = btnMax.x - tsz.x - 2.0f; + float cy2 = btnMax.y - tsz.y - 1.0f; + auto* cdl = ImGui::GetWindowDrawList(); + cdl->AddText(ImVec2(cx2 + 1.0f, cy2 + 1.0f), IM_COL32(0, 0, 0, 200), countStr); + cdl->AddText(ImVec2(cx2, cy2), + totalCount <= 1 ? IM_COL32(220, 100, 100, 255) : IM_COL32(255, 255, 255, 255), + countStr); + } + } + + // Ready glow: animate a gold border for ~1.5s when a cooldown just expires + { + static std::unordered_map slotGlowTimers; // absSlot -> remaining glow seconds + static std::unordered_map slotWasOnCooldown; // absSlot -> last frame state + + float dt = ImGui::GetIO().DeltaTime; + bool wasOnCd = slotWasOnCooldown.count(absSlot) ? slotWasOnCooldown[absSlot] : false; + + // Trigger glow when transitioning from on-cooldown to ready (and slot isn't empty) + if (wasOnCd && !onCooldown && !slot.isEmpty()) { + slotGlowTimers[absSlot] = 1.5f; + } + slotWasOnCooldown[absSlot] = onCooldown; + + auto git = slotGlowTimers.find(absSlot); + if (git != slotGlowTimers.end() && git->second > 0.0f) { + git->second -= dt; + float t = git->second / 1.5f; // 1.0 → 0.0 over lifetime + // Pulse: bright when fresh, fading out + float pulse = std::sin(t * IM_PI * 4.0f) * 0.5f + 0.5f; // 4 pulses + uint8_t alpha = static_cast(200 * t * (0.5f + 0.5f * pulse)); + if (alpha > 0) { + ImVec2 bMin = ImGui::GetItemRectMin(); + ImVec2 bMax = ImGui::GetItemRectMax(); + auto* gdl = ImGui::GetWindowDrawList(); + // Gold glow border (2px inset, 3px thick) + gdl->AddRect(ImVec2(bMin.x - 2, bMin.y - 2), + ImVec2(bMax.x + 2, bMax.y + 2), + IM_COL32(255, 215, 0, alpha), 3.0f, 0, 3.0f); + } + if (git->second <= 0.0f) slotGlowTimers.erase(git); + } + } + // Key label below ImGui::TextDisabled("%s", keyLabel); @@ -5035,6 +9644,29 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (i > 0) ImGui::SameLine(0, spacing); renderBarSlot(i, keyLabels1[i]); } + + // Macro editor modal — opened by "Edit" in action bar context menus + if (macroEditorOpen_) { + ImGui::OpenPopup("Edit Macro###MacroEdit"); + macroEditorOpen_ = false; + } + if (ImGui::BeginPopupModal("Edit Macro###MacroEdit", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) { + ImGui::Text("Macro #%u (all lines execute; [cond] Spell; Default supported)", macroEditorId_); + ImGui::SetNextItemWidth(320.0f); + ImGui::InputTextMultiline("##MacroText", macroEditorBuf_, sizeof(macroEditorBuf_), + ImVec2(320.0f, 80.0f)); + if (ImGui::Button("Save")) { + gameHandler.setMacroText(macroEditorId_, std::string(macroEditorBuf_)); + macroPrimarySpellCache_.clear(); // invalidate resolved spell IDs + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } ImGui::End(); @@ -5099,6 +9731,38 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { ImGui::PopStyleVar(4); } + // Vehicle exit button (WotLK): floating button above action bar when player is in a vehicle + if (gameHandler.isInVehicle()) { + const float btnW = 120.0f; + const float btnH = 32.0f; + const float btnX = (screenW - btnW) / 2.0f; + const float btnY = barY - btnH - 6.0f; + + ImGui::SetNextWindowPos(ImVec2(btnX, btnY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(btnW, btnH), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + ImGuiWindowFlags vFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground; + if (ImGui::Begin("##VehicleExit", nullptr, vFlags)) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.0f, 0.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f); + if (ImGui::Button("Leave Vehicle", ImVec2(btnW - 8.0f, btnH - 8.0f))) { + gameHandler.sendRequestVehicleExit(); + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + } + ImGui::End(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(3); + } + // Handle action bar drag: render icon at cursor and detect drop outside if (actionBarDragSlot_ >= 0) { ImVec2 mousePos = ImGui::GetMousePos(); @@ -5130,6 +9794,142 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } } +// ============================================================ +// Stance / Form / Presence Bar +// Shown for Warriors (stances), Death Knights (presences), +// Druids (shapeshift forms), Rogues (stealth), Priests (Shadowform). +// Buttons display the player's known stance/form spells. +// Active form is detected by checking permanent player auras. +// ============================================================ + +void GameScreen::renderStanceBar(game::GameHandler& gameHandler) { + uint8_t playerClass = gameHandler.getPlayerClass(); + + // Stance/form spell IDs per class (ordered by display priority) + // Class IDs: 1=Warrior, 4=Rogue, 5=Priest, 6=DeathKnight, 11=Druid + static const uint32_t warriorStances[] = { 2457, 71, 2458 }; // Battle, Defensive, Berserker + static const uint32_t dkPresences[] = { 48266, 48263, 48265 }; // Blood, Frost, Unholy + static const uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 }; + // Bear, DireBear, Cat, Travel, Aquatic, Moonkin, Tree, Flight, SwiftFlight + static const uint32_t rogueForms[] = { 1784 }; // Stealth + static const uint32_t priestForms[] = { 15473 }; // Shadowform + + const uint32_t* stanceArr = nullptr; + int stanceCount = 0; + switch (playerClass) { + case 1: stanceArr = warriorStances; stanceCount = 3; break; + case 6: stanceArr = dkPresences; stanceCount = 3; break; + case 11: stanceArr = druidForms; stanceCount = 9; break; + case 4: stanceArr = rogueForms; stanceCount = 1; break; + case 5: stanceArr = priestForms; stanceCount = 1; break; + default: return; + } + + // Filter to spells the player actually knows + const auto& known = gameHandler.getKnownSpells(); + std::vector available; + available.reserve(stanceCount); + for (int i = 0; i < stanceCount; ++i) + if (known.count(stanceArr[i])) available.push_back(stanceArr[i]); + + if (available.empty()) return; + + // Detect active stance from permanent player auras (maxDurationMs == -1) + uint32_t activeStance = 0; + for (const auto& aura : gameHandler.getPlayerAuras()) { + if (aura.isEmpty() || aura.maxDurationMs != -1) continue; + for (uint32_t sid : available) { + if (aura.spellId == sid) { activeStance = sid; break; } + } + if (activeStance) break; + } + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + auto* assetMgr = core::Application::getInstance().getAssetManager(); + + // Match the action bar slot size so they align neatly + float slotSize = 38.0f; + float spacing = 4.0f; + float padding = 6.0f; + int count = static_cast(available.size()); + + float barW = count * slotSize + (count - 1) * spacing + padding * 2.0f; + float barH = slotSize + padding * 2.0f; + + // Position the stance bar immediately to the left of the action bar + float actionSlot = 48.0f * pendingActionBarScale; + float actionBarW = 12.0f * actionSlot + 11.0f * 4.0f + 8.0f * 2.0f; + float actionBarX = (screenW - actionBarW) / 2.0f; + float actionBarH = actionSlot + 24.0f; + float actionBarY = screenH - actionBarH; + + float barX = actionBarX - barW - 8.0f; + float barY = actionBarY + (actionBarH - barH) / 2.0f; + + ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollbar; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f)); + + if (ImGui::Begin("##StanceBar", nullptr, flags)) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + + for (int i = 0; i < count; ++i) { + if (i > 0) ImGui::SameLine(0.0f, spacing); + ImGui::PushID(i); + + uint32_t spellId = available[i]; + bool isActive = (spellId == activeStance); + + VkDescriptorSet iconTex = assetMgr ? getSpellIcon(spellId, assetMgr) : VK_NULL_HANDLE; + + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImVec2 posEnd = ImVec2(pos.x + slotSize, pos.y + slotSize); + + // Background — green tint when active + ImU32 bgCol = isActive ? IM_COL32(30, 70, 30, 230) : IM_COL32(20, 20, 20, 220); + ImU32 borderCol = isActive ? IM_COL32(80, 220, 80, 255) : IM_COL32(80, 80, 80, 200); + dl->AddRectFilled(pos, posEnd, bgCol, 4.0f); + + if (iconTex) { + dl->AddImage((ImTextureID)(uintptr_t)iconTex, pos, posEnd); + // Darken inactive buttons slightly + if (!isActive) + dl->AddRectFilled(pos, posEnd, IM_COL32(0, 0, 0, 70), 4.0f); + } + dl->AddRect(pos, posEnd, borderCol, 4.0f, 0, 2.0f); + + ImGui::InvisibleButton("##btn", ImVec2(slotSize, slotSize)); + + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + gameHandler.castSpell(spellId); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr); + if (!name.empty()) ImGui::TextUnformatted(name.c_str()); + else ImGui::Text("Spell #%u", spellId); + ImGui::EndTooltip(); + } + + ImGui::PopID(); + } + } + ImGui::End(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(4); +} + // ============================================================ // Bag Bar // ============================================================ @@ -5415,8 +10215,12 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { // ============================================================ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { - uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp(); - if (nextLevelXp == 0) return; // No XP data yet (level 80 or not initialized) + uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp(); + uint32_t playerLevel = gameHandler.getPlayerLevel(); + // At max level, server sends nextLevelXp=0. Only skip entirely when we have + // no level info at all (not yet logged in / no update-field data). + const bool isMaxLevel = (nextLevelXp == 0 && playerLevel > 0); + if (nextLevelXp == 0 && !isMaxLevel) return; uint32_t currentXp = gameHandler.getPlayerXp(); uint32_t restedXp = gameHandler.getPlayerRestedXp(); @@ -5462,15 +10266,32 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f)); if (ImGui::Begin("##XpBar", nullptr, flags)) { + ImVec2 barMin = ImGui::GetCursorScreenPos(); + ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, xpBarH - 4.0f); + ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y); + auto* drawList = ImGui::GetWindowDrawList(); + + if (isMaxLevel) { + // Max-level bar: fully filled in muted gold with "Max Level" label + ImU32 bgML = IM_COL32(15, 12, 5, 220); + ImU32 fgML = IM_COL32(180, 140, 40, 200); + drawList->AddRectFilled(barMin, barMax, bgML, 2.0f); + drawList->AddRectFilled(barMin, barMax, fgML, 2.0f); + drawList->AddRect(barMin, barMax, IM_COL32(100, 80, 20, 220), 2.0f); + const char* mlLabel = "Max Level"; + ImVec2 mlSz = ImGui::CalcTextSize(mlLabel); + drawList->AddText( + ImVec2(barMin.x + (barSize.x - mlSz.x) * 0.5f, + barMin.y + (barSize.y - mlSz.y) * 0.5f), + IM_COL32(255, 230, 120, 255), mlLabel); + ImGui::Dummy(barSize); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Level %u — Maximum level reached", playerLevel); + } else { float pct = static_cast(currentXp) / static_cast(nextLevelXp); if (pct > 1.0f) pct = 1.0f; // Custom segmented XP bar (20 bubbles) - ImVec2 barMin = ImGui::GetCursorScreenPos(); - ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, xpBarH - 4.0f); - ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y); - auto* drawList = ImGui::GetWindowDrawList(); - ImU32 bg = IM_COL32(15, 15, 20, 220); ImU32 fg = IM_COL32(148, 51, 238, 255); ImU32 fgRest = IM_COL32(200, 170, 255, 220); // lighter purple for rested portion @@ -5524,6 +10345,28 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { drawList->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), overlay); ImGui::Dummy(barSize); + + // Tooltip with XP-to-level and rested details + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0; + ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience"); + ImGui::Separator(); + float xpPct = nextLevelXp > 0 ? (100.0f * currentXp / nextLevelXp) : 0.0f; + ImGui::Text("Current: %u / %u XP (%.1f%%)", currentXp, nextLevelXp, xpPct); + ImGui::Text("To next level: %u XP", xpToLevel); + if (restedXp > 0) { + float restedLevels = static_cast(restedXp) / static_cast(nextLevelXp); + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.78f, 0.60f, 1.0f, 1.0f), + "Rested: +%u XP (%.1f%% of a level)", restedXp, restedLevels * 100.0f); + if (isResting) + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), + "Resting — accumulating bonus XP"); + } + ImGui::EndTooltip(); + } + } } ImGui::End(); @@ -5531,6 +10374,126 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { ImGui::PopStyleVar(2); } +// ============================================================ +// Reputation Bar +// ============================================================ + +void GameScreen::renderRepBar(game::GameHandler& gameHandler) { + uint32_t factionId = gameHandler.getWatchedFactionId(); + if (factionId == 0) return; + + const auto& standings = gameHandler.getFactionStandings(); + auto it = standings.find(factionId); + if (it == standings.end()) return; + + int32_t standing = it->second; + + // WoW reputation rank thresholds + struct RepRank { const char* name; int32_t min; int32_t max; ImU32 color; }; + static const RepRank kRanks[] = { + { "Hated", -42000, -6001, IM_COL32(180, 40, 40, 255) }, + { "Hostile", -6000, -3001, IM_COL32(180, 40, 40, 255) }, + { "Unfriendly", -3000, -1, IM_COL32(220, 100, 50, 255) }, + { "Neutral", 0, 2999, IM_COL32(200, 200, 60, 255) }, + { "Friendly", 3000, 8999, IM_COL32( 60, 180, 60, 255) }, + { "Honored", 9000, 20999, IM_COL32( 60, 160, 220, 255) }, + { "Revered", 21000, 41999, IM_COL32(140, 80, 220, 255) }, + { "Exalted", 42000, 42999, IM_COL32(255, 200, 50, 255) }, + }; + constexpr int kNumRanks = static_cast(sizeof(kRanks) / sizeof(kRanks[0])); + + int rankIdx = kNumRanks - 1; // default to Exalted + for (int i = 0; i < kNumRanks; ++i) { + if (standing <= kRanks[i].max) { rankIdx = i; break; } + } + const RepRank& rank = kRanks[rankIdx]; + + float fraction = 1.0f; + if (rankIdx < kNumRanks - 1) { + float range = static_cast(rank.max - rank.min + 1); + fraction = static_cast(standing - rank.min) / range; + fraction = std::max(0.0f, std::min(1.0f, fraction)); + } + + const std::string& factionName = gameHandler.getFactionNamePublic(factionId); + + // Position directly above the XP bar + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + float slotSize = 48.0f * pendingActionBarScale; + float spacing = 4.0f; + float padding = 8.0f; + float barW = 12 * slotSize + 11 * spacing + padding * 2; + float barH_ab = slotSize + 24.0f; + float xpBarH = 20.0f; + float repBarH = 12.0f; + float xpBarW = barW; + float xpBarX = (screenW - xpBarW) / 2.0f; + + float bar1TopY = screenH - barH_ab; + float xpBarY; + if (pendingShowActionBar2) { + float bar2TopY = bar1TopY - barH_ab - 2.0f + pendingActionBar2OffsetY; + xpBarY = bar2TopY - xpBarH - 2.0f; + } else { + xpBarY = bar1TopY - xpBarH - 2.0f; + } + float repBarY = xpBarY - repBarH - 2.0f; + + ImGui::SetNextWindowPos(ImVec2(xpBarX, repBarY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(xpBarW, repBarH + 4.0f), ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 2.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 2.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f)); + + if (ImGui::Begin("##RepBar", nullptr, flags)) { + ImVec2 barMin = ImGui::GetCursorScreenPos(); + ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, repBarH - 4.0f); + ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y); + auto* dl = ImGui::GetWindowDrawList(); + + dl->AddRectFilled(barMin, barMax, IM_COL32(15, 15, 20, 220), 2.0f); + dl->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f); + + float fillW = barSize.x * fraction; + if (fillW > 0.0f) + dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), rank.color, 2.0f); + + // Label: "FactionName - Rank" + char label[96]; + snprintf(label, sizeof(label), "%s - %s", factionName.c_str(), rank.name); + ImVec2 textSize = ImGui::CalcTextSize(label); + float tx = barMin.x + (barSize.x - textSize.x) * 0.5f; + float ty = barMin.y + (barSize.y - textSize.y) * 0.5f; + dl->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), label); + + // Tooltip with exact values on hover + ImGui::Dummy(barSize); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + float cr = ((rank.color ) & 0xFF) / 255.0f; + float cg = ((rank.color >> 8) & 0xFF) / 255.0f; + float cb = ((rank.color >> 16) & 0xFF) / 255.0f; + ImGui::TextColored(ImVec4(cr, cg, cb, 1.0f), "%s", rank.name); + int32_t rankMin = rank.min; + int32_t rankMax = (rankIdx < kNumRanks - 1) ? rank.max : 42000; + ImGui::Text("%s: %d / %d", factionName.c_str(), standing - rankMin, rankMax - rankMin + 1); + ImGui::EndTooltip(); + } + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(2); +} + // ============================================================ // Cast Bar (Phase 3) // ============================================================ @@ -5538,10 +10501,16 @@ void GameScreen::renderXpBar(game::GameHandler& gameHandler) { void GameScreen::renderCastBar(game::GameHandler& gameHandler) { if (!gameHandler.isCasting()) return; + auto* assetMgr = core::Application::getInstance().getAssetManager(); + ImVec2 displaySize = ImGui::GetIO().DisplaySize; float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + uint32_t currentSpellId = gameHandler.getCurrentCastSpellId(); + VkDescriptorSet iconTex = (currentSpellId != 0 && assetMgr) + ? getSpellIcon(currentSpellId, assetMgr) : VK_NULL_HANDLE; + float barW = 300.0f; float barX = (screenW - barW) / 2.0f; float barY = screenH - 120.0f; @@ -5563,24 +10532,66 @@ void GameScreen::renderCastBar(game::GameHandler& gameHandler) { ? (1.0f - gameHandler.getCastProgress()) : gameHandler.getCastProgress(); - ImVec4 barColor = channeling - ? ImVec4(0.3f, 0.6f, 0.9f, 1.0f) // blue for channels - : ImVec4(0.8f, 0.6f, 0.2f, 1.0f); // gold for casts + // Color by spell school for cast identification; channels always blue + ImVec4 barColor; + if (channeling) { + barColor = ImVec4(0.3f, 0.6f, 0.9f, 1.0f); // blue for channels + } else { + uint32_t school = (currentSpellId != 0) ? gameHandler.getSpellSchoolMask(currentSpellId) : 0; + if (school & 0x04) barColor = ImVec4(0.95f, 0.40f, 0.10f, 1.0f); // Fire: orange-red + else if (school & 0x10) barColor = ImVec4(0.30f, 0.65f, 0.95f, 1.0f); // Frost: icy blue + else if (school & 0x20) barColor = ImVec4(0.55f, 0.15f, 0.70f, 1.0f); // Shadow: purple + else if (school & 0x40) barColor = ImVec4(0.65f, 0.30f, 0.85f, 1.0f); // Arcane: violet + else if (school & 0x08) barColor = ImVec4(0.20f, 0.75f, 0.25f, 1.0f); // Nature: green + else if (school & 0x02) barColor = ImVec4(0.90f, 0.80f, 0.30f, 1.0f); // Holy: golden + else barColor = ImVec4(0.80f, 0.60f, 0.20f, 1.0f); // Physical/default: gold + } ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); - char overlay[64]; - uint32_t currentSpellId = gameHandler.getCurrentCastSpellId(); + char overlay[96]; if (currentSpellId == 0) { snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining()); } else { const std::string& spellName = gameHandler.getSpellName(currentSpellId); const char* verb = channeling ? "Channeling" : "Casting"; - if (!spellName.empty()) - snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining()); - else + int queueLeft = gameHandler.getCraftQueueRemaining(); + if (!spellName.empty()) { + if (queueLeft > 0) + snprintf(overlay, sizeof(overlay), "%s (%.1fs) [%d left]", spellName.c_str(), gameHandler.getCastTimeRemaining(), queueLeft); + else + snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining()); + } else { snprintf(overlay, sizeof(overlay), "%s... (%.1fs)", verb, gameHandler.getCastTimeRemaining()); + } + } + + // Queued spell icon (right edge): the next spell queued to fire within 400ms. + uint32_t queuedId = gameHandler.getQueuedSpellId(); + VkDescriptorSet queuedTex = (queuedId != 0 && assetMgr) + ? getSpellIcon(queuedId, assetMgr) : VK_NULL_HANDLE; + + const float iconSz = 20.0f; + const float reservedRight = (queuedTex) ? (iconSz + 4.0f) : 0.0f; + + if (iconTex) { + // Spell icon to the left of the progress bar + ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(iconSz, iconSz)); + ImGui::SameLine(0, 4); + ImGui::ProgressBar(progress, ImVec2(-reservedRight - 1.0f, iconSz), overlay); + } else { + ImGui::ProgressBar(progress, ImVec2(-reservedRight - 1.0f, iconSz), overlay); + } + // Draw queued-spell icon on the right with a ">" arrow prefix tooltip. + if (queuedTex) { + ImGui::SameLine(0, 4); + ImGui::Image((ImTextureID)(uintptr_t)queuedTex, ImVec2(iconSz, iconSz), + ImVec2(0,0), ImVec2(1,1), + ImVec4(1,1,1,0.8f), ImVec4(0,0,0,0)); // slightly dimmed + if (ImGui::IsItemHovered()) { + const std::string& qn = gameHandler.getSpellName(queuedId); + ImGui::SetTooltip("Queued: %s", qn.empty() ? "Unknown" : qn.c_str()); + } } - ImGui::ProgressBar(progress, ImVec2(-1, 20), overlay); ImGui::PopStyleColor(); } ImGui::End(); @@ -5601,7 +10612,7 @@ void GameScreen::renderMirrorTimers(game::GameHandler& gameHandler) { static const struct { const char* label; ImVec4 color; } kTimerInfo[3] = { { "Fatigue", ImVec4(0.8f, 0.4f, 0.1f, 1.0f) }, { "Breath", ImVec4(0.2f, 0.5f, 1.0f, 1.0f) }, - { "Feign", ImVec4(0.6f, 0.6f, 0.6f, 1.0f) }, + { "Feign", kColorGray }, }; float barW = 280.0f; @@ -5641,6 +10652,98 @@ void GameScreen::renderMirrorTimers(game::GameHandler& gameHandler) { } } +// ============================================================ +// Cooldown Tracker — floating panel showing all active spell CDs +// ============================================================ + +void GameScreen::renderCooldownTracker(game::GameHandler& gameHandler) { + if (!showCooldownTracker_) return; + + const auto& cooldowns = gameHandler.getSpellCooldowns(); + if (cooldowns.empty()) return; + + // Collect spells with remaining cooldown > 0.5s (skip GCD noise) + struct CDEntry { uint32_t spellId; float remaining; }; + std::vector active; + active.reserve(16); + for (const auto& [sid, rem] : cooldowns) { + if (rem > 0.5f) active.push_back({sid, rem}); + } + if (active.empty()) return; + + // Sort: longest remaining first + std::sort(active.begin(), active.end(), [](const CDEntry& a, const CDEntry& b) { + return a.remaining > b.remaining; + }); + + auto* assetMgr = core::Application::getInstance().getAssetManager(); + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + constexpr float TRACKER_W = 200.0f; + constexpr int MAX_SHOWN = 12; + float posX = screenW - TRACKER_W - 10.0f; + float posY = screenH - 220.0f; // above the action bar area + + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always, ImVec2(1.0f, 1.0f)); + ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0.0f), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.75f); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoBringToFrontOnFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f)); + + if (ImGui::Begin("##CooldownTracker", nullptr, flags)) { + ImGui::TextDisabled("Cooldowns"); + ImGui::Separator(); + + int shown = 0; + for (const auto& cd : active) { + if (shown >= MAX_SHOWN) break; + + const std::string& name = gameHandler.getSpellName(cd.spellId); + if (name.empty()) continue; // skip unnamed spells (internal/passive) + + // Small icon if available + VkDescriptorSet icon = assetMgr ? getSpellIcon(cd.spellId, assetMgr) : VK_NULL_HANDLE; + if (icon) { + ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(14, 14)); + ImGui::SameLine(0, 3); + } + + // Name (truncated) + remaining time + char timeStr[16]; + if (cd.remaining >= 60.0f) + snprintf(timeStr, sizeof(timeStr), "%dm%ds", static_cast(cd.remaining) / 60, static_cast(cd.remaining) % 60); + else + snprintf(timeStr, sizeof(timeStr), "%.0fs", cd.remaining); + + // Color: red > 30s, orange > 10s, yellow > 5s, green otherwise + ImVec4 cdColor = cd.remaining > 30.0f ? kColorRed : + cd.remaining > 10.0f ? ImVec4(1.0f, 0.6f, 0.2f, 1.0f) : + cd.remaining > 5.0f ? kColorYellow : + ImVec4(0.5f, 1.0f, 0.5f, 1.0f); + + // Truncate name to fit + std::string displayName = name; + if (displayName.size() > 16) displayName = displayName.substr(0, 15) + "\xe2\x80\xa6"; // ellipsis + + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", displayName.c_str()); + ImGui::SameLine(TRACKER_W - 48.0f); + ImGui::TextColored(cdColor, "%s", timeStr); + + ++shown; + } + } + ImGui::End(); + ImGui::PopStyleVar(3); +} + // ============================================================ // Quest Objective Tracker (right-side HUD) // ============================================================ @@ -5677,15 +10780,27 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { } if (toShow.empty()) return; - float x = screenW - TRACKER_W - RIGHT_MARGIN; - float y = 320.0f; // below minimap (210) + buff bar space (up to 3 rows ≈ 114px) + float screenH = ImGui::GetIO().DisplaySize.y > 0.0f ? ImGui::GetIO().DisplaySize.y : 720.0f; - ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0), ImGuiCond_Always); + // Default position: top-right, below minimap + buff bar space. + // questTrackerRightOffset_ stores pixels from the right edge so the tracker + // stays anchored to the right side when the window is resized. + if (!questTrackerPosInit_ || questTrackerRightOffset_ < 0.0f) { + questTrackerRightOffset_ = TRACKER_W + RIGHT_MARGIN; // default: right-aligned + questTrackerPos_.y = 320.0f; + questTrackerPosInit_ = true; + } + // Recompute X from right offset every frame (handles window resize) + questTrackerPos_.x = screenW - questTrackerRightOffset_; - ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBringToFrontOnFocus; + ImGui::SetNextWindowPos(questTrackerPos_, ImGuiCond_Always); + ImGui::SetNextWindowSize(questTrackerSize_, ImGuiCond_FirstUseEver); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoBringToFrontOnFocus; ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.55f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f)); @@ -5701,7 +10816,7 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { : ImVec4(1.0f, 1.0f, 0.85f, 1.0f); ImGui::PushStyleColor(ImGuiCol_Text, titleCol); if (ImGui::Selectable(q.title.c_str(), false, - ImGuiSelectableFlags_DontClosePopups, ImVec2(TRACKER_W - 12.0f, 0))) { + ImGuiSelectableFlags_DontClosePopups, ImVec2(ImGui::GetContentRegionAvail().x, 0))) { questLogScreen.openAndSelectQuest(q.questId); } if (ImGui::IsItemHovered() && !ImGui::IsPopupOpen("##QTCtx")) { @@ -5726,6 +10841,11 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { gameHandler.setQuestTracked(q.questId, true); } } + if (gameHandler.isInGroup() && !q.complete) { + if (ImGui::MenuItem("Share Quest")) { + gameHandler.shareQuestWithParty(q.questId); + } + } if (!q.complete) { ImGui::Separator(); if (ImGui::MenuItem("Abandon Quest")) { @@ -5741,28 +10861,33 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { if (q.complete) { ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), " (Complete)"); } else { - // Kill counts + // Kill counts — green when complete, gray when in progress for (const auto& [entry, progress] : q.killCounts) { + bool objDone = (progress.first >= progress.second && progress.second > 0); + ImVec4 objColor = objDone ? kColorGreen + : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); std::string name = gameHandler.getCachedCreatureName(entry); if (name.empty()) { - // May be a game object objective; fall back to GO name cache. const auto* goInfo = gameHandler.getCachedGameObjectInfo(entry); if (goInfo && !goInfo->name.empty()) name = goInfo->name; } if (!name.empty()) { - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), + ImGui::TextColored(objColor, " %s: %u/%u", name.c_str(), progress.first, progress.second); } else { - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), + ImGui::TextColored(objColor, " %u/%u", progress.first, progress.second); } } - // Item counts + // Item counts — green when complete, gray when in progress for (const auto& [itemId, count] : q.itemCounts) { uint32_t required = 1; auto reqIt = q.requiredItemCounts.find(itemId); if (reqIt != q.requiredItemCounts.end()) required = reqIt->second; + bool objDone = (count >= required); + ImVec4 objColor = objDone ? kColorGreen + : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); const auto* info = gameHandler.getItemInfo(itemId); const char* itemName = (info && !info->name.empty()) ? info->name.c_str() : nullptr; @@ -5771,14 +10896,29 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { VkDescriptorSet iconTex = dispId ? inventoryScreen.getItemIcon(dispId) : VK_NULL_HANDLE; if (iconTex) { ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(12, 12)); + if (info && info->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + inventoryScreen.renderItemTooltip(*info); + ImGui::EndTooltip(); + } ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), + ImGui::TextColored(objColor, "%s: %u/%u", itemName ? itemName : "Item", count, required); + if (info && info->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + inventoryScreen.renderItemTooltip(*info); + ImGui::EndTooltip(); + } } else if (itemName) { - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), + ImGui::TextColored(objColor, " %s: %u/%u", itemName, count, required); + if (info && info->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + inventoryScreen.renderItemTooltip(*info); + ImGui::EndTooltip(); + } } else { - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), + ImGui::TextColored(objColor, " Item: %u/%u", count, required); } } @@ -5798,6 +10938,29 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { ImGui::Spacing(); } } + + // Capture position and size after drag/resize + ImVec2 newPos = ImGui::GetWindowPos(); + ImVec2 newSize = ImGui::GetWindowSize(); + bool changed = false; + + // Clamp within screen + newPos.x = std::clamp(newPos.x, 0.0f, screenW - newSize.x); + newPos.y = std::clamp(newPos.y, 0.0f, screenH - 40.0f); + + if (std::abs(newPos.x - questTrackerPos_.x) > 0.5f || + std::abs(newPos.y - questTrackerPos_.y) > 0.5f) { + questTrackerPos_ = newPos; + // Update right offset so resizes keep the new position anchored + questTrackerRightOffset_ = screenW - newPos.x; + changed = true; + } + if (std::abs(newSize.x - questTrackerSize_.x) > 0.5f || + std::abs(newSize.y - questTrackerSize_.y) > 0.5f) { + questTrackerSize_ = newSize; + changed = true; + } + if (changed) saveSettings(); } ImGui::End(); @@ -5805,6 +10968,102 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } +// ============================================================ +// Raid Warning / Boss Emote Center-Screen Overlay +// ============================================================ + +void GameScreen::renderRaidWarningOverlay(game::GameHandler& gameHandler) { + // Scan chat history for new RAID_WARNING / RAID_BOSS_EMOTE messages + const auto& chatHistory = gameHandler.getChatHistory(); + size_t newCount = chatHistory.size(); + if (newCount > raidWarnChatSeenCount_) { + // Walk only the new messages (deque — iterate from back by skipping old ones) + size_t toScan = newCount - raidWarnChatSeenCount_; + size_t startIdx = newCount > toScan ? newCount - toScan : 0; + auto* renderer = core::Application::getInstance().getRenderer(); + for (size_t i = startIdx; i < newCount; ++i) { + const auto& msg = chatHistory[i]; + if (msg.type == game::ChatType::RAID_WARNING || + msg.type == game::ChatType::RAID_BOSS_EMOTE || + msg.type == game::ChatType::MONSTER_EMOTE) { + bool isBoss = (msg.type != game::ChatType::RAID_WARNING); + // Limit display text length to avoid giant overlay + std::string text = msg.message; + if (text.size() > 200) text = text.substr(0, 200) + "..."; + raidWarnEntries_.push_back({text, 0.0f, isBoss}); + if (raidWarnEntries_.size() > 3) + raidWarnEntries_.erase(raidWarnEntries_.begin()); + } + // Whisper audio notification + if (msg.type == game::ChatType::WHISPER && renderer) { + if (auto* ui = renderer->getUiSoundManager()) + ui->playWhisperReceived(); + } + } + raidWarnChatSeenCount_ = newCount; + } + + // Age and remove expired entries + float dt = ImGui::GetIO().DeltaTime; + for (auto& e : raidWarnEntries_) e.age += dt; + raidWarnEntries_.erase( + std::remove_if(raidWarnEntries_.begin(), raidWarnEntries_.end(), + [](const RaidWarnEntry& e){ return e.age >= RaidWarnEntry::LIFETIME; }), + raidWarnEntries_.end()); + + if (raidWarnEntries_.empty()) return; + + ImGuiIO& io = ImGui::GetIO(); + float screenW = io.DisplaySize.x; + float screenH = io.DisplaySize.y; + ImDrawList* fg = ImGui::GetForegroundDrawList(); + + // Stack entries vertically near upper-center (below target frame area) + float baseY = screenH * 0.28f; + for (const auto& e : raidWarnEntries_) { + float alpha = std::clamp(1.0f - (e.age / RaidWarnEntry::LIFETIME), 0.0f, 1.0f); + // Fade in quickly, hold, then fade out last 20% + if (e.age < 0.3f) alpha = e.age / 0.3f; + + // Truncate to fit screen width reasonably + const char* txt = e.text.c_str(); + const float fontSize = 22.0f; + ImFont* font = ImGui::GetFont(); + + // Word-wrap manually: compute text size, center horizontally + float maxW = screenW * 0.7f; + ImVec2 textSz = font->CalcTextSizeA(fontSize, maxW, maxW, txt); + float tx = (screenW - textSz.x) * 0.5f; + + ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast(alpha * 200)); + ImU32 mainCol; + if (e.isBossEmote) { + mainCol = IM_COL32(255, 185, 60, static_cast(alpha * 255)); // amber + } else { + // Raid warning: alternating red/yellow flash during first second + float flashT = std::fmod(e.age * 4.0f, 1.0f); + if (flashT < 0.5f) + mainCol = IM_COL32(255, 50, 50, static_cast(alpha * 255)); + else + mainCol = IM_COL32(255, 220, 50, static_cast(alpha * 255)); + } + + // Background dim box for readability + float pad = 8.0f; + fg->AddRectFilled(ImVec2(tx - pad, baseY - pad), + ImVec2(tx + textSz.x + pad, baseY + textSz.y + pad), + IM_COL32(0, 0, 0, static_cast(alpha * 120)), 4.0f); + + // Shadow + main text + fg->AddText(font, fontSize, ImVec2(tx + 2.0f, baseY + 2.0f), shadowCol, txt, + nullptr, maxW); + fg->AddText(font, fontSize, ImVec2(tx, baseY), mainCol, txt, + nullptr, maxW); + + baseY += textSz.y + 6.0f; + } +} + // ============================================================ // Floating Combat Text (Phase 2) // ============================================================ @@ -5814,131 +11073,489 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { if (entries.empty()) return; auto* window = core::Application::getInstance().getWindow(); - float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + if (!window) return; + const float screenW = static_cast(window->getWidth()); + const float screenH = static_cast(window->getHeight()); - // Render combat text entries overlaid on screen - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowSize(ImVec2(screenW, 400)); + // Camera for world-space projection + auto* appRenderer = core::Application::getInstance().getRenderer(); + rendering::Camera* camera = appRenderer ? appRenderer->getCamera() : nullptr; + glm::mat4 viewProj; + if (camera) viewProj = camera->getProjectionMatrix() * camera->getViewMatrix(); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | - ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + const float baseFontSize = ImGui::GetFontSize(); - if (ImGui::Begin("##CombatText", nullptr, flags)) { - // Incoming events (enemy attacks player) float near screen center (over the player). - // Outgoing events (player attacks enemy) float on the right side (near the target). - const float incomingX = screenW * 0.40f; - const float outgoingX = screenW * 0.68f; + // HUD fallback: entries without world-space anchor use classic screen-position layout. + // We still need an ImGui window for those. + const float hudIncomingX = screenW * 0.40f; + const float hudOutgoingX = screenW * 0.68f; + int hudInIdx = 0, hudOutIdx = 0; + bool needsHudWindow = false; - int inIdx = 0, outIdx = 0; - for (const auto& entry : entries) { - float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME); - float yOffset = 200.0f - entry.age * 60.0f; - const bool outgoing = entry.isPlayerSource; + for (const auto& entry : entries) { + const float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME); + const bool outgoing = entry.isPlayerSource; - ImVec4 color; - char text[64]; - switch (entry.type) { - case game::CombatTextEntry::MELEE_DAMAGE: - case game::CombatTextEntry::SPELL_DAMAGE: - snprintf(text, sizeof(text), "-%d", entry.amount); - color = outgoing ? - ImVec4(1.0f, 1.0f, 0.3f, alpha) : // Outgoing = yellow - ImVec4(1.0f, 0.3f, 0.3f, alpha); // Incoming = red - break; - case game::CombatTextEntry::CRIT_DAMAGE: - snprintf(text, sizeof(text), "-%d!", entry.amount); - color = outgoing ? - ImVec4(1.0f, 0.8f, 0.0f, alpha) : // Outgoing crit = bright yellow - ImVec4(1.0f, 0.5f, 0.0f, alpha); // Incoming crit = orange - break; - case game::CombatTextEntry::HEAL: - snprintf(text, sizeof(text), "+%d", entry.amount); - color = ImVec4(0.3f, 1.0f, 0.3f, alpha); - break; - case game::CombatTextEntry::CRIT_HEAL: - snprintf(text, sizeof(text), "+%d!", entry.amount); - color = ImVec4(0.3f, 1.0f, 0.3f, alpha); - break; - case game::CombatTextEntry::MISS: - snprintf(text, sizeof(text), "Miss"); - color = ImVec4(0.7f, 0.7f, 0.7f, alpha); - break; - case game::CombatTextEntry::DODGE: - // outgoing=true: enemy dodged player's attack - // outgoing=false: player dodged incoming attack - snprintf(text, sizeof(text), outgoing ? "Dodge" : "You Dodge"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::PARRY: - snprintf(text, sizeof(text), outgoing ? "Parry" : "You Parry"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::BLOCK: - if (entry.amount > 0) - snprintf(text, sizeof(text), outgoing ? "Block %d" : "You Block %d", entry.amount); + // --- Format text and color (identical logic for both world and HUD paths) --- + ImVec4 color; + char text[128]; + switch (entry.type) { + case game::CombatTextEntry::MELEE_DAMAGE: + case game::CombatTextEntry::SPELL_DAMAGE: + snprintf(text, sizeof(text), "-%d", entry.amount); + color = outgoing ? + ImVec4(1.0f, 1.0f, 0.3f, alpha) : + ImVec4(1.0f, 0.3f, 0.3f, alpha); + break; + case game::CombatTextEntry::CRIT_DAMAGE: + snprintf(text, sizeof(text), "-%d!", entry.amount); + color = outgoing ? + ImVec4(1.0f, 0.8f, 0.0f, alpha) : + ImVec4(1.0f, 0.5f, 0.0f, alpha); + break; + case game::CombatTextEntry::HEAL: + snprintf(text, sizeof(text), "+%d", entry.amount); + color = ImVec4(0.3f, 1.0f, 0.3f, alpha); + break; + case game::CombatTextEntry::CRIT_HEAL: + snprintf(text, sizeof(text), "+%d!", entry.amount); + color = ImVec4(0.3f, 1.0f, 0.3f, alpha); + break; + case game::CombatTextEntry::MISS: + snprintf(text, sizeof(text), "Miss"); + color = ImVec4(0.7f, 0.7f, 0.7f, alpha); + break; + case game::CombatTextEntry::DODGE: + snprintf(text, sizeof(text), outgoing ? "Dodge" : "You Dodge"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::PARRY: + snprintf(text, sizeof(text), outgoing ? "Parry" : "You Parry"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::BLOCK: + if (entry.amount > 0) + snprintf(text, sizeof(text), outgoing ? "Block %d" : "You Block %d", entry.amount); + else + snprintf(text, sizeof(text), outgoing ? "Block" : "You Block"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::EVADE: + snprintf(text, sizeof(text), outgoing ? "Evade" : "You Evade"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::PERIODIC_DAMAGE: + snprintf(text, sizeof(text), "-%d", entry.amount); + color = outgoing ? + ImVec4(1.0f, 0.9f, 0.3f, alpha) : + ImVec4(1.0f, 0.4f, 0.4f, alpha); + break; + case game::CombatTextEntry::PERIODIC_HEAL: + snprintf(text, sizeof(text), "+%d", entry.amount); + color = ImVec4(0.4f, 1.0f, 0.5f, alpha); + break; + case game::CombatTextEntry::ENVIRONMENTAL: { + const char* envLabel = ""; + switch (entry.powerType) { + case 0: envLabel = "Fatigue "; break; + case 1: envLabel = "Drowning "; break; + case 2: envLabel = ""; break; + case 3: envLabel = "Lava "; break; + case 4: envLabel = "Slime "; break; + case 5: envLabel = "Fire "; break; + default: envLabel = ""; break; + } + snprintf(text, sizeof(text), "%s-%d", envLabel, entry.amount); + color = ImVec4(0.9f, 0.5f, 0.2f, alpha); + break; + } + case game::CombatTextEntry::ENERGIZE: + snprintf(text, sizeof(text), "+%d", entry.amount); + switch (entry.powerType) { + case 1: color = ImVec4(1.0f, 0.2f, 0.2f, alpha); break; + case 2: color = ImVec4(1.0f, 0.6f, 0.1f, alpha); break; + case 3: color = ImVec4(1.0f, 0.9f, 0.2f, alpha); break; + case 6: color = ImVec4(0.3f, 0.9f, 0.8f, alpha); break; + default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; + } + break; + case game::CombatTextEntry::POWER_DRAIN: + snprintf(text, sizeof(text), "-%d", entry.amount); + switch (entry.powerType) { + case 1: color = ImVec4(1.0f, 0.35f, 0.35f, alpha); break; + case 2: color = ImVec4(1.0f, 0.7f, 0.2f, alpha); break; + case 3: color = ImVec4(1.0f, 0.95f, 0.35f, alpha); break; + case 6: color = ImVec4(0.45f, 0.95f, 0.85f, alpha); break; + default: color = ImVec4(0.45f, 0.75f, 1.0f, alpha); break; + } + break; + case game::CombatTextEntry::XP_GAIN: + snprintf(text, sizeof(text), "+%d XP", entry.amount); + color = ImVec4(0.7f, 0.3f, 1.0f, alpha); + break; + case game::CombatTextEntry::IMMUNE: + snprintf(text, sizeof(text), "Immune!"); + color = ImVec4(0.9f, 0.9f, 0.9f, alpha); + break; + case game::CombatTextEntry::ABSORB: + if (entry.amount > 0) + snprintf(text, sizeof(text), "Absorbed %d", entry.amount); + else + snprintf(text, sizeof(text), "Absorbed"); + color = ImVec4(0.5f, 0.8f, 1.0f, alpha); + break; + case game::CombatTextEntry::RESIST: + if (entry.amount > 0) + snprintf(text, sizeof(text), "Resisted %d", entry.amount); + else + snprintf(text, sizeof(text), "Resisted"); + color = ImVec4(0.7f, 0.7f, 0.7f, alpha); + break; + case game::CombatTextEntry::DEFLECT: + snprintf(text, sizeof(text), outgoing ? "Deflect" : "You Deflect"); + color = outgoing ? ImVec4(0.7f, 0.7f, 0.7f, alpha) + : ImVec4(0.5f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::REFLECT: { + const std::string& reflectName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; + if (!reflectName.empty()) + snprintf(text, sizeof(text), outgoing ? "Reflected: %s" : "Reflect: %s", reflectName.c_str()); + else + snprintf(text, sizeof(text), outgoing ? "Reflected" : "You Reflect"); + color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha) + : ImVec4(0.75f, 0.85f, 1.0f, alpha); + break; + } + case game::CombatTextEntry::PROC_TRIGGER: { + const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; + if (!procName.empty()) + snprintf(text, sizeof(text), "%s!", procName.c_str()); + else + snprintf(text, sizeof(text), "PROC!"); + color = ImVec4(1.0f, 0.85f, 0.0f, alpha); + break; + } + case game::CombatTextEntry::DISPEL: + if (entry.spellId != 0) { + const std::string& dispelledName = gameHandler.getSpellName(entry.spellId); + if (!dispelledName.empty()) + snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str()); else - snprintf(text, sizeof(text), outgoing ? "Block" : "You Block"); - color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) - : ImVec4(0.4f, 0.9f, 1.0f, alpha); - break; - case game::CombatTextEntry::PERIODIC_DAMAGE: - snprintf(text, sizeof(text), "-%d", entry.amount); - color = outgoing ? - ImVec4(1.0f, 0.9f, 0.3f, alpha) : // Outgoing DoT = pale yellow - ImVec4(1.0f, 0.4f, 0.4f, alpha); // Incoming DoT = pale red - break; - case game::CombatTextEntry::PERIODIC_HEAL: - snprintf(text, sizeof(text), "+%d", entry.amount); - color = ImVec4(0.4f, 1.0f, 0.5f, alpha); - break; - case game::CombatTextEntry::ENVIRONMENTAL: - snprintf(text, sizeof(text), "-%d", entry.amount); - color = ImVec4(0.9f, 0.5f, 0.2f, alpha); // Orange for environmental - break; - case game::CombatTextEntry::ENERGIZE: - snprintf(text, sizeof(text), "+%d", entry.amount); - color = ImVec4(0.3f, 0.6f, 1.0f, alpha); // Blue for mana/energy - break; - case game::CombatTextEntry::XP_GAIN: - snprintf(text, sizeof(text), "+%d XP", entry.amount); - color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP - break; - case game::CombatTextEntry::IMMUNE: - snprintf(text, sizeof(text), "Immune!"); - color = ImVec4(0.9f, 0.9f, 0.9f, alpha); // White for immune - break; - case game::CombatTextEntry::ABSORB: - if (entry.amount > 0) - snprintf(text, sizeof(text), "Absorbed %d", entry.amount); + snprintf(text, sizeof(text), "Dispel"); + } else { + snprintf(text, sizeof(text), "Dispel"); + } + color = ImVec4(0.6f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::STEAL: + if (entry.spellId != 0) { + const std::string& stolenName = gameHandler.getSpellName(entry.spellId); + if (!stolenName.empty()) + snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str()); else - snprintf(text, sizeof(text), "Absorbed"); - color = ImVec4(0.5f, 0.8f, 1.0f, alpha); // Light blue for absorb - break; - case game::CombatTextEntry::RESIST: - if (entry.amount > 0) - snprintf(text, sizeof(text), "Resisted %d", entry.amount); - else - snprintf(text, sizeof(text), "Resisted"); - color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist - break; - default: - snprintf(text, sizeof(text), "%d", entry.amount); - color = ImVec4(1.0f, 1.0f, 1.0f, alpha); - break; + snprintf(text, sizeof(text), "Spellsteal"); + } else { + snprintf(text, sizeof(text), "Spellsteal"); + } + color = ImVec4(0.8f, 0.7f, 1.0f, alpha); + break; + case game::CombatTextEntry::INTERRUPT: { + const std::string& interruptedName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; + if (!interruptedName.empty()) + snprintf(text, sizeof(text), "Interrupt %s", interruptedName.c_str()); + else + snprintf(text, sizeof(text), "Interrupt"); + color = ImVec4(1.0f, 0.6f, 0.9f, alpha); + break; + } + case game::CombatTextEntry::INSTAKILL: + snprintf(text, sizeof(text), outgoing ? "Kill!" : "Killed!"); + color = outgoing ? ImVec4(1.0f, 0.25f, 0.25f, alpha) + : ImVec4(1.0f, 0.1f, 0.1f, alpha); + break; + case game::CombatTextEntry::HONOR_GAIN: + snprintf(text, sizeof(text), "+%d Honor", entry.amount); + color = ImVec4(1.0f, 0.85f, 0.0f, alpha); + break; + case game::CombatTextEntry::GLANCING: + snprintf(text, sizeof(text), "~%d", entry.amount); + color = outgoing ? + ImVec4(0.75f, 0.75f, 0.5f, alpha) : + ImVec4(0.75f, 0.35f, 0.35f, alpha); + break; + case game::CombatTextEntry::CRUSHING: + snprintf(text, sizeof(text), "%d!", entry.amount); + color = outgoing ? + ImVec4(1.0f, 0.55f, 0.1f, alpha) : + ImVec4(1.0f, 0.15f, 0.15f, alpha); + break; + default: + snprintf(text, sizeof(text), "%d", entry.amount); + color = ImVec4(1.0f, 1.0f, 1.0f, alpha); + break; + } + + // --- Rendering style --- + bool isCrit = (entry.type == game::CombatTextEntry::CRIT_DAMAGE || + entry.type == game::CombatTextEntry::CRIT_HEAL); + float renderFontSize = isCrit ? baseFontSize * 1.35f : baseFontSize; + + ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast(alpha * 180)); + ImU32 textCol = ImGui::ColorConvertFloat4ToU32(color); + + // --- Try world-space anchor if we have a destination entity --- + // Types that should always stay as HUD elements (no world anchor) + bool isHudOnly = (entry.type == game::CombatTextEntry::XP_GAIN || + entry.type == game::CombatTextEntry::HONOR_GAIN || + entry.type == game::CombatTextEntry::PROC_TRIGGER); + + bool rendered = false; + if (!isHudOnly && camera && entry.dstGuid != 0) { + // Look up the destination entity's render position + glm::vec3 renderPos; + bool havePos = core::Application::getInstance().getRenderPositionForGuid(entry.dstGuid, renderPos); + if (!havePos) { + // Fallback to entity canonical position + auto entity = gameHandler.getEntityManager().getEntity(entry.dstGuid); + if (entity) { + auto* unit = dynamic_cast(entity.get()); + if (unit) { + renderPos = core::coords::canonicalToRender( + glm::vec3(unit->getX(), unit->getY(), unit->getZ())); + havePos = true; + } + } } - // Outgoing → right side (near target), incoming → center-left (near player) - int& idx = outgoing ? outIdx : inIdx; - float baseX = outgoing ? outgoingX : incomingX; + if (havePos) { + // Float upward from above the entity's head + renderPos.z += 2.5f + entry.age * 1.2f; + + // Project to screen + glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f); + if (clipPos.w > 0.01f) { + glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w; + if (ndc.x >= -1.5f && ndc.x <= 1.5f && ndc.y >= -1.5f && ndc.y <= 1.5f) { + float sx = (ndc.x * 0.5f + 0.5f) * screenW; + float sy = (ndc.y * 0.5f + 0.5f) * screenH; + + // Horizontal stagger using the random seed + sx += entry.xSeed * 40.0f; + + // Center the text horizontally on the projected point + ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text); + sx -= ts.x * 0.5f; + + // Clamp to screen bounds + sx = std::max(2.0f, std::min(sx, screenW - ts.x - 2.0f)); + + drawList->AddText(font, renderFontSize, + ImVec2(sx + 1.0f, sy + 1.0f), shadowCol, text); + drawList->AddText(font, renderFontSize, + ImVec2(sx, sy), textCol, text); + rendered = true; + } + } + } + } + + // --- HUD fallback for entries without world anchor or HUD-only types --- + if (!rendered) { + if (!needsHudWindow) { + needsHudWindow = true; + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(screenW, 400)); + ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; + ImGui::Begin("##CombatText", nullptr, flags); + } + + float yOffset = 200.0f - entry.age * 60.0f; + int& idx = outgoing ? hudOutIdx : hudInIdx; + float baseX = outgoing ? hudOutgoingX : hudIncomingX; float xOffset = baseX + (idx % 3 - 1) * 60.0f; ++idx; + ImGui::SetCursorPos(ImVec2(xOffset, yOffset)); - ImGui::TextColored(color, "%s", text); + ImVec2 screenPos = ImGui::GetCursorScreenPos(); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddText(font, renderFontSize, ImVec2(screenPos.x + 1.0f, screenPos.y + 1.0f), + shadowCol, text); + dl->AddText(font, renderFontSize, screenPos, textCol, text); + + ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text); + ImGui::Dummy(ts); + } + } + + if (needsHudWindow) { + ImGui::End(); + } +} + +// ============================================================ +// DPS / HPS Meter +// ============================================================ + +void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) { + if (!showDPSMeter_) return; + if (gameHandler.getState() != game::WorldState::IN_WORLD) return; + + const float dt = ImGui::GetIO().DeltaTime; + + // Track combat duration for accurate DPS denominator in short fights + bool inCombat = gameHandler.isInCombat(); + if (inCombat && !dpsWasInCombat_) { + // Just entered combat — reset encounter accumulators + dpsEncounterDamage_ = 0.0f; + dpsEncounterHeal_ = 0.0f; + dpsLogSeenCount_ = gameHandler.getCombatLog().size(); + dpsCombatAge_ = 0.0f; + } + if (inCombat) { + dpsCombatAge_ += dt; + // Scan any new log entries since last frame + const auto& log = gameHandler.getCombatLog(); + while (dpsLogSeenCount_ < log.size()) { + const auto& e = log[dpsLogSeenCount_++]; + if (!e.isPlayerSource) continue; + switch (e.type) { + case game::CombatTextEntry::MELEE_DAMAGE: + case game::CombatTextEntry::SPELL_DAMAGE: + case game::CombatTextEntry::CRIT_DAMAGE: + case game::CombatTextEntry::PERIODIC_DAMAGE: + case game::CombatTextEntry::GLANCING: + case game::CombatTextEntry::CRUSHING: + dpsEncounterDamage_ += static_cast(e.amount); + break; + case game::CombatTextEntry::HEAL: + case game::CombatTextEntry::CRIT_HEAL: + case game::CombatTextEntry::PERIODIC_HEAL: + dpsEncounterHeal_ += static_cast(e.amount); + break; + default: break; + } + } + } else if (dpsWasInCombat_) { + // Just left combat — keep encounter totals but stop accumulating + } + dpsWasInCombat_ = inCombat; + + // Sum all player-source damage and healing in the current combat-text window + float totalDamage = 0.0f, totalHeal = 0.0f; + for (const auto& e : gameHandler.getCombatText()) { + if (!e.isPlayerSource) continue; + switch (e.type) { + case game::CombatTextEntry::MELEE_DAMAGE: + case game::CombatTextEntry::SPELL_DAMAGE: + case game::CombatTextEntry::CRIT_DAMAGE: + case game::CombatTextEntry::PERIODIC_DAMAGE: + case game::CombatTextEntry::GLANCING: + case game::CombatTextEntry::CRUSHING: + totalDamage += static_cast(e.amount); + break; + case game::CombatTextEntry::HEAL: + case game::CombatTextEntry::CRIT_HEAL: + case game::CombatTextEntry::PERIODIC_HEAL: + totalHeal += static_cast(e.amount); + break; + default: break; + } + } + + // Only show if there's something to report (rolling window or lingering encounter data) + if (totalDamage < 1.0f && totalHeal < 1.0f && !inCombat && + dpsEncounterDamage_ < 1.0f && dpsEncounterHeal_ < 1.0f) return; + + // DPS window = min(combat age, combat-text lifetime) to avoid under-counting + // at the start of a fight and over-counting when entries expire. + float window = std::min(dpsCombatAge_, game::CombatTextEntry::LIFETIME); + if (window < 0.1f) window = 0.1f; + + float dps = totalDamage / window; + float hps = totalHeal / window; + + // Format numbers with K/M suffix for readability + auto fmtNum = [](float v, char* buf, int bufSz) { + if (v >= 1e6f) snprintf(buf, bufSz, "%.1fM", v / 1e6f); + else if (v >= 1000.f) snprintf(buf, bufSz, "%.1fK", v / 1000.f); + else snprintf(buf, bufSz, "%.0f", v); + }; + + char dpsBuf[16], hpsBuf[16]; + fmtNum(dps, dpsBuf, sizeof(dpsBuf)); + fmtNum(hps, hpsBuf, sizeof(hpsBuf)); + + // Position: small floating label just above the action bar, right of center + auto* appWin = core::Application::getInstance().getWindow(); + float screenW = appWin ? static_cast(appWin->getWidth()) : 1280.0f; + float screenH = appWin ? static_cast(appWin->getHeight()) : 720.0f; + + // Show encounter row when fight has been going long enough (> 3s) + bool showEnc = (dpsCombatAge_ > 3.0f || (!inCombat && dpsEncounterDamage_ > 0.0f)); + float encDPS = (dpsCombatAge_ > 0.1f) ? dpsEncounterDamage_ / dpsCombatAge_ : 0.0f; + float encHPS = (dpsCombatAge_ > 0.1f) ? dpsEncounterHeal_ / dpsCombatAge_ : 0.0f; + + char encDpsBuf[16], encHpsBuf[16]; + fmtNum(encDPS, encDpsBuf, sizeof(encDpsBuf)); + fmtNum(encHPS, encHpsBuf, sizeof(encHpsBuf)); + + constexpr float WIN_W = 90.0f; + // Extra rows for encounter DPS/HPS if active + int extraRows = 0; + if (showEnc && encDPS > 0.5f) ++extraRows; + if (showEnc && encHPS > 0.5f) ++extraRows; + float WIN_H = 18.0f + extraRows * 14.0f; + if (dps > 0.5f || hps > 0.5f) WIN_H = std::max(WIN_H, 36.0f); + float wx = screenW * 0.5f + 160.0f; // right of cast bar + float wy = screenH - 130.0f; // above action bar area + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoInputs; + ImGui::SetNextWindowPos(ImVec2(wx, wy), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(WIN_W, WIN_H), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.55f); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 3)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.7f)); + + if (ImGui::Begin("##DPSMeter", nullptr, flags)) { + if (dps > 0.5f) { + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.15f, 1.0f), "%s", dpsBuf); + ImGui::SameLine(0, 2); + ImGui::TextDisabled("dps"); + } + if (hps > 0.5f) { + ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f), "%s", hpsBuf); + ImGui::SameLine(0, 2); + ImGui::TextDisabled("hps"); + } + // Encounter totals (full-fight average, shown when fight > 3s) + if (showEnc && encDPS > 0.5f) { + ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.25f, 0.80f), "%s", encDpsBuf); + ImGui::SameLine(0, 2); + ImGui::TextDisabled("enc"); + } + if (showEnc && encHPS > 0.5f) { + ImGui::TextColored(ImVec4(0.50f, 1.0f, 0.50f, 0.80f), "%s", encHpsBuf); + ImGui::SameLine(0, 2); + ImGui::TextDisabled("enc"); } } ImGui::End(); + + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); } // ============================================================ @@ -5948,6 +11565,9 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { void GameScreen::renderNameplates(game::GameHandler& gameHandler) { if (gameHandler.getState() != game::WorldState::IN_WORLD) return; + // Reset mouseover each frame; we'll set it below when the cursor is over a nameplate + gameHandler.setMouseoverGuid(0); + auto* appRenderer = core::Application::getInstance().getRenderer(); if (!appRenderer) return; rendering::Camera* camera = appRenderer->getCamera(); @@ -5995,7 +11615,8 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { bool isPlayer = (entityPtr->getType() == game::ObjectType::PLAYER); bool isTarget = (guid == targetGuid); - // Player nameplates are always shown; NPC nameplates respect the V-key toggle + // Player nameplates use Shift+V toggle; NPC/enemy nameplates use V toggle + if (isPlayer && !showFriendlyNameplates_) continue; if (!isPlayer && !showNameplates_) continue; // For corpses (dead units), only show a minimal grey nameplate if selected @@ -6041,15 +11662,60 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { barColor = IM_COL32(140, 140, 140, A(200)); bgColor = IM_COL32(70, 70, 70, A(160)); } else if (unit->isHostile()) { - barColor = IM_COL32(220, 60, 60, A(200)); - bgColor = IM_COL32(100, 25, 25, A(160)); + // Check if mob is tapped by another player (grey nameplate) + uint32_t dynFlags = unit->getDynamicFlags(); + bool tappedByOther = (dynFlags & 0x0004) != 0 && (dynFlags & 0x0008) == 0; // TAPPED but not TAPPED_BY_ALL_THREAT_LIST + if (tappedByOther) { + barColor = IM_COL32(160, 160, 160, A(200)); + bgColor = IM_COL32(80, 80, 80, A(160)); + } else { + barColor = IM_COL32(220, 60, 60, A(200)); + bgColor = IM_COL32(100, 25, 25, A(160)); + } + } else if (isPlayer) { + // Player nameplates: use class color for easy identification + uint8_t cid = entityClassId(unit); + if (cid != 0) { + ImVec4 cv = classColorVec4(cid); + barColor = IM_COL32( + static_cast(cv.x * 255), + static_cast(cv.y * 255), + static_cast(cv.z * 255), A(210)); + bgColor = IM_COL32( + static_cast(cv.x * 80), + static_cast(cv.y * 80), + static_cast(cv.z * 80), A(160)); + } else { + barColor = IM_COL32(60, 200, 80, A(200)); + bgColor = IM_COL32(25, 100, 35, A(160)); + } } else { barColor = IM_COL32(60, 200, 80, A(200)); bgColor = IM_COL32(25, 100, 35, A(160)); } + // Check if this unit is targeting the local player (threat indicator) + bool isTargetingPlayer = false; + if (unit->isHostile() && !isCorpse) { + const auto& fields = entityPtr->getFields(); + auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO)); + if (loIt != fields.end() && loIt->second != 0) { + uint64_t unitTarget = loIt->second; + auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI)); + if (hiIt != fields.end()) + unitTarget |= (static_cast(hiIt->second) << 32); + isTargetingPlayer = (unitTarget == playerGuid); + } + } + // Creature rank for border styling (Elite=gold double border, Boss=red, Rare=silver) + int creatureRank = -1; + if (!isPlayer) creatureRank = gameHandler.getCreatureRank(unit->getEntry()); + + // Border: gold = currently selected, orange = targeting player, dark = default ImU32 borderColor = isTarget ? IM_COL32(255, 215, 0, A(255)) - : IM_COL32(20, 20, 20, A(180)); + : isTargetingPlayer + ? IM_COL32(255, 140, 0, A(220)) // orange = this mob is targeting you + : IM_COL32(20, 20, 20, A(180)); // Bar geometry const float barW = 80.0f * nameplateScale_; @@ -6067,6 +11733,213 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { } drawList->AddRect (ImVec2(barX - 1.0f, sy - 1.0f), ImVec2(barX + barW + 1.0f, sy + barH + 1.0f), borderColor, 2.0f); + // Elite/Boss/Rare decoration: extra outer border with rank-specific color + if (creatureRank == 1 || creatureRank == 2) { + // Elite / Rare Elite: gold double border + drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f), + ImVec2(barX + barW + 3.0f, sy + barH + 3.0f), + IM_COL32(255, 200, 50, A(200)), 3.0f); + } else if (creatureRank == 3) { + // Boss: red double border + drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f), + ImVec2(barX + barW + 3.0f, sy + barH + 3.0f), + IM_COL32(255, 40, 40, A(200)), 3.0f); + } else if (creatureRank == 4) { + // Rare: silver double border + drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f), + ImVec2(barX + barW + 3.0f, sy + barH + 3.0f), + IM_COL32(170, 200, 230, A(200)), 3.0f); + } + + // HP % text centered on health bar (non-corpse, non-full-health for readability) + if (!isCorpse && unit->getMaxHealth() > 0) { + int hpPct = static_cast(healthPct * 100.0f + 0.5f); + char hpBuf[8]; + snprintf(hpBuf, sizeof(hpBuf), "%d%%", hpPct); + ImVec2 hpTextSz = ImGui::CalcTextSize(hpBuf); + float hpTx = sx - hpTextSz.x * 0.5f; + float hpTy = sy + (barH - hpTextSz.y) * 0.5f; + drawList->AddText(ImVec2(hpTx + 1.0f, hpTy + 1.0f), IM_COL32(0, 0, 0, A(140)), hpBuf); + drawList->AddText(ImVec2(hpTx, hpTy), IM_COL32(255, 255, 255, A(200)), hpBuf); + } + + // Cast bar below health bar when unit is casting + float castBarBaseY = sy + barH + 2.0f; + float nameplateBottom = castBarBaseY; // tracks lowest drawn element for debuff dots + { + const auto* cs = gameHandler.getUnitCastState(guid); + if (cs && cs->casting && cs->timeTotal > 0.0f) { + float castPct = std::clamp((cs->timeTotal - cs->timeRemaining) / cs->timeTotal, 0.0f, 1.0f); + const float cbH = 6.0f * nameplateScale_; + + // Spell icon + name above the cast bar + const std::string& spellName = gameHandler.getSpellName(cs->spellId); + { + auto* castAm = core::Application::getInstance().getAssetManager(); + VkDescriptorSet castIcon = (cs->spellId && castAm) + ? getSpellIcon(cs->spellId, castAm) : VK_NULL_HANDLE; + float iconSz = cbH + 8.0f; + if (castIcon) { + // Draw icon to the left of the cast bar + float iconX = barX - iconSz - 2.0f; + float iconY = castBarBaseY; + drawList->AddImage((ImTextureID)(uintptr_t)castIcon, + ImVec2(iconX, iconY), + ImVec2(iconX + iconSz, iconY + iconSz)); + drawList->AddRect(ImVec2(iconX - 1.0f, iconY - 1.0f), + ImVec2(iconX + iconSz + 1.0f, iconY + iconSz + 1.0f), + IM_COL32(0, 0, 0, A(180)), 1.0f); + } + if (!spellName.empty()) { + ImVec2 snSz = ImGui::CalcTextSize(spellName.c_str()); + float snX = sx - snSz.x * 0.5f; + float snY = castBarBaseY; + drawList->AddText(ImVec2(snX + 1.0f, snY + 1.0f), IM_COL32(0, 0, 0, A(140)), spellName.c_str()); + drawList->AddText(ImVec2(snX, snY), IM_COL32(255, 210, 100, A(220)), spellName.c_str()); + castBarBaseY += snSz.y + 2.0f; + } + } + + // Cast bar: green = interruptible, red = uninterruptible; both pulse when >80% complete + ImU32 cbBg = IM_COL32(30, 25, 40, A(180)); + ImU32 cbFill; + if (castPct > 0.8f && unit->isHostile()) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); + cbFill = cs->interruptible + ? IM_COL32(static_cast(40 * pulse), static_cast(220 * pulse), static_cast(40 * pulse), A(220)) // green pulse + : IM_COL32(static_cast(255 * pulse), static_cast(30 * pulse), static_cast(30 * pulse), A(220)); // red pulse + } else { + cbFill = cs->interruptible + ? IM_COL32(50, 190, 50, A(200)) // green = interruptible + : IM_COL32(190, 40, 40, A(200)); // red = uninterruptible + } + drawList->AddRectFilled(ImVec2(barX, castBarBaseY), + ImVec2(barX + barW, castBarBaseY + cbH), cbBg, 2.0f); + drawList->AddRectFilled(ImVec2(barX, castBarBaseY), + ImVec2(barX + barW * castPct, castBarBaseY + cbH), cbFill, 2.0f); + drawList->AddRect (ImVec2(barX - 1.0f, castBarBaseY - 1.0f), + ImVec2(barX + barW + 1.0f, castBarBaseY + cbH + 1.0f), + IM_COL32(20, 10, 40, A(200)), 2.0f); + + // Time remaining text + char timeBuf[12]; + snprintf(timeBuf, sizeof(timeBuf), "%.1fs", cs->timeRemaining); + ImVec2 timeSz = ImGui::CalcTextSize(timeBuf); + float timeX = sx - timeSz.x * 0.5f; + float timeY = castBarBaseY + (cbH - timeSz.y) * 0.5f; + drawList->AddText(ImVec2(timeX + 1.0f, timeY + 1.0f), IM_COL32(0, 0, 0, A(140)), timeBuf); + drawList->AddText(ImVec2(timeX, timeY), IM_COL32(220, 200, 255, A(220)), timeBuf); + nameplateBottom = castBarBaseY + cbH + 2.0f; + } + } + + // Debuff dot indicators: small colored squares below the nameplate showing + // player-applied auras on the current hostile target. + // Colors: Magic=blue, Curse=purple, Disease=yellow, Poison=green, Other=grey + if (isTarget && unit->isHostile() && !isCorpse) { + const auto& auras = gameHandler.getTargetAuras(); + const uint64_t pguid = gameHandler.getPlayerGuid(); + const float dotSize = 6.0f * nameplateScale_; + const float dotGap = 2.0f; + float dotX = barX; + for (const auto& aura : auras) { + if (aura.isEmpty() || aura.casterGuid != pguid) continue; + uint8_t dispelType = gameHandler.getSpellDispelType(aura.spellId); + ImU32 dotCol; + switch (dispelType) { + case 1: dotCol = IM_COL32( 64, 128, 255, A(210)); break; // Magic - blue + case 2: dotCol = IM_COL32(160, 32, 240, A(210)); break; // Curse - purple + case 3: dotCol = IM_COL32(180, 140, 40, A(210)); break; // Disease - yellow-brown + case 4: dotCol = IM_COL32( 50, 200, 50, A(210)); break; // Poison - green + default: dotCol = IM_COL32(170, 170, 170, A(170)); break; // Other - grey + } + drawList->AddRectFilled(ImVec2(dotX, nameplateBottom), + ImVec2(dotX + dotSize, nameplateBottom + dotSize), dotCol, 1.0f); + drawList->AddRect (ImVec2(dotX - 1.0f, nameplateBottom - 1.0f), + ImVec2(dotX + dotSize + 1.0f, nameplateBottom + dotSize + 1.0f), + IM_COL32(0, 0, 0, A(150)), 1.0f); + + // Duration clock-sweep overlay (like target frame auras) + uint64_t nowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + int32_t remainMs = aura.getRemainingMs(nowMs); + if (aura.maxDurationMs > 0 && remainMs > 0) { + float pct = 1.0f - static_cast(remainMs) / static_cast(aura.maxDurationMs); + pct = std::clamp(pct, 0.0f, 1.0f); + float cx = dotX + dotSize * 0.5f; + float cy = nameplateBottom + dotSize * 0.5f; + float r = dotSize * 0.5f; + float startAngle = -IM_PI * 0.5f; + float endAngle = startAngle + pct * IM_PI * 2.0f; + ImVec2 center(cx, cy); + const int segments = 12; + for (int seg = 0; seg < segments; seg++) { + float a0 = startAngle + (endAngle - startAngle) * seg / segments; + float a1 = startAngle + (endAngle - startAngle) * (seg + 1) / segments; + drawList->AddTriangleFilled( + center, + ImVec2(cx + r * std::cos(a0), cy + r * std::sin(a0)), + ImVec2(cx + r * std::cos(a1), cy + r * std::sin(a1)), + IM_COL32(0, 0, 0, A(100))); + } + } + + // Stack count on dot (upper-left corner) + if (aura.charges > 1) { + char stackBuf[8]; + snprintf(stackBuf, sizeof(stackBuf), "%d", aura.charges); + drawList->AddText(ImVec2(dotX + 1.0f, nameplateBottom), IM_COL32(0, 0, 0, A(200)), stackBuf); + drawList->AddText(ImVec2(dotX, nameplateBottom - 1.0f), IM_COL32(255, 255, 255, A(240)), stackBuf); + } + + // Duration text below dot + if (remainMs > 0) { + char durBuf[8]; + if (remainMs >= 60000) + snprintf(durBuf, sizeof(durBuf), "%dm", remainMs / 60000); + else + snprintf(durBuf, sizeof(durBuf), "%d", remainMs / 1000); + ImVec2 durSz = ImGui::CalcTextSize(durBuf); + float durX = dotX + (dotSize - durSz.x) * 0.5f; + float durY = nameplateBottom + dotSize + 1.0f; + drawList->AddText(ImVec2(durX + 1.0f, durY + 1.0f), IM_COL32(0, 0, 0, A(180)), durBuf); + // Color: red if < 5s, yellow if < 15s, white otherwise + ImU32 durCol = remainMs < 5000 ? IM_COL32(255, 60, 60, A(240)) + : remainMs < 15000 ? IM_COL32(255, 200, 60, A(240)) + : IM_COL32(230, 230, 230, A(220)); + drawList->AddText(ImVec2(durX, durY), durCol, durBuf); + } + + // Spell name + duration tooltip on hover + { + ImVec2 mouse = ImGui::GetMousePos(); + if (mouse.x >= dotX && mouse.x < dotX + dotSize && + mouse.y >= nameplateBottom && mouse.y < nameplateBottom + dotSize) { + const std::string& dotSpellName = gameHandler.getSpellName(aura.spellId); + if (!dotSpellName.empty()) { + if (remainMs > 0) { + int secs = remainMs / 1000; + int mins = secs / 60; + secs %= 60; + char tipBuf[128]; + if (mins > 0) + snprintf(tipBuf, sizeof(tipBuf), "%s (%dm %ds)", dotSpellName.c_str(), mins, secs); + else + snprintf(tipBuf, sizeof(tipBuf), "%s (%ds)", dotSpellName.c_str(), secs); + ImGui::SetTooltip("%s", tipBuf); + } else { + ImGui::SetTooltip("%s", dotSpellName.c_str()); + } + } + } + } + + dotX += dotSize + dotGap; + if (dotX + dotSize > barX + barW) break; + } + } + // Name + level label above health bar uint32_t level = unit->getLevel(); const std::string& unitName = unit->getName(); @@ -6097,15 +11970,55 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(labelBuf); float nameX = sx - textSize.x * 0.5f; float nameY = sy - barH - 12.0f; - // Name color: other player=cyan, hostile=red, non-hostile=yellow (WoW convention) - ImU32 nameColor = isPlayer - ? IM_COL32( 80, 200, 255, A(230)) // cyan — other players - : unit->isHostile() + // Name color: players get WoW class colors; NPCs use hostility (red/yellow) + ImU32 nameColor; + if (isPlayer) { + // Class color with cyan fallback for unknown class + uint8_t cid = entityClassId(unit); + ImVec4 cc = (cid != 0) ? classColorVec4(cid) : ImVec4(0.31f, 0.78f, 1.0f, 1.0f); + nameColor = IM_COL32(static_cast(cc.x*255), static_cast(cc.y*255), + static_cast(cc.z*255), A(230)); + } else { + nameColor = unit->isHostile() ? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC : IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC + } + // Sub-label below the name: guild tag for players, subtitle for NPCs + std::string subLabel; + if (isPlayer) { + uint32_t guildId = gameHandler.getEntityGuildId(guid); + if (guildId != 0) { + const std::string& gn = gameHandler.lookupGuildName(guildId); + if (!gn.empty()) subLabel = "<" + gn + ">"; + } + } else { + // NPC subtitle (e.g. "", "") + std::string sub = gameHandler.getCachedCreatureSubName(unit->getEntry()); + if (!sub.empty()) subLabel = "<" + sub + ">"; + } + if (!subLabel.empty()) nameY -= 10.0f; // shift name up for sub-label line + drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf); drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf); + // Sub-label below the name (WoW-style or in lighter color) + if (!subLabel.empty()) { + ImVec2 subSz = ImGui::CalcTextSize(subLabel.c_str()); + float subX = sx - subSz.x * 0.5f; + float subY = nameY + textSize.y + 1.0f; + drawList->AddText(ImVec2(subX + 1.0f, subY + 1.0f), IM_COL32(0, 0, 0, A(120)), subLabel.c_str()); + drawList->AddText(ImVec2(subX, subY), IM_COL32(180, 180, 180, A(200)), subLabel.c_str()); + } + + // Group leader crown to the right of the name on player nameplates + if (isPlayer && gameHandler.isInGroup() && + gameHandler.getPartyData().leaderGuid == guid) { + float crownX = nameX + textSize.x + 3.0f; + const char* crownSym = "\xe2\x99\x9b"; // ♛ + drawList->AddText(ImVec2(crownX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), crownSym); + drawList->AddText(ImVec2(crownX, nameY), IM_COL32(255, 215, 0, A(240)), crownSym); + } + // Raid mark (if any) to the left of the name { static const struct { const char* sym; ImU32 col; } kNPMarks[] = { @@ -6126,11 +12039,35 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { } // Quest kill objective indicator: small yellow sword icon to the right of the name + float questIconX = nameX + textSize.x + 4.0f; if (!isPlayer && questKillEntries.count(unit->getEntry())) { const char* objSym = "\xe2\x9a\x94"; // ⚔ crossed swords (UTF-8) - float objX = nameX + textSize.x + 4.0f; - drawList->AddText(ImVec2(objX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), objSym); - drawList->AddText(ImVec2(objX, nameY), IM_COL32(255, 220, 0, A(230)), objSym); + drawList->AddText(ImVec2(questIconX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), objSym); + drawList->AddText(ImVec2(questIconX, nameY), IM_COL32(255, 220, 0, A(230)), objSym); + questIconX += ImGui::CalcTextSize("\xe2\x9a\x94").x + 2.0f; + } + + // Quest giver indicator: "!" for available quests, "?" for completable/incomplete + if (!isPlayer) { + using QGS = game::QuestGiverStatus; + QGS qgs = gameHandler.getQuestGiverStatus(guid); + const char* qSym = nullptr; + ImU32 qCol = IM_COL32(255, 210, 0, A(255)); + if (qgs == QGS::AVAILABLE) { + qSym = "!"; + } else if (qgs == QGS::AVAILABLE_LOW) { + qSym = "!"; + qCol = IM_COL32(160, 160, 160, A(220)); + } else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) { + qSym = "?"; + } else if (qgs == QGS::INCOMPLETE) { + qSym = "?"; + qCol = IM_COL32(160, 160, 160, A(220)); + } + if (qSym) { + drawList->AddText(ImVec2(questIconX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), qSym); + drawList->AddText(ImVec2(questIconX, nameY), qCol, qSym); + } } } @@ -6142,6 +12079,8 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { float nx1 = nameX + textSize.x + 2.0f; float ny1 = sy + barH + 2.0f; if (mouse.x >= nx0 && mouse.x <= nx1 && mouse.y >= ny0 && mouse.y <= ny1) { + // Track mouseover for [target=mouseover] macro conditionals + gameHandler.setMouseoverGuid(guid); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { gameHandler.setTarget(guid); } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { @@ -6184,6 +12123,16 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { } if (ImGui::MenuItem("Invite to Group")) gameHandler.inviteToGroup(ctxName); + if (ImGui::MenuItem("Trade")) + gameHandler.initiateTrade(nameplateCtxGuid_); + if (ImGui::MenuItem("Duel")) + gameHandler.proposeDuel(nameplateCtxGuid_); + if (ImGui::MenuItem("Inspect")) { + gameHandler.setTarget(nameplateCtxGuid_); + gameHandler.inspectTarget(); + showInspectWindow_ = true; + } + ImGui::Separator(); if (ImGui::MenuItem("Add Friend")) gameHandler.addFriend(ctxName); if (ImGui::MenuItem("Ignore")) @@ -6205,6 +12154,7 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (!gameHandler.isInGroup()) return; + auto* assetMgr = core::Application::getInstance().getAssetManager(); const auto& partyData = gameHandler.getPartyData(); const bool isRaid = (partyData.groupType == 1); float frameY = 120.0f; @@ -6280,19 +12230,64 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { bool isDead = (m.onlineStatus & 0x0020) != 0; bool isGhost = (m.onlineStatus & 0x0010) != 0; - // Name text (truncated); leader name is gold + // Out-of-range check (40 yard threshold) + bool isOOR = false; + if (m.hasPartyStats && isOnline && !isDead && !isGhost && m.zoneId != 0) { + auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); + if (playerEnt) { + float dx = playerEnt->getX() - static_cast(m.posX); + float dy = playerEnt->getY() - static_cast(m.posY); + isOOR = (dx * dx + dy * dy) > (40.0f * 40.0f); + } + } + // Dim cell overlay when out of range + if (isOOR) + draw->AddRectFilled(cellMin, cellMax, IM_COL32(0, 0, 0, 80), 3.0f); + + // Name text (truncated) — class color when alive+online, gray when dead/offline char truncName[16]; snprintf(truncName, sizeof(truncName), "%.12s", m.name.c_str()); bool isMemberLeader = (m.guid == partyData.leaderGuid); - ImU32 nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) : - (!isOnline || isDead || isGhost) - ? IM_COL32(140, 140, 140, 200) : IM_COL32(220, 220, 220, 255); + ImU32 nameCol; + if (!isOnline || isDead || isGhost) { + nameCol = IM_COL32(140, 140, 140, 200); // gray for dead/offline + } else { + // Default: gold for leader, light gray for others + nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) : IM_COL32(220, 220, 220, 255); + // Override with WoW class color if entity is loaded + auto mEnt = gameHandler.getEntityManager().getEntity(m.guid); + uint8_t cid = entityClassId(mEnt.get()); + if (cid != 0) nameCol = classColorU32(cid); + } draw->AddText(ImVec2(cellMin.x + 4.0f, cellMin.y + 3.0f), nameCol, truncName); // Leader crown star in top-right of cell if (isMemberLeader) draw->AddText(ImVec2(cellMax.x - 10.0f, cellMin.y + 2.0f), IM_COL32(255, 215, 0, 255), "*"); + // Raid mark symbol — small, just to the left of the leader crown + { + static const struct { const char* sym; ImU32 col; } kCellMarks[] = { + { "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) }, + { "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) }, + { "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, + { "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, + { "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, + { "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, + { "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, + { "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, + }; + uint8_t rmk = gameHandler.getEntityRaidMark(m.guid); + if (rmk < game::GameHandler::kRaidMarkCount) { + ImFont* rmFont = ImGui::GetFont(); + ImVec2 rmsz = rmFont->CalcTextSizeA(9.0f, FLT_MAX, 0.0f, kCellMarks[rmk].sym); + float rmX = cellMax.x - 10.0f - 2.0f - rmsz.x; + draw->AddText(rmFont, 9.0f, + ImVec2(rmX, cellMin.y + 2.0f), + kCellMarks[rmk].col, kCellMarks[rmk].sym); + } + } + // LFG role badge in bottom-right corner of cell if (m.roles & 0x02) draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(80, 130, 255, 230), "T"); @@ -6301,6 +12296,15 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { else if (m.roles & 0x08) draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D"); + // Tactical role badge in bottom-left corner (flags from SMSG_GROUP_LIST / SMSG_REAL_GROUP_UPDATE) + // 0x01=Assistant, 0x02=Main Tank, 0x04=Main Assist + if (m.flags & 0x02) + draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(255, 140, 0, 230), "MT"); + else if (m.flags & 0x04) + draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(100, 180, 255, 230), "MA"); + else if (m.flags & 0x01) + draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(180, 215, 255, 180), "A"); + // Health bar uint32_t hp = m.hasPartyStats ? m.curHealth : 0; uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0; @@ -6312,13 +12316,17 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { draw->AddRectFilled(barBg, barBgEnd, IM_COL32(40, 40, 40, 200), 2.0f); ImVec2 barFill(barBg.x, barBg.y); ImVec2 barFillEnd(barBg.x + (barBgEnd.x - barBg.x) * pct, barBgEnd.y); - ImU32 hpCol = pct > 0.5f ? IM_COL32(60, 180, 60, 255) : - pct > 0.2f ? IM_COL32(200, 180, 50, 255) : - IM_COL32(200, 60, 60, 255); + ImU32 hpCol = isOOR ? IM_COL32(100, 100, 100, 160) : + pct > 0.5f ? IM_COL32(60, 180, 60, 255) : + pct > 0.2f ? IM_COL32(200, 180, 50, 255) : + IM_COL32(200, 60, 60, 255); draw->AddRectFilled(barFill, barFillEnd, hpCol, 2.0f); - // HP percentage text centered on bar + // HP percentage or OOR text centered on bar char hpPct[8]; - snprintf(hpPct, sizeof(hpPct), "%d%%", static_cast(pct * 100.0f + 0.5f)); + if (isOOR) + snprintf(hpPct, sizeof(hpPct), "OOR"); + else + snprintf(hpPct, sizeof(hpPct), "%d%%", static_cast(pct * 100.0f + 0.5f)); ImVec2 ts = ImGui::CalcTextSize(hpPct); float tx = (barBg.x + barBgEnd.x - ts.x) * 0.5f; float ty = barBg.y + (BAR_H - ts.y) * 0.5f; @@ -6346,12 +12354,69 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { draw->AddRectFilled(barFill, barFillEnd, pwrCol, 2.0f); } + // Dispellable debuff dots at the bottom of the raid cell + // Mirrors party frame debuff indicators for healers in 25/40-man raids + if (!isDead && !isGhost) { + const std::vector* unitAuras = nullptr; + if (m.guid == gameHandler.getPlayerGuid()) + unitAuras = &gameHandler.getPlayerAuras(); + else if (m.guid == gameHandler.getTargetGuid()) + unitAuras = &gameHandler.getTargetAuras(); + else + unitAuras = gameHandler.getUnitAuras(m.guid); + + if (unitAuras) { + bool shown[5] = {}; + float dotX = cellMin.x + 4.0f; + const float dotY = cellMax.y - 5.0f; + const float DOT_R = 3.5f; + ImVec2 mouse = ImGui::GetMousePos(); + for (const auto& aura : *unitAuras) { + if (aura.isEmpty()) continue; + if ((aura.flags & 0x80) == 0) continue; // debuffs only + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + if (dt == 0 || dt > 4 || shown[dt]) continue; + shown[dt] = true; + ImVec4 dc; + switch (dt) { + case 1: dc = ImVec4(0.25f, 0.50f, 1.00f, 0.90f); break; // Magic: blue + case 2: dc = ImVec4(0.70f, 0.15f, 0.90f, 0.90f); break; // Curse: purple + case 3: dc = ImVec4(0.65f, 0.45f, 0.10f, 0.90f); break; // Disease: brown + case 4: dc = ImVec4(0.10f, 0.75f, 0.10f, 0.90f); break; // Poison: green + default: continue; + } + ImU32 dotColU = ImGui::ColorConvertFloat4ToU32(dc); + draw->AddCircleFilled(ImVec2(dotX, dotY), DOT_R, dotColU); + draw->AddCircle(ImVec2(dotX, dotY), DOT_R + 0.5f, IM_COL32(0, 0, 0, 160), 8, 1.0f); + + float mdx = mouse.x - dotX, mdy = mouse.y - dotY; + if (mdx * mdx + mdy * mdy < (DOT_R + 4.0f) * (DOT_R + 4.0f)) { + static const char* kDispelNames[] = { "", "Magic", "Curse", "Disease", "Poison" }; + ImGui::BeginTooltip(); + ImGui::TextColored(dc, "%s", kDispelNames[dt]); + for (const auto& da : *unitAuras) { + if (da.isEmpty() || (da.flags & 0x80) == 0) continue; + if (gameHandler.getSpellDispelType(da.spellId) != dt) continue; + const std::string& dName = gameHandler.getSpellName(da.spellId); + if (!dName.empty()) + ImGui::Text(" %s", dName.c_str()); + } + ImGui::EndTooltip(); + } + dotX += 9.0f; + } + } + } + // Clickable invisible region over the whole cell ImGui::SetCursorScreenPos(cellMin); ImGui::PushID(static_cast(m.guid)); if (ImGui::InvisibleButton("raidCell", ImVec2(CELL_W, CELL_H))) { gameHandler.setTarget(m.guid); } + if (ImGui::IsItemHovered()) { + gameHandler.setMouseoverGuid(m.guid); + } if (ImGui::BeginPopupContextItem("RaidMemberCtx")) { ImGui::TextDisabled("%s", m.name.c_str()); ImGui::Separator(); @@ -6448,12 +12513,31 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { else if (isDead || isGhost) label += " (dead)"; } - // Clickable name to target; leader name is gold - if (isLeader) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); + // Clickable name to target — use WoW class colors when entity is loaded, + // fall back to gold for leader / light gray for others + ImVec4 nameColor = isLeader + ? ImVec4(1.0f, 0.85f, 0.0f, 1.0f) + : ImVec4(0.85f, 0.85f, 0.85f, 1.0f); + { + auto memberEntity = gameHandler.getEntityManager().getEntity(member.guid); + uint8_t cid = entityClassId(memberEntity.get()); + if (cid != 0) nameColor = classColorVec4(cid); + } + ImGui::PushStyleColor(ImGuiCol_Text, nameColor); if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) { gameHandler.setTarget(member.guid); } - if (isLeader) ImGui::PopStyleColor(); + // Set mouseover for [target=mouseover] macro conditionals + if (ImGui::IsItemHovered()) { + gameHandler.setMouseoverGuid(member.guid); + } + // Zone tooltip on name hover + if (ImGui::IsItemHovered() && member.hasPartyStats && member.zoneId != 0) { + std::string zoneName = gameHandler.getWhoAreaName(member.zoneId); + if (!zoneName.empty()) + ImGui::SetTooltip("%s", zoneName.c_str()); + } + ImGui::PopStyleColor(); // LFG role badge (Tank/Healer/DPS) — shown on same line as name when set if (member.roles != 0) { @@ -6463,6 +12547,39 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); } } + // Tactical role badge (MT/MA/Asst) from group flags + if (member.flags & 0x02) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.0f, 0.9f), "[MT]"); + } else if (member.flags & 0x04) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 0.9f), "[MA]"); + } else if (member.flags & 0x01) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 0.7f), "[A]"); + } + + // Raid mark symbol — shown on same line as name when this party member has a mark + { + static const struct { const char* sym; ImU32 col; } kPartyMarks[] = { + { "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) }, // 0 Star + { "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) }, // 1 Circle + { "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, // 2 Diamond + { "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, // 3 Triangle + { "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, // 4 Moon + { "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, // 5 Square + { "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, // 6 Cross + { "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, // 7 Skull + }; + uint8_t pmk = gameHandler.getEntityRaidMark(member.guid); + if (pmk < game::GameHandler::kRaidMarkCount) { + ImGui::SameLine(); + ImGui::TextColored( + ImGui::ColorConvertU32ToFloat4(kPartyMarks[pmk].col), + "%s", kPartyMarks[pmk].sym); + } + } + // Health bar: prefer party stats, fall back to entity uint32_t hp = 0, maxHp = 0; if (member.hasPartyStats && member.maxHealth > 0) { @@ -6476,24 +12593,68 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { maxHp = unit->getMaxHealth(); } } - if (maxHp > 0) { + // Check dead/ghost state for health bar rendering + bool memberDead = false; + bool memberOffline = false; + if (member.hasPartyStats) { + bool isOnline2 = (member.onlineStatus & 0x0001) != 0; + bool isDead2 = (member.onlineStatus & 0x0020) != 0; + bool isGhost2 = (member.onlineStatus & 0x0010) != 0; + memberDead = isDead2 || isGhost2; + memberOffline = !isOnline2; + } + + // Out-of-range check: compare player position to member's reported position + // Range threshold: 40 yards (standard heal/spell range) + bool memberOutOfRange = false; + if (member.hasPartyStats && !memberOffline && !memberDead && + member.zoneId != 0) { + // Same map: use 2D Euclidean distance in WoW coordinates (yards) + auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid()); + if (playerEntity) { + float dx = playerEntity->getX() - static_cast(member.posX); + float dy = playerEntity->getY() - static_cast(member.posY); + float distSq = dx * dx + dy * dy; + memberOutOfRange = (distSq > 40.0f * 40.0f); + } + } + + if (memberDead) { + // Gray "Dead" bar for fallen party members + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.35f, 0.35f, 0.35f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f)); + ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Dead"); + ImGui::PopStyleColor(2); + } else if (memberOffline) { + // Dim bar for offline members + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.25f, 0.25f, 0.25f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.1f, 0.6f)); + ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Offline"); + ImGui::PopStyleColor(2); + } else if (maxHp > 0) { float pct = static_cast(hp) / static_cast(maxHp); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, - pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : - pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : - ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + // Out-of-range: desaturate health bar to gray + ImVec4 hpBarColor = memberOutOfRange + ? ImVec4(0.45f, 0.45f, 0.45f, 0.7f) + : (pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) : + pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) : + ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpBarColor); char hpText[32]; - if (maxHp >= 10000) + if (memberOutOfRange) { + snprintf(hpText, sizeof(hpText), "OOR"); + } else if (maxHp >= 10000) { snprintf(hpText, sizeof(hpText), "%dk/%dk", - (int)hp / 1000, (int)maxHp / 1000); - else + static_cast(hp) / 1000, static_cast(maxHp) / 1000); + } else { snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp); + } ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText); ImGui::PopStyleColor(); } - // Power bar (mana/rage/energy) from party stats - if (member.hasPartyStats && member.maxPower > 0) { + // Power bar (mana/rage/energy) from party stats — hidden for dead/offline/OOR + if (!memberDead && !memberOffline && member.hasPartyStats && member.maxPower > 0) { float powerPct = static_cast(member.curPower) / static_cast(member.maxPower); ImVec4 powerColor; switch (member.powerType) { @@ -6504,13 +12665,78 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { case 4: powerColor = ImVec4(0.5f, 0.9f, 0.3f, 1.0f); break; // Happiness (green) case 6: powerColor = ImVec4(0.8f, 0.1f, 0.2f, 1.0f); break; // Runic Power (crimson) case 7: powerColor = ImVec4(0.4f, 0.1f, 0.6f, 1.0f); break; // Soul Shards (purple) - default: powerColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); break; + default: powerColor = kColorDarkGray; break; } ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor); ImGui::ProgressBar(powerPct, ImVec2(-1, 8), ""); ImGui::PopStyleColor(); } + // Dispellable debuff indicators — small colored dots for party member debuffs + // Only show magic/curse/disease/poison (types 1-4); skip non-dispellable + if (!memberDead && !memberOffline) { + const std::vector* unitAuras = nullptr; + if (member.guid == gameHandler.getPlayerGuid()) + unitAuras = &gameHandler.getPlayerAuras(); + else if (member.guid == gameHandler.getTargetGuid()) + unitAuras = &gameHandler.getTargetAuras(); + else + unitAuras = gameHandler.getUnitAuras(member.guid); + + if (unitAuras) { + bool anyDebuff = false; + for (const auto& aura : *unitAuras) { + if (aura.isEmpty()) continue; + if ((aura.flags & 0x80) == 0) continue; // only debuffs + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + if (dt == 0) continue; // skip non-dispellable + anyDebuff = true; + break; + } + if (anyDebuff) { + // Render one dot per unique dispel type present + bool shown[5] = {}; + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 1.0f)); + for (const auto& aura : *unitAuras) { + if (aura.isEmpty()) continue; + if ((aura.flags & 0x80) == 0) continue; + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + if (dt == 0 || dt > 4 || shown[dt]) continue; + shown[dt] = true; + ImVec4 dotCol; + switch (dt) { + case 1: dotCol = ImVec4(0.25f, 0.50f, 1.00f, 1.0f); break; // Magic: blue + case 2: dotCol = ImVec4(0.70f, 0.15f, 0.90f, 1.0f); break; // Curse: purple + case 3: dotCol = ImVec4(0.65f, 0.45f, 0.10f, 1.0f); break; // Disease: brown + case 4: dotCol = ImVec4(0.10f, 0.75f, 0.10f, 1.0f); break; // Poison: green + default: break; + } + ImGui::PushStyleColor(ImGuiCol_Button, dotCol); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, dotCol); + ImGui::Button("##d", ImVec2(8.0f, 8.0f)); + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + static const char* kDispelNames[] = { "", "Magic", "Curse", "Disease", "Poison" }; + // Find spell name(s) of this dispel type + ImGui::BeginTooltip(); + ImGui::TextColored(dotCol, "%s", kDispelNames[dt]); + for (const auto& da : *unitAuras) { + if (da.isEmpty() || (da.flags & 0x80) == 0) continue; + if (gameHandler.getSpellDispelType(da.spellId) != dt) continue; + const std::string& dName = gameHandler.getSpellName(da.spellId); + if (!dName.empty()) + ImGui::Text(" %s", dName.c_str()); + } + ImGui::EndTooltip(); + } + ImGui::SameLine(); + } + ImGui::NewLine(); + ImGui::PopStyleVar(); + } + } + } + // Party member cast bar — shows when the party member is casting if (auto* cs = gameHandler.getUnitCastState(member.guid)) { float castPct = (cs->timeTotal > 0.0f) @@ -6522,7 +12748,17 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { snprintf(pcastLabel, sizeof(pcastLabel), "%s (%.1fs)", spellNm.c_str(), cs->timeRemaining); else snprintf(pcastLabel, sizeof(pcastLabel), "Casting... (%.1fs)", cs->timeRemaining); - ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel); + { + VkDescriptorSet pIcon = (cs->spellId != 0 && assetMgr) + ? getSpellIcon(cs->spellId, assetMgr) : VK_NULL_HANDLE; + if (pIcon) { + ImGui::Image((ImTextureID)(uintptr_t)pIcon, ImVec2(10, 10)); + ImGui::SameLine(0, 2); + ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel); + } else { + ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel); + } + } ImGui::PopStyleColor(); } @@ -6602,6 +12838,108 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +// ============================================================ +// Durability Warning (equipment damage indicator) +// ============================================================ + +void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (!renderer) return; + + // Build path: ~/.wowee/screenshots/WoWee_YYYYMMDD_HHMMSS.png + const char* home = std::getenv("HOME"); + if (!home) home = std::getenv("USERPROFILE"); + if (!home) home = "/tmp"; + std::string dir = std::string(home) + "/.wowee/screenshots"; + + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &tt); +#else + localtime_r(&tt, &tm); +#endif + + char filename[128]; + std::snprintf(filename, sizeof(filename), + "WoWee_%04d%02d%02d_%02d%02d%02d.png", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); + + std::string path = dir + "/" + filename; + + if (renderer->captureScreenshot(path)) { + game::MessageChatData sysMsg; + sysMsg.type = game::ChatType::SYSTEM; + sysMsg.language = game::ChatLanguage::UNIVERSAL; + sysMsg.message = "Screenshot saved: " + path; + core::Application::getInstance().getGameHandler()->addLocalChatMessage(sysMsg); + } +} + +void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) { + if (gameHandler.getPlayerGuid() == 0) return; + + const auto& inv = gameHandler.getInventory(); + + // Scan all equipment slots (skip bag slots which have no durability) + float minDurPct = 1.0f; + bool hasBroken = false; + + for (int i = static_cast(game::EquipSlot::HEAD); + i < static_cast(game::EquipSlot::BAG1); ++i) { + const auto& slot = inv.getEquipSlot(static_cast(i)); + if (slot.empty() || slot.item.maxDurability == 0) continue; + if (slot.item.curDurability == 0) { + hasBroken = true; + } + float pct = static_cast(slot.item.curDurability) / + static_cast(slot.item.maxDurability); + if (pct < minDurPct) minDurPct = pct; + } + + // Only show warning below 20% + if (minDurPct >= 0.2f && !hasBroken) return; + + ImGuiIO& io = ImGui::GetIO(); + const float screenW = io.DisplaySize.x; + const float screenH = io.DisplaySize.y; + + // Position: just above the XP bar / action bar area (bottom-center) + const float warningW = 220.0f; + const float warningH = 26.0f; + const float posX = (screenW - warningW) * 0.5f; + const float posY = screenH - 140.0f; // above action bar + + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(warningW, warningH), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.75f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6, 4)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus; + + if (ImGui::Begin("##durability_warn", nullptr, flags)) { + if (hasBroken) { + ImGui::TextColored(ImVec4(1.0f, 0.15f, 0.15f, 1.0f), + "\xef\x94\x9b Gear broken! Visit a repair NPC"); + } else { + int pctInt = static_cast(minDurPct * 100.0f); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.1f, 1.0f), + "\xef\x94\x9b Low durability: %d%%", pctInt); + } + if (ImGui::IsWindowHovered()) + ImGui::SetTooltip("Your equipment is damaged. Visit any blacksmith or repair NPC."); + } + ImGui::End(); + ImGui::PopStyleVar(3); +} + // ============================================================ // UI Error Frame (WoW-style center-bottom error overlay) // ============================================================ @@ -6722,18 +13060,18 @@ void GameScreen::renderRepToasts(float deltaTime) { ImVec2 br(toastX + toastW, toastY + toastH); // Background - draw->AddRectFilled(tl, br, IM_COL32(15, 15, 20, (int)(alpha * 200)), 4.0f); + draw->AddRectFilled(tl, br, IM_COL32(15, 15, 20, static_cast(alpha * 200)), 4.0f); // Border: green for gain, red for loss ImU32 borderCol = (e.delta > 0) - ? IM_COL32(80, 200, 80, (int)(alpha * 220)) - : IM_COL32(200, 60, 60, (int)(alpha * 220)); + ? IM_COL32(80, 200, 80, static_cast(alpha * 220)) + : IM_COL32(200, 60, 60, static_cast(alpha * 220)); draw->AddRect(tl, br, borderCol, 4.0f, 0, 1.5f); // Delta text: "+250" or "-250" char deltaBuf[16]; snprintf(deltaBuf, sizeof(deltaBuf), "%+d", e.delta); - ImU32 deltaCol = (e.delta > 0) ? IM_COL32(80, 220, 80, (int)(alpha * 255)) - : IM_COL32(220, 70, 70, (int)(alpha * 255)); + ImU32 deltaCol = (e.delta > 0) ? IM_COL32(80, 220, 80, static_cast(alpha * 255)) + : IM_COL32(220, 70, 70, static_cast(alpha * 255)); draw->AddText(font, fontSize, ImVec2(tl.x + 6.0f, tl.y + (toastH - fontSize) * 0.5f), deltaCol, deltaBuf); @@ -6741,7 +13079,193 @@ void GameScreen::renderRepToasts(float deltaTime) { char nameBuf[64]; snprintf(nameBuf, sizeof(nameBuf), "%s (%s)", e.factionName.c_str(), standingLabel(e.standing)); draw->AddText(font, fontSize * 0.85f, ImVec2(tl.x + 44.0f, tl.y + (toastH - fontSize * 0.85f) * 0.5f), - IM_COL32(210, 210, 210, (int)(alpha * 220)), nameBuf); + IM_COL32(210, 210, 210, static_cast(alpha * 220)), nameBuf); + } +} + +void GameScreen::renderQuestCompleteToasts(float deltaTime) { + for (auto& e : questCompleteToasts_) e.age += deltaTime; + questCompleteToasts_.erase( + std::remove_if(questCompleteToasts_.begin(), questCompleteToasts_.end(), + [](const QuestCompleteToastEntry& e) { return e.age >= kQuestCompleteToastLifetime; }), + questCompleteToasts_.end()); + + if (questCompleteToasts_.empty()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + const float toastW = 260.0f; + const float toastH = 40.0f; + const float padY = 4.0f; + const float baseY = screenH - 220.0f; // above rep toasts + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + + for (int i = 0; i < static_cast(questCompleteToasts_.size()); ++i) { + const auto& e = questCompleteToasts_[i]; + constexpr float kSlideDur = 0.3f; + float slideIn = std::min(e.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kQuestCompleteToastLifetime - e.age), kSlideDur) / kSlideDur; + float slide = std::min(slideIn, slideOut); + float alpha = std::clamp(slide, 0.0f, 1.0f); + + float xFull = screenW - 14.0f - toastW; + float xStart = screenW + 10.0f; + float toastX = xStart + (xFull - xStart) * slide; + float toastY = baseY - i * (toastH + padY); + + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + toastW, toastY + toastH); + + // Background + gold border (quest completion) + draw->AddRectFilled(tl, br, IM_COL32(20, 18, 8, static_cast(alpha * 210)), 5.0f); + draw->AddRect(tl, br, IM_COL32(220, 180, 30, static_cast(alpha * 230)), 5.0f, 0, 1.5f); + + // Scroll icon placeholder (gold diamond) + float iconCx = tl.x + 18.0f; + float iconCy = tl.y + toastH * 0.5f; + draw->AddCircleFilled(ImVec2(iconCx, iconCy), 7.0f, IM_COL32(210, 170, 20, static_cast(alpha * 230))); + draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, static_cast(alpha * 200))); + + // "Quest Complete" header in gold + const char* header = "Quest Complete"; + draw->AddText(font, fontSize * 0.78f, + ImVec2(tl.x + 34.0f, tl.y + 4.0f), + IM_COL32(240, 200, 40, static_cast(alpha * 240)), header); + + // Quest title in off-white + const char* titleStr = e.title.empty() ? "Unknown Quest" : e.title.c_str(); + draw->AddText(font, fontSize * 0.82f, + ImVec2(tl.x + 34.0f, tl.y + toastH * 0.5f + 1.0f), + IM_COL32(220, 215, 195, static_cast(alpha * 220)), titleStr); + } +} + +// ============================================================ +// Zone Entry Toast +// ============================================================ + +void GameScreen::renderZoneToasts(float deltaTime) { + for (auto& e : zoneToasts_) e.age += deltaTime; + zoneToasts_.erase( + std::remove_if(zoneToasts_.begin(), zoneToasts_.end(), + [](const ZoneToastEntry& e) { return e.age >= kZoneToastLifetime; }), + zoneToasts_.end()); + + if (zoneToasts_.empty()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + + for (int i = 0; i < static_cast(zoneToasts_.size()); ++i) { + const auto& e = zoneToasts_[i]; + constexpr float kSlideDur = 0.35f; + float slideIn = std::min(e.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kZoneToastLifetime - e.age), kSlideDur) / kSlideDur; + float slide = std::min(slideIn, slideOut); + float alpha = std::clamp(slide, 0.0f, 1.0f); + + // Measure text to size the toast + ImVec2 nameSz = font->CalcTextSizeA(14.0f, FLT_MAX, 0.0f, e.zoneName.c_str()); + const char* header = "Entering:"; + ImVec2 hdrSz = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, header); + + float toastW = std::max(nameSz.x, hdrSz.x) + 28.0f; + float toastH = 42.0f; + + // Center the toast horizontally, appear just below the zone name area (top-center) + float toastX = (screenW - toastW) * 0.5f; + float toastY = 56.0f + i * (toastH + 4.0f); + // Slide down from above + float offY = (1.0f - slide) * (-toastH - 10.0f); + toastY += offY; + + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + toastW, toastY + toastH); + + draw->AddRectFilled(tl, br, IM_COL32(10, 10, 16, static_cast(alpha * 200)), 6.0f); + draw->AddRect(tl, br, IM_COL32(160, 140, 80, static_cast(alpha * 220)), 6.0f, 0, 1.2f); + + float cx = tl.x + toastW * 0.5f; + draw->AddText(font, 11.0f, + ImVec2(cx - hdrSz.x * 0.5f, tl.y + 5.0f), + IM_COL32(180, 170, 120, static_cast(alpha * 200)), header); + draw->AddText(font, 14.0f, + ImVec2(cx - nameSz.x * 0.5f, tl.y + toastH * 0.5f + 1.0f), + IM_COL32(255, 230, 140, static_cast(alpha * 240)), e.zoneName.c_str()); + } +} + +// ─── Area Trigger Message Toasts ───────────────────────────────────────────── +void GameScreen::renderAreaTriggerToasts(float deltaTime, game::GameHandler& gameHandler) { + // Drain any pending messages from GameHandler + while (gameHandler.hasAreaTriggerMsg()) { + AreaTriggerToast t; + t.text = gameHandler.popAreaTriggerMsg(); + t.age = 0.0f; + areaTriggerToasts_.push_back(std::move(t)); + if (areaTriggerToasts_.size() > 4) + areaTriggerToasts_.erase(areaTriggerToasts_.begin()); + } + + // Age and prune + constexpr float kLifetime = 4.5f; + for (auto& t : areaTriggerToasts_) t.age += deltaTime; + areaTriggerToasts_.erase( + std::remove_if(areaTriggerToasts_.begin(), areaTriggerToasts_.end(), + [](const AreaTriggerToast& t) { return t.age >= kLifetime; }), + areaTriggerToasts_.end()); + if (areaTriggerToasts_.empty()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImDrawList* draw = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + constexpr float kSlideDur = 0.35f; + + for (int i = 0; i < static_cast(areaTriggerToasts_.size()); ++i) { + const auto& t = areaTriggerToasts_[i]; + + float slideIn = std::min(t.age, kSlideDur) / kSlideDur; + float slideOut = std::min(std::max(0.0f, kLifetime - t.age), kSlideDur) / kSlideDur; + float alpha = std::clamp(std::min(slideIn, slideOut), 0.0f, 1.0f); + + // Measure text + ImVec2 txtSz = font->CalcTextSizeA(13.0f, FLT_MAX, 0.0f, t.text.c_str()); + float toastW = txtSz.x + 30.0f; + float toastH = 30.0f; + + // Center horizontally, place below zone text (center of lower-third) + float toastX = (screenW - toastW) * 0.5f; + float toastY = screenH * 0.62f + i * (toastH + 3.0f); + // Slide up from below + float offY = (1.0f - std::min(slideIn, slideOut)) * (toastH + 12.0f); + toastY += offY; + + ImVec2 tl(toastX, toastY); + ImVec2 br(toastX + toastW, toastY + toastH); + + draw->AddRectFilled(tl, br, IM_COL32(8, 12, 22, static_cast(alpha * 190)), 5.0f); + draw->AddRect(tl, br, IM_COL32(100, 160, 220, static_cast(alpha * 200)), 5.0f, 0, 1.0f); + + float cx = tl.x + toastW * 0.5f; + // Shadow + draw->AddText(font, 13.0f, + ImVec2(cx - txtSz.x * 0.5f + 1, tl.y + (toastH - txtSz.y) * 0.5f + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 180)), t.text.c_str()); + // Text in light blue + draw->AddText(font, 13.0f, + ImVec2(cx - txtSz.x * 0.5f, tl.y + (toastH - txtSz.y) * 0.5f), + IM_COL32(180, 220, 255, static_cast(alpha * 240)), t.text.c_str()); } } @@ -6750,6 +13274,8 @@ void GameScreen::renderRepToasts(float deltaTime) { // ============================================================ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { + auto* assetMgr = core::Application::getInstance().getAssetManager(); + // Collect active boss unit slots struct BossSlot { uint32_t slot; uint64_t guid; }; std::vector active; @@ -6777,17 +13303,22 @@ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { for (const auto& bs : active) { ImGui::PushID(static_cast(bs.guid)); - // Try to resolve name and health from entity manager + // Try to resolve name, health, and power from entity manager std::string name = "Boss"; uint32_t hp = 0, maxHp = 0; + uint8_t bossPowerType = 0; + uint32_t bossPower = 0, bossMaxPower = 0; auto entity = gameHandler.getEntityManager().getEntity(bs.guid); if (entity && (entity->getType() == game::ObjectType::UNIT || entity->getType() == game::ObjectType::PLAYER)) { auto unit = std::static_pointer_cast(entity); const auto& n = unit->getName(); if (!n.empty()) name = n; - hp = unit->getHealth(); - maxHp = unit->getMaxHealth(); + hp = unit->getHealth(); + maxHp = unit->getMaxHealth(); + bossPowerType = unit->getPowerType(); + bossPower = unit->getPower(); + bossMaxPower = unit->getMaxPower(); } // Clickable name to target @@ -6808,6 +13339,25 @@ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); } + // Boss power bar — shown when boss has a non-zero power pool + // Energy bosses (type 3) are particularly important: full energy signals ability use + if (bossMaxPower > 0 && bossPower > 0) { + float bpPct = static_cast(bossPower) / static_cast(bossMaxPower); + ImVec4 bpColor; + switch (bossPowerType) { + case 0: bpColor = ImVec4(0.2f, 0.3f, 0.9f, 1.0f); break; // Mana: blue + case 1: bpColor = ImVec4(0.9f, 0.2f, 0.2f, 1.0f); break; // Rage: red + case 2: bpColor = ImVec4(0.9f, 0.6f, 0.1f, 1.0f); break; // Focus: orange + case 3: bpColor = ImVec4(0.9f, 0.9f, 0.1f, 1.0f); break; // Energy: yellow + default: bpColor = ImVec4(0.4f, 0.8f, 0.4f, 1.0f); break; + } + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bpColor); + char bpLabel[24]; + std::snprintf(bpLabel, sizeof(bpLabel), "%u", bossPower); + ImGui::ProgressBar(bpPct, ImVec2(-1, 6), bpLabel); + ImGui::PopStyleColor(); + } + // Boss cast bar — shown when the boss is casting (critical for interrupt) if (auto* cs = gameHandler.getUnitCastState(bs.guid)) { float castPct = (cs->timeTotal > 0.0f) @@ -6815,17 +13365,185 @@ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { uint32_t bspell = cs->spellId; const std::string& bcastName = (bspell != 0) ? gameHandler.getSpellName(bspell) : ""; - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.9f, 0.3f, 0.2f, 1.0f)); + // Green = interruptible, Red = immune; pulse when > 80% complete + ImVec4 bcastColor; + if (castPct > 0.8f) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 8.0f); + bcastColor = cs->interruptible + ? ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f) + : ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); + } else { + bcastColor = cs->interruptible + ? ImVec4(0.2f, 0.75f, 0.2f, 1.0f) + : ImVec4(0.9f, 0.15f, 0.15f, 1.0f); + } + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bcastColor); char bcastLabel[72]; if (!bcastName.empty()) snprintf(bcastLabel, sizeof(bcastLabel), "%s (%.1fs)", bcastName.c_str(), cs->timeRemaining); else snprintf(bcastLabel, sizeof(bcastLabel), "Casting... (%.1fs)", cs->timeRemaining); - ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel); + { + VkDescriptorSet bIcon = (bspell != 0 && assetMgr) + ? getSpellIcon(bspell, assetMgr) : VK_NULL_HANDLE; + if (bIcon) { + ImGui::Image((ImTextureID)(uintptr_t)bIcon, ImVec2(12, 12)); + ImGui::SameLine(0, 2); + ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel); + } else { + ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel); + } + } ImGui::PopStyleColor(); } + // Boss aura row: debuffs first (player DoTs), then boss buffs + { + const std::vector* bossAuras = nullptr; + if (bs.guid == gameHandler.getTargetGuid()) + bossAuras = &gameHandler.getTargetAuras(); + else + bossAuras = gameHandler.getUnitAuras(bs.guid); + + if (bossAuras) { + int bossActive = 0; + for (const auto& a : *bossAuras) if (!a.isEmpty()) bossActive++; + if (bossActive > 0) { + constexpr float BA_ICON = 16.0f; + constexpr int BA_PER_ROW = 10; + + uint64_t baNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + // Sort: player-applied debuffs first (most relevant), then others + const uint64_t pguid = gameHandler.getPlayerGuid(); + std::vector baIdx; + baIdx.reserve(bossAuras->size()); + for (size_t i = 0; i < bossAuras->size(); ++i) + if (!(*bossAuras)[i].isEmpty()) baIdx.push_back(i); + std::sort(baIdx.begin(), baIdx.end(), [&](size_t a, size_t b) { + const auto& aa = (*bossAuras)[a]; + const auto& ab = (*bossAuras)[b]; + bool aPlayerDot = (aa.flags & 0x80) != 0 && aa.casterGuid == pguid; + bool bPlayerDot = (ab.flags & 0x80) != 0 && ab.casterGuid == pguid; + if (aPlayerDot != bPlayerDot) return aPlayerDot > bPlayerDot; + bool aDebuff = (aa.flags & 0x80) != 0; + bool bDebuff = (ab.flags & 0x80) != 0; + if (aDebuff != bDebuff) return aDebuff > bDebuff; + int32_t ra = aa.getRemainingMs(baNowMs); + int32_t rb = ab.getRemainingMs(baNowMs); + if (ra < 0 && rb < 0) return false; + if (ra < 0) return false; + if (rb < 0) return true; + return ra < rb; + }); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); + int baShown = 0; + for (size_t si = 0; si < baIdx.size() && baShown < 20; ++si) { + const auto& aura = (*bossAuras)[baIdx[si]]; + bool isBuff = (aura.flags & 0x80) == 0; + bool isPlayerCast = (aura.casterGuid == pguid); + + if (baShown > 0 && baShown % BA_PER_ROW != 0) ImGui::SameLine(); + ImGui::PushID(static_cast(baIdx[si]) + 7000); + + ImVec4 borderCol; + if (isBuff) { + // Boss buffs: gold for important enrage/shield types + borderCol = ImVec4(0.8f, 0.6f, 0.1f, 0.9f); + } else { + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + switch (dt) { + case 1: borderCol = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; + case 2: borderCol = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; + case 3: borderCol = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; + case 4: borderCol = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; + default: borderCol = isPlayerCast + ? ImVec4(0.90f, 0.30f, 0.10f, 0.9f) // player DoT: orange-red + : ImVec4(0.60f, 0.20f, 0.20f, 0.9f); // other debuff: dark red + break; + } + } + + VkDescriptorSet baIcon = assetMgr + ? getSpellIcon(aura.spellId, assetMgr) : VK_NULL_HANDLE; + if (baIcon) { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::ImageButton("##baura", + (ImTextureID)(uintptr_t)baIcon, + ImVec2(BA_ICON - 2, BA_ICON - 2)); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, borderCol); + char lab[8]; + snprintf(lab, sizeof(lab), "%u", aura.spellId % 10000); + ImGui::Button(lab, ImVec2(BA_ICON, BA_ICON)); + ImGui::PopStyleColor(); + } + + // Duration overlay + int32_t baRemain = aura.getRemainingMs(baNowMs); + if (baRemain > 0) { + ImVec2 imin = ImGui::GetItemRectMin(); + ImVec2 imax = ImGui::GetItemRectMax(); + char ts[12]; + int s = (baRemain + 999) / 1000; + if (s >= 3600) snprintf(ts, sizeof(ts), "%dh", s / 3600); + else if (s >= 60) snprintf(ts, sizeof(ts), "%d:%02d", s / 60, s % 60); + else snprintf(ts, sizeof(ts), "%d", s); + ImVec2 tsz = ImGui::CalcTextSize(ts); + float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f; + float cy = imax.y - tsz.y; + ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts); + ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts); + } + + // Stack / charge count — upper-left corner (parity with target/focus frames) + if (aura.charges > 1) { + ImVec2 baMin = ImGui::GetItemRectMin(); + char chargeStr[8]; + snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast(aura.charges)); + ImGui::GetWindowDrawList()->AddText(ImVec2(baMin.x + 2, baMin.y + 2), + IM_COL32(0, 0, 0, 200), chargeStr); + ImGui::GetWindowDrawList()->AddText(ImVec2(baMin.x + 1, baMin.y + 1), + IM_COL32(255, 220, 50, 255), chargeStr); + } + + // Tooltip + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + bool richOk = spellbookScreen.renderSpellInfoTooltip( + aura.spellId, gameHandler, assetMgr); + if (!richOk) { + std::string nm = spellbookScreen.lookupSpellName(aura.spellId, assetMgr); + if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId); + ImGui::Text("%s", nm.c_str()); + } + if (isPlayerCast && !isBuff) + ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.3f, 1.0f), "Your DoT"); + if (baRemain > 0) { + int s = baRemain / 1000; + char db[32]; + if (s < 60) snprintf(db, sizeof(db), "Remaining: %ds", s); + else snprintf(db, sizeof(db), "Remaining: %dm %ds", s / 60, s % 60); + ImGui::TextColored(ui::colors::kLightGray, "%s", db); + } + ImGui::EndTooltip(); + } + + ImGui::PopID(); + baShown++; + } + ImGui::PopStyleVar(); + } + } + } + ImGui::PopID(); ImGui::Spacing(); } @@ -6849,7 +13567,7 @@ void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); - if (ImGui::Begin("Group Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Group Invite", nullptr, kDialogFlags)) { ImGui::Text("%s has invited you to a group.", gameHandler.getPendingInviterName().c_str()); ImGui::Spacing(); @@ -6873,7 +13591,7 @@ void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 250), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); - if (ImGui::Begin("Duel Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Duel Request", nullptr, kDialogFlags)) { ImGui::Text("%s challenges you to a duel!", gameHandler.getDuelChallengerName().c_str()); ImGui::Spacing(); @@ -6888,6 +13606,47 @@ void GameScreen::renderDuelRequestPopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderDuelCountdown(game::GameHandler& gameHandler) { + float remaining = gameHandler.getDuelCountdownRemaining(); + if (remaining <= 0.0f) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + auto* dl = ImGui::GetForegroundDrawList(); + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + + // Show integer countdown or "Fight!" when under 0.5s + char buf[32]; + if (remaining > 0.5f) { + snprintf(buf, sizeof(buf), "%d", static_cast(std::ceil(remaining))); + } else { + snprintf(buf, sizeof(buf), "Fight!"); + } + + // Large font by scaling — use 4x font size for dramatic effect + float scale = 4.0f; + float scaledSize = fontSize * scale; + ImVec2 textSz = font->CalcTextSizeA(scaledSize, FLT_MAX, 0.0f, buf); + float tx = (screenW - textSz.x) * 0.5f; + float ty = screenH * 0.35f - textSz.y * 0.5f; + + // Pulsing alpha: fades in and out per second + float pulse = 0.75f + 0.25f * std::sin(static_cast(ImGui::GetTime()) * 6.28f); + uint8_t alpha = static_cast(255 * pulse); + + // Color: golden countdown, red "Fight!" + ImU32 color = (remaining > 0.5f) + ? IM_COL32(255, 200, 50, alpha) + : IM_COL32(255, 60, 60, alpha); + + // Drop shadow + dl->AddText(font, scaledSize, ImVec2(tx + 2.0f, ty + 2.0f), IM_COL32(0, 0, 0, alpha / 2), buf); + dl->AddText(font, scaledSize, ImVec2(tx, ty), color, buf); +} + void GameScreen::renderItemTextWindow(game::GameHandler& gameHandler) { if (!gameHandler.isItemTextOpen()) return; @@ -6933,7 +13692,7 @@ void GameScreen::renderSharedQuestPopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 490), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); - if (ImGui::Begin("Shared Quest", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Shared Quest", nullptr, kDialogFlags)) { ImGui::Text("%s has shared a quest with you:", gameHandler.getSharedQuestSharerName().c_str()); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "\"%s\"", gameHandler.getSharedQuestTitle().c_str()); ImGui::Spacing(); @@ -6963,7 +13722,7 @@ void GameScreen::renderSummonRequestPopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 430), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); - if (ImGui::Begin("Summon Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Summon Request", nullptr, kDialogFlags)) { ImGui::Text("%s is summoning you.", gameHandler.getSummonerName().c_str()); float t = gameHandler.getSummonTimeoutSec(); if (t > 0.0f) { @@ -6991,7 +13750,7 @@ void GameScreen::renderTradeRequestPopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 370), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); - if (ImGui::Begin("Trade Request", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Trade Request", nullptr, kDialogFlags)) { ImGui::Text("%s wants to trade with you.", gameHandler.getTradePeerName().c_str()); ImGui::Spacing(); @@ -7024,7 +13783,7 @@ void GameScreen::renderTradeWindow(game::GameHandler& gameHandler) { bool open = true; if (ImGui::Begin(("Trade with " + peerName).c_str(), &open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + kDialogFlags)) { auto formatGold = [](uint64_t copper, char* buf, size_t bufsz) { uint64_t g = copper / 10000; @@ -7181,18 +13940,38 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 310), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); - if (ImGui::Begin("Loot Roll", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Loot Roll", nullptr, kDialogFlags)) { // Quality color for item name - static const ImVec4 kQualityColors[] = { - ImVec4(0.6f, 0.6f, 0.6f, 1.0f), // 0=poor (grey) - ImVec4(1.0f, 1.0f, 1.0f, 1.0f), // 1=common (white) - ImVec4(0.1f, 1.0f, 0.1f, 1.0f), // 2=uncommon (green) - ImVec4(0.0f, 0.44f, 0.87f, 1.0f),// 3=rare (blue) - ImVec4(0.64f, 0.21f, 0.93f, 1.0f),// 4=epic (purple) - ImVec4(1.0f, 0.5f, 0.0f, 1.0f), // 5=legendary (orange) - }; uint8_t q = roll.itemQuality; - ImVec4 col = (q < 6) ? kQualityColors[q] : kQualityColors[1]; + ImVec4 col = ui::getQualityColor(static_cast(q)); + + // Countdown bar + { + auto now = std::chrono::steady_clock::now(); + float elapsedMs = static_cast( + std::chrono::duration_cast(now - roll.rollStartedAt).count()); + float totalMs = static_cast(roll.rollCountdownMs > 0 ? roll.rollCountdownMs : 60000); + float fraction = 1.0f - std::min(elapsedMs / totalMs, 1.0f); + float remainSec = (totalMs - elapsedMs) / 1000.0f; + if (remainSec < 0.0f) remainSec = 0.0f; + + // Color: green → yellow → red + ImVec4 barColor; + if (fraction > 0.5f) + barColor = ImVec4(0.2f + (1.0f - fraction) * 1.4f, 0.85f, 0.2f, 1.0f); + else if (fraction > 0.2f) + barColor = ImVec4(1.0f, fraction * 1.7f, 0.1f, 1.0f); + else { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 6.0f); + barColor = ImVec4(pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); + } + + char timeBuf[16]; + std::snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remainSec); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); + ImGui::ProgressBar(fraction, ImVec2(-1, 12), timeBuf); + ImGui::PopStyleColor(); + } ImGui::Text("An item is up for rolls:"); @@ -7204,7 +13983,14 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { ImGui::Image((ImTextureID)(uintptr_t)rollIcon, ImVec2(24, 24)); ImGui::SameLine(); } - ImGui::TextColored(col, "[%s]", roll.itemName.c_str()); + // Prefer live item info (arrives via SMSG_ITEM_QUERY_SINGLE_RESPONSE after the + // roll popup opens); fall back to the name cached at SMSG_LOOT_START_ROLL time. + const char* displayName = (rollInfo && rollInfo->valid && !rollInfo->name.empty()) + ? rollInfo->name.c_str() + : roll.itemName.c_str(); + if (rollInfo && rollInfo->valid) + col = ui::getQualityColor(static_cast(rollInfo->quality)); + ImGui::TextColored(col, "[%s]", displayName); if (ImGui::IsItemHovered() && rollInfo && rollInfo->valid) { inventoryScreen.renderItemTooltip(*rollInfo); } @@ -7220,20 +14006,72 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { } ImGui::Spacing(); - if (ImGui::Button("Need", ImVec2(80, 30))) { - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0); + // voteMask bits: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant + const uint8_t vm = roll.voteMask; + bool first = true; + if (vm & 0x02) { + if (ImGui::Button("Need", ImVec2(80, 30))) + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 0); + first = false; } - ImGui::SameLine(); - if (ImGui::Button("Greed", ImVec2(80, 30))) { - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1); + if (vm & 0x04) { + if (!first) ImGui::SameLine(); + if (ImGui::Button("Greed", ImVec2(80, 30))) + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 1); + first = false; } - ImGui::SameLine(); - if (ImGui::Button("Disenchant", ImVec2(95, 30))) { - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2); + if (vm & 0x08) { + if (!first) ImGui::SameLine(); + if (ImGui::Button("Disenchant", ImVec2(95, 30))) + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 2); + first = false; } - ImGui::SameLine(); - if (ImGui::Button("Pass", ImVec2(70, 30))) { - gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); + if (vm & 0x01) { + if (!first) ImGui::SameLine(); + if (ImGui::Button("Pass", ImVec2(70, 30))) + gameHandler.sendLootRoll(roll.objectGuid, roll.slot, 96); + } + + // Live roll results from group members + if (!roll.playerRolls.empty()) { + ImGui::Separator(); + ImGui::TextDisabled("Rolls so far:"); + // Roll-type label + color + static const char* kRollLabels[] = {"Need", "Greed", "Disenchant", "Pass"}; + static const ImVec4 kRollColors[] = { + ImVec4(0.2f, 0.9f, 0.2f, 1.0f), // Need — green + ImVec4(0.3f, 0.6f, 1.0f, 1.0f), // Greed — blue + ImVec4(0.7f, 0.3f, 0.9f, 1.0f), // Disenchant — purple + kColorDarkGray, // Pass — gray + }; + auto rollTypeIndex = [](uint8_t t) -> int { + if (t == 0) return 0; + if (t == 1) return 1; + if (t == 2) return 2; + return 3; // pass (96 or unknown) + }; + + if (ImGui::BeginTable("##lootrolls", 3, + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 72.0f); + ImGui::TableSetupColumn("Roll", ImGuiTableColumnFlags_WidthFixed, 32.0f); + for (const auto& r : roll.playerRolls) { + int ri = rollTypeIndex(r.rollType); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(r.playerName.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(kRollColors[ri], "%s", kRollLabels[ri]); + ImGui::TableSetColumnIndex(2); + if (r.rollType != 96) { + ImGui::TextColored(kRollColors[ri], "%d", static_cast(r.rollNum)); + } else { + ImGui::TextDisabled("—"); + } + } + ImGui::EndTable(); + } } } ImGui::End(); @@ -7248,7 +14086,7 @@ void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, 250), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); - if (ImGui::Begin("Guild Invite", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Guild Invite", nullptr, kDialogFlags)) { ImGui::TextWrapped("%s has invited you to join %s.", gameHandler.getPendingGuildInviterName().c_str(), gameHandler.getPendingGuildInviteGuildName().c_str()); @@ -7275,7 +14113,7 @@ void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) { ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 175, screenH / 2 - 60), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); - if (ImGui::Begin("Ready Check", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + if (ImGui::Begin("Ready Check", nullptr, kDialogFlags)) { const std::string& initiator = gameHandler.getReadyCheckInitiator(); if (initiator.empty()) { ImGui::Text("A ready check has been initiated!"); @@ -7293,6 +14131,29 @@ void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) { gameHandler.respondToReadyCheck(false); gameHandler.dismissReadyCheck(); } + + // Live player responses + const auto& results = gameHandler.getReadyCheckResults(); + if (!results.empty()) { + ImGui::Separator(); + if (ImGui::BeginTable("##rcresults", 2, + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Player", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 72.0f); + for (const auto& r : results) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(r.name.c_str()); + ImGui::TableSetColumnIndex(1); + if (r.ready) { + ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "Ready"); + } else { + ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "Not Ready"); + } + } + ImGui::EndTable(); + } + } } ImGui::End(); } @@ -7335,22 +14196,8 @@ void GameScreen::renderBgInvitePopup(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Battleground Ready!", nullptr, popupFlags)) { - // BG name - std::string bgName; - if (slot->arenaType > 0) { - bgName = std::to_string(slot->arenaType) + "v" + std::to_string(slot->arenaType) + " Arena"; - } else { - switch (slot->bgTypeId) { - case 1: bgName = "Alterac Valley"; break; - case 2: bgName = "Warsong Gulch"; break; - case 3: bgName = "Arathi Basin"; break; - case 7: bgName = "Eye of the Storm"; break; - case 9: bgName = "Strand of the Ancients"; break; - case 11: bgName = "Isle of Conquest"; break; - default: bgName = "Battleground"; break; - } - } - + // BG name from stored queue data + std::string bgName = slot->bgName.empty() ? "Battleground" : slot->bgName; ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", bgName.c_str()); ImGui::TextWrapped("A spot has opened! You have %d seconds to enter.", static_cast(remaining)); ImGui::Spacing(); @@ -7390,6 +14237,63 @@ void GameScreen::renderBgInvitePopup(game::GameHandler& gameHandler) { ImGui::PopStyleColor(3); } +void GameScreen::renderBfMgrInvitePopup(game::GameHandler& gameHandler) { + // Only shown on WotLK servers (outdoor battlefields like Wintergrasp use the BF Manager) + if (!gameHandler.hasBfMgrInvite()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 190.0f, screenH / 2.0f - 55.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(380.0f, 0.0f), ImGuiCond_Always); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.10f, 0.20f, 0.96f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 1.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.45f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); + + const ImGuiWindowFlags flags = + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; + + if (ImGui::Begin("Battlefield", nullptr, flags)) { + // Resolve zone name for Wintergrasp (zoneId 4197) + uint32_t zoneId = gameHandler.getBfMgrZoneId(); + const char* zoneName = nullptr; + if (zoneId == 4197) zoneName = "Wintergrasp"; + else if (zoneId == 5095) zoneName = "Tol Barad"; + + if (zoneName) { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", zoneName); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "Outdoor Battlefield"); + } + ImGui::Spacing(); + ImGui::TextWrapped("You are invited to join the outdoor battlefield. Do you want to enter?"); + ImGui::Spacing(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f)); + if (ImGui::Button("Enter Battlefield", ImVec2(178, 28))) { + gameHandler.acceptBfMgrInvite(); + } + ImGui::PopStyleColor(2); + + ImGui::SameLine(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f)); + if (ImGui::Button("Decline", ImVec2(175, 28))) { + gameHandler.declineBfMgrInvite(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); +} + void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) { using LfgState = game::GameHandler::LfgState; if (gameHandler.getLfgState() != LfgState::Proposal) return; @@ -7410,7 +14314,7 @@ void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; if (ImGui::Begin("Dungeon Finder", nullptr, flags)) { - ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "A group has been found!"); + ImGui::TextColored(kColorGreen, "A group has been found!"); ImGui::Spacing(); ImGui::TextWrapped("Please accept or decline to join the dungeon."); ImGui::Spacing(); @@ -7439,9 +14343,76 @@ void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) { ImGui::PopStyleColor(3); } +void GameScreen::renderLfgRoleCheckPopup(game::GameHandler& gameHandler) { + using LfgState = game::GameHandler::LfgState; + if (gameHandler.getLfgState() != LfgState::RoleCheck) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 160.0f, screenH / 2.0f - 80.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(320.0f, 0.0f), ImGuiCond_Always); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.96f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.5f, 0.9f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.1f, 0.1f, 0.3f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); + + const ImGuiWindowFlags flags = + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse; + + if (ImGui::Begin("Role Check##LfgRoleCheck", nullptr, flags)) { + ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "Confirm your role:"); + ImGui::Spacing(); + + // Role checkboxes + bool isTank = (lfgRoles_ & 0x02) != 0; + bool isHealer = (lfgRoles_ & 0x04) != 0; + bool isDps = (lfgRoles_ & 0x08) != 0; + + if (ImGui::Checkbox("Tank", &isTank)) lfgRoles_ = (lfgRoles_ & ~0x02) | (isTank ? 0x02 : 0); + ImGui::SameLine(120.0f); + if (ImGui::Checkbox("Healer", &isHealer)) lfgRoles_ = (lfgRoles_ & ~0x04) | (isHealer ? 0x04 : 0); + ImGui::SameLine(220.0f); + if (ImGui::Checkbox("DPS", &isDps)) lfgRoles_ = (lfgRoles_ & ~0x08) | (isDps ? 0x08 : 0); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + bool hasRole = (lfgRoles_ & 0x0E) != 0; + if (!hasRole) ImGui::BeginDisabled(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.4f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.6f, 0.2f, 1.0f)); + if (ImGui::Button("Accept", ImVec2(140.0f, 28.0f))) { + gameHandler.lfgSetRoles(lfgRoles_); + } + ImGui::PopStyleColor(2); + + if (!hasRole) ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.6f, 0.2f, 0.2f, 1.0f)); + if (ImGui::Button("Leave Queue", ImVec2(140.0f, 28.0f))) { + gameHandler.lfgLeave(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); +} + void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { // Guild Roster toggle (customizable keybind) - if (!ImGui::GetIO().WantCaptureKeyboard && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) { + if (!chatInputActive && !ImGui::GetIO().WantTextInput && + !ImGui::GetIO().WantCaptureKeyboard && + KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) { showGuildRoster_ = !showGuildRoster_; if (showGuildRoster_) { // Open friends tab directly if not in guild @@ -7470,10 +14441,8 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::Text("Create Guild Charter"); ImGui::Separator(); uint32_t cost = gameHandler.getPetitionCost(); - uint32_t gold = cost / 10000; - uint32_t silver = (cost % 10000) / 100; - uint32_t copper = cost % 100; - ImGui::Text("Cost: %ug %us %uc", gold, silver, copper); + ImGui::TextDisabled("Cost:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(cost); ImGui::Spacing(); ImGui::Text("Guild Name:"); ImGui::InputText("##petitionname", petitionNameBuffer_, sizeof(petitionNameBuffer_)); @@ -7493,6 +14462,62 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::EndPopup(); } + // Petition signatures window (shown when a petition item is used or offered) + if (gameHandler.hasPetitionSignaturesUI()) { + ImGui::OpenPopup("PetitionSignatures"); + gameHandler.clearPetitionSignaturesUI(); + } + if (ImGui::BeginPopupModal("PetitionSignatures", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + const auto& pInfo = gameHandler.getPetitionInfo(); + if (!pInfo.guildName.empty()) + ImGui::Text("Guild Charter: %s", pInfo.guildName.c_str()); + else + ImGui::Text("Guild Charter"); + ImGui::Separator(); + + ImGui::Text("Signatures: %u / %u", pInfo.signatureCount, pInfo.signaturesRequired); + ImGui::Spacing(); + + if (!pInfo.signatures.empty()) { + for (size_t i = 0; i < pInfo.signatures.size(); ++i) { + const auto& sig = pInfo.signatures[i]; + // Try to resolve name from entity manager + std::string sigName; + if (sig.playerGuid != 0) { + auto entity = gameHandler.getEntityManager().getEntity(sig.playerGuid); + if (entity) { + auto* unit = dynamic_cast(entity.get()); + if (unit) sigName = unit->getName(); + } + } + if (sigName.empty()) + sigName = "Player " + std::to_string(i + 1); + ImGui::BulletText("%s", sigName.c_str()); + } + ImGui::Spacing(); + } + + // If we're not the owner, show Sign button + bool isOwner = (pInfo.ownerGuid == gameHandler.getPlayerGuid()); + if (!isOwner) { + if (ImGui::Button("Sign", ImVec2(120, 0))) { + gameHandler.signPetition(pInfo.petitionGuid); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + } else if (pInfo.signatureCount >= pInfo.signaturesRequired) { + // Owner with enough sigs — turn in + if (ImGui::Button("Turn In", ImVec2(120, 0))) { + gameHandler.turnInPetition(pInfo.petitionGuid); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + } + if (ImGui::Button("Close", ImVec2(120, 0))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + if (!showGuildRoster_) return; // Get zone manager for name lookup @@ -7531,7 +14556,7 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { for (const auto& m : roster.members) { if (m.online) ++onlineCount; } - ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount); + ImGui::Text("%d members (%d online)", static_cast(roster.members.size()), onlineCount); ImGui::Separator(); const auto& rankNames = gameHandler.getGuildRankNames(); @@ -7556,19 +14581,14 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { return a.name < b.name; }); - static const char* classNames[] = { - "Unknown", "Warrior", "Paladin", "Hunter", "Rogue", - "Priest", "Death Knight", "Shaman", "Mage", "Warlock", - "", "Druid" - }; - for (const auto& m : sortedMembers) { ImGui::TableNextRow(); ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) - : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + : kColorDarkGray; + ImVec4 nameColor = m.online ? classColorVec4(m.classId) : textColor; ImGui::TableNextColumn(); - ImGui::TextColored(textColor, "%s", m.name.c_str()); + ImGui::TextColored(nameColor, "%s", m.name.c_str()); // Right-click context menu if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { @@ -7588,8 +14608,9 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::TextColored(textColor, "%u", m.level); ImGui::TableNextColumn(); - const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown"; - ImGui::TextColored(textColor, "%s", className); + const char* className = classNameStr(m.classId); + ImVec4 classCol = m.online ? classColorVec4(m.classId) : textColor; + ImGui::TextColored(classCol, "%s", className); ImGui::TableNextColumn(); // Zone name lookup @@ -7742,7 +14763,7 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { if (!roster.motd.empty()) { ImGui::TextWrapped("%s", roster.motd.c_str()); } else { - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "(not set)"); + ImGui::TextColored(kColorDarkGray, "(not set)"); } if (ImGui::Button("Set MOTD")) { showMotdEdit_ = true; @@ -7795,7 +14816,7 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str()); if (!perms.empty()) { ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%s]", perms.c_str()); + ImGui::TextColored(kColorDarkGray, "[%s]", perms.c_str()); } } else { ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str()); @@ -7928,17 +14949,37 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { ImGui::EndTooltip(); } - // Level and status + // Level, class, and status if (c.isOnline()) { - ImGui::SameLine(160.0f); + ImGui::SameLine(150.0f); const char* statusLabel = (c.status == 2) ? " (AFK)" : (c.status == 3) ? " (DND)" : ""; - if (c.level > 0) { + // Class color for the level/class display + ImVec4 friendClassCol = classColorVec4(static_cast(c.classId)); + const char* friendClassName = classNameStr(static_cast(c.classId)); + if (c.level > 0 && c.classId > 0) { + ImGui::TextColored(friendClassCol, "Lv%u %s%s", c.level, friendClassName, statusLabel); + } else if (c.level > 0) { ImGui::TextDisabled("Lv %u%s", c.level, statusLabel); } else if (*statusLabel) { ImGui::TextDisabled("%s", statusLabel + 1); } + + // Tooltip: zone info + if (ImGui::IsItemHovered() && c.areaId != 0) { + ImGui::BeginTooltip(); + if (zoneManager) { + const auto* zi = zoneManager->getZoneInfo(c.areaId); + if (zi && !zi->name.empty()) + ImGui::Text("Zone: %s", zi->name.c_str()); + else + ImGui::TextDisabled("Area ID: %u", c.areaId); + } else { + ImGui::TextDisabled("Area ID: %u", c.areaId); + } + ImGui::EndTooltip(); + } } ImGui::PopID(); @@ -8043,6 +15084,10 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.92f)); + // State for "Set Note" inline editing + static int noteEditContactIdx = -1; + static char noteEditBuf[128] = {}; + bool open = showSocialFrame_; char socialTitle[32]; snprintf(socialTitle, sizeof(socialTitle), "Social (%d online)##SocialFrame", onlineCount); @@ -8050,6 +15095,11 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) { + // Get zone manager for area name lookups + game::ZoneManager* socialZoneMgr = nullptr; + if (auto* rend = core::Application::getInstance().getRenderer()) + socialZoneMgr = rend->getZoneManager(); + if (ImGui::BeginTabBar("##SocialTabs")) { // ---- Friends tab ---- if (ImGui::BeginTabItem("Friends")) { @@ -8081,13 +15131,36 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str(); ImVec4 nameCol = c.isOnline() - ? ImVec4(0.9f, 0.9f, 0.9f, 1.0f) - : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + ? classColorVec4(static_cast(c.classId)) + : kColorDarkGray; ImGui::TextColored(nameCol, "%s", displayName); if (c.isOnline() && c.level > 0) { ImGui::SameLine(); - ImGui::TextDisabled("Lv%u", c.level); + // Show level and class name in class color + ImGui::TextColored(classColorVec4(static_cast(c.classId)), + "Lv%u %s", c.level, classNameStr(static_cast(c.classId))); + } + + // Tooltip: zone info and note + if (ImGui::IsItemHovered() || (c.isOnline() && ImGui::IsItemHovered())) { + if (c.isOnline() && (c.areaId != 0 || !c.note.empty())) { + ImGui::BeginTooltip(); + if (c.areaId != 0) { + const char* zoneName = nullptr; + if (socialZoneMgr) { + const auto* zi = socialZoneMgr->getZoneInfo(c.areaId); + if (zi && !zi->name.empty()) zoneName = zi->name.c_str(); + } + if (zoneName) + ImGui::Text("Zone: %s", zoneName); + else + ImGui::Text("Area ID: %u", c.areaId); + } + if (!c.note.empty()) + ImGui::TextDisabled("Note: %s", c.note.c_str()); + ImGui::EndTooltip(); + } } // Right-click context menu @@ -8104,6 +15177,14 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { } if (ImGui::MenuItem("Invite to Group")) gameHandler.inviteToGroup(c.name); + if (c.guid != 0 && ImGui::MenuItem("Trade")) + gameHandler.initiateTrade(c.guid); + } + if (ImGui::MenuItem("Set Note")) { + noteEditContactIdx = static_cast(ci); + strncpy(noteEditBuf, c.note.c_str(), sizeof(noteEditBuf) - 1); + noteEditBuf[sizeof(noteEditBuf) - 1] = '\0'; + ImGui::OpenPopup("##SetFriendNote"); } if (ImGui::MenuItem("Remove Friend")) gameHandler.removeFriend(c.name); @@ -8124,6 +15205,31 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { } ImGui::EndChild(); + + // "Set Note" modal popup + if (ImGui::BeginPopup("##SetFriendNote")) { + const std::string& noteName = (noteEditContactIdx >= 0 && + noteEditContactIdx < static_cast(contacts.size())) + ? contacts[noteEditContactIdx].name : ""; + ImGui::TextDisabled("Note for %s:", noteName.c_str()); + ImGui::SetNextItemWidth(180.0f); + bool confirm = ImGui::InputText("##noteinput", noteEditBuf, sizeof(noteEditBuf), + ImGuiInputTextFlags_EnterReturnsTrue); + ImGui::SameLine(); + if (confirm || ImGui::Button("OK")) { + if (!noteName.empty()) + gameHandler.setFriendNote(noteName, noteEditBuf); + noteEditContactIdx = -1; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + noteEditContactIdx = -1; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::Separator(); // Add friend @@ -8215,6 +15321,108 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } + // ---- Arena tab (WotLK: shows per-team rating/record + roster) ---- + const auto& arenaStats = gameHandler.getArenaTeamStats(); + if (!arenaStats.empty()) { + if (ImGui::BeginTabItem("Arena")) { + ImGui::BeginChild("##ArenaList", ImVec2(0, 0), false); + + for (size_t ai = 0; ai < arenaStats.size(); ++ai) { + const auto& ts = arenaStats[ai]; + ImGui::PushID(static_cast(ai)); + + // Team header: "2v2: Team Name" or fallback "Team #id" + std::string teamLabel; + if (ts.teamType > 0) + teamLabel = std::to_string(ts.teamType) + "v" + std::to_string(ts.teamType) + ": "; + if (!ts.teamName.empty()) + teamLabel += ts.teamName; + else + teamLabel += "Team #" + std::to_string(ts.teamId); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", teamLabel.c_str()); + + ImGui::Indent(8.0f); + // Rating and rank + ImGui::Text("Rating: %u", ts.rating); + if (ts.rank > 0) { + ImGui::SameLine(0, 6); + ImGui::TextDisabled("(Rank #%u)", ts.rank); + } + + // Weekly record + uint32_t weekLosses = ts.weekGames > ts.weekWins + ? ts.weekGames - ts.weekWins : 0; + ImGui::Text("Week: %u W / %u L", ts.weekWins, weekLosses); + + // Season record + uint32_t seasLosses = ts.seasonGames > ts.seasonWins + ? ts.seasonGames - ts.seasonWins : 0; + ImGui::Text("Season: %u W / %u L", ts.seasonWins, seasLosses); + + // Roster members (from SMSG_ARENA_TEAM_ROSTER) + const auto* roster = gameHandler.getArenaTeamRoster(ts.teamId); + if (roster && !roster->members.empty()) { + ImGui::Spacing(); + ImGui::TextDisabled("-- Roster (%zu members) --", + roster->members.size()); + ImGui::SameLine(); + if (ImGui::SmallButton("Refresh")) + gameHandler.requestArenaTeamRoster(ts.teamId); + + // Column headers + ImGui::Columns(4, "##arenaRosterCols", false); + ImGui::SetColumnWidth(0, 110.0f); + ImGui::SetColumnWidth(1, 60.0f); + ImGui::SetColumnWidth(2, 60.0f); + ImGui::SetColumnWidth(3, 60.0f); + ImGui::TextDisabled("Name"); ImGui::NextColumn(); + ImGui::TextDisabled("Rating"); ImGui::NextColumn(); + ImGui::TextDisabled("Week"); ImGui::NextColumn(); + ImGui::TextDisabled("Season"); ImGui::NextColumn(); + ImGui::Separator(); + + for (const auto& m : roster->members) { + // Name coloured green (online) or grey (offline) + if (m.online) + ImGui::TextColored(ImVec4(0.4f,1.0f,0.4f,1.0f), + "%s", m.name.c_str()); + else + ImGui::TextDisabled("%s", m.name.c_str()); + ImGui::NextColumn(); + + ImGui::Text("%u", m.personalRating); + ImGui::NextColumn(); + + uint32_t wL = m.weekGames > m.weekWins + ? m.weekGames - m.weekWins : 0; + ImGui::Text("%uW/%uL", m.weekWins, wL); + ImGui::NextColumn(); + + uint32_t sL = m.seasonGames > m.seasonWins + ? m.seasonGames - m.seasonWins : 0; + ImGui::Text("%uW/%uL", m.seasonWins, sL); + ImGui::NextColumn(); + } + ImGui::Columns(1); + } else { + ImGui::Spacing(); + if (ImGui::SmallButton("Load Roster")) + gameHandler.requestArenaTeamRoster(ts.teamId); + } + + ImGui::Unindent(8.0f); + + if (ai + 1 < arenaStats.size()) + ImGui::Separator(); + + ImGui::PopID(); + } + + ImGui::EndChild(); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); } } @@ -8260,12 +15468,33 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f)); if (ImGui::Begin("##BuffBar", nullptr, flags)) { - // Separate buffs and debuffs; show buffs first, then debuffs with a visual gap + // Pre-sort auras: buffs first, then debuffs; within each group, shorter remaining first + uint64_t buffNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + std::vector buffSortedIdx; + buffSortedIdx.reserve(auras.size()); + for (size_t i = 0; i < auras.size(); ++i) + if (!auras[i].isEmpty()) buffSortedIdx.push_back(i); + std::sort(buffSortedIdx.begin(), buffSortedIdx.end(), [&](size_t a, size_t b) { + const auto& aa = auras[a]; const auto& ab = auras[b]; + bool aDebuff = (aa.flags & 0x80) != 0; + bool bDebuff = (ab.flags & 0x80) != 0; + if (aDebuff != bDebuff) return aDebuff < bDebuff; // buffs (0) first + int32_t ra = aa.getRemainingMs(buffNowMs); + int32_t rb = ab.getRemainingMs(buffNowMs); + if (ra < 0 && rb < 0) return false; + if (ra < 0) return false; + if (rb < 0) return true; + return ra < rb; + }); + // Render one pass for buffs, one for debuffs for (int pass = 0; pass < 2; ++pass) { bool wantBuff = (pass == 0); int shown = 0; - for (size_t i = 0; i < auras.size() && shown < 40; ++i) { + for (size_t si = 0; si < buffSortedIdx.size() && shown < 40; ++si) { + size_t i = buffSortedIdx[si]; const auto& aura = auras[i]; if (aura.isEmpty()) continue; @@ -8276,7 +15505,22 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImGui::PushID(static_cast(i) + (pass * 256)); - ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f); + // Determine border color: buffs = green; debuffs use WoW dispel-type colors + ImVec4 borderColor; + if (isBuff) { + borderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); // green + } else { + // Debuff: color by dispel type (0=none/red, 1=magic/blue, 2=curse/purple, + // 3=disease/brown, 4=poison/green, other=dark-red) + uint8_t dt = gameHandler.getSpellDispelType(aura.spellId); + switch (dt) { + case 1: borderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue + case 2: borderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple + case 3: borderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown + case 4: borderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green + default: borderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red + } + } // Try to get spell icon VkDescriptorSet iconTex = VK_NULL_HANDLE; @@ -8306,6 +15550,32 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { std::chrono::steady_clock::now().time_since_epoch()).count()); int32_t remainMs = aura.getRemainingMs(nowMs); + // Clock-sweep overlay: dark fan shows elapsed time (WoW style) + if (remainMs > 0 && aura.maxDurationMs > 0) { + ImVec2 iconMin2 = ImGui::GetItemRectMin(); + ImVec2 iconMax2 = ImGui::GetItemRectMax(); + float cx2 = (iconMin2.x + iconMax2.x) * 0.5f; + float cy2 = (iconMin2.y + iconMax2.y) * 0.5f; + float fanR2 = (iconMax2.x - iconMin2.x) * 0.5f; + float total2 = static_cast(aura.maxDurationMs); + float elapsedFrac2 = std::clamp( + 1.0f - static_cast(remainMs) / total2, 0.0f, 1.0f); + if (elapsedFrac2 > 0.005f) { + constexpr int SWEEP_SEGS = 24; + float sa = -IM_PI * 0.5f; + float ea = sa + elapsedFrac2 * 2.0f * IM_PI; + ImVec2 pts[SWEEP_SEGS + 2]; + pts[0] = ImVec2(cx2, cy2); + for (int s = 0; s <= SWEEP_SEGS; ++s) { + float a = sa + (ea - sa) * s / static_cast(SWEEP_SEGS); + pts[s + 1] = ImVec2(cx2 + std::cos(a) * fanR2, + cy2 + std::sin(a) * fanR2); + } + ImGui::GetWindowDrawList()->AddConvexPolyFilled( + pts, SWEEP_SEGS + 2, IM_COL32(0, 0, 0, 145)); + } + } + // Duration countdown overlay — always visible on the icon bottom if (remainMs > 0) { ImVec2 iconMin = ImGui::GetItemRectMin(); @@ -8321,11 +15591,26 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(timeStr); float cx = iconMin.x + (iconMax.x - iconMin.x - textSize.x) * 0.5f; float cy = iconMax.y - textSize.y - 2.0f; + // Choose timer color based on urgency + ImU32 timerColor; + if (remainMs < 10000) { + // < 10s: pulse red + float pulse = 0.7f + 0.3f * std::sin( + static_cast(ImGui::GetTime()) * 6.0f); + timerColor = IM_COL32( + static_cast(255 * pulse), + static_cast(80 * pulse), + static_cast(60 * pulse), 255); + } else if (remainMs < 30000) { + timerColor = IM_COL32(255, 165, 0, 255); // orange + } else { + timerColor = IM_COL32(255, 255, 255, 255); // white + } // Drop shadow for readability over any icon colour ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 200), timeStr); ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), - IM_COL32(255, 255, 255, 255), timeStr); + timerColor, timeStr); } // Stack / charge count overlay — upper-left corner of the icon @@ -8363,7 +15648,7 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { char durBuf[32]; if (seconds < 60) snprintf(durBuf, sizeof(durBuf), "Remaining: %ds", seconds); else snprintf(durBuf, sizeof(durBuf), "Remaining: %dm %ds", seconds / 60, seconds % 60); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", durBuf); + ImGui::TextColored(ui::colors::kLightGray, "%s", durBuf); } ImGui::EndTooltip(); } @@ -8385,6 +15670,60 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { } ImGui::PopStyleColor(2); } + + // Temporary weapon enchant timers (Shaman imbues, Rogue poisons, whetstones, etc.) + { + const auto& timers = gameHandler.getTempEnchantTimers(); + if (!timers.empty()) { + ImGui::Spacing(); + ImGui::Separator(); + static const ImVec4 kEnchantSlotColors[] = { + ImVec4(0.9f, 0.6f, 0.1f, 1.0f), // main-hand: gold + ImVec4(0.5f, 0.8f, 0.9f, 1.0f), // off-hand: teal + ImVec4(0.7f, 0.5f, 0.9f, 1.0f), // ranged: purple + }; + uint64_t enchNowMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + + for (const auto& t : timers) { + if (t.slot > 2) continue; + uint64_t remMs = (t.expireMs > enchNowMs) ? (t.expireMs - enchNowMs) : 0; + if (remMs == 0) continue; + + ImVec4 col = kEnchantSlotColors[t.slot]; + // Flash red when < 60s remaining + if (remMs < 60000) { + float pulse = 0.6f + 0.4f * std::sin( + static_cast(ImGui::GetTime()) * 4.0f); + col = ImVec4(pulse, 0.2f, 0.1f, 1.0f); + } + + // Format remaining time + uint32_t secs = static_cast((remMs + 999) / 1000); + char timeStr[16]; + if (secs >= 3600) + snprintf(timeStr, sizeof(timeStr), "%dh%02dm", secs / 3600, (secs % 3600) / 60); + else if (secs >= 60) + snprintf(timeStr, sizeof(timeStr), "%d:%02d", secs / 60, secs % 60); + else + snprintf(timeStr, sizeof(timeStr), "%ds", secs); + + ImGui::PushID(static_cast(t.slot) + 5000); + ImGui::PushStyleColor(ImGuiCol_Button, col); + char label[40]; + snprintf(label, sizeof(label), "~%s %s", + game::GameHandler::kTempEnchantSlotNames[t.slot], timeStr); + ImGui::Button(label, ImVec2(-1, 16)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Temporary weapon enchant: %s\nRemaining: %s", + game::GameHandler::kTempEnchantSlotNames[t.slot], + timeStr); + ImGui::PopStyleColor(); + ImGui::PopID(); + } + } + } } ImGui::End(); @@ -8409,10 +15748,11 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { const auto& loot = gameHandler.getCurrentLoot(); - // Gold + // Gold (auto-looted on open; shown for feedback) if (loot.gold > 0) { - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%ug %us %uc", - loot.getGold(), loot.getSilver(), loot.getCopper()); + ImGui::TextDisabled("Gold:"); + ImGui::SameLine(0, 4); + renderCoinsText(loot.getGold(), loot.getSilver(), loot.getCopper()); ImGui::Separator(); } @@ -8433,6 +15773,7 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { itemName = "Item #" + std::to_string(item.itemId); } ImVec4 qColor = InventoryScreen::getQualityColor(quality); + bool startsQuest = (info && info->startQuestId != 0); // Get item icon uint32_t displayId = item.displayInfoId; @@ -8465,8 +15806,9 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { // Show item tooltip on hover if (hovered && info && info->valid) { inventoryScreen.renderItemTooltip(*info); - } else if (hovered && !itemName.empty() && itemName[0] != 'I') { - ImGui::SetTooltip("%s", itemName.c_str()); + } else if (hovered && info && !info->name.empty()) { + // Item info received but not yet fully valid — show name at minimum + ImGui::SetTooltip("%s", info->name.c_str()); } ImDrawList* drawList = ImGui::GetWindowDrawList(); @@ -8492,6 +15834,14 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize), IM_COL32(80, 80, 80, 200)); } + // Quest-starter: gold outer glow border + "!" badge on top-right corner + if (startsQuest) { + drawList->AddRect(ImVec2(cursor.x - 2.0f, cursor.y - 2.0f), + ImVec2(cursor.x + iconSize + 2.0f, cursor.y + iconSize + 2.0f), + IM_COL32(255, 210, 0, 210), 0.0f, 0, 2.0f); + drawList->AddText(ImVec2(cursor.x + iconSize - 10.0f, cursor.y + 1.0f), + IM_COL32(255, 210, 0, 255), "!"); + } // Draw item name float textX = cursor.x + iconSize + 6.0f; @@ -8499,12 +15849,15 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { drawList->AddText(ImVec2(textX, textY), ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str()); - // Draw count if > 1 - if (item.count > 1) { + // Draw count or "Begins a Quest" label on second line + float secondLineY = textY + ImGui::GetTextLineHeight(); + if (startsQuest) { + drawList->AddText(ImVec2(textX, secondLineY), + IM_COL32(255, 210, 0, 255), "Begins a Quest"); + } else if (item.count > 1) { char countStr[32]; snprintf(countStr, sizeof(countStr), "x%u", item.count); - float countY = textY + ImGui::GetTextLineHeight(); - drawList->AddText(ImVec2(textX, countY), IM_COL32(200, 200, 200, 220), countStr); + drawList->AddText(ImVec2(textX, secondLineY), IM_COL32(200, 200, 200, 220), countStr); } ImGui::PopID(); @@ -8512,7 +15865,43 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) { // Process deferred loot pickup (after loop to avoid iterator invalidation) if (lootSlotClicked >= 0) { - gameHandler.lootItem(static_cast(lootSlotClicked)); + if (gameHandler.hasMasterLootCandidates()) { + // Master looter: open popup to choose recipient + char popupId[32]; + snprintf(popupId, sizeof(popupId), "##MLGive%d", lootSlotClicked); + ImGui::OpenPopup(popupId); + } else { + gameHandler.lootItem(static_cast(lootSlotClicked)); + } + } + + // Master loot "Give to" popups + if (gameHandler.hasMasterLootCandidates()) { + for (const auto& item : loot.items) { + char popupId[32]; + snprintf(popupId, sizeof(popupId), "##MLGive%d", item.slotIndex); + if (ImGui::BeginPopup(popupId)) { + ImGui::TextDisabled("Give to:"); + ImGui::Separator(); + const auto& candidates = gameHandler.getMasterLootCandidates(); + for (uint64_t candidateGuid : candidates) { + auto entity = gameHandler.getEntityManager().getEntity(candidateGuid); + auto* unit = entity ? dynamic_cast(entity.get()) : nullptr; + const char* cName = unit ? unit->getName().c_str() : nullptr; + char nameBuf[64]; + if (!cName || cName[0] == '\0') { + snprintf(nameBuf, sizeof(nameBuf), "Player 0x%llx", + static_cast(candidateGuid)); + cName = nameBuf; + } + if (ImGui::MenuItem(cName)) { + gameHandler.lootMasterGive(item.slotIndex, candidateGuid); + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } + } } if (loot.items.empty() && loot.gold == 0) { @@ -8658,7 +16047,7 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { if (!gossip.quests.empty()) { ImGui::Spacing(); ImGui::Separator(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:"); + ImGui::TextColored(kColorYellow, "Quests:"); for (size_t qi = 0; qi < gossip.quests.size(); qi++) { const auto& quest = gossip.quests[qi]; ImGui::PushID(static_cast(qi)); @@ -8667,7 +16056,7 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { // 5=INCOMPLETE (gray?), 6=REWARD_REP (yellow?), 7=AVAILABLE_LOW (gray!), // 8=AVAILABLE (yellow!), 10=REWARD (yellow?) const char* statusIcon = "!"; - ImVec4 statusColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // yellow + ImVec4 statusColor = kColorYellow; // yellow switch (quest.questIcon) { case 5: // INCOMPLETE — in progress but not done statusIcon = "?"; @@ -8676,7 +16065,7 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { case 6: // REWARD_REP — repeatable, ready to turn in case 10: // REWARD — ready to turn in statusIcon = "?"; - statusColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // yellow + statusColor = kColorYellow; // yellow break; case 7: // AVAILABLE_LOW — available but gray (low-level) statusIcon = "!"; @@ -8684,7 +16073,7 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { break; default: // AVAILABLE (8) and any others statusIcon = "!"; - statusColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // yellow + statusColor = kColorYellow; // yellow break; } @@ -8815,17 +16204,13 @@ void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) { ImGui::Text(" %u experience", quest.rewardXp); } if (quest.rewardMoney > 0) { - uint32_t gold = quest.rewardMoney / 10000; - uint32_t silver = (quest.rewardMoney % 10000) / 100; - uint32_t copper = quest.rewardMoney % 100; - if (gold > 0) ImGui::Text(" %ug %us %uc", gold, silver, copper); - else if (silver > 0) ImGui::Text(" %us %uc", silver, copper); - else ImGui::Text(" %uc", copper); + ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(quest.rewardMoney); } } if (quest.suggestedPlayers > 1) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), + ImGui::TextColored(ui::colors::kLightGray, "Suggested players: %u", quest.suggestedPlayers); } @@ -8931,10 +16316,8 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) { if (quest.requiredMoney > 0) { ImGui::Spacing(); - uint32_t g = quest.requiredMoney / 10000; - uint32_t s = (quest.requiredMoney % 10000) / 100; - uint32_t c = quest.requiredMoney % 100; - ImGui::Text("Required money: %ug %us %uc", g, s, c); + ImGui::TextDisabled("Required money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(quest.requiredMoney); } // Complete / Cancel buttons @@ -9109,12 +16492,8 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) { if (quest.rewardXp > 0) ImGui::Text(" %u experience", quest.rewardXp); if (quest.rewardMoney > 0) { - uint32_t g = quest.rewardMoney / 10000; - uint32_t s = (quest.rewardMoney % 10000) / 100; - uint32_t c = quest.rewardMoney % 100; - if (g > 0) ImGui::Text(" %ug %us %uc", g, s, c); - else if (s > 0) ImGui::Text(" %us %uc", s, c); - else ImGui::Text(" %uc", c); + ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(quest.rewardMoney); } } @@ -9152,6 +16531,61 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) { } } +// ============================================================ +// ItemExtendedCost.dbc loader +// ============================================================ + +void GameScreen::loadExtendedCostDBC() { + if (extendedCostDbLoaded_) return; + extendedCostDbLoaded_ = true; + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + auto dbc = am->loadDBC("ItemExtendedCost.dbc"); + if (!dbc || !dbc->isLoaded()) return; + // WotLK ItemExtendedCost.dbc: field 0=ID, 1=honorPoints, 2=arenaPoints, + // 3=arenaSlotRestrictions, 4-8=itemId[5], 9-13=itemCount[5], 14=reqRating, 15=purchaseGroup + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t id = dbc->getUInt32(i, 0); + if (id == 0) continue; + ExtendedCostEntry e; + e.honorPoints = dbc->getUInt32(i, 1); + e.arenaPoints = dbc->getUInt32(i, 2); + for (int j = 0; j < 5; ++j) { + e.itemId[j] = dbc->getUInt32(i, 4 + j); + e.itemCount[j] = dbc->getUInt32(i, 9 + j); + } + extendedCostCache_[id] = e; + } + LOG_INFO("ItemExtendedCost.dbc: loaded ", extendedCostCache_.size(), " entries"); +} + +std::string GameScreen::formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler) { + loadExtendedCostDBC(); + auto it = extendedCostCache_.find(extendedCostId); + if (it == extendedCostCache_.end()) return "[Tokens]"; + const auto& e = it->second; + std::string result; + if (e.honorPoints > 0) { + result += std::to_string(e.honorPoints) + " Honor"; + } + if (e.arenaPoints > 0) { + if (!result.empty()) result += ", "; + result += std::to_string(e.arenaPoints) + " Arena"; + } + for (int j = 0; j < 5; ++j) { + if (e.itemId[j] == 0 || e.itemCount[j] == 0) continue; + if (!result.empty()) result += ", "; + gameHandler.ensureItemInfo(e.itemId[j]); // query if not cached + const auto* itemInfo = gameHandler.getItemInfo(e.itemId[j]); + if (itemInfo && itemInfo->valid && !itemInfo->name.empty()) { + result += std::to_string(e.itemCount[j]) + "x " + itemInfo->name; + } else { + result += std::to_string(e.itemCount[j]) + "x Item#" + std::to_string(e.itemId[j]); + } + } + return result.empty() ? "[Tokens]" : result; +} + // ============================================================ // Vendor Window (Phase 5) // ============================================================ @@ -9171,10 +16605,8 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { // Show player money uint64_t money = gameHandler.getMoneyCopper(); - uint32_t mg = static_cast(money / 10000); - uint32_t ms = static_cast((money / 100) % 100); - uint32_t mc = static_cast(money % 100); - ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); + ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(money); if (vendor.canRepair) { ImGui::SameLine(); @@ -9183,12 +16615,36 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { gameHandler.repairAll(vendor.vendorGuid, false); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Repair all equipped items"); + // Show durability summary of all equipment + const auto& inv = gameHandler.getInventory(); + int damagedCount = 0; + int brokenCount = 0; + for (int s = 0; s < static_cast(game::EquipSlot::BAG1); s++) { + const auto& slot = inv.getEquipSlot(static_cast(s)); + if (slot.empty() || slot.item.maxDurability == 0) continue; + if (slot.item.curDurability == 0) brokenCount++; + else if (slot.item.curDurability < slot.item.maxDurability) damagedCount++; + } + if (brokenCount > 0) + ImGui::SetTooltip("Repair all equipped items\n%d damaged, %d broken", damagedCount, brokenCount); + else if (damagedCount > 0) + ImGui::SetTooltip("Repair all equipped items\n%d item%s need repair", damagedCount, damagedCount > 1 ? "s" : ""); + else + ImGui::SetTooltip("All equipment is in good condition"); + } + if (gameHandler.isInGuild()) { + ImGui::SameLine(); + if (ImGui::SmallButton("Repair (Guild)")) { + gameHandler.repairAll(vendor.vendorGuid, true); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Repair all equipped items using guild bank funds"); + } } } ImGui::Separator(); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Right-click bag items to sell"); + ImGui::TextColored(ui::colors::kLightGray, "Right-click bag items to sell"); // Count grey (POOR quality) sellable items across backpack and bags const auto& inv = gameHandler.getInventory(); @@ -9275,9 +16731,11 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered() && bbInfo && bbInfo->valid) inventoryScreen.renderItemTooltip(*bbInfo); ImGui::TableSetColumnIndex(2); - if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::Text("%ug %us %uc", g, s, c); - if (!canAfford) ImGui::PopStyleColor(); + if (canAfford) { + renderCoinsText(g, s, c); + } else { + ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c); + } ImGui::TableSetColumnIndex(3); if (!canAfford) ImGui::BeginDisabled(); char bbLabel[32]; @@ -9359,7 +16817,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { ImVec4 qc = InventoryScreen::getQualityColor(static_cast(info->quality)); ImGui::TextColored(qc, "%s", info->name.c_str()); if (ImGui::IsItemHovered()) { - inventoryScreen.renderItemTooltip(*info); + inventoryScreen.renderItemTooltip(*info, &gameHandler.getInventory()); } // Shift-click: insert item link into chat if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) { @@ -9377,23 +16835,33 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(2); if (item.buyPrice == 0 && item.extendedCost != 0) { - // Token-only item (no gold cost) - ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "[Tokens]"); + // Token-only item — show detailed cost from ItemExtendedCost.dbc + std::string costStr = formatExtendedCost(item.extendedCost, gameHandler); + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", costStr.c_str()); } else { uint32_t g = item.buyPrice / 10000; uint32_t s = (item.buyPrice / 100) % 100; uint32_t c = item.buyPrice % 100; bool canAfford = money >= item.buyPrice; - if (!canAfford) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::Text("%ug %us %uc", g, s, c); - if (!canAfford) ImGui::PopStyleColor(); + if (canAfford) { + renderCoinsText(g, s, c); + } else { + ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c); + } + // Show additional token cost if both gold and tokens are required + if (item.extendedCost != 0) { + std::string costStr = formatExtendedCost(item.extendedCost, gameHandler); + if (costStr != "[Tokens]") { + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 0.8f), "+ %s", costStr.c_str()); + } + } } ImGui::TableSetColumnIndex(3); if (item.maxCount < 0) { ImGui::TextDisabled("Inf"); } else if (item.maxCount == 0) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Out"); + ImGui::TextColored(kColorRed, "Out"); } else if (item.maxCount <= 5) { ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), "%d", item.maxCount); } else { @@ -9407,8 +16875,19 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (ImGui::SmallButton(buyBtnId.c_str())) { int qty = vendorBuyQty; if (item.maxCount > 0 && qty > item.maxCount) qty = item.maxCount; - gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, - static_cast(qty)); + uint32_t totalCost = item.buyPrice * static_cast(qty); + if (totalCost >= 10000) { // >= 1 gold: confirm + vendorConfirmOpen_ = true; + vendorConfirmGuid_ = vendor.vendorGuid; + vendorConfirmItemId_ = item.itemId; + vendorConfirmSlot_ = item.slot; + vendorConfirmQty_ = static_cast(qty); + vendorConfirmPrice_ = totalCost; + vendorConfirmItemName_ = (info && info->valid) ? info->name : "Item"; + } else { + gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot, + static_cast(qty)); + } } if (outOfStock) ImGui::EndDisabled(); @@ -9424,6 +16903,33 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (!open) { gameHandler.closeVendor(); } + + // Vendor purchase confirmation popup for expensive items + if (vendorConfirmOpen_) { + ImGui::OpenPopup("Confirm Purchase##vendor"); + vendorConfirmOpen_ = false; + } + if (ImGui::BeginPopupModal("Confirm Purchase##vendor", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { + ImGui::Text("Buy %s", vendorConfirmItemName_.c_str()); + if (vendorConfirmQty_ > 1) + ImGui::Text("Quantity: %u", vendorConfirmQty_); + uint32_t g = vendorConfirmPrice_ / 10000; + uint32_t s = (vendorConfirmPrice_ / 100) % 100; + uint32_t c = vendorConfirmPrice_ % 100; + ImGui::Text("Cost: %ug %us %uc", g, s, c); + ImGui::Spacing(); + if (ImGui::Button("Buy", ImVec2(80, 0))) { + gameHandler.buyItem(vendorConfirmGuid_, vendorConfirmItemId_, + vendorConfirmSlot_, vendorConfirmQty_); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } // ============================================================ @@ -9442,7 +16948,15 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { bool open = true; if (ImGui::Begin("Trainer", &open)) { + // If user clicked window close, short-circuit before rendering large trainer tables. + if (!open) { + ImGui::End(); + gameHandler.closeTrainer(); + return; + } + const auto& trainer = gameHandler.getTrainerSpells(); + const bool isProfessionTrainer = (trainer.trainerType == 2); // NPC name auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid); @@ -9461,10 +16975,8 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { // Player money uint64_t money = gameHandler.getMoneyCopper(); - uint32_t mg = static_cast(money / 10000); - uint32_t ms = static_cast((money / 100) % 100); - uint32_t mc = static_cast(money % 100); - ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); + ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(money); // Filter controls static bool showUnavailable = false; @@ -9583,12 +17095,20 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); if (!name.empty()) { - ImGui::Text("%s", name.c_str()); - if (!rank.empty()) ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", rank.c_str()); + ImGui::TextColored(kColorYellow, "%s", name.c_str()); + if (!rank.empty()) ImGui::TextColored(kColorGray, "%s", rank.c_str()); } - ImGui::Text("Status: %s", statusLabel); + const std::string& spDesc = gameHandler.getSpellDescription(spell->spellId); + if (!spDesc.empty()) { + ImGui::Spacing(); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f); + ImGui::TextWrapped("%s", spDesc.c_str()); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); + } + ImGui::TextDisabled("Status: %s", statusLabel); if (spell->reqLevel > 0) { - ImVec4 lvlColor = levelMet ? ImVec4(0.7f, 0.7f, 0.7f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); + ImVec4 lvlColor = levelMet ? ui::colors::kLightGray : kColorRed; ImGui::TextColored(lvlColor, "Required Level: %u", spell->reqLevel); } if (spell->reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell->reqSkill, spell->reqSkillValue); @@ -9596,7 +17116,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { if (node == 0) return; bool met = isKnown(node); const std::string& pname = gameHandler.getSpellName(node); - ImVec4 pcolor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); + ImVec4 pcolor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1.0f) : kColorRed; if (!pname.empty()) ImGui::TextColored(pcolor, "Requires: %s%s", pname.c_str(), met ? " (known)" : ""); else @@ -9619,8 +17139,11 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { uint32_t s = (spell->spellCost / 100) % 100; uint32_t c = spell->spellCost % 100; bool canAfford = money >= spell->spellCost; - ImVec4 costColor = canAfford ? color : ImVec4(1.0f, 0.3f, 0.3f, 1.0f); - ImGui::TextColored(costColor, "%ug %us %uc", g, s, c); + if (canAfford) { + renderCoinsText(g, s, c); + } else { + ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c); + } } else { ImGui::TextColored(color, "Free"); } @@ -9641,7 +17164,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { } if (logCount < 3) { LOG_INFO("Trainer button debug: spellId=", spell->spellId, - " alreadyKnown=", alreadyKnown, " state=", (int)spell->state, + " alreadyKnown=", alreadyKnown, " state=", static_cast(spell->state), " prereqsMet=", prereqsMet, " (", prereq1Met, ",", prereq2Met, ",", prereq3Met, ")", " levelMet=", levelMet, " reqLevel=", spell->reqLevel, " playerLevel=", playerLevel, @@ -9651,11 +17174,21 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { logCount++; } - if (!canTrain) ImGui::BeginDisabled(); - if (ImGui::SmallButton("Train")) { - gameHandler.trainSpell(spell->spellId); + if (isProfessionTrainer && alreadyKnown) { + // Profession trainer: known recipes show "Create" button to craft + bool isCasting = gameHandler.isCasting(); + if (isCasting) ImGui::BeginDisabled(); + if (ImGui::SmallButton("Create")) { + gameHandler.castSpell(spell->spellId, 0); + } + if (isCasting) ImGui::EndDisabled(); + } else { + if (!canTrain) ImGui::BeginDisabled(); + if (ImGui::SmallButton("Train")) { + gameHandler.trainSpell(spell->spellId); + } + if (!canTrain) ImGui::EndDisabled(); } - if (!canTrain) ImGui::EndDisabled(); ImGui::PopID(); } @@ -9759,6 +17292,85 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { } } if (!hasTrainable) ImGui::EndDisabled(); + + // Profession trainer: craft quantity controls + if (isProfessionTrainer) { + ImGui::Separator(); + static int craftQuantity = 1; + static uint32_t selectedCraftSpell = 0; + + // Show craft queue status if active + int queueRemaining = gameHandler.getCraftQueueRemaining(); + if (queueRemaining > 0) { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), + "Crafting... %d remaining", queueRemaining); + ImGui::SameLine(); + if (ImGui::SmallButton("Stop")) { + gameHandler.cancelCraftQueue(); + gameHandler.cancelCast(); + } + } else { + // Spell selector + quantity input + // Build list of known (craftable) spells + std::vector craftable; + for (const auto& spell : trainer.spells) { + if (isKnown(spell.spellId)) { + craftable.push_back(&spell); + } + } + if (!craftable.empty()) { + // Combo box for recipe selection + const char* previewName = "Select recipe..."; + for (const auto* sp : craftable) { + if (sp->spellId == selectedCraftSpell) { + const std::string& n = gameHandler.getSpellName(sp->spellId); + if (!n.empty()) previewName = n.c_str(); + break; + } + } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.55f); + if (ImGui::BeginCombo("##CraftSelect", previewName)) { + for (const auto* sp : craftable) { + const std::string& n = gameHandler.getSpellName(sp->spellId); + const std::string& r = gameHandler.getSpellRank(sp->spellId); + char label[128]; + if (!r.empty()) + snprintf(label, sizeof(label), "%s (%s)##%u", + n.empty() ? "???" : n.c_str(), r.c_str(), sp->spellId); + else + snprintf(label, sizeof(label), "%s##%u", + n.empty() ? "???" : n.c_str(), sp->spellId); + if (ImGui::Selectable(label, sp->spellId == selectedCraftSpell)) { + selectedCraftSpell = sp->spellId; + } + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(50.0f); + ImGui::InputInt("##CraftQty", &craftQuantity, 0, 0); + if (craftQuantity < 1) craftQuantity = 1; + if (craftQuantity > 99) craftQuantity = 99; + ImGui::SameLine(); + bool canCraft = selectedCraftSpell != 0 && !gameHandler.isCasting(); + if (!canCraft) ImGui::BeginDisabled(); + if (ImGui::Button("Create")) { + if (craftQuantity == 1) { + gameHandler.castSpell(selectedCraftSpell, 0); + } else { + gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity); + } + } + ImGui::SameLine(); + if (ImGui::Button("Create All")) { + // Queue a large count — server stops the queue automatically + // when materials run out (sends SPELL_FAILED_REAGENTS). + gameHandler.startCraftQueue(selectedCraftSpell, 999); + } + if (!canCraft) ImGui::EndDisabled(); + } + } + } } } ImGui::End(); @@ -9834,6 +17446,236 @@ void GameScreen::renderEscapeMenu() { ImGui::End(); } +// ============================================================ +// Barber Shop Window +// ============================================================ + +void GameScreen::renderBarberShopWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isBarberShopOpen()) { + barberInitialized_ = false; + return; + } + + const auto* ch = gameHandler.getActiveCharacter(); + if (!ch) return; + + uint8_t race = static_cast(ch->race); + game::Gender gender = ch->gender; + game::Race raceEnum = ch->race; + + // Initialize sliders from current appearance + if (!barberInitialized_) { + barberOrigHairStyle_ = static_cast((ch->appearanceBytes >> 16) & 0xFF); + barberOrigHairColor_ = static_cast((ch->appearanceBytes >> 24) & 0xFF); + barberOrigFacialHair_ = static_cast(ch->facialFeatures); + barberHairStyle_ = barberOrigHairStyle_; + barberHairColor_ = barberOrigHairColor_; + barberFacialHair_ = barberOrigFacialHair_; + barberInitialized_ = true; + } + + int maxHairStyle = static_cast(game::getMaxHairStyle(raceEnum, gender)); + int maxHairColor = static_cast(game::getMaxHairColor(raceEnum, gender)); + int maxFacialHair = static_cast(game::getMaxFacialFeature(raceEnum, gender)); + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + float winW = 300.0f; + float winH = 220.0f; + ImGui::SetNextWindowPos(ImVec2((screenW - winW) / 2.0f, (screenH - winH) / 2.0f), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Appearing); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; + bool open = true; + if (ImGui::Begin("Barber Shop", &open, flags)) { + ImGui::Text("Choose your new look:"); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::PushItemWidth(-1); + + // Hair Style + ImGui::Text("Hair Style"); + ImGui::SliderInt("##HairStyle", &barberHairStyle_, 0, maxHairStyle, + "%d"); + + // Hair Color + ImGui::Text("Hair Color"); + ImGui::SliderInt("##HairColor", &barberHairColor_, 0, maxHairColor, + "%d"); + + // Facial Hair / Piercings / Markings + const char* facialLabel = (gender == game::Gender::FEMALE) ? "Piercings" : "Facial Hair"; + // Some races use "Markings" or "Tusks" etc. + if (race == 8 || race == 6) facialLabel = "Features"; // Trolls, Tauren + ImGui::Text("%s", facialLabel); + ImGui::SliderInt("##FacialHair", &barberFacialHair_, 0, maxFacialHair, + "%d"); + + ImGui::PopItemWidth(); + + ImGui::Spacing(); + ImGui::Separator(); + + // Show whether anything changed + bool changed = (barberHairStyle_ != barberOrigHairStyle_ || + barberHairColor_ != barberOrigHairColor_ || + barberFacialHair_ != barberOrigFacialHair_); + + // OK / Reset / Cancel buttons + float btnW = 80.0f; + float totalW = btnW * 3 + ImGui::GetStyle().ItemSpacing.x * 2; + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) / 2.0f); + + if (!changed) ImGui::BeginDisabled(); + if (ImGui::Button("OK", ImVec2(btnW, 0))) { + gameHandler.sendAlterAppearance( + static_cast(barberHairStyle_), + static_cast(barberHairColor_), + static_cast(barberFacialHair_)); + // Keep window open — server will respond with SMSG_BARBER_SHOP_RESULT + } + if (!changed) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (!changed) ImGui::BeginDisabled(); + if (ImGui::Button("Reset", ImVec2(btnW, 0))) { + barberHairStyle_ = barberOrigHairStyle_; + barberHairColor_ = barberOrigHairColor_; + barberFacialHair_ = barberOrigFacialHair_; + } + if (!changed) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(btnW, 0))) { + gameHandler.closeBarberShop(); + } + } + ImGui::End(); + + if (!open) { + gameHandler.closeBarberShop(); + } +} + +// ============================================================ +// Pet Stable Window +// ============================================================ + +void GameScreen::renderStableWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isStableWindowOpen()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 240.0f, screenH / 2.0f - 180.0f), + ImGuiCond_Once); + ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once); + + bool open = true; + if (!ImGui::Begin("Pet Stable", &open, + kDialogFlags)) { + ImGui::End(); + if (!open) { + // User closed the window; clear stable state + gameHandler.closeStableWindow(); + } + return; + } + + const auto& pets = gameHandler.getStabledPets(); + uint8_t numSlots = gameHandler.getStableSlots(); + + ImGui::TextDisabled("Stable slots: %u", static_cast(numSlots)); + ImGui::Separator(); + + // Active pets section + bool hasActivePets = false; + for (const auto& p : pets) { + if (p.isActive) { hasActivePets = true; break; } + } + + if (hasActivePets) { + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "Active / Summoned"); + for (const auto& p : pets) { + if (!p.isActive) continue; + ImGui::PushID(static_cast(p.petNumber) * -1 - 1); + + const std::string displayName = p.name.empty() + ? ("Pet #" + std::to_string(p.petNumber)) + : p.name; + ImGui::Text(" %s (Level %u)", displayName.c_str(), p.level); + ImGui::SameLine(); + ImGui::TextDisabled("[Active]"); + + // Offer to stable the active pet if there are free slots + uint8_t usedSlots = 0; + for (const auto& sp : pets) { if (!sp.isActive) ++usedSlots; } + if (usedSlots < numSlots) { + ImGui::SameLine(); + if (ImGui::SmallButton("Store in stable")) { + // Slot 1 is first stable slot; server handles free slot assignment. + gameHandler.stablePet(1); + } + } + ImGui::PopID(); + } + ImGui::Separator(); + } + + // Stabled pets section + ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.4f, 1.0f), "Stabled Pets"); + + bool hasStabledPets = false; + for (const auto& p : pets) { + if (!p.isActive) { hasStabledPets = true; break; } + } + + if (!hasStabledPets) { + ImGui::TextDisabled(" (No pets in stable)"); + } else { + for (const auto& p : pets) { + if (p.isActive) continue; + ImGui::PushID(static_cast(p.petNumber)); + + const std::string displayName = p.name.empty() + ? ("Pet #" + std::to_string(p.petNumber)) + : p.name; + ImGui::Text(" %s (Level %u, Entry %u)", + displayName.c_str(), p.level, p.entry); + ImGui::SameLine(); + if (ImGui::SmallButton("Retrieve")) { + gameHandler.unstablePet(p.petNumber); + } + ImGui::PopID(); + } + } + + // Empty slots + uint8_t usedStableSlots = 0; + for (const auto& p : pets) { if (!p.isActive) ++usedStableSlots; } + if (usedStableSlots < numSlots) { + ImGui::TextDisabled(" %u empty slot(s) available", + static_cast(numSlots - usedStableSlots)); + } + + ImGui::Separator(); + if (ImGui::Button("Refresh")) { + gameHandler.requestStabledPetList(); + } + ImGui::SameLine(); + if (ImGui::Button("Close")) { + gameHandler.closeStableWindow(); + } + + ImGui::End(); + if (!open) { + gameHandler.closeStableWindow(); + } +} + // ============================================================ // Taxi Window // ============================================================ @@ -9900,13 +17742,7 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { } ImGui::TableSetColumnIndex(1); - if (gold > 0) { - ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); - } else if (silver > 0) { - ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%us %uc", silver, copper); - } else { - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.2f, 1.0f), "%uc", copper); - } + renderCoinsText(gold, silver, copper); ImGui::TableSetColumnIndex(2); if (ImGui::SmallButton("Fly")) { @@ -9922,7 +17758,7 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { } if (destCount == 0) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No destinations available."); + ImGui::TextColored(ui::colors::kLightGray, "No destinations available."); } ImGui::Spacing(); @@ -9942,12 +17778,85 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { } } +// ============================================================ +// Logout Countdown +// ============================================================ + +void GameScreen::renderLogoutCountdown(game::GameHandler& gameHandler) { + if (!gameHandler.isLoggingOut()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + constexpr float W = 280.0f; + constexpr float H = 80.0f; + ImGui::SetNextWindowPos(ImVec2((screenW - W) * 0.5f, screenH * 0.5f - H * 0.5f - 60.0f), + ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(W, H), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.88f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.8f, 1.0f)); + + if (ImGui::Begin("##LogoutCountdown", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus)) { + + float cd = gameHandler.getLogoutCountdown(); + if (cd > 0.0f) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 6.0f); + ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out in 20s...").x) * 0.5f); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), + "Logging out in %ds...", static_cast(std::ceil(cd))); + + // Progress bar (20 second countdown) + float frac = 1.0f - std::min(cd / 20.0f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.5f, 0.5f, 0.9f, 1.0f)); + ImGui::ProgressBar(frac, ImVec2(-1.0f, 8.0f), ""); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } else { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 14.0f); + ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out...").x) * 0.5f); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "Logging out..."); + ImGui::Spacing(); + } + + // Cancel button — only while countdown is still running + if (cd > 0.0f) { + float btnW = 100.0f; + ImGui::SetCursorPosX((W - btnW) * 0.5f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f)); + if (ImGui::Button("Cancel", ImVec2(btnW, 0))) { + gameHandler.cancelLogout(); + } + ImGui::PopStyleColor(2); + } + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + // ============================================================ // Death Screen // ============================================================ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { - if (!gameHandler.showDeathDialog()) return; + if (!gameHandler.showDeathDialog()) { + deathTimerRunning_ = false; + deathElapsed_ = 0.0f; + return; + } + float dt = ImGui::GetIO().DeltaTime; + if (!deathTimerRunning_) { + deathElapsed_ = 0.0f; + deathTimerRunning_ = true; + } else { + deathElapsed_ += dt; + } auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; @@ -9964,8 +17873,10 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); // "Release Spirit" dialog centered on screen + const bool hasSelfRes = gameHandler.canSelfRes(); float dlgW = 280.0f; - float dlgH = 100.0f; + // Extra height when self-res button is available; +20 for the "wait for res" hint + float dlgH = hasSelfRes ? 190.0f : 150.0f; ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); @@ -9984,9 +17895,34 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::SetCursorPosX((dlgW - textW) / 2); ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "%s", deathText); + // Respawn timer: show how long until the server auto-releases the spirit + float timeLeft = kForcedReleaseSec - deathElapsed_; + if (timeLeft > 0.0f) { + int mins = static_cast(timeLeft) / 60; + int secs = static_cast(timeLeft) % 60; + char timerBuf[48]; + snprintf(timerBuf, sizeof(timerBuf), "Auto-release in %d:%02d", mins, secs); + float tw = ImGui::CalcTextSize(timerBuf).x; + ImGui::SetCursorPosX((dlgW - tw) / 2); + ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "%s", timerBuf); + } + ImGui::Spacing(); ImGui::Spacing(); + // Self-resurrection button (Reincarnation / Twisting Nether / Deathpact) + if (hasSelfRes) { + float btnW2 = 220.0f; + ImGui::SetCursorPosX((dlgW - btnW2) / 2); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.55f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.5f, 0.75f, 1.0f)); + if (ImGui::Button("Use Self-Resurrection", ImVec2(btnW2, 30))) { + gameHandler.useSelfRes(); + } + ImGui::PopStyleColor(2); + ImGui::Spacing(); + } + // Center the Release Spirit button float btnW = 180.0f; ImGui::SetCursorPosX((dlgW - btnW) / 2); @@ -9996,6 +17932,12 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { gameHandler.releaseSpirit(); } ImGui::PopStyleColor(2); + + // Hint: player can stay dead and wait for another player to cast Resurrection + const char* resHint = "Or wait for a player to resurrect you."; + float hw = ImGui::CalcTextSize(resHint).x; + ImGui::SetCursorPosX((dlgW - hw) / 2); + ImGui::TextColored(ImVec4(0.5f, 0.6f, 0.5f, 0.85f), "%s", resHint); } ImGui::End(); ImGui::PopStyleColor(2); @@ -10009,28 +17951,48 @@ void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) { float screenW = window ? static_cast(window->getWidth()) : 1280.0f; float screenH = window ? static_cast(window->getHeight()) : 720.0f; + float delaySec = gameHandler.getCorpseReclaimDelaySec(); + bool onDelay = (delaySec > 0.0f); + float btnW = 220.0f, btnH = 36.0f; + float winH = btnH + 16.0f + (onDelay ? 20.0f : 0.0f); ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, btnH + 16.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, winH), ImGuiCond_Always); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 8.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.7f)); if (ImGui::Begin("##ReclaimCorpse", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus)) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f)); - if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) { - gameHandler.reclaimCorpse(); - } - ImGui::PopStyleColor(2); - float corpDist = gameHandler.getCorpseDistance(); - if (corpDist >= 0.0f) { - char distBuf[48]; - snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist); - float dw = ImGui::CalcTextSize(distBuf).x; - ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f); - ImGui::TextDisabled("%s", distBuf); + if (onDelay) { + // Greyed-out button while PvP reclaim timer ticks down + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.25f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f)); + ImGui::BeginDisabled(true); + char delayLabel[64]; + snprintf(delayLabel, sizeof(delayLabel), "Resurrect from Corpse (%.0fs)", delaySec); + ImGui::Button(delayLabel, ImVec2(btnW, btnH)); + ImGui::EndDisabled(); + ImGui::PopStyleColor(2); + const char* waitMsg = "You cannot reclaim your corpse yet."; + float tw = ImGui::CalcTextSize(waitMsg).x; + ImGui::SetCursorPosX((btnW + 16.0f - tw) * 0.5f); + ImGui::TextColored(ImVec4(0.8f, 0.5f, 0.2f, 1.0f), "%s", waitMsg); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f)); + if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) { + gameHandler.reclaimCorpse(); + } + ImGui::PopStyleColor(2); + float corpDist = gameHandler.getCorpseDistance(); + if (corpDist >= 0.0f) { + char distBuf[48]; + snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist); + float dw = ImGui::CalcTextSize(distBuf).x; + ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f); + ImGui::TextDisabled("%s", distBuf); + } } } ImGui::End(); @@ -10169,10 +18131,666 @@ void GameScreen::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +void GameScreen::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) { + if (!gameHandler.showPetUnlearnDialog()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + float dlgW = 340.0f; + float dlgH = 130.0f; + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f)); + + if (ImGui::Begin("##PetUnlearnDialog", nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { + + ImGui::Spacing(); + uint32_t cost = gameHandler.getPetUnlearnCost(); + uint32_t gold = cost / 10000; + uint32_t silver = (cost % 10000) / 100; + uint32_t copper = cost % 100; + char costStr[64]; + if (gold > 0) + std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper); + else if (silver > 0) + std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper); + else + std::snprintf(costStr, sizeof(costStr), "%uc", copper); + + std::string text = std::string("Reset your pet's talents for ") + costStr + "?"; + float textW = ImGui::CalcTextSize(text.c_str()).x; + ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2)); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str()); + + ImGui::Spacing(); + ImGui::SetCursorPosX(8.0f); + ImGui::TextDisabled("All pet talent points will be refunded."); + ImGui::Spacing(); + + float btnW = 110.0f; + float spacing = 20.0f; + ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f)); + if (ImGui::Button("Confirm##petunlearn", ImVec2(btnW, 30))) { + gameHandler.confirmPetUnlearn(); + } + ImGui::PopStyleColor(2); + + ImGui::SameLine(0, spacing); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f)); + if (ImGui::Button("Cancel##petunlearn", ImVec2(btnW, 30))) { + gameHandler.cancelPetUnlearn(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + // ============================================================ // Settings Window // ============================================================ +void GameScreen::renderSettingsInterfaceTab() { +ImGui::Spacing(); +ImGui::BeginChild("InterfaceSettings", ImVec2(0, 360), true); + +ImGui::SeparatorText("Action Bars"); +ImGui::Spacing(); +ImGui::SetNextItemWidth(200.0f); +if (ImGui::SliderFloat("Action Bar Scale", &pendingActionBarScale, 0.5f, 1.5f, "%.2fx")) { + saveSettings(); +} +ImGui::Spacing(); + +if (ImGui::Checkbox("Show Second Action Bar", &pendingShowActionBar2)) { + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(Shift+1 through Shift+=)"); + +if (pendingShowActionBar2) { + ImGui::Spacing(); + ImGui::TextUnformatted("Second Bar Position Offset"); + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat("Horizontal##bar2x", &pendingActionBar2OffsetX, -600.0f, 600.0f, "%.0f px")) { + saveSettings(); + } + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat("Vertical##bar2y", &pendingActionBar2OffsetY, -400.0f, 400.0f, "%.0f px")) { + saveSettings(); + } + if (ImGui::Button("Reset Position##bar2")) { + pendingActionBar2OffsetX = 0.0f; + pendingActionBar2OffsetY = 0.0f; + saveSettings(); + } +} + +ImGui::Spacing(); +if (ImGui::Checkbox("Show Right Side Bar", &pendingShowRightBar)) { + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(Slots 25-36)"); +if (pendingShowRightBar) { + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat("Vertical Offset##rbar", &pendingRightBarOffsetY, -400.0f, 400.0f, "%.0f px")) { + saveSettings(); + } +} + +ImGui::Spacing(); +if (ImGui::Checkbox("Show Left Side Bar", &pendingShowLeftBar)) { + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(Slots 37-48)"); +if (pendingShowLeftBar) { + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat("Vertical Offset##lbar", &pendingLeftBarOffsetY, -400.0f, 400.0f, "%.0f px")) { + saveSettings(); + } +} + +ImGui::Spacing(); +ImGui::SeparatorText("Nameplates"); +ImGui::Spacing(); +ImGui::SetNextItemWidth(200.0f); +if (ImGui::SliderFloat("Nameplate Scale", &nameplateScale_, 0.5f, 2.0f, "%.2fx")) { + saveSettings(); +} + +ImGui::Spacing(); +ImGui::SeparatorText("Network"); +ImGui::Spacing(); +if (ImGui::Checkbox("Show Latency Meter", &pendingShowLatencyMeter)) { + showLatencyMeter_ = pendingShowLatencyMeter; + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(ms indicator near minimap)"); + +if (ImGui::Checkbox("Show DPS/HPS Meter", &showDPSMeter_)) { + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(damage/healing per second above action bar)"); + +if (ImGui::Checkbox("Show Cooldown Tracker", &showCooldownTracker_)) { + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(active spell cooldowns near action bar)"); + +ImGui::Spacing(); +ImGui::SeparatorText("Screen Effects"); +ImGui::Spacing(); +if (ImGui::Checkbox("Damage Flash", &damageFlashEnabled_)) { + if (!damageFlashEnabled_) damageFlashAlpha_ = 0.0f; + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(red vignette on taking damage)"); + +if (ImGui::Checkbox("Low Health Vignette", &lowHealthVignetteEnabled_)) { + saveSettings(); +} +ImGui::SameLine(); +ImGui::TextDisabled("(pulsing red edges below 20%% HP)"); + +ImGui::EndChild(); +} + +void GameScreen::renderSettingsGameplayTab() { + auto* renderer = core::Application::getInstance().getRenderer(); +ImGui::Spacing(); + +ImGui::Text("Controls"); +ImGui::Separator(); +if (ImGui::SliderFloat("Mouse Sensitivity", &pendingMouseSensitivity, 0.05f, 1.0f, "%.2f")) { + if (renderer) { + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setMouseSensitivity(pendingMouseSensitivity); + } + } + saveSettings(); +} +if (ImGui::Checkbox("Invert Mouse", &pendingInvertMouse)) { + if (renderer) { + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setInvertMouse(pendingInvertMouse); + } + } + saveSettings(); +} +if (ImGui::Checkbox("Extended Camera Zoom", &pendingExtendedZoom)) { + if (renderer) { + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setExtendedZoom(pendingExtendedZoom); + } + } + saveSettings(); +} +if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Allow the camera to zoom out further than normal"); + +if (ImGui::SliderFloat("Field of View", &pendingFov, 45.0f, 110.0f, "%.0f°")) { + if (renderer) { + if (auto* camera = renderer->getCamera()) { + camera->setFov(pendingFov); + } + } + saveSettings(); +} +if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Camera field of view in degrees (default: 70)"); + +ImGui::Spacing(); +ImGui::Spacing(); + +ImGui::Text("Interface"); +ImGui::Separator(); +if (ImGui::SliderInt("UI Opacity", &pendingUiOpacity, 20, 100, "%d%%")) { + uiOpacity_ = static_cast(pendingUiOpacity) / 100.0f; + saveSettings(); +} +if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) { + // Force north-up minimap. + minimapRotate_ = false; + pendingMinimapRotate = false; + if (renderer) { + if (auto* minimap = renderer->getMinimap()) { + minimap->setRotateWithCamera(false); + } + } + saveSettings(); +} +if (ImGui::Checkbox("Square Minimap", &pendingMinimapSquare)) { + minimapSquare_ = pendingMinimapSquare; + if (renderer) { + if (auto* minimap = renderer->getMinimap()) { + minimap->setSquareShape(minimapSquare_); + } + } + saveSettings(); +} +if (ImGui::Checkbox("Show Nearby NPC Dots", &pendingMinimapNpcDots)) { + minimapNpcDots_ = pendingMinimapNpcDots; + saveSettings(); +} +// Zoom controls +ImGui::Text("Minimap Zoom:"); +ImGui::SameLine(); +if (ImGui::Button(" - ")) { + if (renderer) { + if (auto* minimap = renderer->getMinimap()) { + minimap->zoomOut(); + saveSettings(); + } + } +} +ImGui::SameLine(); +if (ImGui::Button(" + ")) { + if (renderer) { + if (auto* minimap = renderer->getMinimap()) { + minimap->zoomIn(); + saveSettings(); + } + } +} + +ImGui::Spacing(); +ImGui::Text("Loot"); +ImGui::Separator(); +if (ImGui::Checkbox("Auto Loot", &pendingAutoLoot)) { + saveSettings(); // per-frame sync applies pendingAutoLoot to gameHandler +} +if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Automatically pick up all items when looting"); +if (ImGui::Checkbox("Auto Sell Greys", &pendingAutoSellGrey)) { + saveSettings(); +} +if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Automatically sell all grey (poor quality) items when opening a vendor"); +if (ImGui::Checkbox("Auto Repair", &pendingAutoRepair)) { + saveSettings(); +} +if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Automatically repair all damaged equipment when opening an armorer vendor"); + +ImGui::Spacing(); +ImGui::Text("Bags"); +ImGui::Separator(); +if (ImGui::Checkbox("Separate Bag Windows", &pendingSeparateBags)) { + inventoryScreen.setSeparateBags(pendingSeparateBags); + saveSettings(); +} +if (ImGui::Checkbox("Show Key Ring", &pendingShowKeyring)) { + inventoryScreen.setShowKeyring(pendingShowKeyring); + saveSettings(); +} + +ImGui::Spacing(); +ImGui::Separator(); +ImGui::Spacing(); + +if (ImGui::Button("Restore Gameplay Defaults", ImVec2(-1, 0))) { + pendingMouseSensitivity = 0.2f; + pendingInvertMouse = false; + pendingExtendedZoom = false; + pendingUiOpacity = 65; + pendingMinimapRotate = false; + pendingMinimapSquare = false; + pendingMinimapNpcDots = false; + pendingSeparateBags = true; + inventoryScreen.setSeparateBags(true); + pendingShowKeyring = true; + inventoryScreen.setShowKeyring(true); + uiOpacity_ = 0.65f; + minimapRotate_ = false; + minimapSquare_ = false; + minimapNpcDots_ = false; + if (renderer) { + if (auto* cameraController = renderer->getCameraController()) { + cameraController->setMouseSensitivity(pendingMouseSensitivity); + cameraController->setInvertMouse(pendingInvertMouse); + cameraController->setExtendedZoom(pendingExtendedZoom); + } + if (auto* minimap = renderer->getMinimap()) { + minimap->setRotateWithCamera(minimapRotate_); + minimap->setSquareShape(minimapSquare_); + } + } + saveSettings(); +} + +} + +void GameScreen::renderSettingsControlsTab() { +ImGui::Spacing(); + +ImGui::Text("Keybindings"); +ImGui::Separator(); + +auto& km = ui::KeybindingManager::getInstance(); +int numActions = km.getActionCount(); + +for (int i = 0; i < numActions; ++i) { + auto action = static_cast(i); + const char* actionName = km.getActionName(action); + ImGuiKey currentKey = km.getKeyForAction(action); + + // Display current binding + ImGui::Text("%s:", actionName); + ImGui::SameLine(200); + + // Get human-readable key name (basic implementation) + const char* keyName = "Unknown"; + if (currentKey >= ImGuiKey_A && currentKey <= ImGuiKey_Z) { + static char keyBuf[16]; + snprintf(keyBuf, sizeof(keyBuf), "%c", 'A' + (currentKey - ImGuiKey_A)); + keyName = keyBuf; + } else if (currentKey >= ImGuiKey_0 && currentKey <= ImGuiKey_9) { + static char keyBuf[16]; + snprintf(keyBuf, sizeof(keyBuf), "%c", '0' + (currentKey - ImGuiKey_0)); + keyName = keyBuf; + } else if (currentKey == ImGuiKey_Escape) { + keyName = "Escape"; + } else if (currentKey == ImGuiKey_Enter) { + keyName = "Enter"; + } else if (currentKey == ImGuiKey_Tab) { + keyName = "Tab"; + } else if (currentKey == ImGuiKey_Space) { + keyName = "Space"; + } else if (currentKey >= ImGuiKey_F1 && currentKey <= ImGuiKey_F12) { + static char keyBuf[16]; + snprintf(keyBuf, sizeof(keyBuf), "F%d", 1 + (currentKey - ImGuiKey_F1)); + keyName = keyBuf; + } + + ImGui::Text("[%s]", keyName); + + // Rebind button + ImGui::SameLine(350); + if (ImGui::Button(awaitingKeyPress && pendingRebindAction == i ? "Waiting..." : "Rebind", ImVec2(100, 0))) { + pendingRebindAction = i; + awaitingKeyPress = true; + } +} + +// Handle key press during rebinding +if (awaitingKeyPress && pendingRebindAction >= 0) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("Press any key to bind to this action (Esc to cancel)..."); + + // Check for any key press + bool foundKey = false; + ImGuiKey newKey = ImGuiKey_None; + for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k) { + if (ImGui::IsKeyPressed(static_cast(k), false)) { + if (k == ImGuiKey_Escape) { + // Cancel rebinding + awaitingKeyPress = false; + pendingRebindAction = -1; + foundKey = true; + break; + } + newKey = static_cast(k); + foundKey = true; + break; + } + } + + if (foundKey && newKey != ImGuiKey_None) { + auto action = static_cast(pendingRebindAction); + km.setKeyForAction(action, newKey); + awaitingKeyPress = false; + pendingRebindAction = -1; + saveSettings(); + } +} + +ImGui::Spacing(); +ImGui::Separator(); +ImGui::Spacing(); + +if (ImGui::Button("Reset to Defaults", ImVec2(-1, 0))) { + km.resetToDefaults(); + awaitingKeyPress = false; + pendingRebindAction = -1; + saveSettings(); +} + +} + +void GameScreen::renderSettingsAudioTab() { + auto* renderer = core::Application::getInstance().getRenderer(); +ImGui::Spacing(); +ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true); + +// Helper lambda to apply audio settings +auto applyAudioSettings = [&]() { + applyAudioVolumes(renderer); + saveSettings(); +}; + +ImGui::Text("Master Volume"); +if (ImGui::SliderInt("##MasterVolume", &pendingMasterVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::Separator(); + +if (ImGui::Checkbox("Enable WoWee Music", &pendingUseOriginalSoundtrack)) { + if (renderer) { + if (auto* zm = renderer->getZoneManager()) { + zm->setUseOriginalSoundtrack(pendingUseOriginalSoundtrack); + } + } + saveSettings(); +} +if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Include WoWee music tracks in zone music rotation"); +ImGui::Separator(); + +ImGui::Text("Music"); +if (ImGui::SliderInt("##MusicVolume", &pendingMusicVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} + +ImGui::Spacing(); +ImGui::Text("Ambient Sounds"); +if (ImGui::SliderInt("##AmbientVolume", &pendingAmbientVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::TextWrapped("Weather, zones, cities, emitters"); + +ImGui::Spacing(); +ImGui::Text("UI Sounds"); +if (ImGui::SliderInt("##UiVolume", &pendingUiVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::TextWrapped("Buttons, loot, quest complete"); + +ImGui::Spacing(); +ImGui::Text("Combat Sounds"); +if (ImGui::SliderInt("##CombatVolume", &pendingCombatVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::TextWrapped("Weapon swings, impacts, grunts"); + +ImGui::Spacing(); +ImGui::Text("Spell Sounds"); +if (ImGui::SliderInt("##SpellVolume", &pendingSpellVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::TextWrapped("Magic casting and impacts"); + +ImGui::Spacing(); +ImGui::Text("Movement Sounds"); +if (ImGui::SliderInt("##MovementVolume", &pendingMovementVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::TextWrapped("Water splashes, jump/land"); + +ImGui::Spacing(); +ImGui::Text("Footsteps"); +if (ImGui::SliderInt("##FootstepVolume", &pendingFootstepVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} + +ImGui::Spacing(); +ImGui::Text("NPC Voices"); +if (ImGui::SliderInt("##NpcVoiceVolume", &pendingNpcVoiceVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} + +ImGui::Spacing(); +ImGui::Text("Mount Sounds"); +if (ImGui::SliderInt("##MountVolume", &pendingMountVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} + +ImGui::Spacing(); +ImGui::Text("Activity Sounds"); +if (ImGui::SliderInt("##ActivityVolume", &pendingActivityVolume, 0, 100, "%d%%")) { + applyAudioSettings(); +} +ImGui::TextWrapped("Swimming, eating, drinking"); + +ImGui::EndChild(); + +if (ImGui::Button("Restore Audio Defaults", ImVec2(-1, 0))) { + pendingMasterVolume = 100; + pendingMusicVolume = 30; // default music volume + pendingAmbientVolume = 100; + pendingUiVolume = 100; + pendingCombatVolume = 100; + pendingSpellVolume = 100; + pendingMovementVolume = 100; + pendingFootstepVolume = 100; + pendingNpcVoiceVolume = 100; + pendingMountVolume = 100; + pendingActivityVolume = 100; + applyAudioSettings(); +} + +} + +void GameScreen::renderSettingsChatTab() { +ImGui::Spacing(); + +ImGui::Text("Appearance"); +ImGui::Separator(); + +if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps_)) { + saveSettings(); +} +ImGui::SetItemTooltip("Show [HH:MM] before each chat message"); + +const char* fontSizes[] = { "Small", "Medium", "Large" }; +if (ImGui::Combo("Chat Font Size", &chatFontSize_, fontSizes, 3)) { + saveSettings(); +} + +ImGui::Spacing(); +ImGui::Spacing(); +ImGui::Text("Auto-Join Channels"); +ImGui::Separator(); + +if (ImGui::Checkbox("General", &chatAutoJoinGeneral_)) saveSettings(); +if (ImGui::Checkbox("Trade", &chatAutoJoinTrade_)) saveSettings(); +if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense_)) saveSettings(); +if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG_)) saveSettings(); +if (ImGui::Checkbox("Local", &chatAutoJoinLocal_)) saveSettings(); + +ImGui::Spacing(); +ImGui::Spacing(); +ImGui::Text("Joined Channels"); +ImGui::Separator(); + +ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels."); + +ImGui::Spacing(); +ImGui::Separator(); +ImGui::Spacing(); + +if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) { + chatShowTimestamps_ = false; + chatFontSize_ = 1; + chatAutoJoinGeneral_ = true; + chatAutoJoinTrade_ = true; + chatAutoJoinLocalDefense_ = true; + chatAutoJoinLFG_ = true; + chatAutoJoinLocal_ = true; + saveSettings(); +} + +} + +void GameScreen::renderSettingsAboutTab() { +ImGui::Spacing(); +ImGui::Spacing(); + +ImGui::TextWrapped("WoWee - World of Warcraft Client Emulator"); +ImGui::Spacing(); +ImGui::Separator(); +ImGui::Spacing(); + +ImGui::Text("Developer"); +ImGui::Indent(); +ImGui::Text("Kelsi Davis"); +ImGui::Unindent(); +ImGui::Spacing(); + +ImGui::Text("GitHub"); +ImGui::Indent(); +ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "https://github.com/Kelsidavis/WoWee"); +if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("Click to copy"); +} +if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText("https://github.com/Kelsidavis/WoWee"); +} +ImGui::Unindent(); +ImGui::Spacing(); + +ImGui::Text("Contact"); +ImGui::Indent(); +ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "github.com/Kelsidavis"); +if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("Click to copy"); +} +if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText("https://github.com/Kelsidavis"); +} +ImGui::Unindent(); + +ImGui::Spacing(); +ImGui::Separator(); +ImGui::Spacing(); + +ImGui::TextWrapped("A multi-expansion WoW client supporting Classic, TBC, and WotLK (3.3.5a)."); +ImGui::Spacing(); +ImGui::TextDisabled("Built with Vulkan, SDL2, and ImGui"); + +} + void GameScreen::renderSettingsWindow() { if (!showSettingsWindow) return; @@ -10310,18 +18928,11 @@ void GameScreen::renderSettingsWindow() { } } { - bool fsrActive = renderer && (renderer->isFSREnabled() || renderer->isFSR2Enabled()); - if (!fsrActive && pendingWaterRefraction) { - // FSR was disabled while refraction was on — auto-disable - pendingWaterRefraction = false; - if (renderer) renderer->setWaterRefractionEnabled(false); - } - if (!fsrActive) ImGui::BeginDisabled(); - if (ImGui::Checkbox("Water Refraction (requires FSR)", &pendingWaterRefraction)) { + if (ImGui::Checkbox("Water Refraction", &pendingWaterRefraction)) { if (renderer) renderer->setWaterRefractionEnabled(pendingWaterRefraction); + updateGraphicsPresetFromCurrentSettings(); saveSettings(); } - if (!fsrActive) ImGui::EndDisabled(); } { const char* aaLabels[] = { "Off", "2x MSAA", "4x MSAA", "8x MSAA" }; @@ -10340,6 +18951,20 @@ void GameScreen::renderSettingsWindow() { updateGraphicsPresetFromCurrentSettings(); saveSettings(); } + // FXAA — post-process, combinable with MSAA or FSR3 + { + if (ImGui::Checkbox("FXAA (post-process)", &pendingFXAA)) { + if (renderer) renderer->setFXAAEnabled(pendingFXAA); + updateGraphicsPresetFromCurrentSettings(); + saveSettings(); + } + if (ImGui::IsItemHovered()) { + if (fsr2Active) + ImGui::SetTooltip("FXAA applies spatial anti-aliasing after FSR3 upscaling.\nFSR3 + FXAA is the recommended ultra-quality combination."); + else + ImGui::SetTooltip("FXAA smooths jagged edges as a post-process pass.\nCan be combined with MSAA for extra quality."); + } + } } // FSR Upscaling { @@ -10497,6 +19122,16 @@ void GameScreen::renderSettingsWindow() { ImGui::Separator(); ImGui::Spacing(); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::SliderInt("Brightness", &pendingBrightness, 0, 100, "%d%%")) { + if (renderer) renderer->setBrightness(static_cast(pendingBrightness) / 50.0f); + saveSettings(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + if (ImGui::Button("Restore Video Defaults", ImVec2(-1, 0))) { pendingFullscreen = kDefaultFullscreen; pendingVsync = kDefaultVsync; @@ -10509,9 +19144,11 @@ void GameScreen::renderSettingsWindow() { pendingPOM = true; pendingPOMQuality = 1; pendingResIndex = defaultResIndex; + pendingBrightness = 50; window->setFullscreen(pendingFullscreen); window->setVsync(pendingVsync); window->applyResolution(kResolutions[pendingResIndex][0], kResolutions[pendingResIndex][1]); + if (renderer) renderer->setBrightness(1.0f); pendingWaterRefraction = false; if (renderer) { renderer->setShadowsEnabled(pendingShadows); @@ -10548,96 +19185,7 @@ void GameScreen::renderSettingsWindow() { // INTERFACE TAB // ============================================================ if (ImGui::BeginTabItem("Interface")) { - ImGui::Spacing(); - ImGui::BeginChild("InterfaceSettings", ImVec2(0, 360), true); - - ImGui::SeparatorText("Action Bars"); - ImGui::Spacing(); - ImGui::SetNextItemWidth(200.0f); - if (ImGui::SliderFloat("Action Bar Scale", &pendingActionBarScale, 0.5f, 1.5f, "%.2fx")) { - saveSettings(); - } - ImGui::Spacing(); - - if (ImGui::Checkbox("Show Second Action Bar", &pendingShowActionBar2)) { - saveSettings(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(Shift+1 through Shift+=)"); - - if (pendingShowActionBar2) { - ImGui::Spacing(); - ImGui::TextUnformatted("Second Bar Position Offset"); - ImGui::SetNextItemWidth(160.0f); - if (ImGui::SliderFloat("Horizontal##bar2x", &pendingActionBar2OffsetX, -600.0f, 600.0f, "%.0f px")) { - saveSettings(); - } - ImGui::SetNextItemWidth(160.0f); - if (ImGui::SliderFloat("Vertical##bar2y", &pendingActionBar2OffsetY, -400.0f, 400.0f, "%.0f px")) { - saveSettings(); - } - if (ImGui::Button("Reset Position##bar2")) { - pendingActionBar2OffsetX = 0.0f; - pendingActionBar2OffsetY = 0.0f; - saveSettings(); - } - } - - ImGui::Spacing(); - if (ImGui::Checkbox("Show Right Side Bar", &pendingShowRightBar)) { - saveSettings(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(Slots 25-36)"); - if (pendingShowRightBar) { - ImGui::SetNextItemWidth(160.0f); - if (ImGui::SliderFloat("Vertical Offset##rbar", &pendingRightBarOffsetY, -400.0f, 400.0f, "%.0f px")) { - saveSettings(); - } - } - - ImGui::Spacing(); - if (ImGui::Checkbox("Show Left Side Bar", &pendingShowLeftBar)) { - saveSettings(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(Slots 37-48)"); - if (pendingShowLeftBar) { - ImGui::SetNextItemWidth(160.0f); - if (ImGui::SliderFloat("Vertical Offset##lbar", &pendingLeftBarOffsetY, -400.0f, 400.0f, "%.0f px")) { - saveSettings(); - } - } - - ImGui::Spacing(); - ImGui::SeparatorText("Nameplates"); - ImGui::Spacing(); - ImGui::SetNextItemWidth(200.0f); - if (ImGui::SliderFloat("Nameplate Scale", &nameplateScale_, 0.5f, 2.0f, "%.2fx")) { - saveSettings(); - } - - ImGui::Spacing(); - ImGui::SeparatorText("Network"); - ImGui::Spacing(); - if (ImGui::Checkbox("Show Latency Meter", &pendingShowLatencyMeter)) { - showLatencyMeter_ = pendingShowLatencyMeter; - saveSettings(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(ms indicator near minimap)"); - - ImGui::Spacing(); - ImGui::SeparatorText("Screen Effects"); - ImGui::Spacing(); - if (ImGui::Checkbox("Damage Flash", &damageFlashEnabled_)) { - if (!damageFlashEnabled_) damageFlashAlpha_ = 0.0f; - saveSettings(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(red vignette on taking damage)"); - - ImGui::EndChild(); + renderSettingsInterfaceTab(); ImGui::EndTabItem(); } @@ -10645,147 +19193,7 @@ void GameScreen::renderSettingsWindow() { // AUDIO TAB // ============================================================ if (ImGui::BeginTabItem("Audio")) { - ImGui::Spacing(); - ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true); - - // Helper lambda to apply audio settings - auto applyAudioSettings = [&]() { - if (!renderer) return; - float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; - audio::AudioEngine::instance().setMasterVolume(masterScale); - if (auto* music = renderer->getMusicManager()) { - music->setVolume(pendingMusicVolume); - } - if (auto* ambient = renderer->getAmbientSoundManager()) { - ambient->setVolumeScale(pendingAmbientVolume / 100.0f); - } - if (auto* ui = renderer->getUiSoundManager()) { - ui->setVolumeScale(pendingUiVolume / 100.0f); - } - if (auto* combat = renderer->getCombatSoundManager()) { - combat->setVolumeScale(pendingCombatVolume / 100.0f); - } - if (auto* spell = renderer->getSpellSoundManager()) { - spell->setVolumeScale(pendingSpellVolume / 100.0f); - } - if (auto* movement = renderer->getMovementSoundManager()) { - movement->setVolumeScale(pendingMovementVolume / 100.0f); - } - if (auto* footstep = renderer->getFootstepManager()) { - footstep->setVolumeScale(pendingFootstepVolume / 100.0f); - } - if (auto* npcVoice = renderer->getNpcVoiceManager()) { - npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f); - } - if (auto* mount = renderer->getMountSoundManager()) { - mount->setVolumeScale(pendingMountVolume / 100.0f); - } - if (auto* activity = renderer->getActivitySoundManager()) { - activity->setVolumeScale(pendingActivityVolume / 100.0f); - } - saveSettings(); - }; - - ImGui::Text("Master Volume"); - if (ImGui::SliderInt("##MasterVolume", &pendingMasterVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::Separator(); - - if (ImGui::Checkbox("Enable WoWee Music", &pendingUseOriginalSoundtrack)) { - if (renderer) { - if (auto* zm = renderer->getZoneManager()) { - zm->setUseOriginalSoundtrack(pendingUseOriginalSoundtrack); - } - } - saveSettings(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Include WoWee music tracks in zone music rotation"); - ImGui::Separator(); - - ImGui::Text("Music"); - if (ImGui::SliderInt("##MusicVolume", &pendingMusicVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - - ImGui::Spacing(); - ImGui::Text("Ambient Sounds"); - if (ImGui::SliderInt("##AmbientVolume", &pendingAmbientVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::TextWrapped("Weather, zones, cities, emitters"); - - ImGui::Spacing(); - ImGui::Text("UI Sounds"); - if (ImGui::SliderInt("##UiVolume", &pendingUiVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::TextWrapped("Buttons, loot, quest complete"); - - ImGui::Spacing(); - ImGui::Text("Combat Sounds"); - if (ImGui::SliderInt("##CombatVolume", &pendingCombatVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::TextWrapped("Weapon swings, impacts, grunts"); - - ImGui::Spacing(); - ImGui::Text("Spell Sounds"); - if (ImGui::SliderInt("##SpellVolume", &pendingSpellVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::TextWrapped("Magic casting and impacts"); - - ImGui::Spacing(); - ImGui::Text("Movement Sounds"); - if (ImGui::SliderInt("##MovementVolume", &pendingMovementVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::TextWrapped("Water splashes, jump/land"); - - ImGui::Spacing(); - ImGui::Text("Footsteps"); - if (ImGui::SliderInt("##FootstepVolume", &pendingFootstepVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - - ImGui::Spacing(); - ImGui::Text("NPC Voices"); - if (ImGui::SliderInt("##NpcVoiceVolume", &pendingNpcVoiceVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - - ImGui::Spacing(); - ImGui::Text("Mount Sounds"); - if (ImGui::SliderInt("##MountVolume", &pendingMountVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - - ImGui::Spacing(); - ImGui::Text("Activity Sounds"); - if (ImGui::SliderInt("##ActivityVolume", &pendingActivityVolume, 0, 100, "%d%%")) { - applyAudioSettings(); - } - ImGui::TextWrapped("Swimming, eating, drinking"); - - ImGui::EndChild(); - - if (ImGui::Button("Restore Audio Defaults", ImVec2(-1, 0))) { - pendingMasterVolume = 100; - pendingMusicVolume = kDefaultMusicVolume; - pendingAmbientVolume = 100; - pendingUiVolume = 100; - pendingCombatVolume = 100; - pendingSpellVolume = 100; - pendingMovementVolume = 100; - pendingFootstepVolume = 100; - pendingNpcVoiceVolume = 100; - pendingMountVolume = 100; - pendingActivityVolume = 100; - applyAudioSettings(); - } - + renderSettingsAudioTab(); ImGui::EndTabItem(); } @@ -10793,151 +19201,7 @@ void GameScreen::renderSettingsWindow() { // GAMEPLAY TAB // ============================================================ if (ImGui::BeginTabItem("Gameplay")) { - ImGui::Spacing(); - - ImGui::Text("Controls"); - ImGui::Separator(); - if (ImGui::SliderFloat("Mouse Sensitivity", &pendingMouseSensitivity, 0.05f, 1.0f, "%.2f")) { - if (renderer) { - if (auto* cameraController = renderer->getCameraController()) { - cameraController->setMouseSensitivity(pendingMouseSensitivity); - } - } - saveSettings(); - } - if (ImGui::Checkbox("Invert Mouse", &pendingInvertMouse)) { - if (renderer) { - if (auto* cameraController = renderer->getCameraController()) { - cameraController->setInvertMouse(pendingInvertMouse); - } - } - saveSettings(); - } - if (ImGui::Checkbox("Extended Camera Zoom", &pendingExtendedZoom)) { - if (renderer) { - if (auto* cameraController = renderer->getCameraController()) { - cameraController->setExtendedZoom(pendingExtendedZoom); - } - } - saveSettings(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Allow the camera to zoom out further than normal"); - - if (ImGui::SliderFloat("Field of View", &pendingFov, 45.0f, 110.0f, "%.0f°")) { - if (renderer) { - if (auto* camera = renderer->getCamera()) { - camera->setFov(pendingFov); - } - } - saveSettings(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Camera field of view in degrees (default: 70)"); - - ImGui::Spacing(); - ImGui::Spacing(); - - ImGui::Text("Interface"); - ImGui::Separator(); - if (ImGui::SliderInt("UI Opacity", &pendingUiOpacity, 20, 100, "%d%%")) { - uiOpacity_ = static_cast(pendingUiOpacity) / 100.0f; - saveSettings(); - } - if (ImGui::Checkbox("Rotate Minimap", &pendingMinimapRotate)) { - // Force north-up minimap. - minimapRotate_ = false; - pendingMinimapRotate = false; - if (renderer) { - if (auto* minimap = renderer->getMinimap()) { - minimap->setRotateWithCamera(false); - } - } - saveSettings(); - } - if (ImGui::Checkbox("Square Minimap", &pendingMinimapSquare)) { - minimapSquare_ = pendingMinimapSquare; - if (renderer) { - if (auto* minimap = renderer->getMinimap()) { - minimap->setSquareShape(minimapSquare_); - } - } - saveSettings(); - } - if (ImGui::Checkbox("Show Nearby NPC Dots", &pendingMinimapNpcDots)) { - minimapNpcDots_ = pendingMinimapNpcDots; - saveSettings(); - } - // Zoom controls - ImGui::Text("Minimap Zoom:"); - ImGui::SameLine(); - if (ImGui::Button(" - ")) { - if (renderer) { - if (auto* minimap = renderer->getMinimap()) { - minimap->zoomOut(); - saveSettings(); - } - } - } - ImGui::SameLine(); - if (ImGui::Button(" + ")) { - if (renderer) { - if (auto* minimap = renderer->getMinimap()) { - minimap->zoomIn(); - saveSettings(); - } - } - } - - ImGui::Spacing(); - ImGui::Text("Loot"); - ImGui::Separator(); - if (ImGui::Checkbox("Auto Loot", &pendingAutoLoot)) { - saveSettings(); // per-frame sync applies pendingAutoLoot to gameHandler - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Automatically pick up all items when looting"); - - ImGui::Spacing(); - ImGui::Text("Bags"); - ImGui::Separator(); - if (ImGui::Checkbox("Separate Bag Windows", &pendingSeparateBags)) { - inventoryScreen.setSeparateBags(pendingSeparateBags); - saveSettings(); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - if (ImGui::Button("Restore Gameplay Defaults", ImVec2(-1, 0))) { - pendingMouseSensitivity = kDefaultMouseSensitivity; - pendingInvertMouse = kDefaultInvertMouse; - pendingExtendedZoom = false; - pendingUiOpacity = 65; - pendingMinimapRotate = false; - pendingMinimapSquare = false; - pendingMinimapNpcDots = false; - pendingSeparateBags = true; - inventoryScreen.setSeparateBags(true); - uiOpacity_ = 0.65f; - minimapRotate_ = false; - minimapSquare_ = false; - minimapNpcDots_ = false; - if (renderer) { - if (auto* cameraController = renderer->getCameraController()) { - cameraController->setMouseSensitivity(pendingMouseSensitivity); - cameraController->setInvertMouse(pendingInvertMouse); - cameraController->setExtendedZoom(pendingExtendedZoom); - } - if (auto* minimap = renderer->getMinimap()) { - minimap->setRotateWithCamera(minimapRotate_); - minimap->setSquareShape(minimapSquare_); - } - } - saveSettings(); - } - + renderSettingsGameplayTab(); ImGui::EndTabItem(); } @@ -10945,101 +19209,7 @@ void GameScreen::renderSettingsWindow() { // CONTROLS TAB // ============================================================ if (ImGui::BeginTabItem("Controls")) { - ImGui::Spacing(); - - ImGui::Text("Keybindings"); - ImGui::Separator(); - - auto& km = ui::KeybindingManager::getInstance(); - int numActions = km.getActionCount(); - - for (int i = 0; i < numActions; ++i) { - auto action = static_cast(i); - const char* actionName = km.getActionName(action); - ImGuiKey currentKey = km.getKeyForAction(action); - - // Display current binding - ImGui::Text("%s:", actionName); - ImGui::SameLine(200); - - // Get human-readable key name (basic implementation) - const char* keyName = "Unknown"; - if (currentKey >= ImGuiKey_A && currentKey <= ImGuiKey_Z) { - static char keyBuf[16]; - snprintf(keyBuf, sizeof(keyBuf), "%c", 'A' + (currentKey - ImGuiKey_A)); - keyName = keyBuf; - } else if (currentKey >= ImGuiKey_0 && currentKey <= ImGuiKey_9) { - static char keyBuf[16]; - snprintf(keyBuf, sizeof(keyBuf), "%c", '0' + (currentKey - ImGuiKey_0)); - keyName = keyBuf; - } else if (currentKey == ImGuiKey_Escape) { - keyName = "Escape"; - } else if (currentKey == ImGuiKey_Enter) { - keyName = "Enter"; - } else if (currentKey == ImGuiKey_Tab) { - keyName = "Tab"; - } else if (currentKey == ImGuiKey_Space) { - keyName = "Space"; - } else if (currentKey >= ImGuiKey_F1 && currentKey <= ImGuiKey_F12) { - static char keyBuf[16]; - snprintf(keyBuf, sizeof(keyBuf), "F%d", 1 + (currentKey - ImGuiKey_F1)); - keyName = keyBuf; - } - - ImGui::Text("[%s]", keyName); - - // Rebind button - ImGui::SameLine(350); - if (ImGui::Button(awaitingKeyPress && pendingRebindAction == i ? "Waiting..." : "Rebind", ImVec2(100, 0))) { - pendingRebindAction = i; - awaitingKeyPress = true; - } - } - - // Handle key press during rebinding - if (awaitingKeyPress && pendingRebindAction >= 0) { - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Text("Press any key to bind to this action (Esc to cancel)..."); - - // Check for any key press - bool foundKey = false; - ImGuiKey newKey = ImGuiKey_None; - for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k) { - if (ImGui::IsKeyPressed(static_cast(k), false)) { - if (k == ImGuiKey_Escape) { - // Cancel rebinding - awaitingKeyPress = false; - pendingRebindAction = -1; - foundKey = true; - break; - } - newKey = static_cast(k); - foundKey = true; - break; - } - } - - if (foundKey && newKey != ImGuiKey_None) { - auto action = static_cast(pendingRebindAction); - km.setKeyForAction(action, newKey); - awaitingKeyPress = false; - pendingRebindAction = -1; - saveSettings(); - } - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - if (ImGui::Button("Reset to Defaults", ImVec2(-1, 0))) { - km.resetToDefaults(); - awaitingKeyPress = false; - pendingRebindAction = -1; - saveSettings(); - } - + renderSettingsControlsTab(); ImGui::EndTabItem(); } @@ -11047,54 +19217,7 @@ void GameScreen::renderSettingsWindow() { // CHAT TAB // ============================================================ if (ImGui::BeginTabItem("Chat")) { - ImGui::Spacing(); - - ImGui::Text("Appearance"); - ImGui::Separator(); - - if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps_)) { - saveSettings(); - } - ImGui::SetItemTooltip("Show [HH:MM] before each chat message"); - - const char* fontSizes[] = { "Small", "Medium", "Large" }; - if (ImGui::Combo("Chat Font Size", &chatFontSize_, fontSizes, 3)) { - saveSettings(); - } - - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::Text("Auto-Join Channels"); - ImGui::Separator(); - - if (ImGui::Checkbox("General", &chatAutoJoinGeneral_)) saveSettings(); - if (ImGui::Checkbox("Trade", &chatAutoJoinTrade_)) saveSettings(); - if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense_)) saveSettings(); - if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG_)) saveSettings(); - if (ImGui::Checkbox("Local", &chatAutoJoinLocal_)) saveSettings(); - - ImGui::Spacing(); - ImGui::Spacing(); - ImGui::Text("Joined Channels"); - ImGui::Separator(); - - ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels."); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) { - chatShowTimestamps_ = false; - chatFontSize_ = 1; - chatAutoJoinGeneral_ = true; - chatAutoJoinTrade_ = true; - chatAutoJoinLocalDefense_ = true; - chatAutoJoinLFG_ = true; - chatAutoJoinLocal_ = true; - saveSettings(); - } - + renderSettingsChatTab(); ImGui::EndTabItem(); } @@ -11102,53 +19225,7 @@ void GameScreen::renderSettingsWindow() { // ABOUT TAB // ============================================================ if (ImGui::BeginTabItem("About")) { - ImGui::Spacing(); - ImGui::Spacing(); - - ImGui::TextWrapped("WoWee - World of Warcraft Client Emulator"); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::Text("Developer"); - ImGui::Indent(); - ImGui::Text("Kelsi Davis"); - ImGui::Unindent(); - ImGui::Spacing(); - - ImGui::Text("GitHub"); - ImGui::Indent(); - ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "https://github.com/Kelsidavis/WoWee"); - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("Click to copy"); - } - if (ImGui::IsItemClicked()) { - ImGui::SetClipboardText("https://github.com/Kelsidavis/WoWee"); - } - ImGui::Unindent(); - ImGui::Spacing(); - - ImGui::Text("Contact"); - ImGui::Indent(); - ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 1.0f), "github.com/Kelsidavis"); - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("Click to copy"); - } - if (ImGui::IsItemClicked()) { - ImGui::SetClipboardText("https://github.com/Kelsidavis"); - } - ImGui::Unindent(); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::TextWrapped("A multi-expansion WoW client supporting Classic, TBC, and WotLK (3.3.5a)."); - ImGui::Spacing(); - ImGui::TextDisabled("Built with Vulkan, SDL2, and ImGui"); - + renderSettingsAboutTab(); ImGui::EndTabItem(); } @@ -11260,6 +19337,7 @@ void GameScreen::applyGraphicsPreset(GraphicsPreset preset) { pendingShadows = true; pendingShadowDistance = 500.0f; pendingAntiAliasing = 3; // 8x MSAA + pendingFXAA = true; // FXAA on top of MSAA for maximum smoothness pendingNormalMapping = true; pendingNormalMapStrength = 1.2f; pendingPOM = true; @@ -11269,6 +19347,7 @@ void GameScreen::applyGraphicsPreset(GraphicsPreset preset) { renderer->setShadowsEnabled(true); renderer->setShadowDistance(500.0f); renderer->setMsaaSamples(VK_SAMPLE_COUNT_8_BIT); + renderer->setFXAAEnabled(true); if (auto* wr = renderer->getWMORenderer()) { wr->setNormalMappingEnabled(true); wr->setNormalMapStrength(1.2f); @@ -11314,7 +19393,7 @@ void GameScreen::updateGraphicsPresetFromCurrentSettings() { pendingGroundClutterDensity >= 90 && pendingGroundClutterDensity <= 110; case GraphicsPreset::ULTRA: return pendingShadows && pendingShadowDistance >= 480 && pendingAntiAliasing == 3 && - pendingNormalMapping && pendingPOM && pendingGroundClutterDensity >= 140; + pendingFXAA && pendingNormalMapping && pendingPOM && pendingGroundClutterDensity >= 140; default: return false; } @@ -11444,7 +19523,9 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { float sinB = 0.0f; if (minimap->isRotateWithCamera()) { glm::vec3 fwd = camera->getForward(); - bearing = std::atan2(-fwd.x, fwd.y); + // Render space: +X=West, +Y=North. Camera fwd=(cos(yaw),sin(yaw)). + // Clockwise bearing from North: atan2(fwd.y, -fwd.x). + bearing = std::atan2(fwd.y, -fwd.x); cosB = std::cos(bearing); sinB = std::sin(bearing); } @@ -11477,29 +19558,36 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { return true; }; - // Player position marker — always drawn at minimap center with a directional arrow. + // Build sets of entries that are incomplete objectives for tracked quests. + // minimapQuestEntries: NPC creature entries (npcOrGoId > 0) + // minimapQuestGoEntries: game object entries (npcOrGoId < 0, stored as abs value) + std::unordered_set minimapQuestEntries; + std::unordered_set minimapQuestGoEntries; { - // The player is always at centerX, centerY on the minimap. - // Draw a yellow arrow pointing in the player's facing direction. - glm::vec3 fwd = camera->getForward(); - float facing = std::atan2(-fwd.x, fwd.y); // bearing relative to north - float cosF = std::cos(facing - bearing); - float sinF = std::sin(facing - bearing); - float arrowLen = 8.0f; - float arrowW = 4.0f; - ImVec2 tip(centerX + sinF * arrowLen, centerY - cosF * arrowLen); - ImVec2 left(centerX - cosF * arrowW - sinF * arrowLen * 0.3f, - centerY - sinF * arrowW + cosF * arrowLen * 0.3f); - ImVec2 right(centerX + cosF * arrowW - sinF * arrowLen * 0.3f, - centerY + sinF * arrowW + cosF * arrowLen * 0.3f); - drawList->AddTriangleFilled(tip, left, right, IM_COL32(255, 220, 0, 255)); - drawList->AddTriangle(tip, left, right, IM_COL32(0, 0, 0, 180), 1.0f); - // White dot at player center - drawList->AddCircleFilled(ImVec2(centerX, centerY), 2.5f, IM_COL32(255, 255, 255, 220)); + const auto& ql = gameHandler.getQuestLog(); + const auto& tq = gameHandler.getTrackedQuestIds(); + for (const auto& q : ql) { + if (q.complete || q.questId == 0) continue; + if (!tq.empty() && !tq.count(q.questId)) continue; + for (const auto& obj : q.killObjectives) { + if (obj.required == 0) continue; + if (obj.npcOrGoId > 0) { + auto it = q.killCounts.find(static_cast(obj.npcOrGoId)); + if (it == q.killCounts.end() || it->second.first < it->second.second) + minimapQuestEntries.insert(static_cast(obj.npcOrGoId)); + } else if (obj.npcOrGoId < 0) { + uint32_t goEntry = static_cast(-obj.npcOrGoId); + auto it = q.killCounts.find(goEntry); + if (it == q.killCounts.end() || it->second.first < it->second.second) + minimapQuestGoEntries.insert(goEntry); + } + } + } } // Optional base nearby NPC dots (independent of quest status packets). if (minimapNpcDots_) { + ImVec2 mouse = ImGui::GetMousePos(); for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { if (!entity || entity->getType() != game::ObjectType::UNIT) continue; @@ -11510,8 +19598,186 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { float sx = 0.0f, sy = 0.0f; if (!projectToMinimap(npcRender, sx, sy)) continue; - ImU32 baseDot = unit->isHostile() ? IM_COL32(220, 70, 70, 220) : IM_COL32(245, 245, 245, 210); - drawList->AddCircleFilled(ImVec2(sx, sy), 1.0f, baseDot); + bool isQuestTarget = minimapQuestEntries.count(unit->getEntry()) != 0; + if (isQuestTarget) { + // Quest kill objective: larger gold dot with dark outline + drawList->AddCircleFilled(ImVec2(sx, sy), 3.5f, IM_COL32(255, 210, 30, 240)); + drawList->AddCircle(ImVec2(sx, sy), 3.5f, IM_COL32(80, 50, 0, 180), 0, 1.0f); + // Tooltip on hover showing unit name + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy < 64.0f) { + const std::string& nm = unit->getName(); + if (!nm.empty()) ImGui::SetTooltip("%s (quest)", nm.c_str()); + } + } else { + ImU32 baseDot = unit->isHostile() ? IM_COL32(220, 70, 70, 220) : IM_COL32(245, 245, 245, 210); + drawList->AddCircleFilled(ImVec2(sx, sy), 1.0f, baseDot); + } + } + } + + // Nearby other-player dots — shown when NPC dots are enabled. + // Party members are already drawn as squares above; other players get a small circle. + if (minimapNpcDots_) { + const uint64_t selfGuid = gameHandler.getPlayerGuid(); + const auto& partyData = gameHandler.getPartyData(); + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::PLAYER) continue; + if (entity->getGuid() == selfGuid) continue; // skip self (already drawn as arrow) + + // Skip party members (already drawn as squares above) + bool isPartyMember = false; + for (const auto& m : partyData.members) { + if (m.guid == guid) { isPartyMember = true; break; } + } + if (isPartyMember) continue; + + glm::vec3 pRender = core::coords::canonicalToRender( + glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(pRender, sx, sy)) continue; + + // Blue dot for other nearby players + drawList->AddCircleFilled(ImVec2(sx, sy), 2.0f, IM_COL32(80, 160, 255, 220)); + } + } + + // Lootable corpse dots: small yellow-green diamonds on dead, lootable units. + // Shown whenever NPC dots are enabled (or always, since they're always useful). + { + constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001; + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::UNIT) continue; + auto unit = std::static_pointer_cast(entity); + if (!unit) continue; + // Must be dead (health == 0) and marked lootable + if (unit->getHealth() != 0) continue; + if (!(unit->getDynamicFlags() & UNIT_DYNFLAG_LOOTABLE)) continue; + + glm::vec3 npcRender = core::coords::canonicalToRender( + glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(npcRender, sx, sy)) continue; + + // Draw a small diamond (rotated square) in light yellow-green + const float dr = 3.5f; + ImVec2 top (sx, sy - dr); + ImVec2 right(sx + dr, sy ); + ImVec2 bot (sx, sy + dr); + ImVec2 left (sx - dr, sy ); + drawList->AddQuadFilled(top, right, bot, left, IM_COL32(180, 230, 80, 230)); + drawList->AddQuad (top, right, bot, left, IM_COL32(60, 80, 20, 200), 1.0f); + + // Tooltip on hover + if (ImGui::IsMouseHoveringRect(ImVec2(sx - dr, sy - dr), ImVec2(sx + dr, sy + dr))) { + const std::string& nm = unit->getName(); + ImGui::BeginTooltip(); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.3f, 1.0f), "%s", + nm.empty() ? "Lootable corpse" : nm.c_str()); + ImGui::EndTooltip(); + } + } + } + + // Interactable game object dots (chests, resource nodes) when NPC dots are enabled. + // Shown as small orange triangles to distinguish from unit dots and loot corpses. + if (minimapNpcDots_) { + ImVec2 mouse = ImGui::GetMousePos(); + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::GAMEOBJECT) continue; + + // Only show objects that are likely interactive (chests/nodes: type 3; + // also show type 0=Door when open, but filter by dynamic-flag ACTIVATED). + // For simplicity, show all game objects that have a non-empty cached name. + auto go = std::static_pointer_cast(entity); + if (!go) continue; + + // Only show if we have name data (avoids cluttering with unknown objects) + const auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry()); + if (!goInfo || !goInfo->isValid()) continue; + // Skip transport objects (boats/zeppelins): type 15 = MO_TRANSPORT, 11 = TRANSPORT + if (goInfo->type == 11 || goInfo->type == 15) continue; + + glm::vec3 goRender = core::coords::canonicalToRender( + glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(goRender, sx, sy)) continue; + + // Triangle size and color: bright cyan for quest objectives, amber for others + bool isQuestGO = minimapQuestGoEntries.count(go->getEntry()) != 0; + const float ts = isQuestGO ? 4.5f : 3.5f; + ImVec2 goTip (sx, sy - ts); + ImVec2 goLeft (sx - ts, sy + ts * 0.6f); + ImVec2 goRight(sx + ts, sy + ts * 0.6f); + if (isQuestGO) { + drawList->AddTriangleFilled(goTip, goLeft, goRight, IM_COL32(50, 230, 255, 240)); + drawList->AddTriangle(goTip, goLeft, goRight, IM_COL32(0, 60, 80, 200), 1.5f); + } else { + drawList->AddTriangleFilled(goTip, goLeft, goRight, IM_COL32(255, 185, 30, 220)); + drawList->AddTriangle(goTip, goLeft, goRight, IM_COL32(100, 60, 0, 180), 1.0f); + } + + // Tooltip on hover + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy < 64.0f) { + if (isQuestGO) + ImGui::SetTooltip("%s (quest)", goInfo->name.c_str()); + else + ImGui::SetTooltip("%s", goInfo->name.c_str()); + } + } + } + + // Party member dots on minimap — small colored squares with name tooltip on hover + if (gameHandler.isInGroup()) { + const auto& partyData = gameHandler.getPartyData(); + ImVec2 mouse = ImGui::GetMousePos(); + for (const auto& member : partyData.members) { + if (!member.hasPartyStats) continue; + bool isOnline = (member.onlineStatus & 0x0001) != 0; + bool isDead = (member.onlineStatus & 0x0020) != 0; + bool isGhost = (member.onlineStatus & 0x0010) != 0; + if (!isOnline) continue; + if (member.posX == 0 && member.posY == 0) continue; + + // Party stat positions: posY = canonical X (north), posX = canonical Y (west) + glm::vec3 memberRender = core::coords::canonicalToRender( + glm::vec3(static_cast(member.posY), + static_cast(member.posX), 0.0f)); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(memberRender, sx, sy)) continue; + + // Determine dot color: class color > leader gold > light blue + ImU32 dotCol; + if (isDead || isGhost) { + dotCol = IM_COL32(140, 140, 140, 200); // gray for dead + } else { + auto mEnt = gameHandler.getEntityManager().getEntity(member.guid); + uint8_t cid = entityClassId(mEnt.get()); + if (cid != 0) { + ImVec4 cv = classColorVec4(cid); + dotCol = IM_COL32( + static_cast(cv.x * 255), + static_cast(cv.y * 255), + static_cast(cv.z * 255), 230); + } else if (member.guid == partyData.leaderGuid) { + dotCol = IM_COL32(255, 210, 0, 230); // gold for leader + } else { + dotCol = IM_COL32(100, 180, 255, 230); // blue for others + } + } + + // Draw a small square (WoW-style party member dot) + const float hs = 3.5f; + drawList->AddRectFilled(ImVec2(sx - hs, sy - hs), ImVec2(sx + hs, sy + hs), dotCol, 1.0f); + drawList->AddRect(ImVec2(sx - hs, sy - hs), ImVec2(sx + hs, sy + hs), + IM_COL32(0, 0, 0, 180), 1.0f, 0, 1.0f); + + // Name tooltip on hover + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy < 64.0f && !member.name.empty()) { + ImGui::SetTooltip("%s", member.name.c_str()); + } } } @@ -11551,6 +19817,87 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { drawList->AddText(font, 11.0f, ImVec2(sx - textSize.x * 0.5f, sy - textSize.y * 0.5f), IM_COL32(0, 0, 0, 255), marker); + + // Show NPC name and quest status on hover + { + ImVec2 mouse = ImGui::GetMousePos(); + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy < 64.0f) { + std::string npcName; + if (entity->getType() == game::ObjectType::UNIT) { + auto npcUnit = std::static_pointer_cast(entity); + npcName = npcUnit->getName(); + } + if (!npcName.empty()) { + bool hasQuest = (status == game::QuestGiverStatus::AVAILABLE || + status == game::QuestGiverStatus::AVAILABLE_LOW); + ImGui::SetTooltip("%s\n%s", npcName.c_str(), + hasQuest ? "Has a quest for you" : "Quest ready to turn in"); + } + } + } + } + + // Quest kill objective markers — highlight live NPCs matching active quest kill objectives + { + // Build map of NPC entry → (quest title, current, required) for tooltips + struct KillInfo { std::string questTitle; uint32_t current = 0; uint32_t required = 0; }; + std::unordered_map killInfoMap; + const auto& trackedIds = gameHandler.getTrackedQuestIds(); + for (const auto& quest : gameHandler.getQuestLog()) { + if (quest.complete) continue; + if (!trackedIds.empty() && !trackedIds.count(quest.questId)) continue; + for (const auto& obj : quest.killObjectives) { + if (obj.npcOrGoId <= 0 || obj.required == 0) continue; + uint32_t npcEntry = static_cast(obj.npcOrGoId); + auto it = quest.killCounts.find(npcEntry); + uint32_t current = (it != quest.killCounts.end()) ? it->second.first : 0; + if (current < obj.required) { + killInfoMap[npcEntry] = { quest.title, current, obj.required }; + } + } + } + + if (!killInfoMap.empty()) { + ImVec2 mouse = ImGui::GetMousePos(); + for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { + if (!entity || entity->getType() != game::ObjectType::UNIT) continue; + auto unit = std::static_pointer_cast(entity); + if (!unit || unit->getHealth() == 0) continue; + auto infoIt = killInfoMap.find(unit->getEntry()); + if (infoIt == killInfoMap.end()) continue; + + glm::vec3 unitRender = core::coords::canonicalToRender( + glm::vec3(entity->getX(), entity->getY(), entity->getZ())); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(unitRender, sx, sy)) continue; + + // Gold circle with a dark "x" mark — indicates a quest kill target + drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, IM_COL32(255, 185, 0, 240)); + drawList->AddCircle(ImVec2(sx, sy), 5.5f, IM_COL32(0, 0, 0, 180), 12, 1.0f); + drawList->AddLine(ImVec2(sx - 2.5f, sy - 2.5f), ImVec2(sx + 2.5f, sy + 2.5f), + IM_COL32(20, 20, 20, 230), 1.2f); + drawList->AddLine(ImVec2(sx + 2.5f, sy - 2.5f), ImVec2(sx - 2.5f, sy + 2.5f), + IM_COL32(20, 20, 20, 230), 1.2f); + + // Tooltip on hover + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy < 64.0f) { + const auto& ki = infoIt->second; + const std::string& npcName = unit->getName(); + if (!npcName.empty()) { + ImGui::SetTooltip("%s\n%s: %u/%u", + npcName.c_str(), + ki.questTitle.empty() ? "Quest" : ki.questTitle.c_str(), + ki.current, ki.required); + } else { + ImGui::SetTooltip("%s: %u/%u", + ki.questTitle.empty() ? "Quest" : ki.questTitle.c_str(), + ki.current, ki.required); + } + } + } + } } // Gossip POI markers (quest / NPC navigation targets) @@ -11616,16 +19963,103 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { float sx = 0.0f, sy = 0.0f; if (!projectToMinimap(memberRender, sx, sy)) continue; - ImU32 dotColor = (member.guid == leaderGuid) - ? IM_COL32(255, 210, 0, 235) - : IM_COL32(100, 180, 255, 235); + ImU32 dotColor; + { + auto mEnt = gameHandler.getEntityManager().getEntity(member.guid); + uint8_t cid = entityClassId(mEnt.get()); + dotColor = (cid != 0) + ? classColorU32(cid, 235) + : (member.guid == leaderGuid) + ? IM_COL32(255, 210, 0, 235) + : IM_COL32(100, 180, 255, 235); + } drawList->AddCircleFilled(ImVec2(sx, sy), 4.0f, dotColor); drawList->AddCircle(ImVec2(sx, sy), 4.0f, IM_COL32(255, 255, 255, 160), 12, 1.0f); + // Raid mark: tiny symbol drawn above the dot + { + static const struct { const char* sym; ImU32 col; } kMMMarks[] = { + { "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) }, + { "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) }, + { "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, + { "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, + { "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, + { "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, + { "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, + { "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, + }; + uint8_t pmk = gameHandler.getEntityRaidMark(member.guid); + if (pmk < game::GameHandler::kRaidMarkCount) { + ImFont* mmFont = ImGui::GetFont(); + ImVec2 msz = mmFont->CalcTextSizeA(9.0f, FLT_MAX, 0.0f, kMMMarks[pmk].sym); + drawList->AddText(mmFont, 9.0f, + ImVec2(sx - msz.x * 0.5f, sy - 4.0f - msz.y), + kMMMarks[pmk].col, kMMMarks[pmk].sym); + } + } + ImVec2 cursorPos = ImGui::GetMousePos(); float mdx = cursorPos.x - sx, mdy = cursorPos.y - sy; if (!member.name.empty() && (mdx * mdx + mdy * mdy) < 64.0f) { - ImGui::SetTooltip("%s", member.name.c_str()); + uint8_t pmk2 = gameHandler.getEntityRaidMark(member.guid); + if (pmk2 < game::GameHandler::kRaidMarkCount) { + static const char* kMarkNames[] = { + "Star", "Circle", "Diamond", "Triangle", + "Moon", "Square", "Cross", "Skull" + }; + ImGui::SetTooltip("%s {%s}", member.name.c_str(), kMarkNames[pmk2]); + } else { + ImGui::SetTooltip("%s", member.name.c_str()); + } + } + } + } + + // BG flag carrier / important player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS) + { + const auto& bgPositions = gameHandler.getBgPlayerPositions(); + if (!bgPositions.empty()) { + ImVec2 mouse = ImGui::GetMousePos(); + // group 0 = typically ally-held flag / first list; group 1 = enemy + static const ImU32 kBgGroupColors[2] = { + IM_COL32( 80, 180, 255, 240), // group 0: blue (alliance) + IM_COL32(220, 50, 50, 240), // group 1: red (horde) + }; + for (const auto& bp : bgPositions) { + // Packet coords: wowX=canonical X (north), wowY=canonical Y (west) + glm::vec3 bpRender = core::coords::canonicalToRender(glm::vec3(bp.wowX, bp.wowY, 0.0f)); + float sx = 0.0f, sy = 0.0f; + if (!projectToMinimap(bpRender, sx, sy)) continue; + + ImU32 col = kBgGroupColors[bp.group & 1]; + + // Draw a flag-like diamond icon + const float r = 5.0f; + ImVec2 top (sx, sy - r); + ImVec2 right(sx + r, sy ); + ImVec2 bot (sx, sy + r); + ImVec2 left (sx - r, sy ); + drawList->AddQuadFilled(top, right, bot, left, col); + drawList->AddQuad(top, right, bot, left, IM_COL32(255, 255, 255, 180), 1.0f); + + float mdx = mouse.x - sx, mdy = mouse.y - sy; + if (mdx * mdx + mdy * mdy < 64.0f) { + // Show entity name if available, otherwise guid + auto ent = gameHandler.getEntityManager().getEntity(bp.guid); + if (ent) { + std::string nm; + if (ent->getType() == game::ObjectType::PLAYER) { + auto pl = std::static_pointer_cast(ent); + nm = pl ? pl->getName() : ""; + } + if (!nm.empty()) + ImGui::SetTooltip("Flag carrier: %s", nm.c_str()); + else + ImGui::SetTooltip("Flag carrier"); + } else { + ImGui::SetTooltip("Flag carrier"); + } + } } } } @@ -11698,6 +20132,46 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } + // Player position arrow at minimap center, pointing in camera facing direction. + // On a rotating minimap the map already turns so forward = screen-up; on a fixed + // minimap we rotate the arrow to match the player's compass heading. + { + // Compute screen-space facing direction for the arrow. + // bearing = clockwise angle from screen-north (0 = facing north/up). + float arrowAngle = 0.0f; // 0 = pointing up (north) + if (!minimap->isRotateWithCamera()) { + // Fixed minimap: arrow must show actual facing relative to north. + glm::vec3 fwd = camera->getForward(); + // +render_y = north = screen-up, +render_x = west = screen-left. + // bearing from north clockwise: atan2(-fwd.x_west, fwd.y_north) + // => sin=east component, cos=north component + // In render coords west=+x, east=-x, so sin(bearing)=east=-fwd.x + arrowAngle = std::atan2(-fwd.x, fwd.y); // clockwise from north in screen space + } + // Screen direction the arrow tip points toward + float nx = std::sin(arrowAngle); // screen +X = east + float ny = -std::cos(arrowAngle); // screen -Y = north + + // Draw a chevron-style arrow: tip, two base corners, and a notch at the back + const float tipLen = 8.0f; // tip forward distance + const float baseW = 5.0f; // half-width at base + const float notchIn = 3.0f; // how far back the center notch sits + // Perpendicular direction (rotated 90°) + float px = ny; // perpendicular x + float py = -nx; // perpendicular y + + ImVec2 tip (centerX + nx * tipLen, centerY + ny * tipLen); + ImVec2 baseL(centerX - nx * baseW + px * baseW, centerY - ny * baseW + py * baseW); + ImVec2 baseR(centerX - nx * baseW - px * baseW, centerY - ny * baseW - py * baseW); + ImVec2 notch(centerX - nx * (baseW - notchIn), centerY - ny * (baseW - notchIn)); + + // Fill: bright white with slight gold tint, dark outline for readability + drawList->AddTriangleFilled(tip, baseL, notch, IM_COL32(255, 248, 200, 245)); + drawList->AddTriangleFilled(tip, notch, baseR, IM_COL32(255, 248, 200, 245)); + drawList->AddTriangle(tip, baseL, notch, IM_COL32(60, 40, 0, 200), 1.2f); + drawList->AddTriangle(tip, notch, baseR, IM_COL32(60, 40, 0, 200), 1.2f); + } + // Scroll wheel over minimap → zoom in/out { float wheel = ImGui::GetIO().MouseWheel; @@ -11736,18 +20210,167 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } } - // Hover tooltip: show player's WoW coordinates (canonical X=North, Y=West) + // Persistent coordinate display below the minimap + { + glm::vec3 playerCanon = core::coords::renderToCanonical(playerRender); + char coordBuf[32]; + std::snprintf(coordBuf, sizeof(coordBuf), "%.1f, %.1f", playerCanon.x, playerCanon.y); + + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, coordBuf); + + float tx = centerX - textSz.x * 0.5f; + float ty = centerY + mapRadius + 3.0f; + + // Semi-transparent dark background pill + float pad = 3.0f; + drawList->AddRectFilled( + ImVec2(tx - pad, ty - pad), + ImVec2(tx + textSz.x + pad, ty + textSz.y + pad), + IM_COL32(0, 0, 0, 140), 4.0f); + // Coordinate text in warm yellow + drawList->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(230, 220, 140, 255), coordBuf); + } + + // Local time clock — displayed just below the coordinate label + { + auto now = std::chrono::system_clock::now(); + std::time_t tt = std::chrono::system_clock::to_time_t(now); + std::tm tmLocal{}; +#if defined(_WIN32) + localtime_s(&tmLocal, &tt); +#else + localtime_r(&tt, &tmLocal); +#endif + char clockBuf[16]; + std::snprintf(clockBuf, sizeof(clockBuf), "%02d:%02d", + tmLocal.tm_hour, tmLocal.tm_min); + + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize() * 0.9f; // slightly smaller than coords + ImVec2 clockSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, clockBuf); + + float tx = centerX - clockSz.x * 0.5f; + // Position below the coordinate line (+fontSize of coord + 2px gap) + float coordLineH = ImGui::GetFontSize(); + float ty = centerY + mapRadius + 3.0f + coordLineH + 2.0f; + + float pad = 2.0f; + drawList->AddRectFilled( + ImVec2(tx - pad, ty - pad), + ImVec2(tx + clockSz.x + pad, ty + clockSz.y + pad), + IM_COL32(0, 0, 0, 120), 3.0f); + drawList->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(200, 200, 220, 220), clockBuf); + } + + // Zone name display — drawn inside the top edge of the minimap circle + { + auto* zmRenderer = renderer ? renderer->getZoneManager() : nullptr; + uint32_t zoneId = gameHandler.getWorldStateZoneId(); + const game::ZoneInfo* zi = (zmRenderer && zoneId != 0) ? zmRenderer->getZoneInfo(zoneId) : nullptr; + if (zi && !zi->name.empty()) { + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, zi->name.c_str()); + float tx = centerX - ts.x * 0.5f; + float ty = centerY - mapRadius + 4.0f; // just inside top edge of the circle + float pad = 2.0f; + drawList->AddRectFilled( + ImVec2(tx - pad, ty - pad), + ImVec2(tx + ts.x + pad, ty + ts.y + pad), + IM_COL32(0, 0, 0, 160), 2.0f); + drawList->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f), + IM_COL32(0, 0, 0, 180), zi->name.c_str()); + drawList->AddText(font, fontSize, ImVec2(tx, ty), + IM_COL32(255, 230, 150, 220), zi->name.c_str()); + } + } + + // Instance difficulty indicator — just below zone name, inside minimap top edge + if (gameHandler.isInInstance()) { + static const char* kDiffLabels[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"}; + uint32_t diff = gameHandler.getInstanceDifficulty(); + const char* label = (diff < 4) ? kDiffLabels[diff] : "Unknown"; + + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize() * 0.85f; + ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label); + float tx = centerX - ts.x * 0.5f; + // Position below zone name: top edge + zone font size + small gap + float ty = centerY - mapRadius + 4.0f + ImGui::GetFontSize() + 2.0f; + float pad = 2.0f; + + // Color-code: heroic=orange, normal=light gray + ImU32 bgCol = gameHandler.isInstanceHeroic() ? IM_COL32(120, 60, 0, 180) : IM_COL32(0, 0, 0, 160); + ImU32 textCol = gameHandler.isInstanceHeroic() ? IM_COL32(255, 180, 50, 255) : IM_COL32(200, 200, 200, 220); + + drawList->AddRectFilled( + ImVec2(tx - pad, ty - pad), + ImVec2(tx + ts.x + pad, ty + ts.y + pad), + bgCol, 2.0f); + drawList->AddText(font, fontSize, ImVec2(tx, ty), textCol, label); + } + + // Hover tooltip and right-click context menu { ImVec2 mouse = ImGui::GetMousePos(); float mdx = mouse.x - centerX; float mdy = mouse.y - centerY; - if (mdx * mdx + mdy * mdy <= mapRadius * mapRadius) { - glm::vec3 playerCanon = core::coords::renderToCanonical(playerRender); + bool overMinimap = (mdx * mdx + mdy * mdy <= mapRadius * mapRadius); + + if (overMinimap) { ImGui::BeginTooltip(); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.5f, 1.0f), - "%.1f, %.1f", playerCanon.x, playerCanon.y); - ImGui::TextDisabled("Ctrl+click to ping"); + // Compute the world coordinate under the mouse cursor + // Inverse of projectToMinimap: pixel offset → world offset in render space → canonical + float rxW = mdx / mapRadius * viewRadius; + float ryW = mdy / mapRadius * viewRadius; + // Un-rotate: [dx, dy] = R^-1 * [rxW, ryW] + // where R applied: rx = -(dx*cosB + dy*sinB), ry = dx*sinB - dy*cosB + float hoverDx = -cosB * rxW + sinB * ryW; + float hoverDy = -sinB * rxW - cosB * ryW; + glm::vec3 hoverRender(playerRender.x + hoverDx, playerRender.y + hoverDy, playerRender.z); + glm::vec3 hoverCanon = core::coords::renderToCanonical(hoverRender); + ImGui::TextColored(ImVec4(0.9f, 0.85f, 0.5f, 1.0f), "%.1f, %.1f", hoverCanon.x, hoverCanon.y); + ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "Ctrl+click to ping"); ImGui::EndTooltip(); + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + ImGui::OpenPopup("##minimapContextMenu"); + } + } + + if (ImGui::BeginPopup("##minimapContextMenu")) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Minimap"); + ImGui::Separator(); + + // Zoom controls + if (ImGui::MenuItem("Zoom In")) { + minimap->zoomIn(); + } + if (ImGui::MenuItem("Zoom Out")) { + minimap->zoomOut(); + } + + ImGui::Separator(); + + // Toggle options with checkmarks + bool rotWithCam = minimap->isRotateWithCamera(); + if (ImGui::MenuItem("Rotate with Camera", nullptr, rotWithCam)) { + minimap->setRotateWithCamera(!rotWithCam); + } + + bool squareShape = minimap->isSquareShape(); + if (ImGui::MenuItem("Square Shape", nullptr, squareShape)) { + minimap->setSquareShape(!squareShape); + } + + bool npcDots = minimapNpcDots_; + if (ImGui::MenuItem("Show NPC Dots", nullptr, npcDots)) { + minimapNpcDots_ = !minimapNpcDots_; + } + + ImGui::EndPopup(); } } @@ -11789,18 +20412,53 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { }; // Zone name label above the minimap (centered, WoW-style) + // Prefer the server-reported zone/area name (from SMSG_INIT_WORLD_STATES) so sub-zones + // like Ironforge or Wailing Caverns display correctly; fall back to renderer zone name. { - const std::string& zoneName = renderer ? renderer->getCurrentZoneName() : std::string{}; + std::string wsZoneName; + uint32_t wsZoneId = gameHandler.getWorldStateZoneId(); + if (wsZoneId != 0) + wsZoneName = gameHandler.getWhoAreaName(wsZoneId); + const std::string& rendererZoneName = renderer ? renderer->getCurrentZoneName() : std::string{}; + const std::string& zoneName = !wsZoneName.empty() ? wsZoneName : rendererZoneName; if (!zoneName.empty()) { auto* fgDl = ImGui::GetForegroundDrawList(); float zoneTextY = centerY - mapRadius - 16.0f; ImFont* font = ImGui::GetFont(); - ImVec2 tsz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, zoneName.c_str()); + + // Weather icon appended to zone name when active + uint32_t wType = gameHandler.getWeatherType(); + float wIntensity = gameHandler.getWeatherIntensity(); + const char* weatherIcon = nullptr; + ImU32 weatherColor = IM_COL32(255, 255, 255, 200); + if (wType == 1 && wIntensity > 0.05f) { // Rain + weatherIcon = " \xe2\x9b\x86"; // U+26C6 ⛆ + weatherColor = IM_COL32(140, 180, 240, 220); + } else if (wType == 2 && wIntensity > 0.05f) { // Snow + weatherIcon = " \xe2\x9d\x84"; // U+2744 ❄ + weatherColor = IM_COL32(210, 230, 255, 220); + } else if (wType == 3 && wIntensity > 0.05f) { // Storm/Fog + weatherIcon = " \xe2\x98\x81"; // U+2601 ☁ + weatherColor = IM_COL32(160, 160, 190, 220); + } + + std::string displayName = zoneName; + // Build combined string if weather active + std::string fullLabel = weatherIcon ? (zoneName + weatherIcon) : zoneName; + ImVec2 tsz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, fullLabel.c_str()); float tzx = centerX - tsz.x * 0.5f; + + // Shadow pass fgDl->AddText(font, 12.0f, ImVec2(tzx + 1.0f, zoneTextY + 1.0f), IM_COL32(0, 0, 0, 180), zoneName.c_str()); + // Zone name in gold fgDl->AddText(font, 12.0f, ImVec2(tzx, zoneTextY), IM_COL32(255, 220, 120, 230), zoneName.c_str()); + // Weather symbol in its own color appended after + if (weatherIcon) { + ImVec2 nameSz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, zoneName.c_str()); + fgDl->AddText(font, 12.0f, ImVec2(tzx + nameSz.x, zoneTextY), weatherColor, weatherIcon); + } } } @@ -11916,6 +20574,37 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } ImGui::End(); + // Clock display at bottom-right of minimap (local time) + { + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + std::tm tmBuf{}; +#ifdef _WIN32 + localtime_s(&tmBuf, &tt); +#else + localtime_r(&tt, &tmBuf); +#endif + char clockText[16]; + std::snprintf(clockText, sizeof(clockText), "%d:%02d %s", + (tmBuf.tm_hour % 12 == 0) ? 12 : tmBuf.tm_hour % 12, + tmBuf.tm_min, + tmBuf.tm_hour >= 12 ? "PM" : "AM"); + ImVec2 clockSz = ImGui::CalcTextSize(clockText); + float clockW = clockSz.x + 10.0f; + float clockH = clockSz.y + 6.0f; + ImGui::SetNextWindowPos(ImVec2(centerX + mapRadius - clockW - 2.0f, + centerY + mapRadius - clockH - 2.0f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(clockW, clockH), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.45f); + ImGuiWindowFlags clockFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoInputs; + if (ImGui::Begin("##MinimapClock", nullptr, clockFlags)) { + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.8f, 0.85f), "%s", clockText); + } + ImGui::End(); + } + // Indicators below the minimap (stacked: new mail, then BG queue, then latency) float indicatorX = centerX - mapRadius; float nextIndicatorY = centerY + mapRadius + 4.0f; @@ -11937,6 +20626,24 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { nextIndicatorY += kIndicatorH; } + // Unspent talent points indicator + { + uint8_t unspent = gameHandler.getUnspentTalentPoints(); + if (unspent > 0) { + ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always); + if (ImGui::Begin("##TalentIndicator", nullptr, indicatorFlags)) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 2.5f); + char talentBuf[40]; + snprintf(talentBuf, sizeof(talentBuf), "! %u Talent Point%s Available", + static_cast(unspent), unspent == 1 ? "" : "s"); + ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f * pulse, pulse), "%s", talentBuf); + } + ImGui::End(); + nextIndicatorY += kIndicatorH; + } + } + // BG queue status indicator (when in queue but not yet invited) for (const auto& slot : gameHandler.getBgQueues()) { if (slot.statusId != 1) continue; // STATUS_WAIT_QUEUE only @@ -11960,26 +20667,109 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always); if (ImGui::Begin("##BgQueueIndicator", nullptr, indicatorFlags)) { float pulse = 0.6f + 0.4f * std::sin(static_cast(ImGui::GetTime()) * 1.5f); - ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse), - "In Queue: %s", bgName.c_str()); + if (slot.avgWaitTimeSec > 0) { + int avgMin = static_cast(slot.avgWaitTimeSec) / 60; + int avgSec = static_cast(slot.avgWaitTimeSec) % 60; + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse), + "Queue: %s (~%d:%02d)", bgName.c_str(), avgMin, avgSec); + } else { + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse), + "In Queue: %s", bgName.c_str()); + } } ImGui::End(); nextIndicatorY += kIndicatorH; break; // Show at most one queue slot indicator } - // Latency indicator — centered at top of screen + // LFG queue indicator — shown when Dungeon Finder queue is active (Queued or RoleCheck) + { + using LfgState = game::GameHandler::LfgState; + LfgState lfgSt = gameHandler.getLfgState(); + if (lfgSt == LfgState::Queued || lfgSt == LfgState::RoleCheck) { + ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always); + if (ImGui::Begin("##LfgQueueIndicator", nullptr, indicatorFlags)) { + if (lfgSt == LfgState::RoleCheck) { + float pulse = 0.6f + 0.4f * std::sin(static_cast(ImGui::GetTime()) * 3.0f); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, pulse), "LFG: Role Check..."); + } else { + uint32_t qMs = gameHandler.getLfgTimeInQueueMs(); + int qMin = static_cast(qMs / 60000); + int qSec = static_cast((qMs % 60000) / 1000); + float pulse = 0.6f + 0.4f * std::sin(static_cast(ImGui::GetTime()) * 1.2f); + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, pulse), + "LFG: %d:%02d", qMin, qSec); + } + } + ImGui::End(); + nextIndicatorY += kIndicatorH; + } + } + + // Calendar pending invites indicator (WotLK only) + { + auto* expReg = core::Application::getInstance().getExpansionRegistry(); + bool isWotLK = expReg && expReg->getActive() && expReg->getActive()->id == "wotlk"; + if (isWotLK) { + uint32_t calPending = gameHandler.getCalendarPendingInvites(); + if (calPending > 0) { + ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always); + if (ImGui::Begin("##CalendarIndicator", nullptr, indicatorFlags)) { + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 2.0f); + char calBuf[48]; + snprintf(calBuf, sizeof(calBuf), "Calendar: %u Invite%s", + calPending, calPending == 1 ? "" : "s"); + ImGui::TextColored(ImVec4(0.6f, 0.5f, 1.0f, pulse), "%s", calBuf); + } + ImGui::End(); + nextIndicatorY += kIndicatorH; + } + } + } + + // Taxi flight indicator — shown while on a flight path + if (gameHandler.isOnTaxiFlight()) { + ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always); + if (ImGui::Begin("##TaxiIndicator", nullptr, indicatorFlags)) { + const std::string& dest = gameHandler.getTaxiDestName(); + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 1.0f); + if (dest.empty()) { + ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, pulse), "\xe2\x9c\x88 In Flight"); + } else { + char buf[64]; + snprintf(buf, sizeof(buf), "\xe2\x9c\x88 \xe2\x86\x92 %s", dest.c_str()); + ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, pulse), "%s", buf); + } + } + ImGui::End(); + nextIndicatorY += kIndicatorH; + } + + // Latency + FPS indicator — centered at top of screen uint32_t latMs = gameHandler.getLatencyMs(); - if (showLatencyMeter_ && latMs > 0 && gameHandler.getState() == game::WorldState::IN_WORLD) { + if (showLatencyMeter_ && gameHandler.getState() == game::WorldState::IN_WORLD) { + float currentFps = ImGui::GetIO().Framerate; ImVec4 latColor; if (latMs < 100) latColor = ImVec4(0.3f, 1.0f, 0.3f, 0.9f); else if (latMs < 250) latColor = ImVec4(1.0f, 1.0f, 0.3f, 0.9f); else if (latMs < 500) latColor = ImVec4(1.0f, 0.6f, 0.1f, 0.9f); else latColor = ImVec4(1.0f, 0.2f, 0.2f, 0.9f); - char latBuf[32]; - snprintf(latBuf, sizeof(latBuf), "%u ms", latMs); - ImVec2 textSize = ImGui::CalcTextSize(latBuf); + ImVec4 fpsColor; + if (currentFps >= 60.0f) fpsColor = ImVec4(0.3f, 1.0f, 0.3f, 0.9f); + else if (currentFps >= 30.0f) fpsColor = ImVec4(1.0f, 1.0f, 0.3f, 0.9f); + else fpsColor = ImVec4(1.0f, 0.3f, 0.3f, 0.9f); + + char infoText[64]; + if (latMs > 0) + snprintf(infoText, sizeof(infoText), "%.0f fps | %u ms", currentFps, latMs); + else + snprintf(infoText, sizeof(infoText), "%.0f fps", currentFps); + + ImVec2 textSize = ImGui::CalcTextSize(infoText); float latW = textSize.x + 16.0f; float latH = textSize.y + 8.0f; ImGuiIO& lio = ImGui::GetIO(); @@ -11988,7 +20778,14 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { ImGui::SetNextWindowSize(ImVec2(latW, latH), ImGuiCond_Always); ImGui::SetNextWindowBgAlpha(0.45f); if (ImGui::Begin("##LatencyIndicator", nullptr, indicatorFlags)) { - ImGui::TextColored(latColor, "%s", latBuf); + // Color the FPS and latency portions differently + ImGui::TextColored(fpsColor, "%.0f fps", currentFps); + if (latMs > 0) { + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.7f), "|"); + ImGui::SameLine(0, 4); + ImGui::TextColored(latColor, "%u ms", latMs); + } } ImGui::End(); } @@ -12157,8 +20954,16 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game: pos += replacement.length(); } + // Resolve class and race names for $C and $R placeholders + std::string className = "Adventurer"; + std::string raceName = "Unknown"; + if (character) { + className = game::getClassName(character->characterClass); + raceName = game::getRaceName(character->race); + } + // Replace simple placeholders. - // $n = player name + // $n/$N = player name, $c/$C = class name, $r/$R = race name // $p = subject pronoun (he/she/they) // $o = object pronoun (him/her/them) // $s = possessive adjective (his/her/their) @@ -12172,6 +20977,8 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game: std::string replacement; switch (code) { case 'n': case 'N': replacement = playerName; break; + case 'c': case 'C': replacement = className; break; + case 'r': case 'R': replacement = raceName; break; case 'p': replacement = pronouns.subject; break; case 'o': replacement = pronouns.object; break; case 's': replacement = pronouns.possessive; break; @@ -12239,7 +21046,9 @@ void GameScreen::renderChatBubbles(game::GameHandler& gameHandler) { glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w); float screenX = (ndc.x * 0.5f + 0.5f) * screenW; - float screenY = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH; // Flip Y + // Camera bakes the Vulkan Y-flip into the projection matrix: + // NDC y=-1 is top, y=1 is bottom — same convention as nameplate/minimap projection. + float screenY = (ndc.y * 0.5f + 0.5f) * screenH; // Skip if off-screen if (screenX < -200.0f || screenX > screenW + 200.0f || @@ -12280,6 +21089,32 @@ void GameScreen::renderChatBubbles(game::GameHandler& gameHandler) { } } +void GameScreen::applyAudioVolumes(rendering::Renderer* renderer) { + if (!renderer) return; + float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; + audio::AudioEngine::instance().setMasterVolume(masterScale); + if (auto* music = renderer->getMusicManager()) + music->setVolume(pendingMusicVolume); + if (auto* ambient = renderer->getAmbientSoundManager()) + ambient->setVolumeScale(pendingAmbientVolume / 100.0f); + if (auto* ui = renderer->getUiSoundManager()) + ui->setVolumeScale(pendingUiVolume / 100.0f); + if (auto* combat = renderer->getCombatSoundManager()) + combat->setVolumeScale(pendingCombatVolume / 100.0f); + if (auto* spell = renderer->getSpellSoundManager()) + spell->setVolumeScale(pendingSpellVolume / 100.0f); + if (auto* movement = renderer->getMovementSoundManager()) + movement->setVolumeScale(pendingMovementVolume / 100.0f); + if (auto* footstep = renderer->getFootstepManager()) + footstep->setVolumeScale(pendingFootstepVolume / 100.0f); + if (auto* npcVoice = renderer->getNpcVoiceManager()) + npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f); + if (auto* mount = renderer->getMountSoundManager()) + mount->setVolumeScale(pendingMountVolume / 100.0f); + if (auto* activity = renderer->getActivitySoundManager()) + activity->setVolumeScale(pendingActivityVolume / 100.0f); +} + void GameScreen::saveSettings() { std::string path = getSettingsPath(); std::filesystem::path dir = std::filesystem::path(path).parent_path(); @@ -12298,9 +21133,13 @@ void GameScreen::saveSettings() { out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n"; out << "minimap_npc_dots=" << (pendingMinimapNpcDots ? 1 : 0) << "\n"; out << "show_latency_meter=" << (pendingShowLatencyMeter ? 1 : 0) << "\n"; + out << "show_dps_meter=" << (showDPSMeter_ ? 1 : 0) << "\n"; + out << "show_cooldown_tracker=" << (showCooldownTracker_ ? 1 : 0) << "\n"; out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; + out << "show_keyring=" << (pendingShowKeyring ? 1 : 0) << "\n"; out << "action_bar_scale=" << pendingActionBarScale << "\n"; out << "nameplate_scale=" << nameplateScale_ << "\n"; + out << "show_friendly_nameplates=" << (showFriendlyNameplates_ ? 1 : 0) << "\n"; out << "show_action_bar2=" << (pendingShowActionBar2 ? 1 : 0) << "\n"; out << "action_bar2_offset_x=" << pendingActionBar2OffsetX << "\n"; out << "action_bar2_offset_y=" << pendingActionBar2OffsetY << "\n"; @@ -12309,6 +21148,7 @@ void GameScreen::saveSettings() { out << "right_bar_offset_y=" << pendingRightBarOffsetY << "\n"; out << "left_bar_offset_y=" << pendingLeftBarOffsetY << "\n"; out << "damage_flash=" << (damageFlashEnabled_ ? 1 : 0) << "\n"; + out << "low_health_vignette=" << (lowHealthVignetteEnabled_ ? 1 : 0) << "\n"; // Audio out << "sound_muted=" << (soundMuted_ ? 1 : 0) << "\n"; @@ -12327,12 +21167,16 @@ void GameScreen::saveSettings() { // Gameplay out << "auto_loot=" << (pendingAutoLoot ? 1 : 0) << "\n"; + out << "auto_sell_grey=" << (pendingAutoSellGrey ? 1 : 0) << "\n"; + out << "auto_repair=" << (pendingAutoRepair ? 1 : 0) << "\n"; out << "graphics_preset=" << static_cast(currentGraphicsPreset) << "\n"; out << "ground_clutter_density=" << pendingGroundClutterDensity << "\n"; out << "shadows=" << (pendingShadows ? 1 : 0) << "\n"; out << "shadow_distance=" << pendingShadowDistance << "\n"; + out << "brightness=" << pendingBrightness << "\n"; out << "water_refraction=" << (pendingWaterRefraction ? 1 : 0) << "\n"; out << "antialiasing=" << pendingAntiAliasing << "\n"; + out << "fxaa=" << (pendingFXAA ? 1 : 0) << "\n"; out << "normal_mapping=" << (pendingNormalMapping ? 1 : 0) << "\n"; out << "normal_map_strength=" << pendingNormalMapStrength << "\n"; out << "pom=" << (pendingPOM ? 1 : 0) << "\n"; @@ -12352,6 +21196,12 @@ void GameScreen::saveSettings() { out << "extended_zoom=" << (pendingExtendedZoom ? 1 : 0) << "\n"; out << "fov=" << pendingFov << "\n"; + // Quest tracker position/size + out << "quest_tracker_right_offset=" << questTrackerRightOffset_ << "\n"; + out << "quest_tracker_y=" << questTrackerPos_.y << "\n"; + out << "quest_tracker_w=" << questTrackerSize_.x << "\n"; + out << "quest_tracker_h=" << questTrackerSize_.y << "\n"; + // Chat out << "chat_active_tab=" << activeChatTab_ << "\n"; out << "chat_timestamps=" << (chatShowTimestamps_ ? 1 : 0) << "\n"; @@ -12405,13 +21255,22 @@ void GameScreen::loadSettings() { } else if (key == "show_latency_meter") { showLatencyMeter_ = (std::stoi(val) != 0); pendingShowLatencyMeter = showLatencyMeter_; + } else if (key == "show_dps_meter") { + showDPSMeter_ = (std::stoi(val) != 0); + } else if (key == "show_cooldown_tracker") { + showCooldownTracker_ = (std::stoi(val) != 0); } else if (key == "separate_bags") { pendingSeparateBags = (std::stoi(val) != 0); inventoryScreen.setSeparateBags(pendingSeparateBags); + } else if (key == "show_keyring") { + pendingShowKeyring = (std::stoi(val) != 0); + inventoryScreen.setShowKeyring(pendingShowKeyring); } else if (key == "action_bar_scale") { pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f); } else if (key == "nameplate_scale") { nameplateScale_ = std::clamp(std::stof(val), 0.5f, 2.0f); + } else if (key == "show_friendly_nameplates") { + showFriendlyNameplates_ = (std::stoi(val) != 0); } else if (key == "show_action_bar2") { pendingShowActionBar2 = (std::stoi(val) != 0); } else if (key == "action_bar2_offset_x") { @@ -12428,6 +21287,8 @@ void GameScreen::loadSettings() { pendingLeftBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f); } else if (key == "damage_flash") { damageFlashEnabled_ = (std::stoi(val) != 0); + } else if (key == "low_health_vignette") { + lowHealthVignetteEnabled_ = (std::stoi(val) != 0); } // Audio else if (key == "sound_muted") { @@ -12451,6 +21312,8 @@ void GameScreen::loadSettings() { else if (key == "activity_volume") pendingActivityVolume = std::clamp(std::stoi(val), 0, 100); // Gameplay else if (key == "auto_loot") pendingAutoLoot = (std::stoi(val) != 0); + else if (key == "auto_sell_grey") pendingAutoSellGrey = (std::stoi(val) != 0); + else if (key == "auto_repair") pendingAutoRepair = (std::stoi(val) != 0); else if (key == "graphics_preset") { int presetVal = std::clamp(std::stoi(val), 0, 4); currentGraphicsPreset = static_cast(presetVal); @@ -12459,8 +21322,14 @@ void GameScreen::loadSettings() { else if (key == "ground_clutter_density") pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150); else if (key == "shadows") pendingShadows = (std::stoi(val) != 0); else if (key == "shadow_distance") pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f); + else if (key == "brightness") { + pendingBrightness = std::clamp(std::stoi(val), 0, 100); + if (auto* r = core::Application::getInstance().getRenderer()) + r->setBrightness(static_cast(pendingBrightness) / 50.0f); + } else if (key == "water_refraction") pendingWaterRefraction = (std::stoi(val) != 0); else if (key == "antialiasing") pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3); + else if (key == "fxaa") pendingFXAA = (std::stoi(val) != 0); else if (key == "normal_mapping") pendingNormalMapping = (std::stoi(val) != 0); else if (key == "normal_map_strength") pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f); else if (key == "pom") pendingPOM = (std::stoi(val) != 0); @@ -12489,6 +21358,25 @@ void GameScreen::loadSettings() { if (auto* camera = renderer->getCamera()) camera->setFov(pendingFov); } } + // Quest tracker position/size + else if (key == "quest_tracker_x") { + // Legacy: ignore absolute X (right_offset supersedes it) + (void)val; + } + else if (key == "quest_tracker_right_offset") { + questTrackerRightOffset_ = std::stof(val); + questTrackerPosInit_ = true; + } + else if (key == "quest_tracker_y") { + questTrackerPos_.y = std::stof(val); + questTrackerPosInit_ = true; + } + else if (key == "quest_tracker_w") { + questTrackerSize_.x = std::max(100.0f, std::stof(val)); + } + else if (key == "quest_tracker_h") { + questTrackerSize_.y = std::max(60.0f, std::stof(val)); + } // Chat else if (key == "chat_active_tab") activeChatTab_ = std::clamp(std::stoi(val), 0, 3); else if (key == "chat_timestamps") chatShowTimestamps_ = (std::stoi(val) != 0); @@ -12525,11 +21413,8 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { const auto& inbox = gameHandler.getMailInbox(); // Top bar: money + compose button - uint64_t money = gameHandler.getMoneyCopper(); - uint32_t mg = static_cast(money / 10000); - uint32_t ms = static_cast((money / 100) % 100); - uint32_t mc = static_cast(money % 100); - ImGui::Text("Your money: %ug %us %uc", mg, ms, mc); + ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(gameHandler.getMoneyCopper()); ImGui::SameLine(ImGui::GetWindowWidth() - 100); if (ImGui::Button("Compose")) { mailRecipientBuffer_[0] = '\0'; @@ -12575,7 +21460,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { } // Sub-info line - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), " From: %s", mail.senderName.c_str()); + ImGui::TextColored(kColorGray, " From: %s", mail.senderName.c_str()); if (mail.money > 0) { ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), " [G]"); @@ -12584,6 +21469,21 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]"); } + // Expiry warning if within 3 days + if (mail.expirationTime > 0.0f) { + auto nowSec = static_cast(std::time(nullptr)); + float secsLeft = mail.expirationTime - nowSec; + if (secsLeft < 3.0f * 86400.0f && secsLeft > 0.0f) { + ImGui::SameLine(); + int daysLeft = static_cast(secsLeft / 86400.0f); + if (daysLeft == 0) { + ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), " [expires today!]"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), + " [expires in %dd]", daysLeft); + } + } + } ImGui::PopID(); } @@ -12604,6 +21504,35 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { if (mail.messageType == 2) { ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]"); } + + // Show expiry date in the detail panel + if (mail.expirationTime > 0.0f) { + auto nowSec = static_cast(std::time(nullptr)); + float secsLeft = mail.expirationTime - nowSec; + // Format absolute expiry as a date using struct tm + time_t expT = static_cast(mail.expirationTime); + struct tm* tmExp = std::localtime(&expT); + if (tmExp) { + static const char* kMon[12] = { + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" + }; + const char* mname = kMon[tmExp->tm_mon]; + int daysLeft = static_cast(secsLeft / 86400.0f); + if (secsLeft <= 0.0f) { + ImGui::TextColored(kColorGray, + "Expired: %s %d, %d", mname, tmExp->tm_mday, 1900 + tmExp->tm_year); + } else if (secsLeft < 3.0f * 86400.0f) { + ImGui::TextColored(kColorRed, + "Expires: %s %d, %d (%d day%s!)", + mname, tmExp->tm_mday, 1900 + tmExp->tm_year, + daysLeft, daysLeft == 1 ? "" : "s"); + } else { + ImGui::TextDisabled("Expires: %s %d, %d", + mname, tmExp->tm_mday, 1900 + tmExp->tm_year); + } + } + } ImGui::Separator(); // Body text @@ -12614,10 +21543,8 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { // Money if (mail.money > 0) { - uint32_t g = mail.money / 10000; - uint32_t s = (mail.money / 100) % 100; - uint32_t c = mail.money % 100; - ImGui::Text("Money: %ug %us %uc", g, s, c); + ImGui::TextDisabled("Money:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(mail.money); ImGui::SameLine(); if (ImGui::SmallButton("Take Money")) { gameHandler.mailTakeMoney(mail.messageId); @@ -12629,7 +21556,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { uint32_t g = mail.cod / 10000; uint32_t s = (mail.cod / 100) % 100; uint32_t c = mail.cod % 100; - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + ImGui::TextColored(kColorRed, "COD: %ug %us %uc (you pay this to take items)", g, s, c); } @@ -12712,7 +21639,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { } ImGui::SameLine(); if (ImGui::SmallButton("Take")) { - gameHandler.mailTakeItem(mail.messageId, att.slot); + gameHandler.mailTakeItem(mail.messageId, att.itemGuidLow); } ImGui::PopID(); @@ -12721,7 +21648,7 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { if (mail.attachments.size() > 1) { if (ImGui::SmallButton("Take All")) { for (const auto& att2 : mail.attachments) { - gameHandler.mailTakeItem(mail.messageId, att2.slot); + gameHandler.mailTakeItem(mail.messageId, att2.itemGuidLow); } } } @@ -12791,7 +21718,7 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { int attachCount = gameHandler.getMailAttachmentCount(); ImGui::Text("Attachments (%d/12):", attachCount); ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Right-click items in bags to attach"); + ImGui::TextColored(kColorGray, "Right-click items in bags to attach"); const auto& attachments = gameHandler.getMailAttachments(); // Show attachment slots in a grid (6 per row) @@ -12823,7 +21750,7 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextColored(qualColor, "%s", att.item.name.c_str()); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Click to remove"); + ImGui::TextColored(ui::colors::kLightGray, "Click to remove"); ImGui::EndTooltip(); } } else { @@ -12862,7 +21789,7 @@ void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) { static_cast(mailComposeMoney_[2]); uint32_t sendCost = attachCount > 0 ? static_cast(30 * attachCount) : 30u; - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Sending cost: %uc", sendCost); + ImGui::TextColored(kColorGray, "Sending cost: %uc", sendCost); ImGui::Spacing(); bool canSend = (strlen(mailRecipientBuffer_) > 0); @@ -13113,9 +22040,8 @@ void GameScreen::renderGuildBankWindow(game::GameHandler& gameHandler) { uint32_t gold = static_cast(data.money / 10000); uint32_t silver = static_cast((data.money / 100) % 100); uint32_t copper = static_cast(data.money % 100); - ImGui::Text("Guild Bank Money: "); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.3f, 1.0f), "%ug %us %uc", gold, silver, copper); + ImGui::TextDisabled("Guild Bank Money:"); ImGui::SameLine(0, 4); + renderCoinsText(gold, silver, copper); // Tab bar if (!data.tabs.empty()) { @@ -13351,7 +22277,8 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { gameHandler.auctionSearch(auctionSearchName_, static_cast(auctionLevelMin_), static_cast(auctionLevelMax_), - q, getSearchClassId(), getSearchSubClassId(), 0, 0, offset); + q, getSearchClassId(), getSearchSubClassId(), 0, + auctionUsableOnly_ ? 1 : 0, offset); }; // Row 1: Name + Level range @@ -13397,6 +22324,8 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { } } + ImGui::SameLine(); + ImGui::Checkbox("Usable", &auctionUsableOnly_); ImGui::SameLine(); float delay = gameHandler.getAuctionSearchDelay(); if (delay > 0.0f) { @@ -13456,6 +22385,12 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { const auto& auction = results.auctions[i]; auto* info = gameHandler.getItemInfo(auction.itemEntry); std::string name = info ? info->name : ("Item #" + std::to_string(auction.itemEntry)); + // Append random suffix name (e.g., "of the Eagle") if present + if (auction.randomPropertyId != 0) { + std::string suffix = gameHandler.getRandomPropertyName( + static_cast(auction.randomPropertyId)); + if (!suffix.empty()) name += " " + suffix; + } game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; ImVec4 qc = InventoryScreen::getQualityColor(quality); @@ -13498,13 +22433,12 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(3); { uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid; - ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); + renderCoinsFromCopper(bid); } ImGui::TableSetColumnIndex(4); if (auction.buyoutPrice > 0) { - ImGui::Text("%ug%us%uc", auction.buyoutPrice / 10000, - (auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100); + renderCoinsFromCopper(auction.buyoutPrice); } else { ImGui::TextDisabled("--"); } @@ -13649,6 +22583,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { const auto& a = results.auctions[bi]; auto* info = gameHandler.getItemInfo(a.itemEntry); std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry)); + if (a.randomPropertyId != 0) { + std::string suffix = gameHandler.getRandomPropertyName( + static_cast(a.randomPropertyId)); + if (!suffix.empty()) name += " " + suffix; + } game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; ImVec4 bqc = InventoryScreen::getQualityColor(quality); @@ -13661,6 +22600,15 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::SameLine(); } } + // High bidder indicator + bool isHighBidder = (a.bidderGuid != 0 && a.bidderGuid == gameHandler.getPlayerGuid()); + if (isHighBidder) { + ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "[Winning]"); + ImGui::SameLine(); + } else if (a.bidderGuid != 0) { + ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[Outbid]"); + ImGui::SameLine(); + } ImGui::TextColored(bqc, "%s", name.c_str()); // Tooltip and shift-click if (ImGui::IsItemHovered() && info && info->valid) @@ -13678,10 +22626,10 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(1); ImGui::Text("%u", a.stackCount); ImGui::TableSetColumnIndex(2); - ImGui::Text("%ug%us%uc", a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100); + renderCoinsFromCopper(a.currentBid); ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) - ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + renderCoinsFromCopper(a.buyoutPrice); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); @@ -13724,6 +22672,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { const auto& a = results.auctions[i]; auto* info = gameHandler.getItemInfo(a.itemEntry); std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry)); + if (a.randomPropertyId != 0) { + std::string suffix = gameHandler.getRandomPropertyName( + static_cast(a.randomPropertyId)); + if (!suffix.empty()) name += " " + suffix; + } game::ItemQuality quality = info ? static_cast(info->quality) : game::ItemQuality::COMMON; ImGui::TableNextRow(); @@ -13736,6 +22689,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::SameLine(); } } + // Bid activity indicator for seller + if (a.bidderGuid != 0) { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "[Bid]"); + ImGui::SameLine(); + } ImGui::TextColored(oqc, "%s", name.c_str()); if (ImGui::IsItemHovered() && info && info->valid) inventoryScreen.renderItemTooltip(*info); @@ -13754,11 +22712,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(2); { uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid; - ImGui::Text("%ug%us%uc", bid / 10000, (bid / 100) % 100, bid % 100); + renderCoinsFromCopper(bid); } ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) - ImGui::Text("%ug%us%uc", a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + renderCoinsFromCopper(a.buyoutPrice); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); @@ -13781,9 +22739,18 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { // Level-Up Ding Animation // ============================================================ -void GameScreen::triggerDing(uint32_t newLevel) { - dingTimer_ = DING_DURATION; - dingLevel_ = newLevel; +void GameScreen::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t manaDelta, + uint32_t str, uint32_t agi, uint32_t sta, + uint32_t intel, uint32_t spi) { + dingTimer_ = DING_DURATION; + dingLevel_ = newLevel; + dingHpDelta_ = hpDelta; + dingManaDelta_ = manaDelta; + dingStats_[0] = str; + dingStats_[1] = agi; + dingStats_[2] = sta; + dingStats_[3] = intel; + dingStats_[4] = spi; auto* renderer = core::Application::getInstance().getRenderer(); if (renderer) { @@ -13825,10 +22792,47 @@ void GameScreen::renderDingEffect() { // Slight black outline for readability draw->AddText(font, fontSize, ImVec2(tx + 2, ty + 2), - IM_COL32(0, 0, 0, (int)(alpha * 180)), buf); + IM_COL32(0, 0, 0, static_cast(alpha * 180)), buf); // Gold text draw->AddText(font, fontSize, ImVec2(tx, ty), - IM_COL32(255, 210, 0, (int)(alpha * 255)), buf); + IM_COL32(255, 210, 0, static_cast(alpha * 255)), buf); + + // Stat gains below the main text (shown only if server sent deltas) + bool hasStatGains = (dingHpDelta_ > 0 || dingManaDelta_ > 0 || + dingStats_[0] || dingStats_[1] || dingStats_[2] || + dingStats_[3] || dingStats_[4]); + if (hasStatGains) { + float smallSize = baseSize * 0.95f; + float yOff = ty + sz.y + 6.0f; + + // Build stat delta string: "+150 HP +80 Mana +2 Str +2 Agi ..." + static const char* kStatLabels[] = { "Str", "Agi", "Sta", "Int", "Spi" }; + char statBuf[128]; + int written = 0; + if (dingHpDelta_ > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u HP ", dingHpDelta_); + if (dingManaDelta_ > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u Mana ", dingManaDelta_); + for (int i = 0; i < 5 && written < static_cast(sizeof(statBuf)) - 1; ++i) { + if (dingStats_[i] > 0) + written += snprintf(statBuf + written, sizeof(statBuf) - written, + "+%u %s ", dingStats_[i], kStatLabels[i]); + } + // Trim trailing spaces + while (written > 0 && statBuf[written - 1] == ' ') --written; + statBuf[written] = '\0'; + + if (written > 0) { + ImVec2 ssz = font->CalcTextSizeA(smallSize, FLT_MAX, 0.0f, statBuf); + float stx = cx - ssz.x * 0.5f; + draw->AddText(font, smallSize, ImVec2(stx + 1, yOff + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), statBuf); + draw->AddText(font, smallSize, ImVec2(stx, yOff), + IM_COL32(100, 220, 100, static_cast(alpha * 230)), statBuf); + } + } } void GameScreen::triggerAchievementToast(uint32_t achievementId, std::string name) { @@ -13877,8 +22881,8 @@ void GameScreen::renderAchievementToast() { // Background panel (gold border, dark fill) ImVec2 tl(toastX, toastY); ImVec2 br(toastX + TOAST_W, toastY + TOAST_H); - draw->AddRectFilled(tl, br, IM_COL32(30, 20, 10, (int)(alpha * 230)), 6.0f); - draw->AddRect(tl, br, IM_COL32(200, 170, 50, (int)(alpha * 255)), 6.0f, 0, 2.0f); + draw->AddRectFilled(tl, br, IM_COL32(30, 20, 10, static_cast(alpha * 230)), 6.0f); + draw->AddRect(tl, br, IM_COL32(200, 170, 50, static_cast(alpha * 255)), 6.0f, 0, 2.0f); // Title ImFont* font = ImGui::GetFont(); @@ -13888,9 +22892,9 @@ void GameScreen::renderAchievementToast() { float titleW = font->CalcTextSizeA(titleSize, FLT_MAX, 0.0f, title).x; float titleX = toastX + (TOAST_W - titleW) * 0.5f; draw->AddText(font, titleSize, ImVec2(titleX + 1, toastY + 8 + 1), - IM_COL32(0, 0, 0, (int)(alpha * 180)), title); + IM_COL32(0, 0, 0, static_cast(alpha * 180)), title); draw->AddText(font, titleSize, ImVec2(titleX, toastY + 8), - IM_COL32(255, 215, 0, (int)(alpha * 255)), title); + IM_COL32(255, 215, 0, static_cast(alpha * 255)), title); // Achievement name (falls back to ID if name not available) char idBuf[256]; @@ -13904,22 +22908,582 @@ void GameScreen::renderAchievementToast() { float idW = font->CalcTextSizeA(bodySize, FLT_MAX, 0.0f, idBuf).x; float idX = toastX + (TOAST_W - idW) * 0.5f; draw->AddText(font, bodySize, ImVec2(idX, toastY + 28), - IM_COL32(220, 200, 150, (int)(alpha * 255)), idBuf); + IM_COL32(220, 200, 150, static_cast(alpha * 255)), idBuf); } // --------------------------------------------------------------------------- +// Area discovery toast — "Discovered: ! (+XP XP)" centered on screen +// --------------------------------------------------------------------------- + +void GameScreen::renderDiscoveryToast() { + if (discoveryToastTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + discoveryToastTimer_ -= dt; + if (discoveryToastTimer_ < 0.0f) discoveryToastTimer_ = 0.0f; + + // Fade: ramp up in first 0.4s, hold, fade out in last 1.0s + float alpha; + if (discoveryToastTimer_ > DISCOVERY_TOAST_DURATION - 0.4f) + alpha = 1.0f - (discoveryToastTimer_ - (DISCOVERY_TOAST_DURATION - 0.4f)) / 0.4f; + else if (discoveryToastTimer_ < 1.0f) + alpha = discoveryToastTimer_; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImFont* font = ImGui::GetFont(); + ImDrawList* draw = ImGui::GetForegroundDrawList(); + + const char* header = "Discovered!"; + float headerSize = 16.0f; + float nameSize = 28.0f; + float xpSize = 14.0f; + + ImVec2 headerDim = font->CalcTextSizeA(headerSize, FLT_MAX, 0.0f, header); + ImVec2 nameDim = font->CalcTextSizeA(nameSize, FLT_MAX, 0.0f, discoveryToastName_.c_str()); + + char xpBuf[48]; + if (discoveryToastXP_ > 0) + snprintf(xpBuf, sizeof(xpBuf), "+%u XP", discoveryToastXP_); + else + xpBuf[0] = '\0'; + ImVec2 xpDim = font->CalcTextSizeA(xpSize, FLT_MAX, 0.0f, xpBuf); + + // Position slightly below zone text (at 37% down screen) + float centreY = screenH * 0.37f; + float headerX = (screenW - headerDim.x) * 0.5f; + float nameX = (screenW - nameDim.x) * 0.5f; + float xpX = (screenW - xpDim.x) * 0.5f; + float headerY = centreY; + float nameY = centreY + headerDim.y + 4.0f; + float xpY = nameY + nameDim.y + 4.0f; + + // "Discovered!" in gold + draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), header); + draw->AddText(font, headerSize, ImVec2(headerX, headerY), + IM_COL32(255, 215, 0, static_cast(alpha * 255)), header); + + // Area name in white + draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 160)), discoveryToastName_.c_str()); + draw->AddText(font, nameSize, ImVec2(nameX, nameY), + IM_COL32(255, 255, 255, static_cast(alpha * 255)), discoveryToastName_.c_str()); + + // XP gain in light green (if any) + if (xpBuf[0] != '\0') { + draw->AddText(font, xpSize, ImVec2(xpX + 1, xpY + 1), + IM_COL32(0, 0, 0, static_cast(alpha * 140)), xpBuf); + draw->AddText(font, xpSize, ImVec2(xpX, xpY), + IM_COL32(100, 220, 100, static_cast(alpha * 230)), xpBuf); + } +} + +// --------------------------------------------------------------------------- +// Quest objective progress toasts — shown at screen bottom-right on kill/item updates +// --------------------------------------------------------------------------- + +void GameScreen::renderQuestProgressToasts() { + if (questToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : questToasts_) t.age += dt; + questToasts_.erase( + std::remove_if(questToasts_.begin(), questToasts_.end(), + [](const QuestProgressToastEntry& t) { return t.age >= QUEST_TOAST_DURATION; }), + questToasts_.end()); + if (questToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Stack at bottom-right, just above action bar area + constexpr float TOAST_W = 240.0f; + constexpr float TOAST_H = 48.0f; + constexpr float TOAST_GAP = 4.0f; + float baseY = screenH * 0.72f; + float toastX = screenW - TOAST_W - 14.0f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(questToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = questToasts_[i]; + + float remaining = QUEST_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.2f) + alpha = toast.age / 0.2f; + else if (remaining < 1.0f) + alpha = remaining; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(200 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: dark amber tint (quest color convention) + bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(35, 25, 5, bgA), 5.0f); + bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(200, 160, 30, static_cast(160 * alpha)), 5.0f, 0, 1.5f); + + // Quest title (gold, small) + bgDL->AddText(ImVec2(toastX + 8.0f, ty + 5.0f), + IM_COL32(220, 180, 50, fgA), toast.questTitle.c_str()); + + // Progress bar + text: "ObjectiveName X / Y" + float barY = ty + 21.0f; + float barX0 = toastX + 8.0f; + float barX1 = toastX + TOAST_W - 8.0f; + float barH = 8.0f; + float pct = (toast.required > 0) + ? std::min(1.0f, static_cast(toast.current) / static_cast(toast.required)) + : 1.0f; + // Bar background + bgDL->AddRectFilled(ImVec2(barX0, barY), ImVec2(barX1, barY + barH), + IM_COL32(50, 40, 10, static_cast(180 * alpha)), 3.0f); + // Bar fill — green when complete, amber otherwise + ImU32 barCol = (pct >= 1.0f) ? IM_COL32(60, 220, 80, fgA) : IM_COL32(200, 160, 30, fgA); + bgDL->AddRectFilled(ImVec2(barX0, barY), + ImVec2(barX0 + (barX1 - barX0) * pct, barY + barH), + barCol, 3.0f); + + // Objective name + count + char progBuf[48]; + if (!toast.objectiveName.empty()) + snprintf(progBuf, sizeof(progBuf), "%.22s: %u/%u", + toast.objectiveName.c_str(), toast.current, toast.required); + else + snprintf(progBuf, sizeof(progBuf), "%u/%u", toast.current, toast.required); + bgDL->AddText(ImVec2(toastX + 8.0f, ty + 32.0f), + IM_COL32(220, 220, 200, static_cast(210 * alpha)), progBuf); + } +} + +// --------------------------------------------------------------------------- +// Item loot toasts — quality-coloured strip at bottom-left when item received +// --------------------------------------------------------------------------- + +void GameScreen::renderItemLootToasts() { + if (itemLootToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : itemLootToasts_) t.age += dt; + itemLootToasts_.erase( + std::remove_if(itemLootToasts_.begin(), itemLootToasts_.end(), + [](const ItemLootToastEntry& t) { return t.age >= ITEM_LOOT_TOAST_DURATION; }), + itemLootToasts_.end()); + if (itemLootToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Quality colours (matching WoW convention) + static const ImU32 kQualityColors[] = { + IM_COL32(157, 157, 157, 255), // 0 grey (poor) + IM_COL32(255, 255, 255, 255), // 1 white (common) + IM_COL32( 30, 255, 30, 255), // 2 green (uncommon) + IM_COL32( 0, 112, 221, 255), // 3 blue (rare) + IM_COL32(163, 53, 238, 255), // 4 purple (epic) + IM_COL32(255, 128, 0, 255), // 5 orange (legendary) + IM_COL32(230, 204, 128, 255), // 6 light gold (artifact) + IM_COL32(230, 204, 128, 255), // 7 light gold (heirloom) + }; + + // Stack at bottom-left above action bars; each item is 24 px tall + constexpr float TOAST_W = 260.0f; + constexpr float TOAST_H = 24.0f; + constexpr float TOAST_GAP = 2.0f; + constexpr float TOAST_X = 14.0f; + float baseY = screenH * 0.68f; // slightly above the whisper toasts + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(itemLootToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = itemLootToasts_[i]; + + float remaining = ITEM_LOOT_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.15f) + alpha = toast.age / 0.15f; + else if (remaining < 0.7f) + alpha = remaining / 0.7f; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Slide-in from left + float slideX = (toast.age < 0.15f) ? (TOAST_W * (1.0f - toast.age / 0.15f)) : 0.0f; + float tx = TOAST_X - slideX; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(180 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: very dark with quality-tinted left border accent + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(12, 12, 12, bgA), 3.0f); + + // Quality colour accent bar on left edge (3px wide) + ImU32 qualCol = kQualityColors[std::min(static_cast(7u), toast.quality)]; + ImU32 qualColA = (qualCol & 0x00FFFFFFu) | (static_cast(fgA) << 24u); + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + 3.0f, ty + TOAST_H), qualColA, 3.0f); + + // "Loot:" label in dim white + bgDL->AddText(ImVec2(tx + 7.0f, ty + 5.0f), + IM_COL32(160, 160, 160, static_cast(200 * alpha)), "Loot:"); + + // Item name in quality colour + std::string displayName = toast.name.empty() ? ("Item #" + std::to_string(toast.itemId)) : toast.name; + if (displayName.size() > 26) { displayName.resize(23); displayName += "..."; } + bgDL->AddText(ImVec2(tx + 42.0f, ty + 5.0f), qualColA, displayName.c_str()); + + // Count (if > 1) + if (toast.count > 1) { + char countBuf[12]; + snprintf(countBuf, sizeof(countBuf), "x%u", toast.count); + bgDL->AddText(ImVec2(tx + TOAST_W - 34.0f, ty + 5.0f), + IM_COL32(200, 200, 200, static_cast(200 * alpha)), countBuf); + } + } +} + +// --------------------------------------------------------------------------- +// PvP honor credit toasts — shown at screen top-right on honorable kill +// --------------------------------------------------------------------------- + +void GameScreen::renderPvpHonorToasts() { + if (pvpHonorToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : pvpHonorToasts_) t.age += dt; + pvpHonorToasts_.erase( + std::remove_if(pvpHonorToasts_.begin(), pvpHonorToasts_.end(), + [](const PvpHonorToastEntry& t) { return t.age >= PVP_HONOR_TOAST_DURATION; }), + pvpHonorToasts_.end()); + if (pvpHonorToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + + // Stack toasts at top-right, below any minimap area + constexpr float TOAST_W = 180.0f; + constexpr float TOAST_H = 30.0f; + constexpr float TOAST_GAP = 3.0f; + constexpr float TOAST_TOP = 10.0f; + float toastX = screenW - TOAST_W - 10.0f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(pvpHonorToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = pvpHonorToasts_[i]; + + float remaining = PVP_HONOR_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.15f) + alpha = toast.age / 0.15f; + else if (remaining < 0.8f) + alpha = remaining / 0.8f; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + float ty = TOAST_TOP + i * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(190 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: dark red (PvP theme) + bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(28, 5, 5, bgA), 4.0f); + bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(200, 50, 50, static_cast(160 * alpha)), 4.0f, 0, 1.2f); + + // Sword ⚔ icon (U+2694, UTF-8: e2 9a 94) + bgDL->AddText(ImVec2(toastX + 7.0f, ty + 7.0f), + IM_COL32(220, 80, 80, fgA), "\xe2\x9a\x94"); + + // "+N Honor" text in gold + char buf[40]; + snprintf(buf, sizeof(buf), "+%u Honor", toast.honor); + bgDL->AddText(ImVec2(toastX + 24.0f, ty + 8.0f), + IM_COL32(255, 210, 50, fgA), buf); + } +} + +// --------------------------------------------------------------------------- +// Nearby player level-up toasts — shown at screen bottom-centre +// --------------------------------------------------------------------------- + +void GameScreen::renderPlayerLevelUpToasts(game::GameHandler& gameHandler) { + if (playerLevelUpToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + for (auto& t : playerLevelUpToasts_) { + t.age += dt; + // Lazy name resolution — fill in once the name cache has it + if (t.playerName.empty() && t.guid != 0) { + t.playerName = gameHandler.lookupName(t.guid); + } + } + playerLevelUpToasts_.erase( + std::remove_if(playerLevelUpToasts_.begin(), playerLevelUpToasts_.end(), + [](const PlayerLevelUpToastEntry& t) { + return t.age >= PLAYER_LEVELUP_TOAST_DURATION; + }), + playerLevelUpToasts_.end()); + if (playerLevelUpToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Stack toasts at screen bottom-centre, above action bars + constexpr float TOAST_W = 230.0f; + constexpr float TOAST_H = 38.0f; + constexpr float TOAST_GAP = 4.0f; + float baseY = screenH * 0.72f; + float toastX = (screenW - TOAST_W) * 0.5f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + const int count = static_cast(playerLevelUpToasts_.size()); + + for (int i = 0; i < count; ++i) { + const auto& toast = playerLevelUpToasts_[i]; + + float remaining = PLAYER_LEVELUP_TOAST_DURATION - toast.age; + float alpha; + if (toast.age < 0.2f) + alpha = toast.age / 0.2f; + else if (remaining < 1.0f) + alpha = remaining; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Subtle pop-up from below during first 0.2s + float slideY = (toast.age < 0.2f) ? (TOAST_H * (1.0f - toast.age / 0.2f)) : 0.0f; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP) + slideY; + + uint8_t bgA = static_cast(200 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background: dark gold tint + bgDL->AddRectFilled(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(30, 22, 5, bgA), 5.0f); + // Gold border with glow at peak + float glowStr = (toast.age < 0.5f) ? (1.0f - toast.age / 0.5f) : 0.0f; + uint8_t borderA = static_cast((160 + 80 * glowStr) * alpha); + bgDL->AddRect(ImVec2(toastX, ty), ImVec2(toastX + TOAST_W, ty + TOAST_H), + IM_COL32(255, 210, 50, borderA), 5.0f, 0, 1.5f + glowStr * 1.5f); + + // Star ★ icon on left + bgDL->AddText(ImVec2(toastX + 8.0f, ty + 10.0f), + IM_COL32(255, 220, 60, fgA), "\xe2\x98\x85"); // UTF-8 ★ + + // " is now level X!" text + const char* displayName = toast.playerName.empty() ? "A player" : toast.playerName.c_str(); + char buf[64]; + snprintf(buf, sizeof(buf), "%.18s is now level %u!", displayName, toast.newLevel); + bgDL->AddText(ImVec2(toastX + 26.0f, ty + 11.0f), + IM_COL32(255, 230, 100, fgA), buf); + } +} + +// --------------------------------------------------------------------------- +// Resurrection flash — brief screen brightening + "You have been resurrected!" +// banner when the player transitions from ghost back to alive. +// --------------------------------------------------------------------------- + +void GameScreen::renderResurrectFlash() { + if (resurrectFlashTimer_ <= 0.0f) return; + + float dt = ImGui::GetIO().DeltaTime; + resurrectFlashTimer_ -= dt; + if (resurrectFlashTimer_ <= 0.0f) { + resurrectFlashTimer_ = 0.0f; + return; + } + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Normalised age in [0, 1] (0 = just fired, 1 = fully elapsed) + float t = 1.0f - resurrectFlashTimer_ / kResurrectFlashDuration; + + // Alpha envelope: fast fade-in (first 0.15s), hold, then fade-out (last 0.8s) + float alpha; + const float fadeIn = 0.15f / kResurrectFlashDuration; // ~5% of lifetime + const float fadeOut = 0.8f / kResurrectFlashDuration; // ~27% of lifetime + if (t < fadeIn) + alpha = t / fadeIn; + else if (t < 1.0f - fadeOut) + alpha = 1.0f; + else + alpha = (1.0f - t) / fadeOut; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + ImDrawList* bg = ImGui::GetBackgroundDrawList(); + + // Soft golden/white vignette — brightening instead of darkening + uint8_t vigA = static_cast(50 * alpha); + bg->AddRectFilled(ImVec2(0, 0), ImVec2(screenW, screenH), + IM_COL32(200, 230, 255, vigA)); + + // Centered banner panel + constexpr float PANEL_W = 360.0f; + constexpr float PANEL_H = 52.0f; + float px = (screenW - PANEL_W) * 0.5f; + float py = screenH * 0.34f; + + uint8_t bgA = static_cast(210 * alpha); + uint8_t borderA = static_cast(255 * alpha); + uint8_t textA = static_cast(255 * alpha); + + // Background: deep blue-black + bg->AddRectFilled(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H), + IM_COL32(10, 18, 40, bgA), 8.0f); + + // Border glow: bright holy gold + bg->AddRect(ImVec2(px, py), ImVec2(px + PANEL_W, py + PANEL_H), + IM_COL32(200, 230, 100, borderA), 8.0f, 0, 2.0f); + // Inner halo line + bg->AddRect(ImVec2(px + 3.0f, py + 3.0f), ImVec2(px + PANEL_W - 3.0f, py + PANEL_H - 3.0f), + IM_COL32(255, 255, 180, static_cast(80 * alpha)), 6.0f, 0, 1.0f); + + // "✦ You have been resurrected! ✦" centered + // UTF-8 heavy four-pointed star U+2726: \xe2\x9c\xa6 + const char* banner = "\xe2\x9c\xa6 You have been resurrected! \xe2\x9c\xa6"; + ImFont* font = ImGui::GetFont(); + float fontSize = ImGui::GetFontSize(); + ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, banner); + float tx = px + (PANEL_W - textSz.x) * 0.5f; + float ty = py + (PANEL_H - textSz.y) * 0.5f; + + // Drop shadow + bg->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f), + IM_COL32(0, 0, 0, static_cast(180 * alpha)), banner); + // Main text in warm gold + bg->AddText(font, fontSize, ImVec2(tx, ty), + IM_COL32(255, 240, 120, textA), banner); +} + +// --------------------------------------------------------------------------- +// Whisper toast notifications — brief overlay when a player whispers you +// --------------------------------------------------------------------------- + +void GameScreen::renderWhisperToasts() { + if (whisperToasts_.empty()) return; + + float dt = ImGui::GetIO().DeltaTime; + + // Age and prune expired toasts + for (auto& t : whisperToasts_) t.age += dt; + whisperToasts_.erase( + std::remove_if(whisperToasts_.begin(), whisperToasts_.end(), + [](const WhisperToastEntry& t) { return t.age >= WHISPER_TOAST_DURATION; }), + whisperToasts_.end()); + if (whisperToasts_.empty()) return; + + ImVec2 displaySize = ImGui::GetIO().DisplaySize; + float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f; + + // Stack toasts at bottom-left, above the action bars (y ≈ screenH * 0.72) + // Each toast is ~56px tall with a 4px gap between them. + constexpr float TOAST_W = 280.0f; + constexpr float TOAST_H = 56.0f; + constexpr float TOAST_GAP = 4.0f; + constexpr float TOAST_X = 14.0f; // left edge (won't cover action bars) + float baseY = screenH * 0.72f; + + ImDrawList* bgDL = ImGui::GetBackgroundDrawList(); + + const int count = static_cast(whisperToasts_.size()); + for (int i = 0; i < count; ++i) { + auto& toast = whisperToasts_[i]; + + // Fade in over 0.25s; fade out in last 1.0s + float alpha; + float remaining = WHISPER_TOAST_DURATION - toast.age; + if (toast.age < 0.25f) + alpha = toast.age / 0.25f; + else if (remaining < 1.0f) + alpha = remaining; + else + alpha = 1.0f; + alpha = std::clamp(alpha, 0.0f, 1.0f); + + // Slide-in from left: offset 0→0 after 0.25s + float slideX = (toast.age < 0.25f) ? (TOAST_W * (1.0f - toast.age / 0.25f)) : 0.0f; + float tx = TOAST_X - slideX; + float ty = baseY - (count - i) * (TOAST_H + TOAST_GAP); + + uint8_t bgA = static_cast(210 * alpha); + uint8_t fgA = static_cast(255 * alpha); + + // Background panel — dark purple tint (whisper color convention) + bgDL->AddRectFilled(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(25, 10, 40, bgA), 6.0f); + // Purple border + bgDL->AddRect(ImVec2(tx, ty), ImVec2(tx + TOAST_W, ty + TOAST_H), + IM_COL32(160, 80, 220, static_cast(180 * alpha)), 6.0f, 0, 1.5f); + + // "Whisper" label (small, purple-ish) + bgDL->AddText(ImVec2(tx + 10.0f, ty + 6.0f), + IM_COL32(190, 110, 255, fgA), "Whisper from:"); + + // Sender name (gold) + bgDL->AddText(ImVec2(tx + 10.0f, ty + 20.0f), + IM_COL32(255, 210, 50, fgA), toast.sender.c_str()); + + // Message preview (white, dimmer) + bgDL->AddText(ImVec2(tx + 10.0f, ty + 36.0f), + IM_COL32(220, 220, 220, static_cast(200 * alpha)), + toast.preview.c_str()); + } +} + // Zone discovery text — "Entering: " fades in/out at screen centre // --------------------------------------------------------------------------- -void GameScreen::renderZoneText() { - // Poll the renderer for zone name changes +void GameScreen::renderZoneText(game::GameHandler& gameHandler) { + // Poll worldStateZoneId for server-driven zone changes (fires on every zone crossing, + // including sub-zones like Ironforge within Dun Morogh). + uint32_t wsZoneId = gameHandler.getWorldStateZoneId(); + if (wsZoneId != 0 && wsZoneId != lastKnownWorldStateZoneId_) { + lastKnownWorldStateZoneId_ = wsZoneId; + std::string wsName = gameHandler.getWhoAreaName(wsZoneId); + if (!wsName.empty()) { + zoneTextName_ = wsName; + zoneTextTimer_ = ZONE_TEXT_DURATION; + } + } + + // Also poll the renderer for zone name changes (covers map-level transitions + // where worldStateZoneId may not change immediately). auto* appRenderer = core::Application::getInstance().getRenderer(); if (appRenderer) { const std::string& zoneName = appRenderer->getCurrentZoneName(); if (!zoneName.empty() && zoneName != lastKnownZoneName_) { lastKnownZoneName_ = zoneName; - zoneTextName_ = zoneName; - zoneTextTimer_ = ZONE_TEXT_DURATION; + // Only override if the worldState hasn't already queued this zone + if (zoneTextName_ != zoneName) { + zoneTextName_ = zoneName; + zoneTextTimer_ = ZONE_TEXT_DURATION; + } } } @@ -13963,15 +23527,138 @@ void GameScreen::renderZoneText() { // "Entering:" in gold draw->AddText(font, headerSize, ImVec2(headerX + 1, headerY + 1), - IM_COL32(0, 0, 0, (int)(alpha * 160)), header); + IM_COL32(0, 0, 0, static_cast(alpha * 160)), header); draw->AddText(font, headerSize, ImVec2(headerX, headerY), - IM_COL32(255, 215, 0, (int)(alpha * 255)), header); + IM_COL32(255, 215, 0, static_cast(alpha * 255)), header); // Zone name in white draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), - IM_COL32(0, 0, 0, (int)(alpha * 160)), zoneTextName_.c_str()); + IM_COL32(0, 0, 0, static_cast(alpha * 160)), zoneTextName_.c_str()); draw->AddText(font, nameSize, ImVec2(nameX, nameY), - IM_COL32(255, 255, 255, (int)(alpha * 255)), zoneTextName_.c_str()); + IM_COL32(255, 255, 255, static_cast(alpha * 255)), zoneTextName_.c_str()); +} + +// --------------------------------------------------------------------------- +// Screen-space weather overlay (rain / snow / storm) +// --------------------------------------------------------------------------- +void GameScreen::renderWeatherOverlay(game::GameHandler& gameHandler) { + uint32_t wType = gameHandler.getWeatherType(); + float intensity = gameHandler.getWeatherIntensity(); + if (wType == 0 || intensity < 0.05f) return; + + const ImGuiIO& io = ImGui::GetIO(); + float sw = io.DisplaySize.x; + float sh = io.DisplaySize.y; + if (sw <= 0.0f || sh <= 0.0f) return; + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + const float dt = std::min(io.DeltaTime, 0.05f); // cap delta at 50ms to avoid teleporting particles + + if (wType == 1 || wType == 3) { + // ── Rain / Storm ───────────────────────────────────────────────────── + constexpr int MAX_DROPS = 300; + struct RainState { + float x[MAX_DROPS], y[MAX_DROPS]; + bool initialized = false; + uint32_t lastType = 0; + float lastW = 0.0f, lastH = 0.0f; + }; + static RainState rs; + + // Re-seed if weather type or screen size changed + if (!rs.initialized || rs.lastType != wType || + rs.lastW != sw || rs.lastH != sh) { + for (int i = 0; i < MAX_DROPS; ++i) { + rs.x[i] = static_cast(std::rand() % (static_cast(sw) + 200)) - 100.0f; + rs.y[i] = static_cast(std::rand() % static_cast(sh)); + } + rs.initialized = true; + rs.lastType = wType; + rs.lastW = sw; + rs.lastH = sh; + } + + const float fallSpeed = (wType == 3) ? 680.0f : 440.0f; + const float windSpeed = (wType == 3) ? 110.0f : 65.0f; + const int numDrops = static_cast(MAX_DROPS * std::min(1.0f, intensity)); + const float alpha = std::min(1.0f, 0.28f + intensity * 0.38f); + const uint8_t alphaU8 = static_cast(alpha * 255.0f); + const ImU32 dropCol = IM_COL32(175, 195, 225, alphaU8); + const float dropLen = 7.0f + intensity * 7.0f; + // Normalised wind direction for the trail endpoint + const float invSpeed = 1.0f / std::sqrt(fallSpeed * fallSpeed + windSpeed * windSpeed); + const float trailDx = -windSpeed * invSpeed * dropLen; + const float trailDy = -fallSpeed * invSpeed * dropLen; + + for (int i = 0; i < numDrops; ++i) { + rs.x[i] += windSpeed * dt; + rs.y[i] += fallSpeed * dt; + if (rs.y[i] > sh + 10.0f) { + rs.y[i] = -10.0f; + rs.x[i] = static_cast(std::rand() % (static_cast(sw) + 200)) - 100.0f; + } + if (rs.x[i] > sw + 100.0f) rs.x[i] -= sw + 200.0f; + dl->AddLine(ImVec2(rs.x[i], rs.y[i]), + ImVec2(rs.x[i] + trailDx, rs.y[i] + trailDy), + dropCol, 1.0f); + } + + // Storm: dark fog-vignette at screen edges + if (wType == 3) { + const float vigAlpha = std::min(1.0f, 0.12f + intensity * 0.18f); + const ImU32 vigCol = IM_COL32(60, 65, 80, static_cast(vigAlpha * 255.0f)); + const float vigW = sw * 0.22f; + const float vigH = sh * 0.22f; + dl->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(vigW, sh), vigCol, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, vigCol); + dl->AddRectFilledMultiColor(ImVec2(sw-vigW, 0), ImVec2(sw, sh), IM_COL32_BLACK_TRANS, vigCol, vigCol, IM_COL32_BLACK_TRANS); + dl->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(sw, vigH), vigCol, vigCol, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS); + dl->AddRectFilledMultiColor(ImVec2(0, sh-vigH),ImVec2(sw, sh), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, vigCol, vigCol); + } + + } else if (wType == 2) { + // ── Snow ───────────────────────────────────────────────────────────── + constexpr int MAX_FLAKES = 120; + struct SnowState { + float x[MAX_FLAKES], y[MAX_FLAKES], phase[MAX_FLAKES]; + bool initialized = false; + float lastW = 0.0f, lastH = 0.0f; + }; + static SnowState ss; + + if (!ss.initialized || ss.lastW != sw || ss.lastH != sh) { + for (int i = 0; i < MAX_FLAKES; ++i) { + ss.x[i] = static_cast(std::rand() % static_cast(sw)); + ss.y[i] = static_cast(std::rand() % static_cast(sh)); + ss.phase[i] = static_cast(std::rand() % 628) * 0.01f; + } + ss.initialized = true; + ss.lastW = sw; + ss.lastH = sh; + } + + const float fallSpeed = 45.0f + intensity * 45.0f; + const int numFlakes = static_cast(MAX_FLAKES * std::min(1.0f, intensity)); + const float alpha = std::min(1.0f, 0.5f + intensity * 0.3f); + const uint8_t alphaU8 = static_cast(alpha * 255.0f); + const float radius = 1.5f + intensity * 1.5f; + const float time = static_cast(ImGui::GetTime()); + + for (int i = 0; i < numFlakes; ++i) { + float sway = std::sin(time * 0.7f + ss.phase[i]) * 18.0f; + ss.x[i] += sway * dt; + ss.y[i] += fallSpeed * dt; + ss.phase[i] += dt * 0.25f; + if (ss.y[i] > sh + 5.0f) { + ss.y[i] = -5.0f; + ss.x[i] = static_cast(std::rand() % static_cast(sw)); + } + if (ss.x[i] < -5.0f) ss.x[i] += sw + 10.0f; + if (ss.x[i] > sw + 5.0f) ss.x[i] -= sw + 10.0f; + // Two-tone: bright centre dot + transparent outer ring for depth + dl->AddCircleFilled(ImVec2(ss.x[i], ss.y[i]), radius, IM_COL32(220, 235, 255, alphaU8)); + dl->AddCircleFilled(ImVec2(ss.x[i], ss.y[i]), radius * 0.45f, IM_COL32(245, 250, 255, std::min(255, alphaU8 + 30))); + } + } } // --------------------------------------------------------------------------- @@ -13979,7 +23666,8 @@ void GameScreen::renderZoneText() { // --------------------------------------------------------------------------- void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { // Toggle Dungeon Finder (customizable keybind) - if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) { + if (!chatInputActive && !ImGui::GetIO().WantTextInput && + KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) { showDungeonFinder_ = !showDungeonFinder_; } @@ -14012,7 +23700,7 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { // ---- Status banner ---- switch (state) { case LfgState::None: - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Status: Not queued"); + ImGui::TextColored(kColorGray, "Status: Not queued"); break; case LfgState::RoleCheck: ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Status: Role check in progress..."); @@ -14022,7 +23710,12 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { uint32_t qMs = gameHandler.getLfgTimeInQueueMs(); int qMin = static_cast(qMs / 60000); int qSec = static_cast((qMs % 60000) / 1000); - ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Status: In queue (%d:%02d)", qMin, qSec); + std::string dName = gameHandler.getCurrentLfgDungeonName(); + if (!dName.empty()) + ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), + "Status: In queue for %s (%d:%02d)", dName.c_str(), qMin, qSec); + else + ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), "Status: In queue (%d:%02d)", qMin, qSec); if (avgSec >= 0) { int aMin = avgSec / 60; int aSec = avgSec % 60; @@ -14031,18 +23724,33 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { } break; } - case LfgState::Proposal: - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!"); + case LfgState::Proposal: { + std::string dName = gameHandler.getCurrentLfgDungeonName(); + if (!dName.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found for %s!", dName.c_str()); + else + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!"); break; + } case LfgState::Boot: - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Status: Vote kick in progress"); + ImGui::TextColored(kColorRed, "Status: Vote kick in progress"); break; - case LfgState::InDungeon: - ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon"); + case LfgState::InDungeon: { + std::string dName = gameHandler.getCurrentLfgDungeonName(); + if (!dName.empty()) + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon (%s)", dName.c_str()); + else + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon"); break; - case LfgState::FinishedDungeon: - ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: Dungeon complete"); + } + case LfgState::FinishedDungeon: { + std::string dName = gameHandler.getCurrentLfgDungeonName(); + if (!dName.empty()) + ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: %s complete", dName.c_str()); + else + ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), "Status: Dungeon complete"); break; + } case LfgState::RaidBrowser: ImGui::TextColored(ImVec4(0.8f, 0.6f, 1.0f, 1.0f), "Status: Raid browser"); break; @@ -14052,8 +23760,13 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { // ---- Proposal accept/decline ---- if (state == LfgState::Proposal) { - ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), - "A group has been found for your dungeon!"); + std::string dName = gameHandler.getCurrentLfgDungeonName(); + if (!dName.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), + "A group has been found for %s!", dName.c_str()); + else + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), + "A group has been found for your dungeon!"); ImGui::Spacing(); if (ImGui::Button("Accept", ImVec2(120, 0))) { gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true); @@ -14067,7 +23780,19 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { // ---- Vote-to-kick buttons ---- if (state == LfgState::Boot) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Vote to kick in progress:"); + ImGui::TextColored(kColorRed, "Vote to kick in progress:"); + const std::string& bootTarget = gameHandler.getLfgBootTargetName(); + const std::string& bootReason = gameHandler.getLfgBootReason(); + if (!bootTarget.empty()) { + ImGui::Text("Player: "); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", bootTarget.c_str()); + } + if (!bootReason.empty()) { + ImGui::Text("Reason: "); + ImGui::SameLine(); + ImGui::TextWrapped("%s", bootReason.c_str()); + } uint32_t bootVotes = gameHandler.getLfgBootVotes(); uint32_t bootTotal = gameHandler.getLfgBootTotal(); uint32_t bootNeeded = gameHandler.getLfgBootNeeded(); @@ -14116,46 +23841,54 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { ImGui::Text("Dungeon:"); struct DungeonEntry { uint32_t id; const char* name; }; - static const DungeonEntry kDungeons[] = { - { 861, "Random Dungeon" }, - { 862, "Random Heroic" }, - // Vanilla classics - { 36, "Deadmines" }, - { 43, "Ragefire Chasm" }, - { 47, "Razorfen Kraul" }, - { 48, "Blackfathom Deeps" }, - { 52, "Uldaman" }, - { 57, "Dire Maul: East" }, - { 70, "Onyxia's Lair" }, - // TBC heroics - { 264, "The Blood Furnace" }, - { 269, "The Shattered Halls" }, - // WotLK normals/heroics - { 576, "The Nexus" }, - { 578, "The Oculus" }, - { 595, "The Culling of Stratholme" }, - { 599, "Halls of Stone" }, - { 600, "Drak'Tharon Keep" }, - { 601, "Azjol-Nerub" }, - { 604, "Gundrak" }, - { 608, "Violet Hold" }, - { 619, "Ahn'kahet: Old Kingdom" }, - { 623, "Halls of Lightning" }, - { 632, "The Forge of Souls" }, - { 650, "Trial of the Champion" }, - { 658, "Pit of Saron" }, - { 668, "Halls of Reflection" }, + // Category 0=Random, 1=Classic, 2=TBC, 3=WotLK + struct DungeonEntryEx { uint32_t id; const char* name; uint8_t cat; }; + static const DungeonEntryEx kDungeons[] = { + { 861, "Random Dungeon", 0 }, + { 862, "Random Heroic", 0 }, + { 36, "Deadmines", 1 }, + { 43, "Ragefire Chasm", 1 }, + { 47, "Razorfen Kraul", 1 }, + { 48, "Blackfathom Deeps", 1 }, + { 52, "Uldaman", 1 }, + { 57, "Dire Maul: East", 1 }, + { 70, "Onyxia's Lair", 1 }, + { 264, "The Blood Furnace", 2 }, + { 269, "The Shattered Halls", 2 }, + { 576, "The Nexus", 3 }, + { 578, "The Oculus", 3 }, + { 595, "The Culling of Stratholme", 3 }, + { 599, "Halls of Stone", 3 }, + { 600, "Drak'Tharon Keep", 3 }, + { 601, "Azjol-Nerub", 3 }, + { 604, "Gundrak", 3 }, + { 608, "Violet Hold", 3 }, + { 619, "Ahn'kahet: Old Kingdom", 3 }, + { 623, "Halls of Lightning", 3 }, + { 632, "The Forge of Souls", 3 }, + { 650, "Trial of the Champion", 3 }, + { 658, "Pit of Saron", 3 }, + { 668, "Halls of Reflection", 3 }, }; + static const char* kCatHeaders[] = { nullptr, "-- Classic --", "-- TBC --", "-- WotLK --" }; // Find current index int curIdx = 0; - for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { + for (int i = 0; i < static_cast(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { if (kDungeons[i].id == lfgSelectedDungeon_) { curIdx = i; break; } } ImGui::SetNextItemWidth(-1); if (ImGui::BeginCombo("##dungeon", kDungeons[curIdx].name)) { - for (int i = 0; i < (int)(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { + uint8_t lastCat = 255; + for (int i = 0; i < static_cast(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) { + if (kDungeons[i].cat != lastCat && kCatHeaders[kDungeons[i].cat]) { + if (lastCat != 255) ImGui::Separator(); + ImGui::TextDisabled("%s", kCatHeaders[kDungeons[i].cat]); + lastCat = kDungeons[i].cat; + } else if (kDungeons[i].cat != lastCat) { + lastCat = kDungeons[i].cat; + } bool selected = (kDungeons[i].id == lfgSelectedDungeon_); if (ImGui::Selectable(kDungeons[i].name, selected)) lfgSelectedDungeon_ = kDungeons[i].id; @@ -14210,26 +23943,8 @@ void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) { const auto& lockouts = gameHandler.getInstanceLockouts(); if (lockouts.empty()) { - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No active instance lockouts."); + ImGui::TextColored(kColorGray, "No active instance lockouts."); } else { - // Build map name lookup from Map.dbc (cached after first call) - static std::unordered_map sMapNames; - static bool sMapNamesLoaded = false; - if (!sMapNamesLoaded) { - sMapNamesLoaded = true; - if (auto* am = core::Application::getInstance().getAssetManager()) { - if (auto dbc = am->loadDBC("Map.dbc"); dbc && dbc->isLoaded()) { - for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { - uint32_t id = dbc->getUInt32(i, 0); - // Field 2 = MapName_enUS (first localized), field 1 = InternalName - std::string name = dbc->getString(i, 2); - if (name.empty()) name = dbc->getString(i, 1); - if (!name.empty()) sMapNames[id] = std::move(name); - } - } - } - } - auto difficultyLabel = [](uint32_t diff) -> const char* { switch (diff) { case 0: return "Normal"; @@ -14255,11 +23970,11 @@ void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) { for (const auto& lo : lockouts) { ImGui::TableNextRow(); - // Instance name + // Instance name — use GameHandler's Map.dbc cache (avoids duplicate DBC load) ImGui::TableSetColumnIndex(0); - auto it = sMapNames.find(lo.mapId); - if (it != sMapNames.end()) { - ImGui::TextUnformatted(it->second.c_str()); + std::string mapName = gameHandler.getMapName(lo.mapId); + if (!mapName.empty()) { + ImGui::TextUnformatted(mapName.c_str()); } else { ImGui::Text("Map %u", lo.mapId); } @@ -14285,7 +24000,7 @@ void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) { static_cast(mins)); } } else { - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Expired"); + ImGui::TextColored(kColorDarkGray, "Expired"); } // Locked / Extended status @@ -14345,6 +24060,8 @@ void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) { { 566, "Eye of the Storm", 2757, 2758, 0, 1600, "resources" }, // Strand of the Ancients (WotLK) { 607, "Strand of the Ancients", 3476, 3477, 0, 4, "" }, + // Isle of Conquest (WotLK): reinforcements (300 default) + { 628, "Isle of Conquest", 4221, 4222, 0, 300, "reinforcements" }, }; const BgScoreDef* def = nullptr; @@ -14424,6 +24141,476 @@ void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) { ImGui::PopStyleVar(2); } +// ─── Who Results Window ─────────────────────────────────────────────────────── +void GameScreen::renderWhoWindow(game::GameHandler& gameHandler) { + if (!showWhoWindow_) return; + + const auto& results = gameHandler.getWhoResults(); + + ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(200, 180), ImGuiCond_FirstUseEver); + + char title[64]; + uint32_t onlineCount = gameHandler.getWhoOnlineCount(); + if (onlineCount > 0) + snprintf(title, sizeof(title), "Players Online: %u###WhoWindow", onlineCount); + else + snprintf(title, sizeof(title), "Who###WhoWindow"); + + if (!ImGui::Begin(title, &showWhoWindow_)) { + ImGui::End(); + return; + } + + // Search bar with Send button + static char whoSearchBuf[64] = {}; + bool doSearch = false; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f); + if (ImGui::InputTextWithHint("##whosearch", "Search players...", whoSearchBuf, sizeof(whoSearchBuf), + ImGuiInputTextFlags_EnterReturnsTrue)) + doSearch = true; + ImGui::SameLine(); + if (ImGui::Button("Search", ImVec2(-1, 0))) + doSearch = true; + if (doSearch) { + gameHandler.queryWho(std::string(whoSearchBuf)); + } + ImGui::Separator(); + + if (results.empty()) { + ImGui::TextDisabled("No results. Type a filter above or use /who [filter]."); + ImGui::End(); + return; + } + + // Table: Name | Guild | Level | Class | Zone + if (ImGui::BeginTable("##WhoTable", 5, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, + ImVec2(0, 0))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.22f); + ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthStretch, 0.20f); + ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f); + ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 0.20f); + ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < results.size(); ++i) { + const auto& e = results[i]; + ImGui::TableNextRow(); + ImGui::PushID(static_cast(i)); + + // Name (class-colored if class is known) + ImGui::TableSetColumnIndex(0); + uint8_t cid = static_cast(e.classId); + ImVec4 nameCol = classColorVec4(cid); + ImGui::TextColored(nameCol, "%s", e.name.c_str()); + + // Right-click context menu on the name + if (ImGui::BeginPopupContextItem("##WhoCtx")) { + ImGui::TextDisabled("%s", e.name.c_str()); + ImGui::Separator(); + if (ImGui::MenuItem("Whisper")) { + selectedChatType = 4; + strncpy(whisperTargetBuffer, e.name.c_str(), sizeof(whisperTargetBuffer) - 1); + whisperTargetBuffer[sizeof(whisperTargetBuffer) - 1] = '\0'; + refocusChatInput = true; + } + if (ImGui::MenuItem("Invite to Group")) + gameHandler.inviteToGroup(e.name); + if (ImGui::MenuItem("Add Friend")) + gameHandler.addFriend(e.name); + if (ImGui::MenuItem("Ignore")) + gameHandler.addIgnore(e.name); + ImGui::EndPopup(); + } + + // Guild + ImGui::TableSetColumnIndex(1); + if (!e.guildName.empty()) + ImGui::TextDisabled("<%s>", e.guildName.c_str()); + + // Level + ImGui::TableSetColumnIndex(2); + ImGui::Text("%u", e.level); + + // Class + ImGui::TableSetColumnIndex(3); + const char* className = game::getClassName(static_cast(e.classId)); + ImGui::TextColored(nameCol, "%s", className); + + // Zone + ImGui::TableSetColumnIndex(4); + if (e.zoneId != 0) { + std::string zoneName = gameHandler.getWhoAreaName(e.zoneId); + ImGui::TextUnformatted(zoneName.empty() ? "Unknown" : zoneName.c_str()); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::End(); +} + +// ─── Combat Log Window ──────────────────────────────────────────────────────── +void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { + if (!showCombatLog_) return; + + const auto& log = gameHandler.getCombatLog(); + + ImGui::SetNextWindowSize(ImVec2(520, 320), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(160, 200), ImGuiCond_FirstUseEver); + + char title[64]; + snprintf(title, sizeof(title), "Combat Log (%zu)###CombatLog", log.size()); + if (!ImGui::Begin(title, &showCombatLog_)) { + ImGui::End(); + return; + } + + // Filter toggles + static bool filterDamage = true; + static bool filterHeal = true; + static bool filterMisc = true; + static bool autoScroll = true; + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2)); + ImGui::Checkbox("Damage", &filterDamage); ImGui::SameLine(); + ImGui::Checkbox("Healing", &filterHeal); ImGui::SameLine(); + ImGui::Checkbox("Misc", &filterMisc); ImGui::SameLine(); + ImGui::Checkbox("Auto-scroll", &autoScroll); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 40.0f); + if (ImGui::SmallButton("Clear")) + gameHandler.clearCombatLog(); + ImGui::PopStyleVar(); + ImGui::Separator(); + + // Helper: categorize entry + auto isDamageType = [](game::CombatTextEntry::Type t) { + using T = game::CombatTextEntry; + return t == T::MELEE_DAMAGE || t == T::SPELL_DAMAGE || + t == T::CRIT_DAMAGE || t == T::PERIODIC_DAMAGE || + t == T::ENVIRONMENTAL || t == T::GLANCING || t == T::CRUSHING; + }; + auto isHealType = [](game::CombatTextEntry::Type t) { + using T = game::CombatTextEntry; + return t == T::HEAL || t == T::CRIT_HEAL || t == T::PERIODIC_HEAL; + }; + + // Two-column table: Time | Event description + ImGuiTableFlags tableFlags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingFixedFit; + float availH = ImGui::GetContentRegionAvail().y; + if (ImGui::BeginTable("##CombatLogTable", 2, tableFlags, ImVec2(0.0f, availH))) { + ImGui::TableSetupScrollFreeze(0, 0); + ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 62.0f); + ImGui::TableSetupColumn("Event", ImGuiTableColumnFlags_WidthStretch); + + for (const auto& e : log) { + // Apply filters + bool isDmg = isDamageType(e.type); + bool isHeal = isHealType(e.type); + bool isMisc = !isDmg && !isHeal; + if (isDmg && !filterDamage) continue; + if (isHeal && !filterHeal) continue; + if (isMisc && !filterMisc) continue; + + // Format timestamp as HH:MM:SS + char timeBuf[10]; + { + struct tm* tm_info = std::localtime(&e.timestamp); + if (tm_info) + snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d:%02d", + tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec); + else + snprintf(timeBuf, sizeof(timeBuf), "--:--:--"); + } + + // Build event description and choose color + char desc[256]; + ImVec4 color; + using T = game::CombatTextEntry; + const char* src = e.sourceName.empty() ? (e.isPlayerSource ? "You" : "?") : e.sourceName.c_str(); + const char* tgt = e.targetName.empty() ? "?" : e.targetName.c_str(); + const std::string& spellName = (e.spellId != 0) ? gameHandler.getSpellName(e.spellId) : std::string(); + const char* spell = spellName.empty() ? nullptr : spellName.c_str(); + + switch (e.type) { + case T::MELEE_DAMAGE: + snprintf(desc, sizeof(desc), "%s hits %s for %d", src, tgt, e.amount); + color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.4f, 0.4f, 1.0f); + break; + case T::CRIT_DAMAGE: + snprintf(desc, sizeof(desc), "%s crits %s for %d!", src, tgt, e.amount); + color = e.isPlayerSource ? ImVec4(1.0f, 1.0f, 0.0f, 1.0f) : ImVec4(1.0f, 0.2f, 0.2f, 1.0f); + break; + case T::SPELL_DAMAGE: + if (spell) + snprintf(desc, sizeof(desc), "%s's %s hits %s for %d", src, spell, tgt, e.amount); + else + snprintf(desc, sizeof(desc), "%s's spell hits %s for %d", src, tgt, e.amount); + color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : ImVec4(1.0f, 0.4f, 0.4f, 1.0f); + break; + case T::PERIODIC_DAMAGE: + if (spell) + snprintf(desc, sizeof(desc), "%s's %s ticks %s for %d", src, spell, tgt, e.amount); + else + snprintf(desc, sizeof(desc), "%s's DoT ticks %s for %d", src, tgt, e.amount); + color = e.isPlayerSource ? ImVec4(0.9f, 0.7f, 0.3f, 1.0f) : ImVec4(0.9f, 0.3f, 0.3f, 1.0f); + break; + case T::HEAL: + if (spell) + snprintf(desc, sizeof(desc), "%s heals %s for %d (%s)", src, tgt, e.amount, spell); + else + snprintf(desc, sizeof(desc), "%s heals %s for %d", src, tgt, e.amount); + color = kColorGreen; + break; + case T::CRIT_HEAL: + if (spell) + snprintf(desc, sizeof(desc), "%s critically heals %s for %d! (%s)", src, tgt, e.amount, spell); + else + snprintf(desc, sizeof(desc), "%s critically heals %s for %d!", src, tgt, e.amount); + color = kColorBrightGreen; + break; + case T::PERIODIC_HEAL: + if (spell) + snprintf(desc, sizeof(desc), "%s's %s heals %s for %d", src, spell, tgt, e.amount); + else + snprintf(desc, sizeof(desc), "%s's HoT heals %s for %d", src, tgt, e.amount); + color = ImVec4(0.4f, 0.9f, 0.4f, 1.0f); + break; + case T::MISS: + if (spell) + snprintf(desc, sizeof(desc), "%s's %s misses %s", src, spell, tgt); + else + snprintf(desc, sizeof(desc), "%s misses %s", src, tgt); + color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); + break; + case T::DODGE: + if (spell) + snprintf(desc, sizeof(desc), "%s dodges %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s dodges %s's attack", tgt, src); + color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); + break; + case T::PARRY: + if (spell) + snprintf(desc, sizeof(desc), "%s parries %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s parries %s's attack", tgt, src); + color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); + break; + case T::BLOCK: + if (spell) + snprintf(desc, sizeof(desc), "%s blocks %s's %s (%d blocked)", tgt, src, spell, e.amount); + else + snprintf(desc, sizeof(desc), "%s blocks %s's attack (%d blocked)", tgt, src, e.amount); + color = ImVec4(0.65f, 0.75f, 0.65f, 1.0f); + break; + case T::EVADE: + if (spell) + snprintf(desc, sizeof(desc), "%s evades %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s evades %s's attack", tgt, src); + color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); + break; + case T::IMMUNE: + if (spell) + snprintf(desc, sizeof(desc), "%s is immune to %s", tgt, spell); + else + snprintf(desc, sizeof(desc), "%s is immune", tgt); + color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + break; + case T::ABSORB: + if (spell && e.amount > 0) + snprintf(desc, sizeof(desc), "%s's %s absorbs %d", src, spell, e.amount); + else if (spell) + snprintf(desc, sizeof(desc), "%s absorbs %s", tgt, spell); + else if (e.amount > 0) + snprintf(desc, sizeof(desc), "%d absorbed", e.amount); + else + snprintf(desc, sizeof(desc), "Absorbed"); + color = ImVec4(0.5f, 0.8f, 1.0f, 1.0f); + break; + case T::RESIST: + if (spell && e.amount > 0) + snprintf(desc, sizeof(desc), "%s resists %s's %s (%d resisted)", tgt, src, spell, e.amount); + else if (spell) + snprintf(desc, sizeof(desc), "%s resists %s's %s", tgt, src, spell); + else if (e.amount > 0) + snprintf(desc, sizeof(desc), "%d resisted", e.amount); + else + snprintf(desc, sizeof(desc), "Resisted"); + color = ImVec4(0.6f, 0.6f, 0.9f, 1.0f); + break; + case T::DEFLECT: + if (spell) + snprintf(desc, sizeof(desc), "%s deflects %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s deflects %s's attack", tgt, src); + color = ImVec4(0.65f, 0.8f, 0.95f, 1.0f); + break; + case T::REFLECT: + if (spell) + snprintf(desc, sizeof(desc), "%s reflects %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src); + color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f); + break; + case T::ENVIRONMENTAL: { + const char* envName = "Environmental"; + switch (e.powerType) { + case 0: envName = "Fatigue"; break; + case 1: envName = "Drowning"; break; + case 2: envName = "Falling"; break; + case 3: envName = "Lava"; break; + case 4: envName = "Slime"; break; + case 5: envName = "Fire"; break; + } + snprintf(desc, sizeof(desc), "%s damage: %d", envName, e.amount); + color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f); + break; + } + case T::ENERGIZE: { + const char* pwrName = "power"; + switch (e.powerType) { + case 0: pwrName = "Mana"; break; + case 1: pwrName = "Rage"; break; + case 2: pwrName = "Focus"; break; + case 3: pwrName = "Energy"; break; + case 4: pwrName = "Happiness"; break; + case 6: pwrName = "Runic Power"; break; + } + if (spell) + snprintf(desc, sizeof(desc), "%s gains %d %s (%s)", tgt, e.amount, pwrName, spell); + else + snprintf(desc, sizeof(desc), "%s gains %d %s", tgt, e.amount, pwrName); + color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); + break; + } + case T::POWER_DRAIN: { + const char* drainName = "power"; + switch (e.powerType) { + case 0: drainName = "Mana"; break; + case 1: drainName = "Rage"; break; + case 2: drainName = "Focus"; break; + case 3: drainName = "Energy"; break; + case 4: drainName = "Happiness"; break; + case 6: drainName = "Runic Power"; break; + } + if (spell) + snprintf(desc, sizeof(desc), "%s loses %d %s to %s's %s", tgt, e.amount, drainName, src, spell); + else + snprintf(desc, sizeof(desc), "%s loses %d %s", tgt, e.amount, drainName); + color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f); + break; + } + case T::XP_GAIN: + snprintf(desc, sizeof(desc), "You gain %d experience", e.amount); + color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f); + break; + case T::PROC_TRIGGER: + if (spell) + snprintf(desc, sizeof(desc), "%s procs!", spell); + else + snprintf(desc, sizeof(desc), "Proc triggered"); + color = ImVec4(1.0f, 0.85f, 0.3f, 1.0f); + break; + case T::DISPEL: + if (spell && e.isPlayerSource) + snprintf(desc, sizeof(desc), "You dispel %s from %s", spell, tgt); + else if (spell) + snprintf(desc, sizeof(desc), "%s dispels %s from %s", src, spell, tgt); + else if (e.isPlayerSource) + snprintf(desc, sizeof(desc), "You dispel from %s", tgt); + else + snprintf(desc, sizeof(desc), "%s dispels from %s", src, tgt); + color = ImVec4(0.6f, 0.9f, 1.0f, 1.0f); + break; + case T::STEAL: + if (spell && e.isPlayerSource) + snprintf(desc, sizeof(desc), "You steal %s from %s", spell, tgt); + else if (spell) + snprintf(desc, sizeof(desc), "%s steals %s from %s", src, spell, tgt); + else if (e.isPlayerSource) + snprintf(desc, sizeof(desc), "You steal from %s", tgt); + else + snprintf(desc, sizeof(desc), "%s steals from %s", src, tgt); + color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f); + break; + case T::INTERRUPT: + if (spell && e.isPlayerSource) + snprintf(desc, sizeof(desc), "You interrupt %s's %s", tgt, spell); + else if (spell) + snprintf(desc, sizeof(desc), "%s interrupts %s's %s", src, tgt, spell); + else if (e.isPlayerSource) + snprintf(desc, sizeof(desc), "You interrupt %s", tgt); + else + snprintf(desc, sizeof(desc), "%s interrupted", tgt); + color = ImVec4(1.0f, 0.6f, 0.9f, 1.0f); + break; + case T::INSTAKILL: + if (spell && e.isPlayerSource) + snprintf(desc, sizeof(desc), "You instantly kill %s with %s", tgt, spell); + else if (spell) + snprintf(desc, sizeof(desc), "%s instantly kills %s with %s", src, tgt, spell); + else if (e.isPlayerSource) + snprintf(desc, sizeof(desc), "You instantly kill %s", tgt); + else + snprintf(desc, sizeof(desc), "%s instantly kills %s", src, tgt); + color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); + break; + case T::HONOR_GAIN: + snprintf(desc, sizeof(desc), "You gain %d honor", e.amount); + color = ImVec4(1.0f, 0.85f, 0.0f, 1.0f); + break; + case T::GLANCING: + snprintf(desc, sizeof(desc), "%s glances %s for %d", src, tgt, e.amount); + color = e.isPlayerSource ? ImVec4(0.75f, 0.75f, 0.5f, 1.0f) + : ImVec4(0.75f, 0.4f, 0.4f, 1.0f); + break; + case T::CRUSHING: + snprintf(desc, sizeof(desc), "%s crushes %s for %d!", src, tgt, e.amount); + color = e.isPlayerSource ? ImVec4(1.0f, 0.55f, 0.1f, 1.0f) + : ImVec4(1.0f, 0.15f, 0.15f, 1.0f); + break; + default: + snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", static_cast(e.type), e.amount); + color = ui::colors::kLightGray; + break; + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextDisabled("%s", timeBuf); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(color, "%s", desc); + // Hover tooltip: show rich spell info for entries with a known spell + if (e.spellId != 0 && ImGui::IsItemHovered()) { + auto* assetMgrLog = core::Application::getInstance().getAssetManager(); + ImGui::BeginTooltip(); + bool richOk = spellbookScreen.renderSpellInfoTooltip(e.spellId, gameHandler, assetMgrLog); + if (!richOk) { + ImGui::Text("%s", spellName.c_str()); + } + ImGui::EndTooltip(); + } + } + + // Auto-scroll to bottom + if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + + ImGui::EndTable(); + } + + ImGui::End(); +} + // ─── Achievement Window ─────────────────────────────────────────────────────── void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { if (!showAchievementWindow_) return; @@ -14451,7 +24638,7 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { if (ImGui::BeginTabBar("##achtabs")) { // --- Earned tab --- char earnedLabel[32]; - snprintf(earnedLabel, sizeof(earnedLabel), "Earned (%u)###earned", (unsigned)earned.size()); + snprintf(earnedLabel, sizeof(earnedLabel), "Earned (%u)###earned", static_cast(earned.size())); if (ImGui::BeginTabItem(earnedLabel)) { if (earned.empty()) { ImGui::TextDisabled("No achievements earned yet."); @@ -14473,7 +24660,37 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { ImGui::TextUnformatted(display.c_str()); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::Text("ID: %u", id); + // Points badge + uint32_t pts = gameHandler.getAchievementPoints(id); + if (pts > 0) { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), + "%u Achievement Point%s", pts, pts == 1 ? "" : "s"); + ImGui::Separator(); + } + // Description + const std::string& desc = gameHandler.getAchievementDescription(id); + if (!desc.empty()) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f); + ImGui::TextUnformatted(desc.c_str()); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); + } + // Earn date + uint32_t packed = gameHandler.getAchievementDate(id); + if (packed != 0) { + // WoW PackedTime: year[31:25] month[24:21] day[20:17] weekday[16:14] hour[13:9] minute[8:3] + int minute = (packed >> 3) & 0x3F; + int hour = (packed >> 9) & 0x1F; + int day = (packed >> 17) & 0x1F; + int month = (packed >> 21) & 0x0F; + int year = ((packed >> 25) & 0x7F) + 2000; + static const char* kMonths[12] = { + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" + }; + const char* mname = (month >= 1 && month <= 12) ? kMonths[month - 1] : "?"; + ImGui::TextDisabled("Earned: %s %d, %d %02d:%02d", mname, day, year, hour, minute); + } ImGui::EndTooltip(); } ImGui::PopID(); @@ -14485,26 +24702,100 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { // --- Criteria progress tab --- char critLabel[32]; - snprintf(critLabel, sizeof(critLabel), "Criteria (%u)###crit", (unsigned)criteria.size()); + snprintf(critLabel, sizeof(critLabel), "Criteria (%u)###crit", static_cast(criteria.size())); if (ImGui::BeginTabItem(critLabel)) { + // Lazy-load AchievementCriteria.dbc for descriptions + struct CriteriaEntry { uint32_t achievementId; uint64_t quantity; std::string description; }; + static std::unordered_map s_criteriaData; + static bool s_criteriaDataLoaded = false; + if (!s_criteriaDataLoaded) { + s_criteriaDataLoaded = true; + auto* am = core::Application::getInstance().getAssetManager(); + if (am && am->isInitialized()) { + auto dbc = am->loadDBC("AchievementCriteria.dbc"); + if (dbc && dbc->isLoaded() && dbc->getFieldCount() >= 10) { + const auto* acL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("AchievementCriteria") : nullptr; + uint32_t achField = acL ? acL->field("AchievementID") : 1u; + uint32_t qtyField = acL ? acL->field("Quantity") : 4u; + uint32_t descField = acL ? acL->field("Description") : 9u; + if (achField == 0xFFFFFFFF) achField = 1; + if (qtyField == 0xFFFFFFFF) qtyField = 4; + if (descField == 0xFFFFFFFF) descField = 9; + uint32_t fc = dbc->getFieldCount(); + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t cid = dbc->getUInt32(r, 0); + if (cid == 0) continue; + CriteriaEntry ce; + ce.achievementId = (achField < fc) ? dbc->getUInt32(r, achField) : 0; + ce.quantity = (qtyField < fc) ? dbc->getUInt32(r, qtyField) : 0; + ce.description = (descField < fc) ? dbc->getString(r, descField) : std::string{}; + s_criteriaData[cid] = std::move(ce); + } + } + } + } + if (criteria.empty()) { ImGui::TextDisabled("No criteria progress received yet."); } else { ImGui::BeginChild("##critlist", ImVec2(0, 0), false); - // Sort criteria by id for stable display std::vector> clist(criteria.begin(), criteria.end()); std::sort(clist.begin(), clist.end()); for (const auto& [cid, cval] : clist) { - std::string label = std::to_string(cid); - if (!filter.empty()) { - std::string lower = label; - for (char& c : lower) c = static_cast(tolower(static_cast(c))); - if (lower.find(filter) == std::string::npos) continue; + auto ceIt = s_criteriaData.find(cid); + + // Build display text for filtering + std::string display; + if (ceIt != s_criteriaData.end() && !ceIt->second.description.empty()) { + display = ceIt->second.description; + } else { + display = std::to_string(cid); } + if (!filter.empty()) { + std::string lower = display; + for (char& c : lower) c = static_cast(tolower(static_cast(c))); + // Also allow filtering by achievement name + if (lower.find(filter) == std::string::npos && ceIt != s_criteriaData.end()) { + const std::string& achName = gameHandler.getAchievementName(ceIt->second.achievementId); + std::string achLower = achName; + for (char& c : achLower) c = static_cast(tolower(static_cast(c))); + if (achLower.find(filter) == std::string::npos) continue; + } else if (lower.find(filter) == std::string::npos) { + continue; + } + } + ImGui::PushID(static_cast(cid)); - ImGui::TextDisabled("Criteria %u:", cid); - ImGui::SameLine(); - ImGui::Text("%llu", static_cast(cval)); + if (ceIt != s_criteriaData.end()) { + // Show achievement name as header (dim) + const std::string& achName = gameHandler.getAchievementName(ceIt->second.achievementId); + if (!achName.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.8f), "%s", achName.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled(">"); + ImGui::SameLine(); + } + if (!ceIt->second.description.empty()) { + ImGui::TextUnformatted(ceIt->second.description.c_str()); + } else { + ImGui::TextDisabled("Criteria %u", cid); + } + ImGui::SameLine(); + if (ceIt->second.quantity > 0) { + ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), + "%llu/%llu", + static_cast(cval), + static_cast(ceIt->second.quantity)); + } else { + ImGui::TextColored(ImVec4(0.6f, 1.0f, 0.6f, 1.0f), + "%llu", static_cast(cval)); + } + } else { + ImGui::TextDisabled("Criteria %u:", cid); + ImGui::SameLine(); + ImGui::Text("%llu", static_cast(cval)); + } ImGui::PopID(); } ImGui::EndChild(); @@ -14519,9 +24810,15 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { // ─── GM Ticket Window ───────────────────────────────────────────────────────── void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { + // Fire a one-shot query when the window first becomes visible + if (showGmTicketWindow_ && !gmTicketWindowWasOpen_) { + gameHandler.requestGmTicket(); + } + gmTicketWindowWasOpen_ = showGmTicketWindow_; + if (!showGmTicketWindow_) return; - ImGui::SetNextWindowSize(ImVec2(400, 260), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(440, 320), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(300, 200), ImGuiCond_FirstUseEver); if (!ImGui::Begin("GM Ticket", &showGmTicketWindow_, @@ -14530,10 +24827,33 @@ void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { return; } + // Show GM support availability + if (!gameHandler.isGmSupportAvailable()) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "GM support is currently unavailable."); + ImGui::Spacing(); + } + + // Show existing open ticket if any + if (gameHandler.hasActiveGmTicket()) { + ImGui::TextColored(kColorGreen, "You have an open GM ticket."); + const std::string& existingText = gameHandler.getGmTicketText(); + if (!existingText.empty()) { + ImGui::TextWrapped("Current ticket: %s", existingText.c_str()); + } + float waitHours = gameHandler.getGmTicketWaitHours(); + if (waitHours > 0.0f) { + char waitBuf[64]; + std::snprintf(waitBuf, sizeof(waitBuf), "Estimated wait: %.1f hours", waitHours); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.4f, 1.0f), "%s", waitBuf); + } + ImGui::Separator(); + ImGui::Spacing(); + } + ImGui::TextWrapped("Describe your issue and a Game Master will contact you."); ImGui::Spacing(); ImGui::InputTextMultiline("##gmticket_body", gmTicketBuf_, sizeof(gmTicketBuf_), - ImVec2(-1, 160)); + ImVec2(-1, 120)); ImGui::Spacing(); bool hasText = (gmTicketBuf_[0] != '\0'); @@ -14550,8 +24870,11 @@ void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { showGmTicketWindow_ = false; } ImGui::SameLine(); - if (ImGui::Button("Delete Ticket", ImVec2(100, 0))) { - gameHandler.deleteGmTicket(); + if (gameHandler.hasActiveGmTicket()) { + if (ImGui::Button("Delete Ticket", ImVec2(110, 0))) { + gameHandler.deleteGmTicket(); + showGmTicketWindow_ = false; + } } ImGui::End(); @@ -14581,10 +24904,38 @@ void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) { uint32_t maxThreat = list->front().threat; + // Pre-scan to find the player's rank and threat percentage + uint64_t playerGuid = gameHandler.getPlayerGuid(); + int playerRank = 0; + float playerPct = 0.0f; + { + int scan = 0; + for (const auto& e : *list) { + ++scan; + if (e.victimGuid == playerGuid) { + playerRank = scan; + playerPct = (maxThreat > 0) ? static_cast(e.threat) / static_cast(maxThreat) : 0.0f; + break; + } + if (scan >= 10) break; + } + } + + // Status bar: aggro alert or rank summary + if (playerRank == 1) { + // Player has aggro — persistent red warning + ImGui::TextColored(ImVec4(1.0f, 0.25f, 0.25f, 1.0f), "!! YOU HAVE AGGRO !!"); + } else if (playerRank > 1 && playerPct >= 0.8f) { + // Close to pulling — pulsing warning + float pulse = 0.55f + 0.45f * sinf(static_cast(ImGui::GetTime()) * 5.0f); + ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.1f, pulse), "! PULLING AGGRO (%.0f%%) !", playerPct * 100.0f); + } else if (playerRank > 0) { + ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "You: #%d %.0f%% threat", playerRank, playerPct * 100.0f); + } + ImGui::TextDisabled("%-19s Threat", "Player"); ImGui::Separator(); - uint64_t playerGuid = gameHandler.getPlayerGuid(); int rank = 0; for (const auto& entry : *list) { ++rank; @@ -14610,10 +24961,10 @@ void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) { // Colour: gold for #1 (tank), red if player is highest, white otherwise ImVec4 col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); if (rank == 1) col = ImVec4(1.0f, 0.82f, 0.0f, 1.0f); // gold - if (isPlayer && rank == 1) col = ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // red — you have aggro + if (isPlayer && rank == 1) col = kColorRed; // red — you have aggro // Threat bar - float pct = (maxThreat > 0) ? (float)entry.threat / (float)maxThreat : 0.0f; + float pct = (maxThreat > 0) ? static_cast(entry.threat) / static_cast(maxThreat) : 0.0f; ImGui::PushStyleColor(ImGuiCol_PlotHistogram, isPlayer ? ImVec4(0.8f, 0.2f, 0.2f, 0.7f) : ImVec4(0.2f, 0.5f, 0.8f, 0.5f)); char barLabel[48]; @@ -14630,110 +24981,274 @@ void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) { ImGui::End(); } -// ─── Quest Objective Tracker ────────────────────────────────────────────────── -void GameScreen::renderObjectiveTracker(game::GameHandler& gameHandler) { - if (gameHandler.getState() != game::WorldState::IN_WORLD) return; +// ─── BG Scoreboard ──────────────────────────────────────────────────────────── +void GameScreen::renderBgScoreboard(game::GameHandler& gameHandler) { + if (!showBgScoreboard_) return; - const auto& questLog = gameHandler.getQuestLog(); - const auto& tracked = gameHandler.getTrackedQuestIds(); + const game::GameHandler::BgScoreboardData* data = gameHandler.getBgScoreboard(); - // Collect quests to show: tracked ones first, then in-progress quests up to a max of 5 total. - std::vector toShow; - for (const auto& q : questLog) { - if (q.questId == 0) continue; - if (tracked.count(q.questId)) toShow.push_back(&q); + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(150, 100), ImGuiCond_FirstUseEver); + + const char* title = data && data->isArena ? "Arena Score###BgScore" + : "Battleground Score###BgScore"; + if (!ImGui::Begin(title, &showBgScoreboard_, ImGuiWindowFlags_NoCollapse)) { + ImGui::End(); + return; } - if (toShow.empty()) { - // No explicitly tracked quests — show up to 5 in-progress quests - for (const auto& q : questLog) { - if (q.questId == 0) continue; - if (!tracked.count(q.questId)) toShow.push_back(&q); - if (toShow.size() >= 5) break; + + if (!data) { + ImGui::TextDisabled("No score data yet."); + ImGui::TextDisabled("Use /score to request the scoreboard while in a battleground or arena."); + ImGui::End(); + return; + } + + // Arena team rating banner (shown only for arenas) + if (data->isArena) { + for (int t = 0; t < 2; ++t) { + const auto& at = data->arenaTeams[t]; + if (at.teamName.empty()) continue; + int32_t ratingDelta = static_cast(at.ratingChange); + ImVec4 teamCol = (t == 0) ? ImVec4(1.0f, 0.35f, 0.35f, 1.0f) // team 0: red + : ImVec4(0.4f, 0.6f, 1.0f, 1.0f); // team 1: blue + ImGui::TextColored(teamCol, "%s", at.teamName.c_str()); + ImGui::SameLine(); + char ratingBuf[32]; + if (ratingDelta >= 0) + std::snprintf(ratingBuf, sizeof(ratingBuf), "Rating: %u (+%d)", at.newRating, ratingDelta); + else + std::snprintf(ratingBuf, sizeof(ratingBuf), "Rating: %u (%d)", at.newRating, ratingDelta); + ImGui::TextDisabled("%s", ratingBuf); + } + ImGui::Separator(); + } + + // Winner banner + if (data->hasWinner) { + const char* winnerStr; + ImVec4 winnerColor; + if (data->isArena) { + // For arenas, winner byte 0/1 refers to team index in arenaTeams[] + const auto& winTeam = data->arenaTeams[data->winner & 1]; + winnerStr = winTeam.teamName.empty() ? "Team 1" : winTeam.teamName.c_str(); + winnerColor = (data->winner == 0) ? ImVec4(1.0f, 0.35f, 0.35f, 1.0f) + : ImVec4(0.4f, 0.6f, 1.0f, 1.0f); + } else { + winnerStr = (data->winner == 1) ? "Alliance" : "Horde"; + winnerColor = (data->winner == 1) ? ImVec4(0.4f, 0.6f, 1.0f, 1.0f) + : ImVec4(1.0f, 0.35f, 0.35f, 1.0f); + } + float textW = ImGui::CalcTextSize(winnerStr).x + ImGui::CalcTextSize(" Victory!").x; + ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - textW) * 0.5f); + ImGui::TextColored(winnerColor, "%s", winnerStr); + ImGui::SameLine(0, 4); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Victory!"); + ImGui::Separator(); + } + + // Refresh button + if (ImGui::SmallButton("Refresh")) { + gameHandler.requestPvpLog(); + } + ImGui::SameLine(); + ImGui::TextDisabled("%zu players", data->players.size()); + + // Score table + constexpr ImGuiTableFlags kTableFlags = + ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | + ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Sortable; + + // Build dynamic column count based on what BG-specific stats are present + int numBgCols = 0; + std::vector bgColNames; + for (const auto& ps : data->players) { + for (const auto& [fieldName, val] : ps.bgStats) { + // Extract short name after last '.' (e.g. "BattlegroundAB.AbFlagCaptures" → "Caps") + std::string shortName = fieldName; + auto dotPos = fieldName.rfind('.'); + if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1); + bool found = false; + for (const auto& n : bgColNames) { if (n == shortName) { found = true; break; } } + if (!found) bgColNames.push_back(shortName); } } + numBgCols = static_cast(bgColNames.size()); - if (toShow.empty()) return; + // Fixed cols: Team | Name | KB | Deaths | HKs | Honor; then BG-specific + int totalCols = 6 + numBgCols; + float tableH = ImGui::GetContentRegionAvail().y; + if (ImGui::BeginTable("##BgScoreTable", totalCols, kTableFlags, ImVec2(0.0f, tableH))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Team", ImGuiTableColumnFlags_WidthFixed, 56.0f); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("KB", ImGuiTableColumnFlags_WidthFixed, 38.0f); + ImGui::TableSetupColumn("Deaths", ImGuiTableColumnFlags_WidthFixed, 52.0f); + ImGui::TableSetupColumn("HKs", ImGuiTableColumnFlags_WidthFixed, 38.0f); + ImGui::TableSetupColumn("Honor", ImGuiTableColumnFlags_WidthFixed, 52.0f); + for (const auto& col : bgColNames) + ImGui::TableSetupColumn(col.c_str(), ImGuiTableColumnFlags_WidthFixed, 52.0f); + ImGui::TableHeadersRow(); - ImVec2 display = ImGui::GetIO().DisplaySize; - float screenW = display.x > 0.0f ? display.x : 1280.0f; - float trackerW = 220.0f; - float trackerX = screenW - trackerW - 12.0f; - float trackerY = 230.0f; // below minimap + // Sort: Alliance first, then Horde; within each team by KB desc + std::vector sorted; + sorted.reserve(data->players.size()); + for (const auto& ps : data->players) sorted.push_back(&ps); + std::stable_sort(sorted.begin(), sorted.end(), + [](const game::GameHandler::BgPlayerScore* a, + const game::GameHandler::BgPlayerScore* b) { + if (a->team != b->team) return a->team > b->team; // Alliance(1) first + return a->killingBlows > b->killingBlows; + }); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoFocusOnAppearing; + uint64_t playerGuid = gameHandler.getPlayerGuid(); + for (const auto* ps : sorted) { + ImGui::TableNextRow(); - ImGui::SetNextWindowPos(ImVec2(trackerX, trackerY), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(trackerW, 0.0f), ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.5f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f)); + // Team + ImGui::TableNextColumn(); + if (ps->team == 1) + ImGui::TextColored(ImVec4(0.4f, 0.6f, 1.0f, 1.0f), "Alliance"); + else + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Horde"); - if (ImGui::Begin("##ObjectiveTracker", nullptr, flags)) { - for (const auto* q : toShow) { - // Quest title - ImVec4 titleColor = q->complete ? ImVec4(0.45f, 1.0f, 0.45f, 1.0f) - : ImVec4(1.0f, 0.84f, 0.0f, 1.0f); - std::string titleStr = q->title.empty() - ? ("Quest #" + std::to_string(q->questId)) : q->title; - // Truncate to fit - if (titleStr.size() > 26) { titleStr.resize(23); titleStr += "..."; } - ImGui::TextColored(titleColor, "%s", titleStr.c_str()); + // Name (highlight player's own row) + ImGui::TableNextColumn(); + bool isSelf = (ps->guid == playerGuid); + if (isSelf) ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); + const char* nameStr = ps->name.empty() ? "Unknown" : ps->name.c_str(); + ImGui::TextUnformatted(nameStr); + if (isSelf) ImGui::PopStyleColor(); - // Kill/entity objectives - bool hasObjectives = false; - for (const auto& ko : q->killObjectives) { - if (ko.npcOrGoId == 0 || ko.required == 0) continue; - hasObjectives = true; - uint32_t entry = (uint32_t)std::abs(ko.npcOrGoId); - auto it = q->killCounts.find(entry); - uint32_t cur = it != q->killCounts.end() ? it->second.first : 0; - std::string name = gameHandler.getCachedCreatureName(entry); - if (name.empty()) { - if (ko.npcOrGoId < 0) { - const auto* goInfo = gameHandler.getCachedGameObjectInfo(entry); - if (goInfo) name = goInfo->name; - } - if (name.empty()) name = "Objective"; + ImGui::TableNextColumn(); ImGui::Text("%u", ps->killingBlows); + ImGui::TableNextColumn(); ImGui::Text("%u", ps->deaths); + ImGui::TableNextColumn(); ImGui::Text("%u", ps->honorableKills); + ImGui::TableNextColumn(); ImGui::Text("%u", ps->bonusHonor); + + for (const auto& col : bgColNames) { + ImGui::TableNextColumn(); + uint32_t val = 0; + for (const auto& [fieldName, fval] : ps->bgStats) { + std::string shortName = fieldName; + auto dotPos = fieldName.rfind('.'); + if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1); + if (shortName == col) { val = fval; break; } } - if (name.size() > 20) { name.resize(17); name += "..."; } - bool done = (cur >= ko.required); - ImVec4 c = done ? ImVec4(0.5f, 0.9f, 0.5f, 1.0f) : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); - ImGui::TextColored(c, " %s: %u/%u", name.c_str(), cur, ko.required); + if (val > 0) ImGui::Text("%u", val); + else ImGui::TextDisabled("-"); } + } + ImGui::EndTable(); + } - // Item objectives - for (const auto& io : q->itemObjectives) { - if (io.itemId == 0 || io.required == 0) continue; - hasObjectives = true; - auto it = q->itemCounts.find(io.itemId); - uint32_t cur = it != q->itemCounts.end() ? it->second : 0; - std::string name; - if (const auto* info = gameHandler.getItemInfo(io.itemId)) name = info->name; - if (name.empty()) name = "Item #" + std::to_string(io.itemId); - if (name.size() > 20) { name.resize(17); name += "..."; } - bool done = (cur >= io.required); - ImVec4 c = done ? ImVec4(0.5f, 0.9f, 0.5f, 1.0f) : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); - ImGui::TextColored(c, " %s: %u/%u", name.c_str(), cur, io.required); - } + ImGui::End(); +} - if (!hasObjectives && q->complete) { - ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), " Ready to turn in!"); - } - ImGui::Dummy(ImVec2(0.0f, 2.0f)); + +// ─── Book / Scroll / Note Window ────────────────────────────────────────────── +void GameScreen::renderBookWindow(game::GameHandler& gameHandler) { + // Auto-open when new pages arrive + if (gameHandler.hasBookOpen() && !showBookWindow_) { + showBookWindow_ = true; + bookCurrentPage_ = 0; + } + if (!showBookWindow_) return; + + const auto& pages = gameHandler.getBookPages(); + if (pages.empty()) { showBookWindow_ = false; return; } + + // Clamp page index + if (bookCurrentPage_ < 0) bookCurrentPage_ = 0; + if (bookCurrentPage_ >= static_cast(pages.size())) + bookCurrentPage_ = static_cast(pages.size()) - 1; + + ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_Appearing); + ImGui::SetNextWindowPos(ImVec2(400, 180), ImGuiCond_Appearing); + + bool open = showBookWindow_; + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.09f, 0.06f, 0.98f)); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.25f, 0.18f, 0.08f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.37f, 0.18f, 1.0f)); + + char title[64]; + if (pages.size() > 1) + snprintf(title, sizeof(title), "Page %d / %d###BookWin", + bookCurrentPage_ + 1, static_cast(pages.size())); + else + snprintf(title, sizeof(title), "###BookWin"); + + if (ImGui::Begin(title, &open, ImGuiWindowFlags_NoCollapse)) { + // Parchment text colour + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.85f, 0.78f, 0.62f, 1.0f)); + + const std::string& text = pages[bookCurrentPage_].text; + // Use a child region with word-wrap + ImGui::SetNextWindowContentSize(ImVec2(ImGui::GetContentRegionAvail().x, 0)); + if (ImGui::BeginChild("##BookText", + ImVec2(0, ImGui::GetContentRegionAvail().y - 34), + false, ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::SetNextItemWidth(-1); + ImGui::TextWrapped("%s", text.c_str()); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + + // Navigation row + ImGui::Separator(); + bool canPrev = (bookCurrentPage_ > 0); + bool canNext = (bookCurrentPage_ < static_cast(pages.size()) - 1); + + if (!canPrev) ImGui::BeginDisabled(); + if (ImGui::Button("< Prev", ImVec2(80, 0))) bookCurrentPage_--; + if (!canPrev) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (!canNext) ImGui::BeginDisabled(); + if (ImGui::Button("Next >", ImVec2(80, 0))) bookCurrentPage_++; + if (!canNext) ImGui::EndDisabled(); + + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60); + if (ImGui::Button("Close", ImVec2(60, 0))) { + open = false; } } ImGui::End(); - ImGui::PopStyleVar(2); + ImGui::PopStyleColor(3); + + if (!open) { + showBookWindow_ = false; + gameHandler.clearBook(); + } } // ─── Inspect Window ─────────────────────────────────────────────────────────── void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { if (!showInspectWindow_) return; + // Lazy-load SpellItemEnchantment.dbc for enchant name lookup + static std::unordered_map s_enchantNames; + static bool s_enchantDbLoaded = false; + auto* assetMgrEnchant = core::Application::getInstance().getAssetManager(); + if (!s_enchantDbLoaded && assetMgrEnchant && assetMgrEnchant->isInitialized()) { + s_enchantDbLoaded = true; + auto dbc = assetMgrEnchant->loadDBC("SpellItemEnchantment.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") + : nullptr; + uint32_t idField = layout ? (*layout)["ID"] : 0; + uint32_t nameField = layout ? (*layout)["Name"] : 8; + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t id = dbc->getUInt32(i, idField); + if (id == 0) continue; + std::string nm = dbc->getString(i, nameField); + if (!nm.empty()) s_enchantNames[id] = std::move(nm); + } + } + } + // Slot index 0..18 maps to equipment slots 1..19 (WoW convention: slot 0 unused on server) static const char* kSlotNames[19] = { "Head", "Neck", "Shoulder", "Shirt", "Chest", @@ -14760,10 +25275,19 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { return; } - // Talent summary - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.82f, 0.0f, 1.0f)); // gold - ImGui::Text("%s", result->playerName.c_str()); - ImGui::PopStyleColor(); + // Player name — class-colored if entity is loaded, else gold + { + auto ent = gameHandler.getEntityManager().getEntity(result->guid); + uint8_t cid = entityClassId(ent.get()); + ImVec4 nameColor = (cid != 0) ? classColorVec4(cid) : ImVec4(1.0f, 0.82f, 0.0f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Text, nameColor); + ImGui::Text("%s", result->playerName.c_str()); + ImGui::PopStyleColor(); + if (cid != 0) { + ImGui::SameLine(); + ImGui::TextColored(classColorVec4(cid), "(%s)", classNameStr(cid)); + } + } ImGui::SameLine(); ImGui::TextDisabled(" %u talent pts", result->totalTalents); if (result->unspentTalents > 0) { @@ -14772,7 +25296,7 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { } if (result->talentGroups > 1) { ImGui::SameLine(); - ImGui::TextDisabled(" Dual spec (active %u)", (unsigned)result->activeTalentGroup + 1); + ImGui::TextDisabled(" Dual spec (active %u)", static_cast(result->activeTalentGroup) + 1); } ImGui::Separator(); @@ -14787,7 +25311,29 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { ImGui::TextDisabled("Equipment data not yet available."); ImGui::TextDisabled("(Gear loads after the player is inspected in-range)"); } else { + // Average item level (only slots that have loaded info and are not shirt/tabard) + // Shirt=slot3, Tabard=slot18 — excluded from gear score by WoW convention + uint32_t iLevelSum = 0; + int iLevelCount = 0; + for (int s = 0; s < 19; ++s) { + if (s == 3 || s == 18) continue; // shirt, tabard + uint32_t entry = result->itemEntries[s]; + if (entry == 0) continue; + const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry); + if (info && info->valid && info->itemLevel > 0) { + iLevelSum += info->itemLevel; + ++iLevelCount; + } + } + if (iLevelCount > 0) { + float avgIlvl = static_cast(iLevelSum) / static_cast(iLevelCount); + ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Avg iLvl: %.1f", avgIlvl); + ImGui::SameLine(); + ImGui::TextDisabled("(%d/%d slots loaded)", iLevelCount, + [&]{ int c=0; for(int s=0;s<19;++s){if(s==3||s==18)continue;if(result->itemEntries[s])++c;} return c; }()); + } if (ImGui::BeginChild("##inspect_gear", ImVec2(0, 0), false)) { + constexpr float kIconSz = 28.0f; for (int s = 0; s < 19; ++s) { uint32_t entry = result->itemEntries[s]; if (entry == 0) continue; @@ -14795,29 +25341,334 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry); if (!info) { gameHandler.ensureItemInfo(entry); + ImGui::PushID(s); ImGui::TextDisabled("[%s] (loading…)", kSlotNames[s]); + ImGui::PopID(); continue; } - ImGui::TextDisabled("%s", kSlotNames[s]); - ImGui::SameLine(90); + ImGui::PushID(s); auto qColor = InventoryScreen::getQualityColor( static_cast(info->quality)); - ImGui::TextColored(qColor, "%s", info->name.c_str()); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextColored(qColor, "%s", info->name.c_str()); - if (info->itemLevel > 0) - ImGui::Text("Item Level %u", info->itemLevel); - if (info->armor > 0) - ImGui::Text("Armor: %d", info->armor); - ImGui::EndTooltip(); + uint16_t enchantId = result->enchantIds[s]; + + // Item icon + VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId); + if (iconTex) { + ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(kIconSz, kIconSz), + ImVec2(0,0), ImVec2(1,1), + ImVec4(1,1,1,1), qColor); + } else { + ImGui::GetWindowDrawList()->AddRectFilled( + ImGui::GetCursorScreenPos(), + ImVec2(ImGui::GetCursorScreenPos().x + kIconSz, + ImGui::GetCursorScreenPos().y + kIconSz), + IM_COL32(40, 40, 50, 200)); + ImGui::Dummy(ImVec2(kIconSz, kIconSz)); } + bool hovered = ImGui::IsItemHovered(); + + ImGui::SameLine(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (kIconSz - ImGui::GetTextLineHeight()) * 0.5f); + ImGui::BeginGroup(); + ImGui::TextDisabled("%s", kSlotNames[s]); + ImGui::TextColored(qColor, "%s", info->name.c_str()); + // Enchant indicator on the same row as the name + if (enchantId != 0) { + auto enchIt = s_enchantNames.find(enchantId); + const std::string& enchName = (enchIt != s_enchantNames.end()) + ? enchIt->second : std::string{}; + ImGui::SameLine(); + if (!enchName.empty()) { + ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f), + "\xe2\x9c\xa6 %s", enchName.c_str()); // UTF-8 ✦ + } else { + ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f), "\xe2\x9c\xa6"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Enchanted (ID %u)", static_cast(enchantId)); + } + } + ImGui::EndGroup(); + hovered = hovered || ImGui::IsItemHovered(); + + if (hovered && info->valid) { + inventoryScreen.renderItemTooltip(*info); + } else if (hovered) { + ImGui::SetTooltip("%s", info->name.c_str()); + } + + ImGui::PopID(); + ImGui::Spacing(); } } ImGui::EndChild(); } + // Arena teams (WotLK — from MSG_INSPECT_ARENA_TEAMS) + if (!result->arenaTeams.empty()) { + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "Arena Teams"); + ImGui::Spacing(); + for (const auto& team : result->arenaTeams) { + const char* bracket = (team.type == 2) ? "2v2" + : (team.type == 3) ? "3v3" + : (team.type == 5) ? "5v5" : "?v?"; + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), + "[%s] %s", bracket, team.name.c_str()); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f), + " Rating: %u", team.personalRating); + if (team.weekGames > 0 || team.seasonGames > 0) { + ImGui::TextDisabled(" Week: %u/%u Season: %u/%u", + team.weekWins, team.weekGames, + team.seasonWins, team.seasonGames); + } + } + } + + ImGui::End(); +} + +// ─── Titles Window ──────────────────────────────────────────────────────────── +void GameScreen::renderTitlesWindow(game::GameHandler& gameHandler) { + if (!showTitlesWindow_) return; + + ImGui::SetNextWindowSize(ImVec2(320, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(240, 170), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin("Titles", &showTitlesWindow_)) { + ImGui::End(); + return; + } + + const auto& knownBits = gameHandler.getKnownTitleBits(); + const int32_t chosen = gameHandler.getChosenTitleBit(); + + if (knownBits.empty()) { + ImGui::TextDisabled("No titles earned yet."); + ImGui::End(); + return; + } + + ImGui::TextUnformatted("Select a title to display:"); + ImGui::Separator(); + + // "No Title" option + bool noTitle = (chosen < 0); + if (ImGui::Selectable("(No Title)", noTitle)) { + if (!noTitle) gameHandler.sendSetTitle(-1); + } + if (noTitle) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "<-- active"); + } + + ImGui::Separator(); + + // Sort known bits for stable display order + std::vector sortedBits(knownBits.begin(), knownBits.end()); + std::sort(sortedBits.begin(), sortedBits.end()); + + ImGui::BeginChild("##titlelist", ImVec2(0, 0), false); + for (uint32_t bit : sortedBits) { + const std::string title = gameHandler.getFormattedTitle(bit); + const std::string display = title.empty() + ? ("Title #" + std::to_string(bit)) : title; + + bool isActive = (chosen >= 0 && static_cast(chosen) == bit); + ImGui::PushID(static_cast(bit)); + + if (isActive) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); + } + if (ImGui::Selectable(display.c_str(), isActive)) { + if (!isActive) gameHandler.sendSetTitle(static_cast(bit)); + } + if (isActive) { + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::TextDisabled("<-- active"); + } + + ImGui::PopID(); + } + ImGui::EndChild(); + + ImGui::End(); +} + +// ─── Equipment Set Manager Window ───────────────────────────────────────────── +void GameScreen::renderEquipSetWindow(game::GameHandler& gameHandler) { + if (!showEquipSetWindow_) return; + + ImGui::SetNextWindowSize(ImVec2(280, 320), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(260, 180), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin("Equipment Sets##equipsets", &showEquipSetWindow_)) { + ImGui::End(); + return; + } + + const auto& sets = gameHandler.getEquipmentSets(); + + if (sets.empty()) { + ImGui::TextDisabled("No equipment sets saved."); + ImGui::Spacing(); + ImGui::TextWrapped("Create equipment sets in-game using the default WoW equipment manager (Shift+click the Equipment Sets button)."); + ImGui::End(); + return; + } + + ImGui::TextUnformatted("Click a set to equip it:"); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::BeginChild("##equipsetlist", ImVec2(0, 0), false); + for (const auto& set : sets) { + ImGui::PushID(static_cast(set.setId)); + + // Icon placeholder (use a coloured square if no icon texture available) + ImVec2 iconSize(32.0f, 32.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.20f, 0.10f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.40f, 0.30f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.60f, 0.45f, 0.20f, 1.0f)); + if (ImGui::Button("##icon", iconSize)) { + gameHandler.useEquipmentSet(set.setId); + } + ImGui::PopStyleColor(3); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Equip set: %s", set.name.c_str()); + } + + ImGui::SameLine(); + + // Name and equip button + ImGui::BeginGroup(); + ImGui::TextUnformatted(set.name.c_str()); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.35f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.30f, 0.50f, 0.22f, 1.0f)); + if (ImGui::SmallButton("Equip")) { + gameHandler.useEquipmentSet(set.setId); + } + ImGui::PopStyleColor(2); + ImGui::EndGroup(); + + ImGui::Spacing(); + ImGui::PopID(); + } + ImGui::EndChild(); + + ImGui::End(); +} + +void GameScreen::renderSkillsWindow(game::GameHandler& gameHandler) { + if (!showSkillsWindow_) return; + + ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(220, 130), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin("Skills & Professions", &showSkillsWindow_)) { + ImGui::End(); + return; + } + + const auto& skills = gameHandler.getPlayerSkills(); + if (skills.empty()) { + ImGui::TextDisabled("No skill data received yet."); + ImGui::End(); + return; + } + + // Organise skills by category + // WoW SkillLine.dbc categories: 6=Weapon, 7=Class, 8=Armor, 9=Secondary, 11=Professions, others=Misc + struct SkillEntry { + uint32_t skillId; + const game::PlayerSkill* skill; + }; + std::map> byCategory; + for (const auto& [id, sk] : skills) { + uint32_t cat = gameHandler.getSkillCategory(id); + byCategory[cat].push_back({id, &sk}); + } + + static const struct { uint32_t cat; const char* label; } kCatOrder[] = { + {11, "Professions"}, + { 9, "Secondary Skills"}, + { 7, "Class Skills"}, + { 6, "Weapon Skills"}, + { 8, "Armor"}, + { 5, "Languages"}, + { 0, "Other"}, + }; + + // Collect handled categories to fall back to "Other" for unknowns + static const uint32_t kKnownCats[] = {11, 9, 7, 6, 8, 5}; + + // Redirect unknown categories into bucket 0 + for (auto& [cat, vec] : byCategory) { + bool known = false; + for (uint32_t kc : kKnownCats) if (cat == kc) { known = true; break; } + if (!known && cat != 0) { + auto& other = byCategory[0]; + other.insert(other.end(), vec.begin(), vec.end()); + vec.clear(); + } + } + + ImGui::BeginChild("##skillscroll", ImVec2(0, 0), false); + + for (const auto& [cat, label] : kCatOrder) { + auto it = byCategory.find(cat); + if (it == byCategory.end() || it->second.empty()) continue; + + auto& entries = it->second; + // Sort alphabetically within each category + std::sort(entries.begin(), entries.end(), [&](const SkillEntry& a, const SkillEntry& b) { + return gameHandler.getSkillName(a.skillId) < gameHandler.getSkillName(b.skillId); + }); + + if (ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)) { + for (const auto& e : entries) { + const std::string& name = gameHandler.getSkillName(e.skillId); + const char* displayName = name.empty() ? "Unknown" : name.c_str(); + uint16_t val = e.skill->effectiveValue(); + uint16_t maxVal = e.skill->maxValue; + + ImGui::PushID(static_cast(e.skillId)); + + // Name column + ImGui::TextUnformatted(displayName); + ImGui::SameLine(170.0f); + + // Progress bar + float fraction = (maxVal > 0) ? static_cast(val) / static_cast(maxVal) : 0.0f; + char overlay[32]; + snprintf(overlay, sizeof(overlay), "%u / %u", val, maxVal); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.20f, 0.55f, 0.20f, 1.0f)); + ImGui::ProgressBar(fraction, ImVec2(160.0f, 14.0f), overlay); + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s", displayName); + ImGui::Separator(); + ImGui::Text("Base: %u", e.skill->value); + if (e.skill->bonusPerm > 0) + ImGui::Text("Permanent bonus: +%u", e.skill->bonusPerm); + if (e.skill->bonusTemp > 0) + ImGui::Text("Temporary bonus: +%u", e.skill->bonusTemp); + ImGui::Text("Max: %u", maxVal); + ImGui::EndTooltip(); + } + + ImGui::PopID(); + } + ImGui::Spacing(); + } + } + + ImGui::EndChild(); ImGui::End(); } diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index e5735977..ed8d3bd6 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -1,4 +1,5 @@ #include "ui/inventory_screen.hpp" +#include "ui/ui_colors.hpp" #include "ui/keybinding_manager.hpp" #include "game/game_handler.hpp" #include "core/application.hpp" @@ -17,6 +18,7 @@ #include #include #include +#include #include namespace wowee { @@ -72,6 +74,7 @@ const game::ItemSlot* findComparableEquipped(const game::Inventory& inventory, u default: return nullptr; } } + } // namespace InventoryScreen::~InventoryScreen() { @@ -80,15 +83,7 @@ InventoryScreen::~InventoryScreen() { } ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) { - switch (quality) { - case game::ItemQuality::POOR: return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); // Grey - case game::ItemQuality::COMMON: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White - case game::ItemQuality::UNCOMMON: return ImVec4(0.12f, 1.0f, 0.0f, 1.0f); // Green - case game::ItemQuality::RARE: return ImVec4(0.0f, 0.44f, 0.87f, 1.0f); // Blue - case game::ItemQuality::EPIC: return ImVec4(0.64f, 0.21f, 0.93f, 1.0f); // Purple - case game::ItemQuality::LEGENDARY: return ImVec4(1.0f, 0.50f, 0.0f, 1.0f); // Orange - default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - } + return ui::getQualityColor(quality); } // ============================================================ @@ -692,10 +687,6 @@ void InventoryScreen::toggleBackpack() { void InventoryScreen::toggleBag(int idx) { if (idx >= 0 && idx < 4) { bagOpen_[idx] = !bagOpen_[idx]; - if (bagOpen_[idx]) { - // Keep backpack as the anchor window at the bottom of the stack. - backpackOpen_ = true; - } } } @@ -725,17 +716,6 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { bool bToggled = bagsDown && !bKeyWasDown; bKeyWasDown = bagsDown; - // Character screen toggle (C key, edge-triggered) - bool characterDown = KeybindingManager::getInstance().isActionPressed( - KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN, false); - if (characterDown && !cKeyWasDown) { - characterOpen = !characterOpen; - if (characterOpen && gameHandler_) { - gameHandler_->requestPlayedTime(); - } - } - cKeyWasDown = characterDown; - bool wantsTextInput = ImGui::GetIO().WantTextInput; if (separateBags_) { @@ -864,6 +844,35 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { ImGui::EndPopup(); } + // Stack split popup + if (splitConfirmOpen_) { + ImVec2 mousePos = ImGui::GetIO().MousePos; + ImGui::SetNextWindowPos(ImVec2(mousePos.x - 80.0f, mousePos.y - 20.0f), ImGuiCond_Always); + ImGui::OpenPopup("##SplitStack"); + splitConfirmOpen_ = false; + } + if (ImGui::BeginPopup("##SplitStack", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { + ImGui::Text("Split %s", splitItemName_.c_str()); + ImGui::Spacing(); + ImGui::SetNextItemWidth(120.0f); + ImGui::SliderInt("##splitcount", &splitCount_, 1, splitMax_ - 1); + ImGui::Spacing(); + if (ImGui::Button("OK", ImVec2(55, 0))) { + if (gameHandler_ && splitCount_ > 0 && splitCount_ < splitMax_) { + gameHandler_->splitItem(splitBag_, splitSlot_, static_cast(splitCount_)); + } + splitItemName_.clear(); + inventoryDirty = true; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(55, 0))) { + splitItemName_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + // Draw held item at cursor renderHeldItem(); } @@ -896,15 +905,23 @@ void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t m float posX = screenW - windowW - 10.0f; float posY = screenH - windowH - 60.0f; - ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); - ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; if (!ImGui::Begin("Bags", &open, flags)) { ImGui::End(); return; } + // Reset to bottom-right if the window ended up outside the screen (resolution change) + ImVec2 winPos = ImGui::GetWindowPos(); + ImVec2 winSize = ImGui::GetWindowSize(); + if (winPos.x > screenW || winPos.y > screenH || + winPos.x + winSize.x < 0 || winPos.y + winSize.y < 0) { + ImGui::SetWindowPos(ImVec2(posX, posY)); + } + renderBackpackPanel(inventory, compactBags_); ImGui::Spacing(); @@ -943,11 +960,7 @@ void InventoryScreen::renderSeparateBags(game::Inventory& inventory, uint64_t mo constexpr int columns = 6; constexpr float baseWindowW = columns * (slotSize + 4.0f) + 30.0f; - bool anyBagOpen = std::any_of(bagOpen_.begin(), bagOpen_.end(), [](bool b) { return b; }); - if (anyBagOpen && !backpackOpen_) { - // Enforce backpack as the bottom-most stack window when any bag is open. - backpackOpen_ = true; - } + // Each bag window is independently closable — no forced backpack constraint. // Anchor stack to the bag bar (bottom-right), opening upward. const float bagBarTop = screenH - (42.0f + 12.0f) - 10.0f; @@ -1010,7 +1023,11 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, int rows = (numSlots + columns - 1) / columns; float contentH = rows * (slotSize + 4.0f) + 10.0f; - if (bagIndex < 0) contentH += 25.0f; // money display for backpack + if (bagIndex < 0) { + int keyringRows = (inventory.getKeyringSize() + columns - 1) / columns; + contentH += 36.0f; // separator + sort button + money display + contentH += 30.0f + keyringRows * (slotSize + 4.0f); // keyring header + slots + } float gridW = columns * (slotSize + 4.0f) + 30.0f; // Ensure window is wide enough for the title + close button const char* displayTitle = title; @@ -1019,8 +1036,7 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, float windowW = std::max(gridW, titleW); float windowH = contentH + 40.0f; // title bar + padding - // Keep separate bag windows anchored to the bag-bar stack. - ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; @@ -1029,6 +1045,16 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, return; } + // Reset position if the window ended up outside the screen (resolution change) + ImVec2 winPos = ImGui::GetWindowPos(); + ImVec2 winSize = ImGui::GetWindowSize(); + float scrW = ImGui::GetIO().DisplaySize.x; + float scrH = ImGui::GetIO().DisplaySize.y; + if (winPos.x > scrW || winPos.y > scrH || + winPos.x + winSize.x < 0 || winPos.y + winSize.y < 0) { + ImGui::SetWindowPos(ImVec2(defaultX, defaultY)); + } + // Render item slots in 4-column grid for (int i = 0; i < numSlots; i++) { if (i % columns != 0) ImGui::SameLine(); @@ -1058,16 +1084,73 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, ImGui::PopID(); } - // Money display at bottom of backpack - if (bagIndex < 0 && moneyCopper > 0) { + if (bagIndex < 0 && showKeyring_) { + constexpr float keySlotSize = 24.0f; + constexpr int keyCols = 8; + // Only show rows that contain items (round up to full row) + int lastOccupied = -1; + for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) { + if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; } + } + int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols; + if (visibleSlots > 0) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); + for (int i = 0; i < visibleSlots; ++i) { + if (i % keyCols != 0) ImGui::SameLine(); + const auto& slot = inventory.getKeyringSlot(i); + char id[32]; + snprintf(id, sizeof(id), "##skr_%d", i); + ImGui::PushID(id); + renderItemSlot(inventory, slot, keySlotSize, nullptr, + SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); + ImGui::PopID(); + } + } + } + + // Footer for backpack: sort button + money display + if (bagIndex < 0) { ImGui::Spacing(); - uint64_t gold = moneyCopper / 10000; - uint64_t silver = (moneyCopper / 100) % 100; - uint64_t copper = moneyCopper % 100; - ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc", - static_cast(gold), - static_cast(silver), - static_cast(copper)); + ImGui::Separator(); + + // Sort Bags button — compute swaps, apply client-side preview, queue server packets + { + bool sorting = !sortSwapQueue_.empty(); + if (sorting) ImGui::BeginDisabled(); + if (ImGui::SmallButton(sorting ? "Sorting..." : "Sort Bags")) { + // Compute the swap operations before modifying local state + auto swaps = inventory.computeSortSwaps(); + // Apply local preview immediately + inventory.sortBags(); + // Queue server-side swaps (one per frame) + for (auto& s : swaps) + sortSwapQueue_.push_back(s); + } + if (sorting) ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::SetTooltip("Sort all bag slots by quality (highest first),\nthen by item ID, then by stack size."); + } + + // Process one queued swap per frame + if (!sortSwapQueue_.empty() && gameHandler_) { + auto op = sortSwapQueue_.front(); + sortSwapQueue_.pop_front(); + gameHandler_->swapContainerItems(op.srcBag, op.srcSlot, op.dstBag, op.dstSlot); + } + } + + if (moneyCopper > 0) { + ImGui::SameLine(); + uint64_t gold = moneyCopper / 10000; + uint64_t silver = (moneyCopper / 100) % 100; + uint64_t copper = moneyCopper % 100; + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc", + static_cast(gold), + static_cast(silver), + static_cast(copper)); + } } ImGui::End(); @@ -1144,7 +1227,9 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { int32_t stats[5]; for (int i = 0; i < 5; ++i) stats[i] = gameHandler.getPlayerStat(i); const int32_t* serverStats = (stats[0] >= 0) ? stats : nullptr; - renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats); + int32_t resists[6]; + for (int i = 0; i < 6; ++i) resists[i] = gameHandler.getResistance(i + 1); + renderStatsPanel(inventory, gameHandler.getPlayerLevel(), gameHandler.getArmorRating(), serverStats, resists, &gameHandler); // Played time (shown if available, fetched on character screen open) uint32_t totalSec = gameHandler.getTotalTimePlayed(); @@ -1169,6 +1254,22 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { ImGui::Text("%s", fmtTime(levelSec).c_str()); ImGui::NextColumn(); ImGui::Columns(1); } + + // PvP Currency (TBC/WotLK only) + uint32_t honor = gameHandler.getHonorPoints(); + uint32_t arena = gameHandler.getArenaPoints(); + if (honor > 0 || arena > 0) { + ImGui::Separator(); + ImGui::TextDisabled("PvP Currency"); + ImGui::Columns(2, "##pvpcurrency", false); + ImGui::SetColumnWidth(0, 130); + ImGui::Text("Honor Points:"); ImGui::NextColumn(); + ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.2f, 1.0f), "%u", honor); ImGui::NextColumn(); + ImGui::Text("Arena Points:"); ImGui::NextColumn(); + ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.2f, 1.0f), "%u", arena); ImGui::NextColumn(); + ImGui::Columns(1); + } + ImGui::EndTabItem(); } @@ -1225,18 +1326,35 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { snprintf(label, sizeof(label), "%s", name.c_str()); } - // Show progress bar with value/max overlay + // Effective value includes temporary and permanent bonuses + uint16_t effective = skill->effectiveValue(); + uint16_t bonus = skill->bonusTemp + skill->bonusPerm; + + // Progress bar reflects effective / max; cap visual fill at 1.0 float ratio = (skill->maxValue > 0) - ? static_cast(skill->value) / static_cast(skill->maxValue) + ? std::min(1.0f, static_cast(effective) / static_cast(skill->maxValue)) : 0.0f; char overlay[64]; - snprintf(overlay, sizeof(overlay), "%u / %u", skill->value, skill->maxValue); + if (bonus > 0) + snprintf(overlay, sizeof(overlay), "%u / %u (+%u)", effective, skill->maxValue, bonus); + else + snprintf(overlay, sizeof(overlay), "%u / %u", effective, skill->maxValue); - ImGui::Text("%s", label); + // Gold name when maxed out, cyan when buffed above base, default otherwise + bool isMaxed = (effective >= skill->maxValue && skill->maxValue > 0); + bool isBuffed = (bonus > 0); + ImVec4 nameColor = isMaxed ? ImVec4(1.0f, 0.82f, 0.0f, 1.0f) + : isBuffed ? ImVec4(0.4f, 0.9f, 1.0f, 1.0f) + : ImVec4(0.85f, 0.85f, 0.85f, 1.0f); + ImGui::TextColored(nameColor, "%s", label); ImGui::SameLine(180.0f); ImGui::SetNextItemWidth(-1.0f); + // Bar color: gold when maxed, green otherwise + ImVec4 barColor = isMaxed ? ImVec4(1.0f, 0.82f, 0.0f, 1.0f) : ImVec4(0.2f, 0.7f, 0.2f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor); ImGui::ProgressBar(ratio, ImVec2(0, 14.0f), overlay); + ImGui::PopStyleColor(); } } } @@ -1325,6 +1443,56 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } + // Equipment Sets tab (WotLK only — requires server support) + if (gameHandler.supportsEquipmentSets() && ImGui::BeginTabItem("Outfits")) { + ImGui::Spacing(); + + // Save current gear as new set + static char newSetName[64] = {}; + ImGui::SetNextItemWidth(160.0f); + ImGui::InputTextWithHint("##newsetname", "New set name...", newSetName, sizeof(newSetName)); + ImGui::SameLine(); + bool canSave = (newSetName[0] != '\0'); + if (!canSave) ImGui::BeginDisabled(); + if (ImGui::SmallButton("Save Current Gear")) { + gameHandler.saveEquipmentSet(newSetName); + newSetName[0] = '\0'; + } + if (!canSave) ImGui::EndDisabled(); + + ImGui::Separator(); + + const auto& eqSets = gameHandler.getEquipmentSets(); + if (eqSets.empty()) { + ImGui::TextDisabled("No saved equipment sets."); + } else { + ImGui::BeginChild("##EqSetsList", ImVec2(0, 0), false); + for (const auto& es : eqSets) { + ImGui::PushID(static_cast(es.setId)); + const char* displayName = es.name.empty() ? "(Unnamed)" : es.name.c_str(); + ImGui::Text("%s", displayName); + float btnAreaW = 150.0f; + ImGui::SameLine(ImGui::GetContentRegionAvail().x - btnAreaW + ImGui::GetCursorPosX()); + if (ImGui::SmallButton("Equip")) { + gameHandler.useEquipmentSet(es.setId); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Update")) { + gameHandler.saveEquipmentSet(es.name, es.iconName, es.setGuid, es.setId); + } + ImGui::SameLine(); + if (ImGui::SmallButton("Delete")) { + gameHandler.deleteEquipmentSet(es.setGuid); + ImGui::PopID(); + break; // Iterator invalidated + } + ImGui::PopID(); + } + ImGui::EndChild(); + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } @@ -1375,10 +1543,13 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) { ImGui::BeginChild("##ReputationList", ImVec2(0, 0), true); - // Sort factions alphabetically by name + // Sort: watched faction first, then alphabetically by name + uint32_t watchedFactionId = gameHandler.getWatchedFactionId(); std::vector> sortedFactions(standings.begin(), standings.end()); std::sort(sortedFactions.begin(), sortedFactions.end(), [&](const auto& a, const auto& b) { + if (a.first == watchedFactionId) return true; + if (b.first == watchedFactionId) return false; const std::string& na = gameHandler.getFactionNamePublic(a.first); const std::string& nb = gameHandler.getFactionNamePublic(b.first); return na < nb; @@ -1390,10 +1561,27 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) { const std::string& factionName = gameHandler.getFactionNamePublic(factionId); const char* displayName = factionName.empty() ? "Unknown Faction" : factionName.c_str(); - // Faction name + tier label on same line + // Determine at-war status via repListId lookup + uint32_t repListId = gameHandler.getRepListIdByFactionId(factionId); + bool atWar = (repListId != 0xFFFFFFFFu) && gameHandler.isFactionAtWar(repListId); + bool isWatched = (factionId == watchedFactionId); + + ImGui::PushID(static_cast(factionId)); + + // Faction name + tier label on same line; mark at-war and watched factions ImGui::TextColored(tier.color, "[%s]", tier.name); ImGui::SameLine(90.0f); - ImGui::Text("%s", displayName); + if (atWar) { + ImGui::TextColored(ui::colors::kRed, "%s", displayName); + ImGui::SameLine(); + ImGui::TextColored(ui::colors::kRed, "(At War)"); + } else if (isWatched) { + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 1.0f), "%s", displayName); + ImGui::SameLine(); + ImGui::TextDisabled("(Tracked)"); + } else { + ImGui::Text("%s", displayName); + } // Progress bar showing position within current tier float ratio = 0.0f; @@ -1415,7 +1603,23 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) { ImGui::SetNextItemWidth(-1.0f); ImGui::ProgressBar(ratio, ImVec2(0, 12.0f), overlay); ImGui::PopStyleColor(); + + // Right-click context menu on the progress bar + if (ImGui::BeginPopupContextItem("##RepCtx")) { + ImGui::TextDisabled("%s", displayName); + ImGui::Separator(); + if (isWatched) { + if (ImGui::MenuItem("Untrack")) + gameHandler.setWatchedFactionId(0); + } else { + if (ImGui::MenuItem("Track on Rep Bar")) + gameHandler.setWatchedFactionId(factionId); + } + ImGui::EndPopup(); + } + ImGui::Spacing(); + ImGui::PopID(); } ImGui::EndChild(); @@ -1542,12 +1746,16 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) { // ============================================================ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, - int32_t serverArmor, const int32_t* serverStats) { + int32_t serverArmor, const int32_t* serverStats, + const int32_t* serverResists, + const game::GameHandler* gh) { // Sum equipment stats for item-query bonus display int32_t itemStr = 0, itemAgi = 0, itemSta = 0, itemInt = 0, itemSpi = 0; // Secondary stat sums from extraStats int32_t itemAP = 0, itemSP = 0, itemHit = 0, itemCrit = 0, itemHaste = 0; int32_t itemResil = 0, itemExpertise = 0, itemMp5 = 0, itemHp5 = 0; + int32_t itemDefense = 0, itemDodge = 0, itemParry = 0, itemBlock = 0, itemBlockVal = 0; + int32_t itemArmorPen = 0, itemSpellPen = 0; for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { const auto& slot = inventory.getEquipSlot(static_cast(s)); if (slot.empty()) continue; @@ -1558,15 +1766,22 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play itemSpi += slot.item.spirit; for (const auto& es : slot.item.extraStats) { switch (es.statType) { - case 16: case 17: case 18: case 31: itemHit += es.statValue; break; - case 19: case 20: case 21: case 32: itemCrit += es.statValue; break; - case 28: case 29: case 30: case 36: itemHaste += es.statValue; break; - case 35: itemResil += es.statValue; break; + case 12: itemDefense += es.statValue; break; + case 13: itemDodge += es.statValue; break; + case 14: itemParry += es.statValue; break; + case 15: itemBlock += es.statValue; break; + case 16: case 17: case 18: case 31: itemHit += es.statValue; break; + case 19: case 20: case 21: case 32: itemCrit += es.statValue; break; + case 28: case 29: case 30: case 36: itemHaste += es.statValue; break; + case 35: itemResil += es.statValue; break; case 37: itemExpertise += es.statValue; break; - case 38: case 39: itemAP += es.statValue; break; - case 41: case 42: case 45: itemSP += es.statValue; break; - case 43: itemMp5 += es.statValue; break; - case 46: itemHp5 += es.statValue; break; + case 38: case 39: itemAP += es.statValue; break; + case 41: case 42: case 45: itemSP += es.statValue; break; + case 43: itemMp5 += es.statValue; break; + case 44: itemArmorPen += es.statValue; break; + case 46: itemHp5 += es.statValue; break; + case 47: itemSpellPen += es.statValue; break; + case 48: itemBlockVal += es.statValue; break; default: break; } } @@ -1581,17 +1796,54 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play } int32_t totalArmor = (serverArmor > 0) ? serverArmor : itemQueryArmor; + // Average item level (exclude shirt/tabard as WoW convention) + { + uint32_t iLvlSum = 0; + int iLvlCount = 0; + for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { + auto eslot = static_cast(s); + if (eslot == game::EquipSlot::SHIRT || eslot == game::EquipSlot::TABARD) continue; + const auto& slot = inventory.getEquipSlot(eslot); + if (!slot.empty() && slot.item.itemLevel > 0) { + iLvlSum += slot.item.itemLevel; + ++iLvlCount; + } + } + if (iLvlCount > 0) { + float avg = static_cast(iLvlSum) / static_cast(iLvlCount); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), + "Average Item Level: %.1f (%d/%d slots)", avg, iLvlCount, + game::Inventory::NUM_EQUIP_SLOTS - 2); // -2 for shirt/tabard + } + ImGui::Separator(); + } + ImVec4 green(0.0f, 1.0f, 0.0f, 1.0f); ImVec4 white(1.0f, 1.0f, 1.0f, 1.0f); ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f); ImVec4 gray(0.6f, 0.6f, 0.6f, 1.0f); + static const char* kStatTooltips[5] = { + "Increases your melee attack power by 2.\nIncreases your block value.", + "Increases your Armor.\nIncreases ranged attack power by 2.\nIncreases your chance to dodge attacks and score critical strikes.", + "Increases Health by 10 per point.", + "Increases your Mana pool.\nIncreases your chance to score a critical strike with spells.", + "Increases Health and Mana regeneration." + }; + // Armor (no base) + ImGui::BeginGroup(); if (totalArmor > 0) { ImGui::TextColored(gold, "Armor: %d", totalArmor); } else { ImGui::TextColored(gray, "Armor: 0"); } + ImGui::EndGroup(); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextWrapped("Reduces damage taken from physical attacks."); + ImGui::EndTooltip(); + } if (serverStats) { // Server-authoritative stats from UNIT_FIELD_STAT0-4: show total and item bonus. @@ -1601,6 +1853,7 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play for (int i = 0; i < 5; ++i) { int32_t total = serverStats[i]; int32_t bonus = itemBonuses[i]; + ImGui::BeginGroup(); if (bonus > 0) { ImGui::TextColored(white, "%s: %d", statNames[i], total); ImGui::SameLine(); @@ -1608,12 +1861,19 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play } else { ImGui::TextColored(gray, "%s: %d", statNames[i], total); } + ImGui::EndGroup(); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextWrapped("%s", kStatTooltips[i]); + ImGui::EndTooltip(); + } } } else { // Fallback: estimated base (20 + level) plus item query bonuses. int32_t baseStat = 20 + static_cast(playerLevel); - auto renderStat = [&](const char* name, int32_t equipBonus) { + auto renderStat = [&](const char* name, int32_t equipBonus, const char* tooltip) { int32_t total = baseStat + equipBonus; + ImGui::BeginGroup(); if (equipBonus > 0) { ImGui::TextColored(white, "%s: %d", name, total); ImGui::SameLine(); @@ -1621,34 +1881,246 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play } else { ImGui::TextColored(gray, "%s: %d", name, total); } + ImGui::EndGroup(); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextWrapped("%s", tooltip); + ImGui::EndTooltip(); + } }; - renderStat("Strength", itemStr); - renderStat("Agility", itemAgi); - renderStat("Stamina", itemSta); - renderStat("Intellect", itemInt); - renderStat("Spirit", itemSpi); + renderStat("Strength", itemStr, kStatTooltips[0]); + renderStat("Agility", itemAgi, kStatTooltips[1]); + renderStat("Stamina", itemSta, kStatTooltips[2]); + renderStat("Intellect", itemInt, kStatTooltips[3]); + renderStat("Spirit", itemSpi, kStatTooltips[4]); } // Secondary stats from equipped items bool hasSecondary = itemAP || itemSP || itemHit || itemCrit || itemHaste || - itemResil || itemExpertise || itemMp5 || itemHp5; + itemResil || itemExpertise || itemMp5 || itemHp5 || + itemDefense || itemDodge || itemParry || itemBlock || itemBlockVal || + itemArmorPen || itemSpellPen; if (hasSecondary) { ImGui::Spacing(); ImGui::Separator(); - auto renderSecondary = [&](const char* name, int32_t val) { + auto renderSecondary = [&](const char* name, int32_t val, const char* tooltip) { if (val > 0) { + ImGui::BeginGroup(); ImGui::TextColored(green, "+%d %s", val, name); + ImGui::EndGroup(); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextWrapped("%s", tooltip); + ImGui::EndTooltip(); + } } }; - renderSecondary("Attack Power", itemAP); - renderSecondary("Spell Power", itemSP); - renderSecondary("Hit Rating", itemHit); - renderSecondary("Crit Rating", itemCrit); - renderSecondary("Haste Rating", itemHaste); - renderSecondary("Resilience", itemResil); - renderSecondary("Expertise", itemExpertise); - renderSecondary("Mana per 5 sec", itemMp5); - renderSecondary("Health per 5 sec",itemHp5); + renderSecondary("Attack Power", itemAP, "Increases the damage of your melee and ranged attacks."); + renderSecondary("Spell Power", itemSP, "Increases the damage and healing of your spells."); + renderSecondary("Hit Rating", itemHit, "Reduces the chance your attacks will miss."); + renderSecondary("Crit Rating", itemCrit, "Increases your critical strike chance."); + renderSecondary("Haste Rating", itemHaste, "Increases attack speed and spell casting speed."); + renderSecondary("Resilience", itemResil, "Reduces the chance you will be critically hit.\nReduces damage taken from critical hits."); + renderSecondary("Expertise", itemExpertise,"Reduces the chance your attacks will be dodged or parried."); + renderSecondary("Defense Rating", itemDefense, "Reduces the chance enemies will critically hit you."); + renderSecondary("Dodge Rating", itemDodge, "Increases your chance to dodge attacks."); + renderSecondary("Parry Rating", itemParry, "Increases your chance to parry attacks."); + renderSecondary("Block Rating", itemBlock, "Increases your chance to block attacks with your shield."); + renderSecondary("Block Value", itemBlockVal, "Increases the amount of damage your shield blocks."); + renderSecondary("Armor Penetration",itemArmorPen, "Reduces the armor of your target."); + renderSecondary("Spell Penetration",itemSpellPen, "Reduces your target's resistance to your spells."); + renderSecondary("Mana per 5 sec", itemMp5, "Restores mana every 5 seconds, even while casting."); + renderSecondary("Health per 5 sec", itemHp5, "Restores health every 5 seconds."); + } + + // Elemental resistances from server update fields + if (serverResists) { + static const char* kResistNames[6] = { + "Holy Resistance", "Fire Resistance", "Nature Resistance", + "Frost Resistance", "Shadow Resistance", "Arcane Resistance" + }; + bool hasResist = false; + for (int i = 0; i < 6; ++i) { + if (serverResists[i] > 0) { hasResist = true; break; } + } + if (hasResist) { + ImGui::Spacing(); + ImGui::Separator(); + for (int i = 0; i < 6; ++i) { + if (serverResists[i] > 0) { + ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 1.0f), + "%s: %d", kResistNames[i], serverResists[i]); + } + } + } + } + + // Server-authoritative combat stats (WotLK update fields — only shown when received) + if (gh) { + int32_t meleeAP = gh->getMeleeAttackPower(); + int32_t rangedAP = gh->getRangedAttackPower(); + int32_t spellPow = gh->getSpellPower(); + int32_t healPow = gh->getHealingPower(); + float dodgePct = gh->getDodgePct(); + float parryPct = gh->getParryPct(); + float blockPct = gh->getBlockPct(); + float critPct = gh->getCritPct(); + float rCritPct = gh->getRangedCritPct(); + float sCritPct = gh->getSpellCritPct(1); // Holy school (avg proxy for spell crit) + // Hit rating: CR_HIT_MELEE=5, CR_HIT_RANGED=6, CR_HIT_SPELL=7 + // Haste rating: CR_HASTE_MELEE=17, CR_HASTE_RANGED=18, CR_HASTE_SPELL=19 + // Other: CR_EXPERTISE=23, CR_ARMOR_PENETRATION=24, CR_CRIT_TAKEN_MELEE=14 + int32_t hitRating = gh->getCombatRating(5); + int32_t hitRangedR = gh->getCombatRating(6); + int32_t hitSpellR = gh->getCombatRating(7); + int32_t expertiseR = gh->getCombatRating(23); + int32_t hasteR = gh->getCombatRating(17); + int32_t hasteRangedR = gh->getCombatRating(18); + int32_t hasteSpellR = gh->getCombatRating(19); + int32_t armorPenR = gh->getCombatRating(24); + int32_t resilR = gh->getCombatRating(14); // CR_CRIT_TAKEN_MELEE = Resilience + + bool hasAny = (meleeAP >= 0 || spellPow >= 0 || dodgePct >= 0.0f || parryPct >= 0.0f || + blockPct >= 0.0f || critPct >= 0.0f || hitRating >= 0); + if (hasAny) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Combat"); + ImVec4 cyan(0.5f, 0.9f, 1.0f, 1.0f); + if (meleeAP >= 0) ImGui::TextColored(cyan, "Attack Power: %d", meleeAP); + if (rangedAP >= 0 && rangedAP != meleeAP) + ImGui::TextColored(cyan, "Ranged Attack Power: %d", rangedAP); + if (spellPow >= 0) ImGui::TextColored(cyan, "Spell Power: %d", spellPow); + if (healPow >= 0 && healPow != spellPow) + ImGui::TextColored(cyan, "Healing Power: %d", healPow); + if (dodgePct >= 0.0f) ImGui::TextColored(cyan, "Dodge: %.2f%%", dodgePct); + if (parryPct >= 0.0f) ImGui::TextColored(cyan, "Parry: %.2f%%", parryPct); + if (blockPct >= 0.0f) ImGui::TextColored(cyan, "Block: %.2f%%", blockPct); + if (critPct >= 0.0f) ImGui::TextColored(cyan, "Melee Crit: %.2f%%", critPct); + if (rCritPct >= 0.0f) ImGui::TextColored(cyan, "Ranged Crit: %.2f%%", rCritPct); + if (sCritPct >= 0.0f) ImGui::TextColored(cyan, "Spell Crit: %.2f%%", sCritPct); + + // Combat ratings with percentage conversion (WotLK level-80 divisors scaled by level). + // Formula: pct = rating / (divisorAt80 * pow(level/80.0, 0.93)) + // Level-80 divisors derived from gtCombatRatings.dbc (well-known WotLK constants): + // Hit: 26.23, Expertise: 8.19/expertise (0.25% each), + // Haste: 32.79, ArmorPen: 13.99, Resilience: 94.27 + uint32_t level = playerLevel > 0 ? playerLevel : gh->getPlayerLevel(); + if (level == 0) level = 80; + double lvlScale = level <= 80 + ? std::pow(static_cast(level) / 80.0, 0.93) + : 1.0; + + auto ratingPct = [&](int32_t rating, double divisorAt80) -> float { + if (rating < 0 || divisorAt80 <= 0.0) return -1.0f; + double d = divisorAt80 * lvlScale; + return static_cast(rating / d); + }; + + if (hitRating >= 0) { + float pct = ratingPct(hitRating, 26.23); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Hit Rating: %d (%.2f%%)", hitRating, pct); + else + ImGui::TextColored(cyan, "Hit Rating: %d", hitRating); + } + // Show ranged/spell hit only when they differ from melee hit + if (hitRangedR >= 0 && hitRangedR != hitRating) { + float pct = ratingPct(hitRangedR, 26.23); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Ranged Hit Rating: %d (%.2f%%)", hitRangedR, pct); + else + ImGui::TextColored(cyan, "Ranged Hit Rating: %d", hitRangedR); + } + if (hitSpellR >= 0 && hitSpellR != hitRating) { + // Spell hit cap at 17% (446 rating at 80); divisor same as melee hit + float pct = ratingPct(hitSpellR, 26.23); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Spell Hit Rating: %d (%.2f%%)", hitSpellR, pct); + else + ImGui::TextColored(cyan, "Spell Hit Rating: %d", hitSpellR); + } + if (expertiseR >= 0) { + // Each expertise point reduces dodge and parry chance by 0.25% + // expertise_points = rating / 8.19 + float exp_pts = ratingPct(expertiseR, 8.19); + if (exp_pts >= 0.0f) { + float exp_pct = exp_pts * 0.25f; // % dodge/parry reduction + ImGui::TextColored(cyan, "Expertise: %d (%.1f / %.2f%%)", + expertiseR, exp_pts, exp_pct); + } else { + ImGui::TextColored(cyan, "Expertise Rating: %d", expertiseR); + } + } + if (hasteR >= 0) { + float pct = ratingPct(hasteR, 32.79); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Haste Rating: %d (%.2f%%)", hasteR, pct); + else + ImGui::TextColored(cyan, "Haste Rating: %d", hasteR); + } + if (hasteRangedR >= 0 && hasteRangedR != hasteR) { + float pct = ratingPct(hasteRangedR, 32.79); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Ranged Haste Rating: %d (%.2f%%)", hasteRangedR, pct); + else + ImGui::TextColored(cyan, "Ranged Haste Rating: %d", hasteRangedR); + } + if (hasteSpellR >= 0 && hasteSpellR != hasteR) { + float pct = ratingPct(hasteSpellR, 32.79); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Spell Haste Rating: %d (%.2f%%)", hasteSpellR, pct); + else + ImGui::TextColored(cyan, "Spell Haste Rating: %d", hasteSpellR); + } + if (armorPenR >= 0) { + float pct = ratingPct(armorPenR, 13.99); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Armor Pen: %d (%.2f%%)", armorPenR, pct); + else + ImGui::TextColored(cyan, "Armor Penetration: %d", armorPenR); + } + if (resilR >= 0) { + // Resilience: reduces crit chance against you by pct%, and crit damage by 2*pct% + float pct = ratingPct(resilR, 94.27); + if (pct >= 0.0f) + ImGui::TextColored(cyan, "Resilience: %d (%.2f%%)", resilR, pct); + else + ImGui::TextColored(cyan, "Resilience: %d", resilR); + } + } + + // Movement speeds (always show when non-default) + { + constexpr float kBaseRun = 7.0f; + constexpr float kBaseFlight = 7.0f; + float runSpeed = gh->getServerRunSpeed(); + float flightSpeed = gh->getServerFlightSpeed(); + float swimSpeed = gh->getServerSwimSpeed(); + + bool showRun = runSpeed > 0.0f && std::fabs(runSpeed - kBaseRun) > 0.05f; + bool showFlight = flightSpeed > 0.0f && std::fabs(flightSpeed - kBaseFlight) > 0.05f; + bool showSwim = swimSpeed > 0.0f && std::fabs(swimSpeed - 4.722f) > 0.05f; + + if (showRun || showFlight || showSwim) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Movement"); + ImVec4 speedColor(0.6f, 1.0f, 0.8f, 1.0f); + if (showRun) { + float pct = (runSpeed / kBaseRun) * 100.0f; + ImGui::TextColored(speedColor, "Run Speed: %.1f%%", pct); + } + if (showFlight) { + float pct = (flightSpeed / kBaseFlight) * 100.0f; + ImGui::TextColored(speedColor, "Flight Speed: %.1f%%", pct); + } + if (showSwim) { + float pct = (swimSpeed / 4.722f) * 100.0f; + ImGui::TextColored(speedColor, "Swim Speed: %.1f%%", pct); + } + } + } } } @@ -1698,6 +2170,31 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory, bool colla ImGui::PopID(); } } + + if (showKeyring_) { + constexpr float keySlotSize = 24.0f; + constexpr int keyCols = 8; + int lastOccupied = -1; + for (int i = inventory.getKeyringSize() - 1; i >= 0; --i) { + if (!inventory.getKeyringSlot(i).empty()) { lastOccupied = i; break; } + } + int visibleSlots = (lastOccupied < 0) ? 0 : ((lastOccupied / keyCols) + 1) * keyCols; + if (visibleSlots > 0) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "Keyring"); + for (int i = 0; i < visibleSlots; ++i) { + if (i % keyCols != 0) ImGui::SameLine(); + const auto& slot = inventory.getKeyringSlot(i); + char sid[32]; + snprintf(sid, sizeof(sid), "##keyring_%d", i); + ImGui::PushID(sid); + renderItemSlot(inventory, slot, keySlotSize, nullptr, + SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS); + ImGui::PopID(); + } + } + } } void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot, @@ -1768,7 +2265,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite if (label && ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label); + ImGui::TextColored(ui::colors::kDarkGray, "%s", label); ImGui::TextColored(ImVec4(0.4f, 0.4f, 0.4f, 1.0f), "Empty"); ImGui::EndTooltip(); } @@ -1877,34 +2374,51 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite } } - // Shift+right-click: open destroy confirmation for non-quest items + // Shift+right-click: split stack (if stackable >1) or destroy confirmation if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && - !holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0 && item.bindType != 4) { - destroyConfirmOpen_ = true; - destroyItemName_ = item.name; - destroyCount_ = static_cast(std::clamp( - std::max(1u, item.stackCount), 1u, 255u)); - if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { - destroyBag_ = 0xFF; - destroySlot_ = static_cast(23 + backpackIndex); - } else if (kind == SlotKind::BACKPACK && isBagSlot) { - destroyBag_ = static_cast(19 + bagIndex); - destroySlot_ = static_cast(bagSlotIndex); - } else if (kind == SlotKind::EQUIPMENT) { - destroyBag_ = 0xFF; - destroySlot_ = static_cast(equipSlot); + !holdingItem && ImGui::GetIO().KeyShift && item.itemId != 0) { + if (item.stackCount > 1 && item.maxStack > 1) { + // Open split popup for stackable items + splitConfirmOpen_ = true; + splitItemName_ = item.name; + splitMax_ = static_cast(item.stackCount); + splitCount_ = splitMax_ / 2; + if (splitCount_ < 1) splitCount_ = 1; + if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + splitBag_ = 0xFF; + splitSlot_ = static_cast(23 + backpackIndex); + } else if (kind == SlotKind::BACKPACK && isBagSlot) { + splitBag_ = static_cast(19 + bagIndex); + splitSlot_ = static_cast(bagSlotIndex); + } + } else if (item.bindType != 4) { + // Destroy confirmation for non-quest, non-stackable items + destroyConfirmOpen_ = true; + destroyItemName_ = item.name; + destroyCount_ = static_cast(std::clamp( + std::max(1u, item.stackCount), 1u, 255u)); + if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { + destroyBag_ = 0xFF; + destroySlot_ = static_cast(23 + backpackIndex); + } else if (kind == SlotKind::BACKPACK && isBagSlot) { + destroyBag_ = static_cast(19 + bagIndex); + destroySlot_ = static_cast(bagSlotIndex); + } else if (kind == SlotKind::EQUIPMENT) { + destroyBag_ = 0xFF; + destroySlot_ = static_cast(equipSlot); + } } } // Right-click: bank deposit (if bank open), vendor sell (if vendor mode), or auto-equip/use // Note: InvisibleButton only tracks left-click by default, so use IsItemHovered+IsMouseClicked if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !holdingItem && !ImGui::GetIO().KeyShift && gameHandler_) { - LOG_WARNING("Right-click slot: kind=", (int)kind, + LOG_WARNING("Right-click slot: kind=", static_cast(kind), " backpackIndex=", backpackIndex, " bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex, " vendorMode=", vendorMode_, " bankOpen=", gameHandler_->isBankOpen(), - " item='", item.name, "' invType=", (int)item.inventoryType); + " item='", item.name, "' invType=", static_cast(item.inventoryType)); if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && backpackIndex >= 0) { gameHandler_->attachItemFromBackpack(backpackIndex); } else if (gameHandler_->isMailComposeOpen() && kind == SlotKind::BACKPACK && isBagSlot) { @@ -1918,25 +2432,45 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite } else if (vendorMode_ && kind == SlotKind::BACKPACK && isBagSlot) { gameHandler_->sellItemInBag(bagIndex, bagSlotIndex); } else if (kind == SlotKind::EQUIPMENT) { - LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot); + LOG_INFO("UI unequip request: equipSlot=", static_cast(equipSlot)); gameHandler_->unequipToBackpack(equipSlot); } else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) { LOG_INFO("Right-click backpack item: name='", item.name, - "' inventoryType=", (int)item.inventoryType, - " itemId=", item.itemId); - if (item.inventoryType > 0) { + "' inventoryType=", static_cast(item.inventoryType), + " itemId=", item.itemId, + " startQuestId=", item.startQuestId); + if (item.startQuestId != 0) { + uint64_t iGuid = gameHandler_->getBackpackItemGuid(backpackIndex); + gameHandler_->offerQuestFromItem(iGuid, item.startQuestId); + } else if (item.inventoryType > 0) { gameHandler_->autoEquipItemBySlot(backpackIndex); } else { - gameHandler_->useItemBySlot(backpackIndex); + // itemClass==1 (Container) with inventoryType==0 means a lockbox; + // use CMSG_OPEN_ITEM so the server checks keyring automatically. + auto* info = gameHandler_->getItemInfo(item.itemId); + if (info && info->valid && info->itemClass == 1) { + gameHandler_->openItemBySlot(backpackIndex); + } else { + gameHandler_->useItemBySlot(backpackIndex); + } } } else if (kind == SlotKind::BACKPACK && isBagSlot) { LOG_INFO("Right-click bag item: name='", item.name, - "' inventoryType=", (int)item.inventoryType, - " bagIndex=", bagIndex, " slotIndex=", bagSlotIndex); - if (item.inventoryType > 0) { + "' inventoryType=", static_cast(item.inventoryType), + " bagIndex=", bagIndex, " slotIndex=", bagSlotIndex, + " startQuestId=", item.startQuestId); + if (item.startQuestId != 0) { + uint64_t iGuid = gameHandler_->getBagItemGuid(bagIndex, bagSlotIndex); + gameHandler_->offerQuestFromItem(iGuid, item.startQuestId); + } else if (item.inventoryType > 0) { gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex); } else { - gameHandler_->useItemInBag(bagIndex, bagSlotIndex); + auto* info = gameHandler_->getItemInfo(item.itemId); + if (info && info->valid && info->itemClass == 1) { + gameHandler_->openItemInBag(bagIndex, bagSlotIndex); + } else { + gameHandler_->useItemInBag(bagIndex, bagSlotIndex); + } } } } @@ -1954,6 +2488,8 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite case game::ItemQuality::RARE: qualHex = "0070dd"; break; case game::ItemQuality::EPIC: qualHex = "a335ee"; break; case game::ItemQuality::LEGENDARY: qualHex = "ff8000"; break; + case game::ItemQuality::ARTIFACT: qualHex = "e6cc80"; break; + case game::ItemQuality::HEIRLOOM: qualHex = "e6cc80"; break; default: break; } char linkBuf[512]; @@ -1966,12 +2502,36 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite if (ImGui::IsItemHovered() && !holdingItem) { // Pass inventory for backpack/bag items only; equipped items compare against themselves otherwise const game::Inventory* tooltipInv = (kind == SlotKind::EQUIPMENT) ? nullptr : &inventory; - renderItemTooltip(item, tooltipInv); + uint64_t slotGuid = 0; + if (kind == SlotKind::EQUIPMENT && gameHandler_) + slotGuid = gameHandler_->getEquipSlotGuid(static_cast(equipSlot)); + renderItemTooltip(item, tooltipInv, slotGuid); } } } -void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory) { +void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::Inventory* inventory, uint64_t itemGuid) { + // Shared SpellItemEnchantment name lookup — used for socket gems, permanent and temp enchants. + static std::unordered_map s_enchLookupB; + static bool s_enchLookupLoadedB = false; + if (!s_enchLookupLoadedB && assetManager_) { + s_enchLookupLoadedB = true; + auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* lay = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr; + uint32_t nf = lay ? lay->field("Name") : 8u; + if (nf == 0xFFFFFFFF) nf = 8; + uint32_t fc = dbc->getFieldCount(); + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t eid = dbc->getUInt32(r, 0); + if (eid == 0 || nf >= fc) continue; + std::string en = dbc->getString(r, nf); + if (!en.empty()) s_enchLookupB[eid] = std::move(en); + } + } + } + ImGui::BeginTooltip(); ImVec4 qColor = getQualityColor(item.quality); @@ -1980,6 +2540,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", item.itemLevel); } + // Heroic / Unique / Unique-Equipped indicators + if (gameHandler_) { + const auto* qi = gameHandler_->getItemInfo(item.itemId); + if (qi && qi->valid) { + constexpr uint32_t kFlagHeroic = 0x8; + constexpr uint32_t kFlagUniqueEquipped = 0x1000000; + if (qi->itemFlags & kFlagHeroic) { + ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic"); + } + if (qi->maxCount == 1) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique"); + } else if (qi->itemFlags & kFlagUniqueEquipped) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped"); + } + } + } + // Binding type switch (item.bindType) { case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break; @@ -1993,18 +2570,26 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I uint32_t mapId = 0; glm::vec3 pos; if (gameHandler_->getHomeBind(mapId, pos)) { - const char* mapName = "Unknown"; - switch (mapId) { - case 0: mapName = "Eastern Kingdoms"; break; - case 1: mapName = "Kalimdor"; break; - case 530: mapName = "Outland"; break; - case 571: mapName = "Northrend"; break; - case 13: mapName = "Test"; break; - case 169: mapName = "Emerald Dream"; break; + std::string homeLocation; + // Prefer the specific zone name from the bind-point zone ID + uint32_t zoneId = gameHandler_->getHomeBindZoneId(); + if (zoneId != 0) + homeLocation = gameHandler_->getWhoAreaName(zoneId); + // Fall back to continent name if zone unavailable + if (homeLocation.empty()) { + switch (mapId) { + case 0: homeLocation = "Eastern Kingdoms"; break; + case 1: homeLocation = "Kalimdor"; break; + case 530: homeLocation = "Outland"; break; + case 571: homeLocation = "Northrend"; break; + case 13: homeLocation = "Test"; break; + case 169: homeLocation = "Emerald Dream"; break; + default: homeLocation = "Unknown"; break; + } } - ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", mapName); + ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Home: %s", homeLocation.c_str()); } else { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Home: not set"); + ImGui::TextColored(ui::colors::kLightGray, "Home: not set"); } ImGui::TextDisabled("Use: Teleport home"); } @@ -2042,9 +2627,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I } if (slotName[0]) { if (!item.subclassName.empty()) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, item.subclassName.c_str()); + ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, item.subclassName.c_str()); } else { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName); + ImGui::TextColored(ui::colors::kLightGray, "%s", slotName); + } + } + + // Show red warning if player lacks proficiency for this weapon/armor type + if (gameHandler_) { + const auto* qi = gameHandler_->getItemInfo(item.itemId); + if (qi && qi->valid) { + bool canUse = true; + if (qi->itemClass == 2) // Weapon + canUse = gameHandler_->canUseWeaponSubclass(qi->subClass); + else if (qi->itemClass == 4 && qi->subClass > 0) // Armor (skip subclass 0 = misc) + canUse = gameHandler_->canUseArmorSubclass(qi->subClass); + if (!canUse) + ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "You can't use this type of item."); } } } @@ -2072,7 +2671,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::Text("%.0f - %.0f Damage", item.damageMin, item.damageMax); ImGui::SameLine(160.0f); ImGui::TextDisabled("Speed %.2f", speed); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(%.1f damage per second)", dps); + ImGui::TextColored(ui::colors::kLightGray, "(%.1f damage per second)", dps); } // Armor appears before stat bonuses — matches WoW tooltip order @@ -2080,6 +2679,21 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::Text("%d Armor", item.armor); } + // Elemental resistances from item query cache (fire resist gear, nature resist gear, etc.) + if (gameHandler_) { + const auto* qi = gameHandler_->getItemInfo(item.itemId); + if (qi && qi->valid) { + const int32_t resValsI[6] = { qi->holyRes, qi->fireRes, qi->natureRes, + qi->frostRes, qi->shadowRes, qi->arcaneRes }; + static const char* resLabelsI[6] = { + "Holy Resistance", "Fire Resistance", "Nature Resistance", + "Frost Resistance", "Shadow Resistance", "Arcane Resistance" + }; + for (int i = 0; i < 6; ++i) + if (resValsI[i] > 0) ImGui::Text("+%d %s", resValsI[i], resLabelsI[i]); + } + } + auto appendBonus = [](std::string& out, int32_t val, const char* shortName) { if (val <= 0) return; if (!out.empty()) out += " "; @@ -2164,17 +2778,22 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I if (sp.spellId == 0) continue; const char* trigger = nullptr; switch (sp.spellTrigger) { - case 0: trigger = "Use"; break; - case 1: trigger = "Equip"; break; - case 2: trigger = "Chance on Hit"; break; - case 6: trigger = "Soulstone"; break; + case 0: trigger = "Use"; break; // on use + case 1: trigger = "Equip"; break; // on equip + case 2: trigger = "Chance on Hit"; break; // proc on melee hit + case 4: trigger = "Use"; break; // soulstone (still shows as Use) + case 5: trigger = "Use"; break; // on use, no delay + case 6: trigger = "Use"; break; // learn spell (recipe/pattern) default: break; } if (!trigger) continue; - const std::string& spName = gameHandler_->getSpellName(sp.spellId); - if (!spName.empty()) { + const std::string& spDesc = gameHandler_->getSpellDescription(sp.spellId); + const std::string& spText = spDesc.empty() ? gameHandler_->getSpellName(sp.spellId) : spDesc; + if (!spText.empty()) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f); ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), - "%s: %s", trigger, spName.c_str()); + "%s: %s", trigger, spText.c_str()); + ImGui::PopTextWrapPos(); } else { ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: Spell #%u", trigger, sp.spellId); @@ -2183,6 +2802,267 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I } } + // Skill / reputation requirements from item query cache + if (gameHandler_) { + const auto* qInfo = gameHandler_->getItemInfo(item.itemId); + if (qInfo && qInfo->valid) { + if (qInfo->requiredSkill != 0 && qInfo->requiredSkillRank > 0) { + static std::unordered_map s_skillNamesB; + static bool s_skillNamesLoadedB = false; + if (!s_skillNamesLoadedB && assetManager_) { + s_skillNamesLoadedB = true; + auto dbc = assetManager_->loadDBC("SkillLine.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0; + uint32_t nameF = layout ? (*layout)["Name"] : 2; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t sid = dbc->getUInt32(r, idF); + if (!sid) continue; + std::string sname = dbc->getString(r, nameF); + if (!sname.empty()) s_skillNamesB[sid] = std::move(sname); + } + } + } + uint32_t playerSkillVal = 0; + const auto& skills = gameHandler_->getPlayerSkills(); + auto skPit = skills.find(qInfo->requiredSkill); + if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue(); + bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= qInfo->requiredSkillRank); + ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + auto skIt = s_skillNamesB.find(qInfo->requiredSkill); + if (skIt != s_skillNamesB.end()) + ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), qInfo->requiredSkillRank); + else + ImGui::TextColored(skColor, "Requires Skill %u (%u)", qInfo->requiredSkill, qInfo->requiredSkillRank); + } + if (qInfo->requiredReputationFaction != 0 && qInfo->requiredReputationRank > 0) { + static std::unordered_map s_factionNamesB; + static bool s_factionNamesLoadedB = false; + if (!s_factionNamesLoadedB && assetManager_) { + s_factionNamesLoadedB = true; + auto dbc = assetManager_->loadDBC("Faction.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0; + uint32_t nameF = layout ? (*layout)["Name"] : 20; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t fid = dbc->getUInt32(r, idF); + if (!fid) continue; + std::string fname = dbc->getString(r, nameF); + if (!fname.empty()) s_factionNamesB[fid] = std::move(fname); + } + } + } + static const char* kRepRankNamesB[] = { + "Hated","Hostile","Unfriendly","Neutral","Friendly","Honored","Revered","Exalted" + }; + const char* rankName = (qInfo->requiredReputationRank < 8) + ? kRepRankNamesB[qInfo->requiredReputationRank] : "Unknown"; + auto fIt = s_factionNamesB.find(qInfo->requiredReputationFaction); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s", + rankName, + fIt != s_factionNamesB.end() ? fIt->second.c_str() : "Unknown Faction"); + } + // Class restriction + if (qInfo->allowableClass != 0) { + static const struct { uint32_t mask; const char* name; } kClassesB[] = { + { 1,"Warrior" },{ 2,"Paladin" },{ 4,"Hunter" },{ 8,"Rogue" }, + { 16,"Priest" },{ 32,"Death Knight" },{ 64,"Shaman" }, + { 128,"Mage" },{ 256,"Warlock" },{ 1024,"Druid" }, + }; + int mc = 0; + for (const auto& kc : kClassesB) if (qInfo->allowableClass & kc.mask) ++mc; + if (mc > 0 && mc < 10) { + char buf[128] = "Classes: "; bool first = true; + for (const auto& kc : kClassesB) { + if (!(qInfo->allowableClass & kc.mask)) continue; + if (!first) strncat(buf, ", ", sizeof(buf)-strlen(buf)-1); + strncat(buf, kc.name, sizeof(buf)-strlen(buf)-1); + first = false; + } + uint8_t pc = gameHandler_->getPlayerClass(); + uint32_t pm = (pc > 0 && pc <= 10) ? (1u << (pc-1)) : 0; + bool ok = (pm == 0 || (qInfo->allowableClass & pm)); + ImGui::TextColored(ok ? ImVec4(1,1,1,0.75f) : ImVec4(1,0.5f,0.5f,1), "%s", buf); + } + } + // Race restriction + if (qInfo->allowableRace != 0) { + static const struct { uint32_t mask; const char* name; } kRacesB[] = { + { 1,"Human" },{ 2,"Orc" },{ 4,"Dwarf" },{ 8,"Night Elf" }, + { 16,"Undead" },{ 32,"Tauren" },{ 64,"Gnome" },{ 128,"Troll" }, + { 512,"Blood Elf" },{ 1024,"Draenei" }, + }; + constexpr uint32_t kAll = 1|2|4|8|16|32|64|128|512|1024; + if ((qInfo->allowableRace & kAll) != kAll) { + int mc = 0; + for (const auto& kr : kRacesB) if (qInfo->allowableRace & kr.mask) ++mc; + if (mc > 0) { + char buf[160] = "Races: "; bool first = true; + for (const auto& kr : kRacesB) { + if (!(qInfo->allowableRace & kr.mask)) continue; + if (!first) strncat(buf, ", ", sizeof(buf)-strlen(buf)-1); + strncat(buf, kr.name, sizeof(buf)-strlen(buf)-1); + first = false; + } + uint8_t pr = gameHandler_->getPlayerRace(); + uint32_t pm = (pr > 0 && pr <= 11) ? (1u << (pr-1)) : 0; + bool ok = (pm == 0 || (qInfo->allowableRace & pm)); + ImGui::TextColored(ok ? ImVec4(1,1,1,0.75f) : ImVec4(1,0.5f,0.5f,1), "%s", buf); + } + } + } + } + } + + // Gem socket slots and item set — look up from query cache + if (gameHandler_) { + const auto* qi2 = gameHandler_->getItemInfo(item.itemId); + if (qi2 && qi2->valid) { + // Gem sockets + { + static const struct { uint32_t mask; const char* label; ImVec4 col; } kSocketTypes[] = { + { 1, "Meta Socket", { 0.7f, 0.7f, 0.9f, 1.0f } }, + { 2, "Red Socket", { 1.0f, 0.3f, 0.3f, 1.0f } }, + { 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } }, + { 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } }, + }; + // Get socket gem enchant IDs for this item (filled from item update fields) + std::array sockGems{}; + if (itemGuid != 0 && gameHandler_) + sockGems = gameHandler_->getItemSocketEnchantIds(itemGuid); + + bool hasSocket = false; + for (int i = 0; i < 3; ++i) { + if (qi2->socketColor[i] == 0) continue; + if (!hasSocket) { ImGui::Spacing(); hasSocket = true; } + for (const auto& st : kSocketTypes) { + if (qi2->socketColor[i] & st.mask) { + if (sockGems[i] != 0) { + auto git = s_enchLookupB.find(sockGems[i]); + if (git != s_enchLookupB.end()) + ImGui::TextColored(st.col, "%s: %s", st.label, git->second.c_str()); + else + ImGui::TextColored(st.col, "%s: (gem %u)", st.label, sockGems[i]); + } else { + ImGui::TextColored(st.col, "%s", st.label); + } + break; + } + } + } + if (hasSocket && qi2->socketBonus != 0) { + auto enchIt = s_enchLookupB.find(qi2->socketBonus); + if (enchIt != s_enchLookupB.end()) + ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str()); + else + ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", qi2->socketBonus); + } + } + // Item set membership + if (qi2->itemSetId != 0) { + struct SetEntryD { + std::string name; + std::array itemIds{}; + std::array spellIds{}; + std::array thresholds{}; + }; + static std::unordered_map s_setDataD; + static bool s_setDataLoadedD = false; + if (!s_setDataLoadedD && assetManager_) { + s_setDataLoadedD = true; + auto dbc = assetManager_->loadDBC("ItemSet.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr; + auto lf = [&](const char* k, uint32_t def) -> uint32_t { + return layout ? (*layout)[k] : def; + }; + uint32_t idF = lf("ID", 0), nameF = lf("Name", 1); + static const char* itemKeys[10] = { + "Item0","Item1","Item2","Item3","Item4", + "Item5","Item6","Item7","Item8","Item9" }; + static const char* spellKeys[10] = { + "Spell0","Spell1","Spell2","Spell3","Spell4", + "Spell5","Spell6","Spell7","Spell8","Spell9" }; + static const char* thrKeys[10] = { + "Threshold0","Threshold1","Threshold2","Threshold3","Threshold4", + "Threshold5","Threshold6","Threshold7","Threshold8","Threshold9" }; + uint32_t itemFB[10], spellFB[10], thrFB[10]; + for (int i = 0; i < 10; ++i) { + itemFB[i] = 18+i; spellFB[i] = 28+i; thrFB[i] = 38+i; + } + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t id = dbc->getUInt32(r, idF); + if (!id) continue; + SetEntryD e; + e.name = dbc->getString(r, nameF); + for (int i = 0; i < 10; ++i) { + e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : itemFB[i]); + e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : spellFB[i]); + e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : thrFB[i]); + } + s_setDataD[id] = std::move(e); + } + } + } + auto setIt = s_setDataD.find(qi2->itemSetId); + ImGui::Spacing(); + if (setIt != s_setDataD.end()) { + const SetEntryD& se = setIt->second; + int equipped = 0, total = 0; + for (int i = 0; i < 10; ++i) { + if (se.itemIds[i] == 0) continue; + ++total; + if (inventory) { + for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { + const auto& eSlot = inventory->getEquipSlot(static_cast(s)); + if (!eSlot.empty() && eSlot.item.itemId == se.itemIds[i]) { ++equipped; break; } + } + } + } + if (total > 0) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), + "%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total); + } else if (!se.name.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "%s", se.name.c_str()); + } + for (int i = 0; i < 10; ++i) { + if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue; + const std::string& bname = gameHandler_->getSpellName(se.spellIds[i]); + bool active = (equipped >= static_cast(se.thresholds[i])); + ImVec4 col = active ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f) + : ImVec4(0.55f, 0.55f, 0.55f, 1.0f); + if (!bname.empty()) + ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str()); + else + ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]); + } + } else { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Set (id %u)", qi2->itemSetId); + } + } + } + } + + // Weapon/armor enchant display for equipped items (reads from item update fields) + if (itemGuid != 0 && gameHandler_) { + auto [permId, tempId] = gameHandler_->getItemEnchantIds(itemGuid); + if (permId != 0) { + auto it2 = s_enchLookupB.find(permId); + const char* ename = (it2 != s_enchLookupB.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "Enchanted: %s", ename); + } + if (tempId != 0) { + auto it2 = s_enchLookupB.find(tempId); + const char* ename = (it2 != s_enchLookupB.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename); + } + } + // "Begins a Quest" line (shown in yellow-green like the game) if (item.startQuestId != 0) { ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Begins a Quest"); @@ -2194,10 +3074,8 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I } if (item.sellPrice > 0) { - uint32_t g = item.sellPrice / 10000; - uint32_t s = (item.sellPrice / 100) % 100; - uint32_t c = item.sellPrice % 100; - ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); + ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(item.sellPrice); } // Shift-hover comparison with currently equipped equivalent. @@ -2223,8 +3101,8 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I else std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (=)", item.itemLevel); ImVec4 ilvlColor = (diff > 0.0f) ? ImVec4(0.0f, 1.0f, 0.0f, 1.0f) - : (diff < 0.0f) ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) - : ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + : (diff < 0.0f) ? ui::colors::kRed + : ui::colors::kLightGray; ImGui::TextColored(ilvlColor, "%s", ilvlBuf); } @@ -2238,10 +3116,10 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "%s", buf); } else if (diff < 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▼%.0f)", label, newVal, -diff); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", buf); + ImGui::TextColored(ui::colors::kRed, "%s", buf); } else { std::snprintf(buf, sizeof(buf), "%s: %.0f (=)", label, newVal); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", buf); + ImGui::TextColored(ui::colors::kLightGray, "%s", buf); } }; @@ -2285,17 +3163,26 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I // Find a label for this stat type const char* lbl = nullptr; switch (t) { - case 31: lbl = "Hit"; break; - case 32: lbl = "Crit"; break; + case 0: lbl = "Mana"; break; + case 1: lbl = "Health"; break; + case 12: lbl = "Defense"; break; + case 13: lbl = "Dodge"; break; + case 14: lbl = "Parry"; break; + case 15: lbl = "Block Rating"; break; + case 16: case 17: case 18: case 31: lbl = "Hit"; break; + case 19: case 20: case 21: case 32: lbl = "Crit"; break; + case 28: case 29: case 30: case 36: lbl = "Haste"; break; case 35: lbl = "Resilience"; break; - case 36: lbl = "Haste"; break; case 37: lbl = "Expertise"; break; case 38: lbl = "Attack Power"; break; case 39: lbl = "Ranged AP"; break; + case 41: lbl = "Healing"; break; + case 42: lbl = "Spell Damage"; break; case 43: lbl = "MP5"; break; case 44: lbl = "Armor Pen"; break; case 45: lbl = "Spell Power"; break; case 46: lbl = "HP5"; break; + case 47: lbl = "Spell Pen"; break; case 48: lbl = "Block Value"; break; default: lbl = nullptr; break; } @@ -2303,6 +3190,10 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I showDiff(lbl, static_cast(nv), static_cast(ev)); } } + } else if (inventory && !ImGui::GetIO().KeyShift && item.inventoryType > 0) { + if (findComparableEquipped(*inventory, item.inventoryType)) { + ImGui::TextDisabled("Hold Shift to compare"); + } } // Destroy hint (not shown for quest items) @@ -2321,7 +3212,28 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I // --------------------------------------------------------------------------- // Tooltip overload for ItemQueryResponseData (used by loot window, etc.) // --------------------------------------------------------------------------- -void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) { +void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info, const game::Inventory* inventory, uint64_t itemGuid) { + // Shared SpellItemEnchantment name lookup — used for socket gems, socket bonus, and enchants. + static std::unordered_map s_enchLookup; + static bool s_enchLookupLoaded = false; + if (!s_enchLookupLoaded && assetManager_) { + s_enchLookupLoaded = true; + auto dbc = assetManager_->loadDBC("SpellItemEnchantment.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* lay = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment") : nullptr; + uint32_t nf = lay ? lay->field("Name") : 8u; + if (nf == 0xFFFFFFFF) nf = 8; + uint32_t fc = dbc->getFieldCount(); + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t eid = dbc->getUInt32(r, 0); + if (eid == 0 || nf >= fc) continue; + std::string en = dbc->getString(r, nf); + if (!en.empty()) s_enchLookup[eid] = std::move(en); + } + } + } + ImGui::BeginTooltip(); ImVec4 qColor = getQualityColor(static_cast(info.quality)); @@ -2330,6 +3242,18 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.7f), "Item Level %u", info.itemLevel); } + // Unique / Heroic indicators + constexpr uint32_t kFlagHeroic = 0x8; // ITEM_FLAG_HEROIC_TOOLTIP + constexpr uint32_t kFlagUniqueEquipped = 0x1000000; // ITEM_FLAG_UNIQUE_EQUIPPABLE + if (info.itemFlags & kFlagHeroic) { + ImGui::TextColored(ImVec4(0.0f, 0.8f, 0.0f, 1.0f), "Heroic"); + } + if (info.maxCount == 1) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique"); + } else if (info.itemFlags & kFlagUniqueEquipped) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Unique-Equipped"); + } + // Binding type switch (info.bindType) { case 1: ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Binds when picked up"); break; @@ -2372,9 +3296,20 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) } if (slotName[0]) { if (!info.subclassName.empty()) - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, info.subclassName.c_str()); + ImGui::TextColored(ui::colors::kLightGray, "%s %s", slotName, info.subclassName.c_str()); else - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName); + ImGui::TextColored(ui::colors::kLightGray, "%s", slotName); + } + + // Proficiency check for vendor/loot tooltips (ItemQueryResponseData has itemClass/subClass) + if (gameHandler_) { + bool canUse = true; + if (info.itemClass == 2) // Weapon + canUse = gameHandler_->canUseWeaponSubclass(info.subClass); + else if (info.itemClass == 4 && info.subClass > 0) // Armor (skip subclass 0 = misc) + canUse = gameHandler_->canUseArmorSubclass(info.subClass); + if (!canUse) + ImGui::TextColored(ImVec4(1.0f, 0.2f, 0.2f, 1.0f), "You can't use this type of item."); } } @@ -2389,11 +3324,23 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) ImGui::Text("%.0f - %.0f Damage", info.damageMin, info.damageMax); ImGui::SameLine(160.0f); ImGui::TextDisabled("Speed %.2f", speed); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "(%.1f damage per second)", dps); + ImGui::TextColored(ui::colors::kLightGray, "(%.1f damage per second)", dps); } if (info.armor > 0) ImGui::Text("%d Armor", info.armor); + // Elemental resistances (fire resist gear, nature resist gear, etc.) + { + const int32_t resVals[6] = { info.holyRes, info.fireRes, info.natureRes, + info.frostRes, info.shadowRes, info.arcaneRes }; + static const char* resLabels[6] = { + "Holy Resistance", "Fire Resistance", "Nature Resistance", + "Frost Resistance", "Shadow Resistance", "Arcane Resistance" + }; + for (int i = 0; i < 6; ++i) + if (resVals[i] > 0) ImGui::Text("+%d %s", resVals[i], resLabels[i]); + } + auto appendBonus = [](std::string& out, int32_t val, const char* name) { if (val <= 0) return; if (!out.empty()) out += " "; @@ -2446,23 +3393,333 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) ImGui::TextColored(reqColor, "Requires Level %u", info.requiredLevel); } + // Required skill (e.g. "Requires Engineering (300)") + if (info.requiredSkill != 0 && info.requiredSkillRank > 0) { + // Lazy-load SkillLine.dbc names + static std::unordered_map s_skillNames; + static bool s_skillNamesLoaded = false; + if (!s_skillNamesLoaded && assetManager_) { + s_skillNamesLoaded = true; + auto dbc = assetManager_->loadDBC("SkillLine.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0; + uint32_t nameF = layout ? (*layout)["Name"] : 2; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t sid = dbc->getUInt32(r, idF); + if (!sid) continue; + std::string sname = dbc->getString(r, nameF); + if (!sname.empty()) s_skillNames[sid] = std::move(sname); + } + } + } + uint32_t playerSkillVal = 0; + if (gameHandler_) { + const auto& skills = gameHandler_->getPlayerSkills(); + auto skPit = skills.find(info.requiredSkill); + if (skPit != skills.end()) playerSkillVal = skPit->second.effectiveValue(); + } + bool meetsSkill = (playerSkillVal == 0 || playerSkillVal >= info.requiredSkillRank); + ImVec4 skColor = meetsSkill ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + auto skIt = s_skillNames.find(info.requiredSkill); + if (skIt != s_skillNames.end()) + ImGui::TextColored(skColor, "Requires %s (%u)", skIt->second.c_str(), info.requiredSkillRank); + else + ImGui::TextColored(skColor, "Requires Skill %u (%u)", info.requiredSkill, info.requiredSkillRank); + } + + // Required reputation (e.g. "Requires Exalted with Argent Dawn") + if (info.requiredReputationFaction != 0 && info.requiredReputationRank > 0) { + static std::unordered_map s_factionNames; + static bool s_factionNamesLoaded = false; + if (!s_factionNamesLoaded && assetManager_) { + s_factionNamesLoaded = true; + auto dbc = assetManager_->loadDBC("Faction.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("Faction") : nullptr; + uint32_t idF = layout ? (*layout)["ID"] : 0; + uint32_t nameF = layout ? (*layout)["Name"] : 20; + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t fid = dbc->getUInt32(r, idF); + if (!fid) continue; + std::string fname = dbc->getString(r, nameF); + if (!fname.empty()) s_factionNames[fid] = std::move(fname); + } + } + } + static const char* kRepRankNames[] = { + "Hated", "Hostile", "Unfriendly", "Neutral", + "Friendly", "Honored", "Revered", "Exalted" + }; + const char* rankName = (info.requiredReputationRank < 8) + ? kRepRankNames[info.requiredReputationRank] : "Unknown"; + auto fIt = s_factionNames.find(info.requiredReputationFaction); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.75f), "Requires %s with %s", + rankName, + fIt != s_factionNames.end() ? fIt->second.c_str() : "Unknown Faction"); + } + + // Class restriction (e.g. "Classes: Paladin, Warrior") + if (info.allowableClass != 0) { + static const struct { uint32_t mask; const char* name; } kClasses[] = { + { 1, "Warrior" }, + { 2, "Paladin" }, + { 4, "Hunter" }, + { 8, "Rogue" }, + { 16, "Priest" }, + { 32, "Death Knight" }, + { 64, "Shaman" }, + { 128, "Mage" }, + { 256, "Warlock" }, + { 1024, "Druid" }, + }; + // Count matching classes + int matchCount = 0; + for (const auto& kc : kClasses) + if (info.allowableClass & kc.mask) ++matchCount; + // Only show if restricted to a subset (not all classes) + if (matchCount > 0 && matchCount < 10) { + char classBuf[128] = "Classes: "; + bool first = true; + for (const auto& kc : kClasses) { + if (!(info.allowableClass & kc.mask)) continue; + if (!first) strncat(classBuf, ", ", sizeof(classBuf) - strlen(classBuf) - 1); + strncat(classBuf, kc.name, sizeof(classBuf) - strlen(classBuf) - 1); + first = false; + } + // Check if player's class is allowed + bool playerAllowed = true; + if (gameHandler_) { + uint8_t pc = gameHandler_->getPlayerClass(); + uint32_t pmask = (pc > 0 && pc <= 10) ? (1u << (pc - 1)) : 0; + playerAllowed = (pmask == 0 || (info.allowableClass & pmask)); + } + ImVec4 clColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + ImGui::TextColored(clColor, "%s", classBuf); + } + } + + // Race restriction (e.g. "Races: Night Elf, Human") + if (info.allowableRace != 0) { + static const struct { uint32_t mask; const char* name; } kRaces[] = { + { 1, "Human" }, + { 2, "Orc" }, + { 4, "Dwarf" }, + { 8, "Night Elf" }, + { 16, "Undead" }, + { 32, "Tauren" }, + { 64, "Gnome" }, + { 128, "Troll" }, + { 512, "Blood Elf" }, + { 1024, "Draenei" }, + }; + constexpr uint32_t kAllPlayable = 1|2|4|8|16|32|64|128|512|1024; + // Only show if not all playable races are allowed + if ((info.allowableRace & kAllPlayable) != kAllPlayable) { + int matchCount = 0; + for (const auto& kr : kRaces) + if (info.allowableRace & kr.mask) ++matchCount; + if (matchCount > 0) { + char raceBuf[160] = "Races: "; + bool first = true; + for (const auto& kr : kRaces) { + if (!(info.allowableRace & kr.mask)) continue; + if (!first) strncat(raceBuf, ", ", sizeof(raceBuf) - strlen(raceBuf) - 1); + strncat(raceBuf, kr.name, sizeof(raceBuf) - strlen(raceBuf) - 1); + first = false; + } + bool playerAllowed = true; + if (gameHandler_) { + uint8_t pr = gameHandler_->getPlayerRace(); + uint32_t pmask = (pr > 0 && pr <= 11) ? (1u << (pr - 1)) : 0; + playerAllowed = (pmask == 0 || (info.allowableRace & pmask)); + } + ImVec4 rColor = playerAllowed ? ImVec4(1.0f, 1.0f, 1.0f, 0.75f) : ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + ImGui::TextColored(rColor, "%s", raceBuf); + } + } + } + // Spell effects for (const auto& sp : info.spells) { if (sp.spellId == 0) continue; const char* trigger = nullptr; switch (sp.spellTrigger) { - case 0: trigger = "Use"; break; - case 1: trigger = "Equip"; break; - case 2: trigger = "Chance on Hit"; break; + case 0: trigger = "Use"; break; // on use + case 1: trigger = "Equip"; break; // on equip + case 2: trigger = "Chance on Hit"; break; // proc on melee hit + case 4: trigger = "Use"; break; // soulstone (still shows as Use) + case 5: trigger = "Use"; break; // on use, no delay + case 6: trigger = "Use"; break; // learn spell (recipe/pattern) default: break; } if (!trigger) continue; if (gameHandler_) { - const std::string& spName = gameHandler_->getSpellName(sp.spellId); - if (!spName.empty()) + // Prefer the spell's tooltip text (the actual effect description). + // Fall back to the spell name if the description is empty. + const std::string& spDesc = gameHandler_->getSpellDescription(sp.spellId); + const std::string& spName = spDesc.empty() ? gameHandler_->getSpellName(sp.spellId) : spDesc; + if (!spName.empty()) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f); ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: %s", trigger, spName.c_str()); - else + ImGui::PopTextWrapPos(); + } else { ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "%s: Spell #%u", trigger, sp.spellId); + } + } + } + + // Gem socket slots + { + static const struct { uint32_t mask; const char* label; ImVec4 col; } kSocketTypes[] = { + { 1, "Meta Socket", { 0.7f, 0.7f, 0.9f, 1.0f } }, + { 2, "Red Socket", { 1.0f, 0.3f, 0.3f, 1.0f } }, + { 4, "Yellow Socket", { 1.0f, 0.9f, 0.3f, 1.0f } }, + { 8, "Blue Socket", { 0.3f, 0.6f, 1.0f, 1.0f } }, + }; + // Get socket gem enchant IDs for this item (filled from item update fields) + std::array sockGems{}; + if (itemGuid != 0 && gameHandler_) + sockGems = gameHandler_->getItemSocketEnchantIds(itemGuid); + + bool hasSocket = false; + for (int i = 0; i < 3; ++i) { + if (info.socketColor[i] == 0) continue; + if (!hasSocket) { ImGui::Spacing(); hasSocket = true; } + for (const auto& st : kSocketTypes) { + if (info.socketColor[i] & st.mask) { + if (sockGems[i] != 0) { + auto git = s_enchLookup.find(sockGems[i]); + if (git != s_enchLookup.end()) + ImGui::TextColored(st.col, "%s: %s", st.label, git->second.c_str()); + else + ImGui::TextColored(st.col, "%s: (gem %u)", st.label, sockGems[i]); + } else { + ImGui::TextColored(st.col, "%s", st.label); + } + break; + } + } + } + if (hasSocket && info.socketBonus != 0) { + auto enchIt = s_enchLookup.find(info.socketBonus); + if (enchIt != s_enchLookup.end()) + ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: %s", enchIt->second.c_str()); + else + ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "Socket Bonus: (id %u)", info.socketBonus); + } + } + + // Weapon/armor enchant display for equipped items + if (itemGuid != 0 && gameHandler_) { + auto [permId, tempId] = gameHandler_->getItemEnchantIds(itemGuid); + if (permId != 0) { + auto it2 = s_enchLookup.find(permId); + const char* ename = (it2 != s_enchLookup.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.0f, 0.8f, 1.0f, 1.0f), "Enchanted: %s", ename); + } + if (tempId != 0) { + auto it2 = s_enchLookup.find(tempId); + const char* ename = (it2 != s_enchLookup.end()) ? it2->second.c_str() : nullptr; + if (ename) ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.4f, 1.0f), "%s (temporary)", ename); + } + } + + // Item set membership + if (info.itemSetId != 0) { + // Lazy-load full ItemSet.dbc data (name + item IDs + bonus spells/thresholds) + struct SetEntry { + std::string name; + std::array itemIds{}; + std::array spellIds{}; + std::array thresholds{}; + }; + static std::unordered_map s_setData; + static bool s_setDataLoaded = false; + if (!s_setDataLoaded && assetManager_) { + s_setDataLoaded = true; + auto dbc = assetManager_->loadDBC("ItemSet.dbc"); + if (dbc && dbc->isLoaded()) { + const auto* layout = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("ItemSet") : nullptr; + auto lf = [&](const char* k, uint32_t def) -> uint32_t { + return layout ? (*layout)[k] : def; + }; + uint32_t idF = lf("ID", 0), nameF = lf("Name", 1); + static const char* itemKeys[10] = { + "Item0","Item1","Item2","Item3","Item4", + "Item5","Item6","Item7","Item8","Item9" + }; + static const char* spellKeys[10] = { + "Spell0","Spell1","Spell2","Spell3","Spell4", + "Spell5","Spell6","Spell7","Spell8","Spell9" + }; + static const char* thrKeys[10] = { + "Threshold0","Threshold1","Threshold2","Threshold3","Threshold4", + "Threshold5","Threshold6","Threshold7","Threshold8","Threshold9" + }; + uint32_t itemFallback[10], spellFallback[10], thrFallback[10]; + for (int i = 0; i < 10; ++i) { + itemFallback[i] = 18 + i; + spellFallback[i] = 28 + i; + thrFallback[i] = 38 + i; + } + for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) { + uint32_t id = dbc->getUInt32(r, idF); + if (!id) continue; + SetEntry e; + e.name = dbc->getString(r, nameF); + for (int i = 0; i < 10; ++i) { + e.itemIds[i] = dbc->getUInt32(r, layout ? (*layout)[itemKeys[i]] : itemFallback[i]); + e.spellIds[i] = dbc->getUInt32(r, layout ? (*layout)[spellKeys[i]] : spellFallback[i]); + e.thresholds[i] = dbc->getUInt32(r, layout ? (*layout)[thrKeys[i]] : thrFallback[i]); + } + s_setData[id] = std::move(e); + } + } + } + + auto setIt = s_setData.find(info.itemSetId); + ImGui::Spacing(); + if (setIt != s_setData.end()) { + const SetEntry& se = setIt->second; + // Count equipped pieces + int equipped = 0, total = 0; + for (int i = 0; i < 10; ++i) { + if (se.itemIds[i] == 0) continue; + ++total; + if (inventory) { + for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) { + const auto& eSlot = inventory->getEquipSlot(static_cast(s)); + if (!eSlot.empty() && eSlot.item.itemId == se.itemIds[i]) { ++equipped; break; } + } + } + } + if (total > 0) { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), + "%s (%d/%d)", se.name.empty() ? "Set" : se.name.c_str(), equipped, total); + } else { + if (!se.name.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "%s", se.name.c_str()); + } + // Show set bonuses: gray if not reached, green if active + if (gameHandler_) { + for (int i = 0; i < 10; ++i) { + if (se.spellIds[i] == 0 || se.thresholds[i] == 0) continue; + const std::string& bname = gameHandler_->getSpellName(se.spellIds[i]); + bool active = (equipped >= static_cast(se.thresholds[i])); + ImVec4 col = active ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f) + : ImVec4(0.55f, 0.55f, 0.55f, 1.0f); + if (!bname.empty()) + ImGui::TextColored(col, "(%u) %s", se.thresholds[i], bname.c_str()); + else + ImGui::TextColored(col, "(%u) Set Bonus", se.thresholds[i]); + } + } + } else { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Set (id %u)", info.itemSetId); } } @@ -2474,10 +3731,102 @@ void InventoryScreen::renderItemTooltip(const game::ItemQueryResponseData& info) } if (info.sellPrice > 0) { - uint32_t g = info.sellPrice / 10000; - uint32_t s = (info.sellPrice / 100) % 100; - uint32_t c = info.sellPrice % 100; - ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Sell: %ug %us %uc", g, s, c); + ImGui::TextDisabled("Sell:"); ImGui::SameLine(0, 4); + renderCoinsFromCopper(info.sellPrice); + } + + // Shift-hover: compare with currently equipped item + if (inventory && ImGui::GetIO().KeyShift && info.inventoryType > 0) { + if (const game::ItemSlot* eq = findComparableEquipped(*inventory, static_cast(info.inventoryType))) { + ImGui::Separator(); + ImGui::TextDisabled("Equipped:"); + VkDescriptorSet eqIcon = getItemIcon(eq->item.displayInfoId); + if (eqIcon) { ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f)); ImGui::SameLine(); } + ImGui::TextColored(getQualityColor(eq->item.quality), "%s", eq->item.name.c_str()); + + auto showDiff = [](const char* label, float nv, float ev) { + if (nv == 0.0f && ev == 0.0f) return; + float diff = nv - ev; + char buf[96]; + if (diff > 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▲%.0f)", label, nv, diff); ImGui::TextColored(ImVec4(0.0f,1.0f,0.0f,1.0f), "%s", buf); } + else if (diff < 0.0f) { std::snprintf(buf, sizeof(buf), "%s: %.0f (▼%.0f)", label, nv, -diff); ImGui::TextColored(ImVec4(1.0f,0.3f,0.3f,1.0f), "%s", buf); } + else { std::snprintf(buf, sizeof(buf), "%s: %.0f (=)", label, nv); ImGui::TextColored(ImVec4(0.7f,0.7f,0.7f,1.0f), "%s", buf); } + }; + + float ilvlDiff = static_cast(info.itemLevel) - static_cast(eq->item.itemLevel); + if (info.itemLevel > 0 || eq->item.itemLevel > 0) { + char ilvlBuf[64]; + if (ilvlDiff > 0) std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▲%.0f)", info.itemLevel, ilvlDiff); + else if (ilvlDiff < 0) std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (▼%.0f)", info.itemLevel, -ilvlDiff); + else std::snprintf(ilvlBuf, sizeof(ilvlBuf), "Item Level: %u (=)", info.itemLevel); + ImVec4 ic = ilvlDiff > 0 ? ImVec4(0,1,0,1) : ilvlDiff < 0 ? ImVec4(1,0.3f,0.3f,1) : ImVec4(0.7f,0.7f,0.7f,1); + ImGui::TextColored(ic, "%s", ilvlBuf); + } + + // DPS comparison for weapons + if (isWeaponInvType(info.inventoryType) && isWeaponInvType(eq->item.inventoryType)) { + float newDps = 0.0f, eqDps = 0.0f; + if (info.damageMax > 0.0f && info.delayMs > 0) + newDps = ((info.damageMin + info.damageMax) * 0.5f) / (info.delayMs / 1000.0f); + if (eq->item.damageMax > 0.0f && eq->item.delayMs > 0) + eqDps = ((eq->item.damageMin + eq->item.damageMax) * 0.5f) / (eq->item.delayMs / 1000.0f); + showDiff("DPS", newDps, eqDps); + } + + showDiff("Armor", static_cast(info.armor), static_cast(eq->item.armor)); + showDiff("Str", static_cast(info.strength), static_cast(eq->item.strength)); + showDiff("Agi", static_cast(info.agility), static_cast(eq->item.agility)); + showDiff("Sta", static_cast(info.stamina), static_cast(eq->item.stamina)); + showDiff("Int", static_cast(info.intellect), static_cast(eq->item.intellect)); + showDiff("Spi", static_cast(info.spirit), static_cast(eq->item.spirit)); + + // Extra stats diff — union of stat types from both items + auto findExtraStat = [](const auto& it, uint32_t type) -> int32_t { + for (const auto& es : it.extraStats) + if (es.statType == type) return es.statValue; + return 0; + }; + std::vector allTypes; + for (const auto& es : info.extraStats) allTypes.push_back(es.statType); + for (const auto& es : eq->item.extraStats) { + bool found = false; + for (uint32_t t : allTypes) if (t == es.statType) { found = true; break; } + if (!found) allTypes.push_back(es.statType); + } + for (uint32_t t : allTypes) { + int32_t nv = findExtraStat(info, t); + int32_t ev = findExtraStat(eq->item, t); + const char* lbl = nullptr; + switch (t) { + case 0: lbl = "Mana"; break; + case 1: lbl = "Health"; break; + case 12: lbl = "Defense"; break; + case 13: lbl = "Dodge"; break; + case 14: lbl = "Parry"; break; + case 15: lbl = "Block Rating"; break; + case 16: case 17: case 18: case 31: lbl = "Hit"; break; + case 19: case 20: case 21: case 32: lbl = "Crit"; break; + case 28: case 29: case 30: case 36: lbl = "Haste"; break; + case 35: lbl = "Resilience"; break; + case 37: lbl = "Expertise"; break; + case 38: lbl = "Attack Power"; break; + case 39: lbl = "Ranged AP"; break; + case 41: lbl = "Healing"; break; + case 42: lbl = "Spell Damage"; break; + case 43: lbl = "MP5"; break; + case 44: lbl = "Armor Pen"; break; + case 45: lbl = "Spell Power"; break; + case 46: lbl = "HP5"; break; + case 47: lbl = "Spell Pen"; break; + case 48: lbl = "Block Value"; break; + default: lbl = nullptr; break; + } + if (!lbl) continue; + showDiff(lbl, static_cast(nv), static_cast(ev)); + } + } + } else if (info.inventoryType > 0) { + ImGui::TextDisabled("Hold Shift to compare"); } ImGui::EndTooltip(); diff --git a/src/ui/keybinding_manager.cpp b/src/ui/keybinding_manager.cpp index 5ac79927..a7f52a3b 100644 --- a/src/ui/keybinding_manager.cpp +++ b/src/ui/keybinding_manager.cpp @@ -1,10 +1,15 @@ #include "ui/keybinding_manager.hpp" +#include "core/logger.hpp" #include #include -#include namespace wowee::ui { +static bool isReservedMovementKey(ImGuiKey key) { + return key == ImGuiKey_W || key == ImGuiKey_A || key == ImGuiKey_S || + key == ImGuiKey_D || key == ImGuiKey_Q || key == ImGuiKey_E; +} + KeybindingManager& KeybindingManager::getInstance() { static KeybindingManager instance; return instance; @@ -22,22 +27,34 @@ void KeybindingManager::initializeDefaults() { bindings_[static_cast(Action::TOGGLE_SPELLBOOK)] = ImGuiKey_P; // WoW standard key bindings_[static_cast(Action::TOGGLE_TALENTS)] = ImGuiKey_N; // WoW standard key bindings_[static_cast(Action::TOGGLE_QUESTS)] = ImGuiKey_L; - bindings_[static_cast(Action::TOGGLE_MINIMAP)] = ImGuiKey_M; + bindings_[static_cast(Action::TOGGLE_MINIMAP)] = ImGuiKey_None; // minimap is always visible; no default toggle bindings_[static_cast(Action::TOGGLE_SETTINGS)] = ImGuiKey_Escape; bindings_[static_cast(Action::TOGGLE_CHAT)] = ImGuiKey_Enter; bindings_[static_cast(Action::TOGGLE_GUILD_ROSTER)] = ImGuiKey_O; bindings_[static_cast(Action::TOGGLE_DUNGEON_FINDER)] = ImGuiKey_J; // Originally I, reassigned to avoid conflict - bindings_[static_cast(Action::TOGGLE_WORLD_MAP)] = ImGuiKey_W; + bindings_[static_cast(Action::TOGGLE_WORLD_MAP)] = ImGuiKey_M; // WoW standard: M opens world map bindings_[static_cast(Action::TOGGLE_NAMEPLATES)] = ImGuiKey_V; bindings_[static_cast(Action::TOGGLE_RAID_FRAMES)] = ImGuiKey_F; // Reassigned from R (now camera reset) - bindings_[static_cast(Action::TOGGLE_QUEST_LOG)] = ImGuiKey_Q; bindings_[static_cast(Action::TOGGLE_ACHIEVEMENTS)] = ImGuiKey_Y; // WoW standard key (Shift+Y in retail) + bindings_[static_cast(Action::TOGGLE_SKILLS)] = ImGuiKey_K; // WoW standard: K opens Skills/Professions } bool KeybindingManager::isActionPressed(Action action, bool repeat) { auto it = bindings_.find(static_cast(action)); if (it == bindings_.end()) return false; - return ImGui::IsKeyPressed(it->second, repeat); + ImGuiKey key = it->second; + if (key == ImGuiKey_None) return false; + + // When typing in a text field (e.g. chat input), never treat A-Z or 0-9 as shortcuts. + const ImGuiIO& io = ImGui::GetIO(); + if (io.WantTextInput) { + if ((key >= ImGuiKey_A && key <= ImGuiKey_Z) || + (key >= ImGuiKey_0 && key <= ImGuiKey_9)) { + return false; + } + } + + return ImGui::IsKeyPressed(key, repeat); } ImGuiKey KeybindingManager::getKeyForAction(Action action) const { @@ -47,6 +64,11 @@ ImGuiKey KeybindingManager::getKeyForAction(Action action) const { } void KeybindingManager::setKeyForAction(Action action, ImGuiKey key) { + // Reserve movement keys so they cannot be used as UI shortcuts. + (void)action; + if (isReservedMovementKey(key)) { + key = ImGuiKey_None; + } bindings_[static_cast(action)] = key; } @@ -71,8 +93,8 @@ const char* KeybindingManager::getActionName(Action action) { case Action::TOGGLE_WORLD_MAP: return "World Map"; case Action::TOGGLE_NAMEPLATES: return "Nameplates"; case Action::TOGGLE_RAID_FRAMES: return "Raid Frames"; - case Action::TOGGLE_QUEST_LOG: return "Quest Log"; case Action::TOGGLE_ACHIEVEMENTS: return "Achievements"; + case Action::TOGGLE_SKILLS: return "Skills / Professions"; case Action::ACTION_COUNT: break; } return "Unknown"; @@ -81,7 +103,7 @@ const char* KeybindingManager::getActionName(Action action) { void KeybindingManager::loadFromConfigFile(const std::string& filePath) { std::ifstream file(filePath); if (!file.is_open()) { - std::cerr << "[KeybindingManager] Failed to open config file: " << filePath << std::endl; + LOG_ERROR("KeybindingManager: Failed to open config file: ", filePath); return; } @@ -136,8 +158,9 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) { else if (action == "toggle_world_map") actionIdx = static_cast(Action::TOGGLE_WORLD_MAP); else if (action == "toggle_nameplates") actionIdx = static_cast(Action::TOGGLE_NAMEPLATES); else if (action == "toggle_raid_frames") actionIdx = static_cast(Action::TOGGLE_RAID_FRAMES); - else if (action == "toggle_quest_log") actionIdx = static_cast(Action::TOGGLE_QUEST_LOG); + else if (action == "toggle_quest_log") actionIdx = static_cast(Action::TOGGLE_QUESTS); // legacy alias else if (action == "toggle_achievements") actionIdx = static_cast(Action::TOGGLE_ACHIEVEMENTS); + else if (action == "toggle_skills") actionIdx = static_cast(Action::TOGGLE_SKILLS); if (actionIdx < 0) continue; @@ -175,13 +198,18 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) { } } - if (key != ImGuiKey_None) { - bindings_[actionIdx] = key; + if (key == ImGuiKey_None) continue; + + // Reserve movement keys so they cannot be used as UI shortcuts. + if (isReservedMovementKey(key)) { + continue; } + + bindings_[actionIdx] = key; } file.close(); - std::cout << "[KeybindingManager] Loaded keybindings from " << filePath << std::endl; + LOG_INFO("KeybindingManager: Loaded keybindings from ", filePath); } void KeybindingManager::saveToConfigFile(const std::string& filePath) const { @@ -228,8 +256,8 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const { {Action::TOGGLE_WORLD_MAP, "toggle_world_map"}, {Action::TOGGLE_NAMEPLATES, "toggle_nameplates"}, {Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"}, - {Action::TOGGLE_QUEST_LOG, "toggle_quest_log"}, {Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"}, + {Action::TOGGLE_SKILLS, "toggle_skills"}, }; for (const auto& [action, nameStr] : actionMap) { @@ -277,9 +305,9 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const { if (outFile.is_open()) { outFile << content; outFile.close(); - std::cout << "[KeybindingManager] Saved keybindings to " << filePath << std::endl; + LOG_INFO("KeybindingManager: Saved keybindings to ", filePath); } else { - std::cerr << "[KeybindingManager] Failed to write config file: " << filePath << std::endl; + LOG_ERROR("KeybindingManager: Failed to write config file: ", filePath); } } diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index 81f8657d..d41ac3e8 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -1,4 +1,5 @@ #include "ui/quest_log_screen.hpp" +#include "ui/ui_colors.hpp" #include "ui/inventory_screen.hpp" #include "ui/keybinding_manager.hpp" #include "core/application.hpp" @@ -82,6 +83,14 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler pos += replacement.length(); } + // Resolve class and race names for $C and $R placeholders + std::string className = "Adventurer"; + std::string raceName = "Unknown"; + if (character) { + className = game::getClassName(character->characterClass); + raceName = game::getRaceName(character->race); + } + // Replace simple placeholders pos = 0; while ((pos = result.find('$', pos)) != std::string::npos) { @@ -92,11 +101,12 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler switch (code) { case 'n': case 'N': replacement = playerName; break; + case 'c': case 'C': replacement = className; break; + case 'r': case 'R': replacement = raceName; break; case 'p': replacement = pronouns.subject; break; case 'o': replacement = pronouns.object; break; case 's': replacement = pronouns.possessive; break; case 'S': replacement = pronouns.possessiveP; break; - case 'r': replacement = pronouns.object; break; case 'b': case 'B': replacement = "\n"; break; case 'g': case 'G': pos++; continue; default: pos++; continue; @@ -205,6 +215,7 @@ std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) { if (s.size() > 72) s = s.substr(0, 72) + "..."; return s; } + } // anonymous namespace void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& invScreen) { @@ -362,6 +373,11 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv if (ImGui::MenuItem(tracked ? "Untrack" : "Track")) { gameHandler.setQuestTracked(q.questId, !tracked); } + if (gameHandler.isInGroup() && !q.complete) { + if (ImGui::MenuItem("Share Quest")) { + gameHandler.shareQuestWithParty(q.questId); + } + } if (!q.complete) { ImGui::Separator(); if (ImGui::MenuItem("Abandon Quest")) { @@ -465,12 +481,28 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv auto reqIt = sel.requiredItemCounts.find(itemId); if (reqIt != sel.requiredItemCounts.end()) required = reqIt->second; VkDescriptorSet iconTex = dispId ? invScreen.getItemIcon(dispId) : VK_NULL_HANDLE; + const auto* objInfo = gameHandler.getItemInfo(itemId); if (iconTex) { ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(14, 14)); + if (objInfo && objInfo->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + invScreen.renderItemTooltip(*objInfo); + ImGui::EndTooltip(); + } ImGui::SameLine(); ImGui::Text("%s: %u/%u", itemLabel.c_str(), count, required); + if (objInfo && objInfo->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + invScreen.renderItemTooltip(*objInfo); + ImGui::EndTooltip(); + } } else { ImGui::BulletText("%s: %u/%u", itemLabel.c_str(), count, required); + if (objInfo && objInfo->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + invScreen.renderItemTooltip(*objInfo); + ImGui::EndTooltip(); + } } } } @@ -488,12 +520,7 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv uint32_t rg = static_cast(sel.rewardMoney) / 10000; uint32_t rs = static_cast(sel.rewardMoney % 10000) / 100; uint32_t rc = static_cast(sel.rewardMoney % 100); - if (rg > 0) - ImGui::Text("%ug %us %uc", rg, rs, rc); - else if (rs > 0) - ImGui::Text("%us %uc", rs, rc); - else - ImGui::Text("%uc", rc); + renderCoinsText(rg, rs, rc); } // Guaranteed reward items @@ -519,6 +546,11 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv ImGui::Text("%s x%u", name.c_str(), ri.count); else ImGui::Text("%s", name.c_str()); + if (info && info->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + invScreen.renderItemTooltip(*info); + ImGui::EndTooltip(); + } } } @@ -545,16 +577,28 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv ImGui::Text("%s x%u", name.c_str(), ri.count); else ImGui::Text("%s", name.c_str()); + if (info && info->valid && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + invScreen.renderItemTooltip(*info); + ImGui::EndTooltip(); + } } } } - // Track / Abandon buttons + // Track / Share / Abandon buttons ImGui::Separator(); bool isTracked = gameHandler.isQuestTracked(sel.questId); if (ImGui::Button(isTracked ? "Untrack" : "Track", ImVec2(100.0f, 0.0f))) { gameHandler.setQuestTracked(sel.questId, !isTracked); } + if (gameHandler.isInGroup() && !sel.complete) { + ImGui::SameLine(); + if (ImGui::Button("Share", ImVec2(80.0f, 0.0f))) { + gameHandler.shareQuestWithParty(sel.questId); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Share this quest with your party"); + } if (!sel.complete) { ImGui::SameLine(); if (ImGui::Button("Abandon Quest", ImVec2(150.0f, 0.0f))) { diff --git a/src/ui/realm_screen.cpp b/src/ui/realm_screen.cpp index 589634a1..d2f8eecf 100644 --- a/src/ui/realm_screen.cpp +++ b/src/ui/realm_screen.cpp @@ -1,4 +1,5 @@ #include "ui/realm_screen.hpp" +#include "ui/ui_colors.hpp" #include namespace wowee { namespace ui { @@ -32,7 +33,7 @@ void RealmScreen::render(auth::AuthHandler& authHandler) { // Status message if (!statusMessage.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, ui::colors::kBrightGreen); ImGui::TextWrapped("%s", statusMessage.c_str()); ImGui::PopStyleColor(); ImGui::Spacing(); @@ -153,9 +154,9 @@ void RealmScreen::render(auth::AuthHandler& authHandler) { ImGui::TableSetColumnIndex(4); const char* status = getRealmStatus(realm.flags); if (realm.lock) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Locked"); + ImGui::TextColored(ui::colors::kRed, "Locked"); } else { - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%s", status); + ImGui::TextColored(ui::colors::kBrightGreen, "%s", status); } } @@ -202,7 +203,7 @@ void RealmScreen::render(auth::AuthHandler& authHandler) { } ImGui::PopStyleColor(2); } else { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Button, ui::colors::kDarkGray); ImGui::Button("Realm Locked", ImVec2(200, 40)); ImGui::PopStyleColor(); } @@ -237,13 +238,13 @@ const char* RealmScreen::getRealmStatus(uint8_t flags) const { ImVec4 RealmScreen::getPopulationColor(float population) const { if (population < 0.5f) { - return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - Low + return ui::colors::kBrightGreen; // Green - Low } else if (population < 1.5f) { return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow - Medium } else if (population < 2.5f) { return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange - High } else { - return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red - Full + return ui::colors::kRed; // Red - Full } } diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index e2c81756..e418c449 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -1,4 +1,5 @@ #include "ui/spellbook_screen.hpp" +#include "ui/ui_colors.hpp" #include "ui/keybinding_manager.hpp" #include "core/input.hpp" #include "core/application.hpp" @@ -203,6 +204,29 @@ std::string SpellbookScreen::lookupSpellName(uint32_t spellId, pipeline::AssetMa return {}; } +uint32_t SpellbookScreen::getSpellMaxRange(uint32_t spellId, pipeline::AssetManager* assetManager) { + if (!dbcLoadAttempted) { + loadSpellDBC(assetManager); + } + auto it = spellData.find(spellId); + if (it != spellData.end()) return it->second.rangeIndex; + return 0; +} + +void SpellbookScreen::getSpellPowerInfo(uint32_t spellId, pipeline::AssetManager* assetManager, + uint32_t& outCost, uint32_t& outPowerType) { + outCost = 0; + outPowerType = 0; + if (!dbcLoadAttempted) { + loadSpellDBC(assetManager); + } + auto it = spellData.find(spellId); + if (it != spellData.end()) { + outCost = it->second.manaCost; + outPowerType = it->second.powerType; + } +} + void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { if (iconDbLoaded) return; iconDbLoaded = true; @@ -502,7 +526,7 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle // Resource cost + cast time on same row (WoW style) if (!info->isPassive()) { - // Left: resource cost + // Left: resource cost (with talent flat/pct modifier applied) char costBuf[64] = ""; if (info->manaCost > 0) { const char* powerName = "Mana"; @@ -512,16 +536,26 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle case 4: powerName = "Focus"; break; default: break; } - std::snprintf(costBuf, sizeof(costBuf), "%u %s", info->manaCost, powerName); + // Apply SMSG_SET_FLAT/PCT_SPELL_MODIFIER Cost modifier (SpellModOp::Cost = 14) + int32_t flatCost = gameHandler.getSpellFlatMod(game::GameHandler::SpellModOp::Cost); + int32_t pctCost = gameHandler.getSpellPctMod(game::GameHandler::SpellModOp::Cost); + uint32_t displayCost = static_cast( + game::GameHandler::applySpellMod(static_cast(info->manaCost), flatCost, pctCost)); + std::snprintf(costBuf, sizeof(costBuf), "%u %s", displayCost, powerName); } - // Right: cast time + // Right: cast time (with talent CastingTime modifier applied) char castBuf[32] = ""; if (info->castTimeMs == 0) { std::snprintf(castBuf, sizeof(castBuf), "Instant cast"); } else { - float secs = info->castTimeMs / 1000.0f; - std::snprintf(castBuf, sizeof(castBuf), "%.1f sec cast", secs); + // Apply SpellModOp::CastingTime (10) modifiers + int32_t flatCT = gameHandler.getSpellFlatMod(game::GameHandler::SpellModOp::CastingTime); + int32_t pctCT = gameHandler.getSpellPctMod(game::GameHandler::SpellModOp::CastingTime); + int32_t modCT = game::GameHandler::applySpellMod( + static_cast(info->castTimeMs), flatCT, pctCT); + float secs = static_cast(modCT) / 1000.0f; + std::snprintf(castBuf, sizeof(castBuf), "%.1f sec cast", secs > 0.0f ? secs : 0.0f); } if (costBuf[0] || castBuf[0]) { @@ -552,7 +586,7 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle // Cooldown if active float cd = gameHandler.getSpellCooldown(info->spellId); if (cd > 0.0f) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Cooldown: %.1fs", cd); + ImGui::TextColored(ui::colors::kRed, "Cooldown: %.1fs", cd); } // Description @@ -564,8 +598,8 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle // Usage hints — only shown when browsing the spellbook, not on action bar hover if (!info->isPassive() && showUsageHints) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Drag to action bar"); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Double-click to cast"); + ImGui::TextColored(ui::colors::kBrightGreen, "Drag to action bar"); + ImGui::TextColored(ui::colors::kBrightGreen, "Double-click to cast"); } ImGui::PopTextWrapPos(); diff --git a/src/ui/talent_screen.cpp b/src/ui/talent_screen.cpp index 5c6bdaf9..ed29ca1f 100644 --- a/src/ui/talent_screen.cpp +++ b/src/ui/talent_screen.cpp @@ -1,4 +1,5 @@ #include "ui/talent_screen.hpp" +#include "ui/ui_colors.hpp" #include "ui/keybinding_manager.hpp" #include "core/input.hpp" #include "core/application.hpp" @@ -76,6 +77,7 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { gameHandler.loadTalentDbc(); loadSpellDBC(assetManager); loadSpellIconDBC(assetManager); + loadGlyphPropertiesDBC(assetManager); } uint8_t playerClass = gameHandler.getPlayerClass(); @@ -140,10 +142,10 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { // Unspent points ImGui::SameLine(0, 20); if (unspent > 0) { - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%u point%s available", + ImGui::TextColored(ui::colors::kBrightGreen, "%u point%s available", unspent, unspent > 1 ? "s" : ""); } else { - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "No points available"); + ImGui::TextColored(ui::colors::kDarkGray, "No points available"); } ImGui::Separator(); @@ -161,8 +163,43 @@ void TalentScreen::renderTalentTrees(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } } + + // Glyphs tab (WotLK only — visible when any glyph slot is populated or DBC data loaded) + if (!glyphProperties_.empty() || [&]() { + const auto& g = gameHandler.getGlyphs(); + for (auto id : g) if (id != 0) return true; + return false; }()) { + if (ImGui::BeginTabItem("Glyphs")) { + renderGlyphs(gameHandler); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); } + + // Talent learn confirmation popup + if (talentConfirmOpen_) { + ImGui::OpenPopup("Learn Talent?##talent_confirm"); + talentConfirmOpen_ = false; + } + if (ImGui::BeginPopupModal("Learn Talent?##talent_confirm", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", pendingTalentName_.c_str()); + ImGui::Text("Rank %u", pendingTalentRank_ + 1); + ImGui::Spacing(); + ImGui::TextWrapped("Spend a talent point?"); + ImGui::Spacing(); + if (ImGui::Button("Learn", ImVec2(80, 0))) { + gameHandler.learnTalent(pendingTalentId_, pendingTalentRank_); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tabId, @@ -188,20 +225,23 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab return a->column < b->column; }); - // Find grid dimensions - uint8_t maxRow = 0, maxCol = 0; + // Find grid dimensions — use int to avoid uint8_t wrap-around infinite loops + int maxRow = 0, maxCol = 0; for (const auto* talent : talents) { - maxRow = std::max(maxRow, talent->row); - maxCol = std::max(maxCol, talent->column); + maxRow = std::max(maxRow, static_cast(talent->row)); + maxCol = std::max(maxCol, static_cast(talent->column)); } + // Sanity-cap to prevent runaway loops from corrupt/unexpected DBC data + maxRow = std::min(maxRow, 15); + maxCol = std::min(maxCol, 15); // WoW talent grids are always 4 columns wide if (maxCol < 3) maxCol = 3; const float iconSize = 40.0f; const float spacing = 8.0f; const float cellSize = iconSize + spacing; - const float gridWidth = (maxCol + 1) * cellSize + spacing; - const float gridHeight = (maxRow + 1) * cellSize + spacing; + const float gridWidth = static_cast(maxCol + 1) * cellSize + spacing; + const float gridHeight = static_cast(maxRow + 1) * cellSize + spacing; // Points in this tree uint32_t pointsInTree = 0; @@ -287,7 +327,7 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab if (fromIt == talentPositions.end() || toIt == talentPositions.end()) continue; uint8_t prereqRank = gameHandler.getTalentRank(talent->prereqTalent[i]); - bool met = prereqRank >= talent->prereqRank[i]; + bool met = prereqRank > talent->prereqRank[i]; // storage 1-indexed, DBC 0-indexed ImU32 lineCol = met ? IM_COL32(100, 220, 100, 200) : IM_COL32(120, 120, 120, 150); ImVec2 from = fromIt->second.center; @@ -309,8 +349,8 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab } // Render talent icons - for (uint8_t row = 0; row <= maxRow; ++row) { - for (uint8_t col = 0; col <= maxCol; ++col) { + for (int row = 0; row <= maxRow; ++row) { + for (int col = 0; col <= maxCol; ++col) { const game::GameHandler::TalentEntry* talent = nullptr; for (const auto* t : talents) { if (t->row == row && t->column == col) { @@ -358,7 +398,7 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, for (int i = 0; i < 3; ++i) { if (talent.prereqTalent[i] != 0) { uint8_t prereqRank = gameHandler.getTalentRank(talent.prereqTalent[i]); - if (prereqRank < talent.prereqRank[i]) { + if (prereqRank <= talent.prereqRank[i]) { // storage 1-indexed, DBC 0-indexed prereqsMet = false; canLearn = false; break; @@ -513,7 +553,7 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, auto tooltipIt = spellTooltips.find(talent.rankSpells[currentRank]); if (tooltipIt != spellTooltips.end() && !tooltipIt->second.empty()) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Next Rank:"); + ImGui::TextColored(ui::colors::kBrightGreen, "Next Rank:"); ImGui::TextWrapped("%s", tooltipIt->second.c_str()); } } @@ -525,14 +565,15 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, if (!prereq || prereq->rankSpells[0] == 0) continue; uint8_t prereqCurrentRank = gameHandler.getTalentRank(talent.prereqTalent[i]); - bool met = prereqCurrentRank >= talent.prereqRank[i]; + bool met = prereqCurrentRank > talent.prereqRank[i]; // storage 1-indexed, DBC 0-indexed ImVec4 pColor = met ? ImVec4(0.3f, 0.9f, 0.3f, 1) : ImVec4(1.0f, 0.3f, 0.3f, 1); const std::string& prereqName = gameHandler.getSpellName(prereq->rankSpells[0]); ImGui::Spacing(); + const uint8_t reqRankDisplay = talent.prereqRank[i] + 1u; // DBC 0-indexed → display 1-indexed ImGui::TextColored(pColor, "Requires %u point%s in %s", - talent.prereqRank[i], - talent.prereqRank[i] > 1 ? "s" : "", + reqRankDisplay, + reqRankDisplay > 1 ? "s" : "", prereqName.empty() ? "prerequisite" : prereqName.c_str()); } @@ -541,7 +582,7 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, uint32_t requiredPoints = talent.row * 5; if (pointsInTree < requiredPoints) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + ImGui::TextColored(ui::colors::kRed, "Requires %u points in this tree (%u/%u)", requiredPoints, pointsInTree, requiredPoints); } @@ -550,23 +591,22 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler, // Action hint if (canLearn && prereqsMet) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Click to learn"); + ImGui::TextColored(ui::colors::kBrightGreen, "Click to learn"); } ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } - // Handle click + // Handle click — open confirmation dialog instead of learning directly if (clicked && canLearn && prereqsMet) { - const auto& learned = gameHandler.getLearnedTalents(); - uint8_t desiredRank; - if (learned.find(talent.talentId) == learned.end()) { - desiredRank = 0; // First rank (0-indexed on wire) - } else { - desiredRank = currentRank; // currentRank is already the next 0-indexed rank to learn - } - gameHandler.learnTalent(talent.talentId, desiredRank); + talentConfirmOpen_ = true; + pendingTalentId_ = talent.talentId; + pendingTalentRank_ = currentRank; + uint32_t nextSpell = (currentRank < 5) ? talent.rankSpells[currentRank] : 0; + pendingTalentName_ = nextSpell ? gameHandler.getSpellName(nextSpell) : ""; + if (pendingTalentName_.empty()) + pendingTalentName_ = spellId ? gameHandler.getSpellName(spellId) : "Talent"; } ImGui::PopID(); @@ -582,15 +622,27 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) { if (!dbc || !dbc->isLoaded()) return; const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr; + uint32_t fieldCount = dbc->getFieldCount(); + // Detect DBC/layout mismatch: Classic layout expects ~173 fields but we may + // load the WotLK base DBC (234 fields). Use WotLK field indices in that case. + uint32_t idField = 0, iconField = 133, tooltipField = 139; + if (spellL) { + uint32_t layoutIcon = (*spellL)["IconID"]; + if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) { + idField = (*spellL)["ID"]; + iconField = layoutIcon; + try { tooltipField = (*spellL)["Tooltip"]; } catch (...) {} + } + } uint32_t count = dbc->getRecordCount(); for (uint32_t i = 0; i < count; ++i) { - uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0); + uint32_t spellId = dbc->getUInt32(i, idField); if (spellId == 0) continue; - uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133); + uint32_t iconId = dbc->getUInt32(i, iconField); spellIconIds[spellId] = iconId; - std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139); + std::string tooltip = dbc->getString(i, tooltipField); if (!tooltip.empty()) { spellTooltips[spellId] = tooltip; } @@ -616,6 +668,99 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) { } } +void TalentScreen::loadGlyphPropertiesDBC(pipeline::AssetManager* assetManager) { + if (glyphDbcLoaded) return; + glyphDbcLoaded = true; + + if (!assetManager || !assetManager->isInitialized()) return; + + auto dbc = assetManager->loadDBC("GlyphProperties.dbc"); + if (!dbc || !dbc->isLoaded()) return; + + // GlyphProperties.dbc: field 0=ID, field 1=SpellID, field 2=GlyphSlotFlags (1=minor), field 3=SpellIconID + for (uint32_t i = 0; i < dbc->getRecordCount(); i++) { + uint32_t id = dbc->getUInt32(i, 0); + uint32_t spellId = dbc->getUInt32(i, 1); + uint32_t flags = dbc->getUInt32(i, 2); + if (id == 0) continue; + GlyphInfo info; + info.spellId = spellId; + info.isMajor = (flags == 0); // flag 0 = major, flag 1 = minor + glyphProperties_[id] = info; + } +} + +void TalentScreen::renderGlyphs(game::GameHandler& gameHandler) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + const auto& glyphs = gameHandler.getGlyphs(); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "Major Glyphs"); + ImGui::Separator(); + + // WotLK: 6 glyph slots total. Slots 0,2,4 are major by convention from the server, + // but we check GlyphProperties.dbc flags when available. + // Display all 6 slots grouped: show major (non-minor) first, then minor. + std::vector> majorSlots, minorSlots; + for (int i = 0; i < game::GameHandler::MAX_GLYPH_SLOTS; i++) { + uint16_t glyphId = glyphs[i]; + bool isMajor = true; + if (glyphId != 0) { + auto git = glyphProperties_.find(glyphId); + if (git != glyphProperties_.end()) isMajor = git->second.isMajor; + else isMajor = (i % 2 == 0); // fallback: even slots = major + } else { + isMajor = (i % 2 == 0); // empty slots follow same pattern + } + if (isMajor) majorSlots.push_back({i, true}); + else minorSlots.push_back({i, false}); + } + + auto renderGlyphSlot = [&](int slotIdx) { + uint16_t glyphId = glyphs[slotIdx]; + char label[64]; + if (glyphId == 0) { + snprintf(label, sizeof(label), "Slot %d [Empty]", slotIdx + 1); + ImGui::TextDisabled("%s", label); + return; + } + + uint32_t spellId = 0; + uint32_t iconId = 0; + auto git = glyphProperties_.find(glyphId); + if (git != glyphProperties_.end()) { + spellId = git->second.spellId; + auto iit = spellIconIds.find(spellId); + if (iit != spellIconIds.end()) iconId = iit->second; + } + + // Icon (24x24) + VkDescriptorSet icon = getSpellIcon(iconId, assetManager); + if (icon != VK_NULL_HANDLE) { + ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(24, 24)); + ImGui::SameLine(0, 6); + } else { + ImGui::Dummy(ImVec2(24, 24)); + ImGui::SameLine(0, 6); + } + + // Spell name + const std::string& name = spellId ? gameHandler.getSpellName(spellId) : ""; + if (!name.empty()) { + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", name.c_str()); + } else { + ImGui::TextColored(ui::colors::kLightGray, "Glyph #%u", static_cast(glyphId)); + } + }; + + for (auto& [idx, major] : majorSlots) renderGlyphSlot(idx); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "Minor Glyphs"); + ImGui::Separator(); + for (auto& [idx, major] : minorSlots) renderGlyphSlot(idx); +} + VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) { if (iconId == 0 || !assetManager) return VK_NULL_HANDLE; diff --git a/tools/diff_classic_turtle_opcodes.py b/tools/diff_classic_turtle_opcodes.py new file mode 100644 index 00000000..0e548b47 --- /dev/null +++ b/tools/diff_classic_turtle_opcodes.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Report the semantic opcode diff between the Classic and Turtle expansion maps. + +The report normalizes: +- hex formatting differences (0x67 vs 0x067) +- alias names that collapse to the same canonical opcode + +It highlights: +- true wire differences for the same canonical opcode +- canonical opcodes present only in Classic or only in Turtle +- name-only differences where the wire matches after aliasing +""" + +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Tuple + +from opcode_map_utils import load_opcode_map + + +RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$") + + +def read_aliases(path: Path) -> Dict[str, str]: + data = json.loads(path.read_text()) + aliases = data.get("aliases", {}) + out: Dict[str, str] = {} + for key, value in aliases.items(): + if isinstance(key, str) and isinstance(value, str): + out[key] = value + return out + + +def canonicalize(name: str, aliases: Dict[str, str]) -> str: + seen = set() + current = name + while current in aliases and current not in seen: + seen.add(current) + current = aliases[current] + return current + + +def load_map(path: Path) -> Dict[str, int]: + data = load_opcode_map(path) + out: Dict[str, int] = {} + for key, value in data.items(): + if not isinstance(key, str) or not RE_OPCODE_NAME.match(key): + continue + if not isinstance(value, str) or not value.lower().startswith("0x"): + continue + out[key] = int(value, 16) + return out + + +@dataclass(frozen=True) +class CanonicalEntry: + canonical_name: str + raw_value: int + raw_names: Tuple[str, ...] + + +def build_canonical_entries( + raw_map: Dict[str, int], aliases: Dict[str, str] +) -> Dict[str, CanonicalEntry]: + grouped: Dict[str, List[Tuple[str, int]]] = {} + for raw_name, raw_value in raw_map.items(): + canonical_name = canonicalize(raw_name, aliases) + grouped.setdefault(canonical_name, []).append((raw_name, raw_value)) + + out: Dict[str, CanonicalEntry] = {} + for canonical_name, entries in grouped.items(): + raw_values = {raw_value for _, raw_value in entries} + if len(raw_values) != 1: + formatted = ", ".join( + f"{name}=0x{raw_value:03X}" for name, raw_value in sorted(entries) + ) + raise ValueError( + f"Expansion map contains multiple wires for canonical opcode " + f"{canonical_name}: {formatted}" + ) + raw_value = next(iter(raw_values)) + raw_names = tuple(sorted(name for name, _ in entries)) + out[canonical_name] = CanonicalEntry(canonical_name, raw_value, raw_names) + return out + + +def format_hex(raw_value: int) -> str: + return f"0x{raw_value:03X}" + + +def emit_section(title: str, rows: Iterable[str], limit: int | None) -> None: + rows = list(rows) + print(f"{title}: {len(rows)}") + if not rows: + return + shown = rows if limit is None else rows[:limit] + for row in shown: + print(f" {row}") + if limit is not None and len(rows) > limit: + print(f" ... {len(rows) - limit} more") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", default=".") + parser.add_argument( + "--limit", + type=int, + default=80, + help="Maximum rows to print per section; use -1 for no limit.", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + aliases = read_aliases(root / "Data/opcodes/aliases.json") + classic_raw = load_map(root / "Data/expansions/classic/opcodes.json") + turtle_raw = load_map(root / "Data/expansions/turtle/opcodes.json") + + classic = build_canonical_entries(classic_raw, aliases) + turtle = build_canonical_entries(turtle_raw, aliases) + + classic_names = set(classic) + turtle_names = set(turtle) + shared_names = classic_names & turtle_names + + different_wire = [] + same_wire_name_only = [] + for canonical_name in sorted(shared_names): + c = classic[canonical_name] + t = turtle[canonical_name] + if c.raw_value != t.raw_value: + different_wire.append( + f"{canonical_name}: classic={format_hex(c.raw_value)} " + f"turtle={format_hex(t.raw_value)}" + ) + elif c.raw_names != t.raw_names: + same_wire_name_only.append( + f"{canonical_name}: wire={format_hex(c.raw_value)} " + f"classic_names={list(c.raw_names)} turtle_names={list(t.raw_names)}" + ) + + classic_only = [ + f"{name}: {format_hex(classic[name].raw_value)} names={list(classic[name].raw_names)}" + for name in sorted(classic_names - turtle_names) + ] + turtle_only = [ + f"{name}: {format_hex(turtle[name].raw_value)} names={list(turtle[name].raw_names)}" + for name in sorted(turtle_names - classic_names) + ] + + limit = None if args.limit < 0 else args.limit + + print(f"classic canonical entries: {len(classic)}") + print(f"turtle canonical entries: {len(turtle)}") + print(f"shared canonical entries: {len(shared_names)}") + print() + emit_section("Different wire", different_wire, limit) + print() + emit_section("Classic only", classic_only, limit) + print() + emit_section("Turtle only", turtle_only, limit) + print() + emit_section("Same wire, name-only differences", same_wire_name_only, limit) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/opcode_map_utils.py b/tools/opcode_map_utils.py new file mode 100644 index 00000000..c3566057 --- /dev/null +++ b/tools/opcode_map_utils.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Dict, Set + + +RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$") + + +def load_opcode_map(path: Path, _seen: Set[Path] | None = None) -> Dict[str, str]: + if _seen is None: + _seen = set() + + path = path.resolve() + if path in _seen: + chain = " -> ".join(str(p) for p in list(_seen) + [path]) + raise ValueError(f"Opcode map inheritance cycle: {chain}") + _seen.add(path) + + data = json.loads(path.read_text()) + merged: Dict[str, str] = {} + + extends = data.get("_extends") + if isinstance(extends, str) and extends: + merged.update(load_opcode_map(path.parent / extends, _seen)) + + remove = data.get("_remove", []) + if isinstance(remove, list): + for name in remove: + if isinstance(name, str): + merged.pop(name, None) + + for key, value in data.items(): + if not isinstance(key, str) or not RE_OPCODE_NAME.match(key): + continue + if isinstance(value, str): + merged[key] = value + elif isinstance(value, int): + merged[key] = str(value) + + _seen.remove(path) + return merged diff --git a/tools/validate_opcode_maps.py b/tools/validate_opcode_maps.py index a562439b..7acb62ed 100644 --- a/tools/validate_opcode_maps.py +++ b/tools/validate_opcode_maps.py @@ -17,6 +17,8 @@ import re from pathlib import Path from typing import Dict, Iterable, List, Set +from opcode_map_utils import load_opcode_map + RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$") RE_CODE_REF = re.compile(r"\bOpcode::((?:CMSG|SMSG|MSG)_[A-Z0-9_]+)\b") @@ -53,12 +55,8 @@ def iter_expansion_files(expansions_dir: Path) -> Iterable[Path]: def load_expansion_names(path: Path) -> Dict[str, str]: - data = json.loads(path.read_text()) - out: Dict[str, str] = {} - for k, v in data.items(): - if RE_OPCODE_NAME.match(k): - out[k] = str(v) - return out + data = load_opcode_map(path) + return {k: str(v) for k, v in data.items() if RE_OPCODE_NAME.match(k)} def collect_code_refs(root: Path) -> Set[str]: