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 013f805b..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 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 e5d0793f..459c9046 100644 --- a/Data/expansions/classic/dbc_layouts.json +++ b/Data/expansions/classic/dbc_layouts.json @@ -1,108 +1,260 @@ { - "Spell": { - "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117, - "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, - "DispelType": 4 + "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 }, - "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "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": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 - }, - "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "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": { + "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/update_fields.json b/Data/expansions/classic/update_fields.json index 4f340df5..4a602e91 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -1,48 +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_FIELD_AURAFLAGS": 98, - "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 da2fb9a5..e11682cf 100644 --- a/Data/expansions/tbc/dbc_layouts.json +++ b/Data/expansions/tbc/dbc_layouts.json @@ -1,124 +1,307 @@ { - "Spell": { - "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 124, - "Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215, - "CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40, - "DispelType": 3 + "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 - }, - "CharTitles": { "ID": 0, "Title": 2, "TitleBit": 20 }, - "AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 }, - "CreatureDisplayInfoExtra": { - "ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4, - "HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7, - "EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10, - "EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13, - "EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16, - "EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20 - }, - "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 }, - "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 }, - "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 }, - "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 }, - "SpellItemEnchantment": { - "ID": 0, "Name": 8 + "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, "Name": 1, - "Item0": 18, "Item1": 19, "Item2": 20, "Item3": 21, "Item4": 22, - "Item5": 23, "Item6": 24, "Item7": 25, "Item8": 26, "Item9": 27, - "Spell0": 28, "Spell1": 29, "Spell2": 30, "Spell3": 31, "Spell4": 32, - "Spell5": 33, "Spell6": 34, "Spell7": 35, "Spell8": 36, "Spell9": 37, - "Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41, - "Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45, - "Threshold8": 46, "Threshold9": 47 + "ID": 0, + "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 + }, + "Light": { + "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 + }, + "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": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 - }, - "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "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": { + "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 cb44c54a..2f580109 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -1,121 +1,297 @@ { - "Spell": { - "ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117, - "Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1, - "CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33, - "DispelType": 4 + "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 }, - "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 }, - "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 }, - "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 }, - "SpellItemEnchantment": { - "ID": 0, "Name": 8 + "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, "Name": 1, - "Item0": 10, "Item1": 11, "Item2": 12, "Item3": 13, "Item4": 14, - "Item5": 15, "Item6": 16, "Item7": 17, "Item8": 18, "Item9": 19, - "Spell0": 20, "Spell1": 21, "Spell2": 22, "Spell3": 23, "Spell4": 24, - "Spell5": 25, "Spell6": 26, "Spell7": 27, "Spell8": 28, "Spell9": 29, - "Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33, - "Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37, - "Threshold8": 38, "Threshold9": 39 + "ID": 0, + "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 + }, + "Light": { + "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 + }, + "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": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 - }, - "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "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": { + "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/update_fields.json b/Data/expansions/turtle/update_fields.json index a27e84f7..4a602e91 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -1,48 +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_FIELD_AURAFLAGS": 98, - "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 4ecbfc32..e563d2c9 100644 --- a/Data/expansions/wotlk/dbc_layouts.json +++ b/Data/expansions/wotlk/dbc_layouts.json @@ -1,129 +1,323 @@ { - "Spell": { - "ID": 0, "Attributes": 4, "AttributesEx": 5, "IconID": 133, - "Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225, - "PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49, - "DispelType": 2 + "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 - }, - "CharTitles": { "ID": 0, "Title": 2, "TitleBit": 36 }, - "Achievement": { "ID": 0, "Title": 4, "Description": 21, "Points": 39 }, - "AchievementCriteria": { "ID": 0, "AchievementID": 1, "Quantity": 4, "Description": 9 }, - "AreaTable": { "ID": 0, "MapID": 1, "ParentAreaNum": 2, "ExploreFlag": 3 }, - "CreatureDisplayInfoExtra": { - "ID": 0, "RaceID": 1, "SexID": 2, "SkinID": 3, "FaceID": 4, - "HairStyleID": 5, "HairColorID": 6, "FacialHairID": 7, - "EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10, - "EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13, - "EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16, - "EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20 - }, - "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 }, - "Light": { - "ID": 0, "MapID": 1, "X": 2, "Z": 3, "Y": 4, - "InnerRadius": 5, "OuterRadius": 6, "LightParamsID": 7, - "LightParamsIDRain": 8, "LightParamsIDUnderwater": 9 + "EmotesTextData": { + "ID": 0, + "Text": 1 }, - "LightParams": { "LightParamsID": 0 }, - "LightIntBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "Faction": { + "ID": 0, + "ReputationBase0": 10, + "ReputationBase1": 11, + "ReputationBase2": 12, + "ReputationBase3": 13, + "ReputationRaceMask0": 2, + "ReputationRaceMask1": 3, + "ReputationRaceMask2": 4, + "ReputationRaceMask3": 5 }, - "LightFloatBand": { - "BlockIndex": 1, "NumKeyframes": 2, "TimeKey0": 3, "Value0": 19 + "FactionTemplate": { + "Enemy0": 6, + "Enemy1": 7, + "Enemy2": 8, + "Enemy3": 9, + "EnemyGroup": 5, + "Faction": 1, + "FactionGroup": 3, + "FriendGroup": 4, + "ID": 0 }, - "WorldMapArea": { - "ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3, - "LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7, - "DisplayMapID": 8, "ParentWorldMapID": 10 + "GameObjectDisplayInfo": { + "ID": 0, + "ModelName": 1 }, - "SpellItemEnchantment": { - "ID": 0, "Name": 8 + "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, "Name": 1, - "Item0": 18, "Item1": 19, "Item2": 20, "Item3": 21, "Item4": 22, - "Item5": 23, "Item6": 24, "Item7": 25, "Item8": 26, "Item9": 27, - "Spell0": 28, "Spell1": 29, "Spell2": 30, "Spell3": 31, "Spell4": 32, - "Spell5": 33, "Spell6": 34, "Spell7": 35, "Spell8": 36, "Spell9": 37, - "Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41, - "Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45, - "Threshold8": 46, "Threshold9": 47 + "ID": 0, + "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 + "ID": 0, + "Name": 1 + }, + "Light": { + "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 + }, + "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": { - "ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8 - }, - "SpellVisualKit": { - "ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13 + "CastKit": 2, + "ID": 0, + "ImpactKit": 3, + "MissileModel": 8 }, "SpellVisualEffectName": { - "ID": 0, "FilePath": 2 + "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": { + "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 1628b94c..06bcbd62 100644 --- a/Data/expansions/wotlk/update_fields.json +++ b/Data/expansions/wotlk/update_fields.json @@ -1,58 +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, - "UNIT_FIELD_ATTACK_POWER": 123, - "UNIT_FIELD_RANGED_ATTACK_POWER": 126, - "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, - "PLAYER_CHOSEN_TITLE": 1349, - "PLAYER_FIELD_MOD_DAMAGE_DONE_POS": 1171, - "PLAYER_FIELD_MOD_HEALING_DONE_POS": 1192, - "PLAYER_BLOCK_PERCENTAGE": 1024, - "PLAYER_DODGE_PERCENTAGE": 1025, - "PLAYER_PARRY_PERCENTAGE": 1026, - "PLAYER_CRIT_PERCENTAGE": 1029, - "PLAYER_RANGED_CRIT_PERCENTAGE": 1030, - "PLAYER_SPELL_CRIT_PERCENTAGE1": 1032, - "PLAYER_FIELD_COMBAT_RATING_1": 1231, - "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/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/docs/status.md b/docs/status.md index fca68f19..bb1e9614 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,6 +1,6 @@ # Project Status -**Last updated**: 2026-03-18 +**Last updated**: 2026-03-24 ## What This Repo Is @@ -30,16 +30,17 @@ Implemented (working in normal use): - 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 +- 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) -- Lava steam particles: sparse in some areas (tuning opportunity) - 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 6423d460..7a9a66b8 100644 --- a/include/audio/ui_sound_manager.hpp +++ b/include/audio/ui_sound_manager.hpp @@ -78,6 +78,9 @@ public: // Chat notifications void playWhisperReceived(); + // Minimap ping + void playMinimapPing(); + private: struct UISample { std::string path; @@ -126,6 +129,7 @@ private: 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 d9b19e39..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_; 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/game_handler.hpp b/include/game/game_handler.hpp index 569261b2..67c8f5f3 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -172,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 @@ -279,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); } @@ -299,6 +338,10 @@ 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_; } @@ -376,7 +419,7 @@ public: bool hasFocus() const { return focusGuid != 0; } // Mouseover targeting — set each frame by the nameplate renderer - void setMouseoverGuid(uint64_t guid) { mouseoverGuid_ = guid; } + void setMouseoverGuid(uint64_t guid); uint64_t getMouseoverGuid() const { return mouseoverGuid_; } // Advanced targeting @@ -788,6 +831,14 @@ public: void togglePetSpellAutocast(uint32_t spellId); const std::unordered_set& getKnownSpells() const { return knownSpells; } + // Spell book tabs — groups known spells by class skill line for Lua API + struct SpellBookTab { + std::string name; + std::string texture; // icon path + std::vector spellIds; // spells in this tab + }; + const std::vector& getSpellBookTabs(); + // ---- Pet Stable ---- struct StabledPet { uint32_t petNumber = 0; // server-side pet number (used for unstable/swap) @@ -840,6 +891,7 @@ public: uint32_t getCurrentCastSpellId() const { return currentCastSpellId; } float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; } float getCastTimeRemaining() const { return castTimeRemaining; } + float getCastTimeTotal() const { return castTimeTotal; } // Repeat-craft queue void startCraftQueue(uint32_t spellId, int count); @@ -849,10 +901,12 @@ public: // 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; @@ -1200,6 +1254,16 @@ public: // 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 { @@ -1247,7 +1311,7 @@ public: // Barber shop bool isBarberShopOpen() const { return barberShopOpen_; } - void closeBarberShop() { barberShopOpen_ = false; } + 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) @@ -1423,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); @@ -1530,7 +1595,11 @@ public: 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); @@ -1614,6 +1683,8 @@ public: std::array rewardChoiceItems{}; // player picks one of these }; const std::vector& getQuestLog() const { return questLog_; } + int getSelectedQuestLogIndex() const { return selectedQuestLogIndex_; } + void setSelectedQuestLogIndex(int idx) { selectedQuestLogIndex_ = idx; } void abandonQuest(uint32_t questId); void shareQuestWithParty(uint32_t questId); // CMSG_PUSHQUESTTOPARTY bool requestQuestQuery(uint32_t questId, bool force = false); @@ -1621,6 +1692,7 @@ public: 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 { @@ -1651,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) @@ -1793,7 +1866,7 @@ public: const std::string& getFactionNamePublic(uint32_t factionId) const; uint32_t getWatchedFactionId() const { return watchedFactionId_; } - void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; } + void setWatchedFactionId(uint32_t factionId); uint32_t getLastContactListMask() const { return lastContactListMask_; } uint32_t getLastContactListCount() const { return lastContactListCount_; } bool isServerMovementAllowed() const { return serverMovementAllowed_; } @@ -1918,7 +1991,19 @@ 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; @@ -2166,6 +2251,9 @@ public: 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; @@ -2186,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; @@ -2225,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. @@ -2239,6 +2333,11 @@ 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(); @@ -2430,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); @@ -2564,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_; @@ -2624,6 +2731,12 @@ 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 @@ -2663,6 +2776,9 @@ private: // ---- 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; @@ -2769,6 +2885,7 @@ 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 { @@ -2906,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; @@ -2950,13 +3069,13 @@ private: // Faction standings (factionId → absolute standing value) std::unordered_map factionStandings_; // Faction name cache (factionId → name), populated lazily from Faction.dbc - std::unordered_map factionNameCache_; + mutable std::unordered_map factionNameCache_; // repListId → factionId mapping (populated with factionNameCache) - std::unordered_map factionRepListToId_; + mutable std::unordered_map factionRepListToId_; // factionId → repListId reverse mapping - std::unordered_map factionIdToRepList_; - bool factionNameCacheLoaded_ = false; - void loadFactionNameCache(); + mutable std::unordered_map factionIdToRepList_; + mutable bool factionNameCacheLoaded_ = false; + void loadFactionNameCache() const; std::string getFactionName(uint32_t factionId) const; // ---- Phase 4: Group ---- @@ -3067,6 +3186,8 @@ private: 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 @@ -3113,6 +3234,7 @@ private: // Quest log std::vector questLog_; + int selectedQuestLogIndex_ = 0; std::unordered_set pendingQuestQueryIds_; std::unordered_set trackedQuestIds_; bool pendingLoginQuestResync_ = false; @@ -3227,15 +3349,20 @@ private: // Trainer bool trainerWindowOpen_ = false; TrainerListData currentTrainerList_; - struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 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; // 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"). - std::unordered_map titleNameCache_; - bool titleNameCacheLoaded_ = false; - void loadTitleNameCache(); + 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. @@ -3261,24 +3388,24 @@ private: 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) - std::unordered_map mapNameCache_; - bool mapNameCacheLoaded_ = false; - void loadMapNameCache(); + mutable std::unordered_map mapNameCache_; + mutable bool mapNameCacheLoaded_ = false; + void loadMapNameCache() const; // LFG dungeon name cache (lazy-loaded from LFGDungeons.dbc; WotLK only) - std::unordered_map lfgDungeonNameCache_; - bool lfgDungeonNameCacheLoaded_ = false; - void loadLfgDungeonDbc(); + 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 @@ -3366,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; @@ -3469,6 +3598,8 @@ 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) ---- diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index cf092ac4..6ae07a2d 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -129,6 +129,18 @@ public: // 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(); diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index b229dc80..261cae66 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -283,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); } }; diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index e4687352..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, @@ -77,6 +78,10 @@ enum class UF : uint16_t { 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/world_packets.hpp b/include/game/world_packets.hpp index c0408743..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 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/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/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 1f19b46e..fe8d7f61 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -323,6 +323,7 @@ public: 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); @@ -412,8 +413,15 @@ 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 diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 588fa3af..80b33fe6 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -138,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; } @@ -334,21 +336,28 @@ private: pipeline::AssetManager* cachedAssetManager = nullptr; // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT - struct SpellVisualInstance { uint32_t instanceId; float elapsed; }; + 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_DURATION = 3.5f; + 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; @@ -659,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); diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 9fa540b3..ab6e881f 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -394,6 +394,11 @@ private: std::unordered_set uploadedM2Ids_; std::mutex uploadedM2IdsMutex_; + // Cross-tile dedup for WMO doodad preparation on background workers + // (prevents re-parsing thousands of doodads when same WMO spans multiple tiles) + std::unordered_set preparedWmoUniqueIds_; + std::mutex preparedWmoUniqueIdsMutex_; + // Dedup set for doodad placements across tile boundaries std::unordered_set placedDoodadIds; diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp index 654729b3..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 { @@ -70,10 +72,16 @@ public: 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; } @@ -114,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 @@ -130,6 +150,8 @@ private: void destroySwapchain(); bool createCommandPools(); bool createSyncObjects(); + bool createPipelineCache(); + void savePipelineCache(); bool createImGuiResources(); void destroyImGuiResources(); @@ -144,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; @@ -227,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/world_map.hpp b/include/rendering/world_map.hpp index eedc88af..fee98b6d 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -67,6 +67,13 @@ public: 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. @@ -148,6 +155,9 @@ private: 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_ = {}; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 4bc10707..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,6 +41,7 @@ public: void saveSettings(); void loadSettings(); + void applyAudioVolumes(rendering::Renderer* renderer); private: // Chat state @@ -62,7 +64,10 @@ private: // Populated by the SpellCastFailedCallback; queried during action bar button rendering. std::unordered_map actionFlashEndTimes_; - // Tab-completion state for slash commands + // 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) @@ -377,6 +382,12 @@ private: 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); @@ -388,6 +399,7 @@ private: 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); @@ -423,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}; @@ -569,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 diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index dca0e5a5..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 @@ -195,6 +196,9 @@ private: 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 3c67b125..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 @@ -86,5 +85,3 @@ private: }; } // namespace wowee::ui - -#endif // WOWEE_KEYBINDING_MANAGER_HPP 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 8ef800f0..6518259e 100644 --- a/src/audio/ui_sound_manager.cpp +++ b/src/audio/ui_sound_manager.cpp @@ -130,6 +130,12 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) { } } + // 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"); @@ -236,5 +242,8 @@ 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 28f2fad1..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"); @@ -476,6 +745,9 @@ void Application::run() { 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 @@ -556,6 +828,15 @@ void Application::run() { LOG_ERROR("GPU device lost — exiting application"); window->setShouldClose(true); } + + // 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); @@ -648,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; @@ -955,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"; @@ -2643,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); @@ -3377,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)); } } @@ -3388,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); @@ -3439,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); @@ -3453,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 { @@ -3658,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 @@ -3725,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; @@ -3799,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; @@ -3937,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()) { @@ -4072,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 @@ -4140,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, ")"); } @@ -4188,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 --- @@ -5006,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(); } @@ -5031,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() { @@ -5039,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) | @@ -5339,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; } @@ -5589,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) @@ -5638,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); @@ -5646,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); } @@ -5760,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; } @@ -6121,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 @@ -6131,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)) { @@ -6152,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 @@ -6182,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); } @@ -6195,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) @@ -6281,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) { @@ -6323,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); @@ -6760,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; } @@ -6838,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; @@ -6883,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; @@ -6893,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; @@ -6968,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 @@ -7077,7 +7390,7 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, // Per-group defaults — overridden below when equipment provides a geoset value. uint16_t geosetGloves = 401; // Bare forearms (group 4, no gloves) - uint16_t geosetBoots = 502; // Bare shins (group 5, no boots) + 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) @@ -7872,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); } 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 16666085..265a23c3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -115,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; @@ -169,28 +173,6 @@ float slowUpdateObjectBlockLogThresholdMs() { constexpr size_t kMaxQueuedInboundPackets = 4096; -bool hasFullPackedGuid(const network::Packet& packet) { - if (packet.getReadPos() >= packet.getSize()) { - 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.getSize() - packet.getReadPos() >= guidBytes; -} - -bool packetHasRemaining(const network::Packet& packet, size_t need) { - const size_t size = packet.getSize(); - const size_t pos = packet.getReadPos(); - return pos <= size && need <= (size - pos); -} - CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) { switch (missInfo) { case 0: return CombatTextEntry::MISS; @@ -637,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"); @@ -662,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() { @@ -700,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; @@ -784,7 +791,22 @@ void GameHandler::disconnect() { wardenLoadedModule_.reset(); pendingIncomingPackets_.clear(); pendingUpdateObjectWork_.clear(); - // Clear entity state so reconnect sees fresh CREATE_OBJECT for all visible objects. + // 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"); @@ -822,19 +844,7 @@ 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; @@ -881,7 +891,7 @@ void GameHandler::update(float deltaTime) { } // Detect RX silence (server stopped sending packets but TCP still open) - if (state == WorldState::IN_WORLD && socket && socket->isConnected() && + if (isInWorld() && socket->isConnected() && lastRxTime_.time_since_epoch().count() > 0) { auto silenceMs = std::chrono::duration_cast( std::chrono::steady_clock::now() - lastRxTime_).count(); @@ -916,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; @@ -967,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); @@ -990,7 +1260,7 @@ 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) { @@ -1009,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) { @@ -1076,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)) { @@ -1086,6 +1356,40 @@ 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) { @@ -1093,7 +1397,7 @@ void GameHandler::update(float deltaTime) { timeSinceLastMoveHeartbeat_ += deltaTime; const float currentPingInterval = - (isClassicLikeExpansion() || isActiveExpansion("tbc")) ? 10.0f : pingInterval; + (isPreWotlk()) ? 10.0f : pingInterval; if (timeSinceLastPing >= currentPingInterval) { if (socket) { sendPing(); @@ -1102,7 +1406,7 @@ 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) | @@ -1203,137 +1507,8 @@ void GameHandler::update(float deltaTime) { if (logoutCountdown_ < 0.0f) logoutCountdown_ = 0.0f; } - // 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; - } - } + updateTaxiAndMountState(deltaTime); - // 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"); - } - } - } // Update transport manager if (transportManager_) { @@ -1341,180 +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; - } + 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); + + // 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; + } + } + }; + 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); + } + } + } + }; + 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; + } + 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 { - autoAttackOutOfRange_ = false; - autoAttackOutOfRangeTime_ = 0.0f; + it->second = mergeCooldownSeconds(it->second, cdSec); } - - 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); + } + // 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!"); + }; - // 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); + // ---- 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); } - } - } - - // 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 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"); + } 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); } - } - } - 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"); + } 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; + } + } + } } - } - } - 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"); + } 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); } - } - } - 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"); + } 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; } } - - // 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(); + }; + // 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) { @@ -1524,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 @@ -1558,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(); @@ -1584,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); @@ -1593,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(); @@ -1654,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; } @@ -1686,6885 +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: - addUIError("You cannot send messages to members of that faction."); - addSystemChatMessage("You cannot send messages to members of that faction."); - break; - case Opcode::SMSG_CHAT_NOT_IN_PARTY: - addUIError("You are not in a party."); - addSystemChatMessage("You are not in a party."); - break; - case Opcode::SMSG_CHAT_RESTRICTED: - addUIError("You cannot send chat messages in this area."); - 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_WHOIS: { - // GM/admin response to /whois command: cstring with account/IP info - // Format: string (the whois result text, typically "Name: ...\nAccount: ...\nIP: ...") - if (packet.getReadPos() < packet.getSize()) { - std::string whoisText = packet.readString(); - if (!whoisText.empty()) { - // Display each line of the whois response in system chat - 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); - } - } - 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) { - if (const ItemQueryResponseData* info = getItemInfo(itemId)) { - // Item info already cached — emit immediately. - std::string itemName = info->name.empty() ? ("item #" + std::to_string(itemId)) : info->name; - 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); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playLootItem(); - } - if (itemLootCallback_) itemLootCallback_(itemId, count, quality, itemName); - } else { - // Item info not yet cached; defer until SMSG_ITEM_QUERY_SINGLE_RESPONSE. - pendingItemPushNotifs_.push_back({itemId, count}); - } - } - 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); - addCombatText(CombatTextEntry::XP_GAIN, - static_cast(xpGained), 0, true); - // XP is updated via PLAYER_XP update fields from the server. - if (areaDiscoveryCallback_) - areaDiscoveryCallback_(areaName, xpGained); - } - } - 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; - addUIError(s); - addSystemChatMessage(s); - } - break; - } - case Opcode::SMSG_PET_ACTION_FEEDBACK: { - // uint8 msg: 1=dead, 2=nothing_to_attack, 3=cant_attack_target, - // 4=target_too_far, 5=no_path, 6=cant_attack_immune - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t msg = packet.readUInt8(); - 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 (msg > 0 && msg < 7 && kPetFeedback[msg]) { - addSystemChatMessage(kPetFeedback[msg]); - } - packet.setReadPos(packet.getSize()); - 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(); - std::string questTitle; - for (const auto& q : questLog_) - if (q.questId == questId && !q.title.empty()) { questTitle = q.title; break; } - addSystemChatMessage(questTitle.empty() - ? std::string("Quest failed!") - : ('"' + questTitle + "\" failed!")); - } - break; - } - case Opcode::SMSG_QUESTUPDATE_FAILEDTIMER: { - // uint32 questId - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t questId = packet.readUInt32(); - std::string questTitle; - for (const auto& q : questLog_) - if (q.questId == questId && !q.title.empty()) { questTitle = q.title; break; } - addSystemChatMessage(questTitle.empty() - ? std::string("Quest timed out!") - : ('"' + questTitle + "\" has timed out.")); - } - 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); - if (honor > 0) - addCombatText(CombatTextEntry::HONOR_GAIN, static_cast(honor), 0, true); - if (pvpHonorCallback_) { - pvpHonorCallback_(honor, victimGuid, rank); - } - } - 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; - lastInteractedGoGuid_ = 0; - // Cancel craft queue and spell queue on cast failure - craftQueueSpellId_ = 0; - craftQueueRemaining_ = 0; - queuedSpellId_ = 0; - queuedSpellTarget_ = 0; - // 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); - if (spellCastFailedCallback_) spellCastFailedCallback_(castResultSpellId); - 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/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId + ... - // TBC: uint64 caster + uint64 victim + uint32 spellId + ... - const bool prUsesFullGuid = isActiveExpansion("tbc"); - auto readPrGuid = [&]() -> uint64_t { - if (prUsesFullGuid) - return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; - return UpdateObjectParser::readPackedGuid(packet); - }; - if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u) - || (!prUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t caster = readPrGuid(); - if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u) - || (!prUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); 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, 0, caster, victim); - } else if (caster == playerGuid) { - addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim); - } - 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; - // Ensure item info is queried so the roll popup can show the name/icon. - // The popup re-reads getItemInfo() live, so the name will populate once - // SMSG_ITEM_QUERY_SINGLE_RESPONSE arrives (usually within ~100 ms). - queryItemInfo(itemId, 0); - auto* info = getItemInfo(itemId); - pendingLootRoll_.itemName = info ? info->name : std::to_string(itemId); - 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, (int)voteMask, std::dec); - break; - } - - // ---- Pet stable list ---- - case Opcode::MSG_LIST_STABLED_PETS: - if (state == WorldState::IN_WORLD) handleListStabledPets(packet); - 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."; - addUIError(msg); break; - default: break; - } - if (msg) addSystemChatMessage(msg); - LOG_INFO("SMSG_STABLE_RESULT: result=", static_cast(result)); - // Refresh the stable list after a result to reflect the new state - if (stableWindowOpen_ && stableMasterGuid_ != 0 && socket && result <= 0x08) { - auto refreshPkt = ListStabledPetsPacket::build(stableMasterGuid_); - socket->send(refreshPkt); - } - 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(); - loadTitleNameCache(); - - // Format the title string using the player's own name - std::string titleStr; - auto tit = titleNameCache_.find(titleBit); - if (tit != titleNameCache_.end() && !tit->second.empty()) { - // Title strings contain "%s" as a player-name placeholder. - // Replace it with the local player's name if known. - auto nameIt = playerNameCache.find(playerGuid); - const std::string& pName = (nameIt != playerNameCache.end()) - ? nameIt->second : "you"; - 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; - } - // Track in known title set - if (isLost) { - knownTitleBits_.erase(titleBit); - } else { - knownTitleBits_.insert(titleBit); - } - - // Only post chat message for actual earned/lost events (isLost and new earn) - // Server sends isLost=0 for all known titles during login — suppress the chat spam - // by only notifying when we already had some titles (after login sequence) - addSystemChatMessage(msg); - LOG_INFO("SMSG_TITLE_EARNED: bit=", titleBit, " lost=", isLost, - " title='", titleStr, "' known=", knownTitleBits_.size()); - 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(); - // Update home bind location so hearthstone tooltip reflects the new zone - 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); - break; - } - case Opcode::SMSG_BINDER_CONFIRM: { - // uint64 npcGuid — fires just before SMSG_PLAYERBOUND; PLAYERBOUND shows - // the zone name so this confirm is redundant. Consume silently. - 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 { - // ResponseCodes for name changes (shared with char create) - static const char* kRenameErrors[] = { - nullptr, // 0 = success - "Name already in use.", // 1 - "Name too short.", // 2 - "Name too long.", // 3 - "Name contains invalid characters.", // 4 - "Name contains a profanity.", // 5 - "Name is reserved.", // 6 - "Character name does not meet requirements.", // 7 - }; - 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); - } - 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 { - addUIError("You are too far from the innkeeper."); - 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."; - addUIError(std::string("Cannot change difficulty: ") + msg); - addSystemChatMessage(std::string("Cannot change difficulty: ") + msg); - } - } - break; - } - case Opcode::SMSG_CORPSE_NOT_IN_INSTANCE: - addUIError("Your corpse is outside this 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: - // Consume — handled by broader object update or not yet implemented - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_FORCED_DEATH_UPDATE: - // Server forces player into dead state (GM command, scripted event, etc.) - playerDead_ = true; - if (ghostStateCallback_) ghostStateCallback_(false); // dead but not ghost yet - addSystemChatMessage("You have been killed."); - LOG_INFO("SMSG_FORCED_DEATH_UPDATE: player force-killed"); - 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 (PvP deaths) - if (packet.getSize() - packet.getReadPos() >= 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"); - } - break; - } - case Opcode::SMSG_DEATH_RELEASE_LOC: { - // uint32 mapId + float x + float y + float z - // This is the GRAVEYARD / ghost-spawn position, NOT the actual corpse location. - // The corpse remains at the death position (already cached when health dropped to 0, - // and updated when the corpse object arrives via SMSG_UPDATE_OBJECT). - // Do NOT overwrite corpseX_/Y_/Z_/MapId_ here — that would break canReclaimCorpse() - // by making it check distance to the graveyard instead of the real corpse. - if (packet.getSize() - packet.getReadPos() >= 16) { - uint32_t relMapId = packet.readUInt32(); - float relX = packet.readFloat(); - float relY = packet.readFloat(); - float relZ = packet.readFloat(); - LOG_INFO("SMSG_DEATH_RELEASE_LOC (graveyard spawn): map=", relMapId, - " x=", relX, " y=", relY, " z=", relZ); - } - break; - } - case Opcode::SMSG_ENABLE_BARBER_SHOP: - // Sent by server when player sits in barber chair — triggers barber shop UI - LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); - barberShopOpen_ = true; - 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: { - // uint32 achievementId — remove from local earned set - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t achId = packet.readUInt32(); - earnedAchievements_.erase(achId); - achievementDates_.erase(achId); - LOG_DEBUG("SMSG_ACHIEVEMENT_DELETED: id=", achId); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_CRITERIA_DELETED: { - // uint32 criteriaId — remove from local criteria progress - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t critId = packet.readUInt32(); - criteriaProgress_.erase(critId); - LOG_DEBUG("SMSG_CRITERIA_DELETED: id=", critId); - } - 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." }; - std::string mountErr = result < 4 ? msgs[result] : "Cannot mount."; - addUIError(mountErr); - addSystemChatMessage(mountErr); - } - 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) { addUIError("Cannot dismount here."); 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); - 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; - 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); - 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); - } - } - break; - } - case Opcode::SMSG_LOOT_SLOT_CHANGED: { - // uint8 slotIndex — another player took the item from this slot in group loot - if (packet.getSize() - packet.getReadPos() >= 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; - } - } - } - break; - } - - // ---- Spell log miss ---- - case Opcode::SMSG_SPELLLOGMISS: { - // 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.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() < (spellMissUsesFullGuid ? 8u : 1u) - || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = readSpellMissGuid(); - if (packet.getSize() - packet.getReadPos() < 5) break; - /*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.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u) - || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { - truncated = true; - break; - } - const uint64_t victimGuid = readSpellMissGuid(); - if (packet.getSize() - packet.getReadPos() < 1) { - truncated = true; - break; - } - const uint8_t missInfo = packet.readUInt8(); - // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult - uint32_t reflectSpellId = 0; - if (missInfo == 11) { - if (packet.getSize() - packet.getReadPos() >= 5) { - reflectSpellId = packet.readUInt32(); - /*uint8_t reflectResult =*/ packet.readUInt8(); - } else { - truncated = true; - break; - } - } - if (i < storedLimit) { - parsedMisses.push_back({victimGuid, missInfo, reflectSpellId}); - } - } - - if (truncated) { - packet.setReadPos(packet.getSize()); - break; - } - - 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); - } - } - 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) { - // 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); - } - 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_CAMERA_SHAKE: { - // uint32 shakeID (CameraShakes.dbc), uint32 shakeType - // We don't parse CameraShakes.dbc; apply a hardcoded moderate shake. - if (packet.getSize() - packet.getReadPos() >= 8) { - uint32_t shakeId = packet.readUInt32(); - uint32_t shakeType = packet.readUInt32(); - (void)shakeType; - // Map shakeId ranges to approximate magnitudes: - // IDs < 50: minor environmental (0.04), others: larger boss effects (0.08) - float magnitude = (shakeId < 50) ? 0.04f : 0.08f; - if (cameraShakeCallback_) { - cameraShakeCallback_(magnitude, 18.0f, 0.5f); - } - LOG_DEBUG("SMSG_CAMERA_SHAKE: id=", shakeId, " type=", shakeType, - " magnitude=", magnitude); - } - 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; - } - 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); - } - } - } - break; - case Opcode::SMSG_ATTACKSWING_NOTSTANDING: - autoAttackOutOfRange_ = false; - autoAttackOutOfRangeTime_ = 0.0f; - if (autoAttackRangeWarnCooldown_ <= 0.0f) { - addSystemChatMessage("You need to stand up to fight."); - autoAttackRangeWarnCooldown_ = 1.25f; - } - break; - case Opcode::SMSG_ATTACKSWING_CANT_ATTACK: - // Target is permanently non-attackable (critter, civilian, already dead, etc.). - // Stop the auto-attack loop so the client doesn't spam the server. - stopAutoAttack(); - if (autoAttackRangeWarnCooldown_ <= 0.0f) { - addSystemChatMessage("You can't attack that."); - autoAttackRangeWarnCooldown_ = 1.25f; - } - 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: { - // uint64 casterGuid + uint32 visualId - if (packet.getSize() - packet.getReadPos() < 12) break; - uint64_t casterGuid = packet.readUInt64(); - uint32_t visualId = packet.readUInt32(); - if (visualId == 0) break; - // Resolve caster world position and spawn the effect - auto* renderer = core::Application::getInstance().getRenderer(); - if (!renderer) break; - glm::vec3 spawnPos; - if (casterGuid == playerGuid) { - spawnPos = renderer->getCharacterPosition(); - } else { - auto entity = entityManager.getEntity(casterGuid); - if (!entity) break; - glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); - spawnPos = core::coords::canonicalToRender(canonical); - } - renderer->playSpellVisual(visualId, spawnPos); - 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 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); - } - } - } - 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; - 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) { - 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"); - } - } - 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!"); - addUIError(msg); - 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] - // 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.getSize() - packet.getReadPos() < 20) break; - dispelCasterGuid = packet.readUInt64(); - /*uint64_t victim =*/ packet.readUInt64(); - dispelSpellId = packet.readUInt32(); - } else { - if (packet.getSize() - packet.getReadPos() < 4) break; - dispelSpellId = packet.readUInt32(); - if (!hasFullPackedGuid(packet)) { - packet.setReadPos(packet.getSize()); break; - } - dispelCasterGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { - packet.setReadPos(packet.getSize()); break; - } - /*uint64_t victim =*/ UpdateObjectParser::readPackedGuid(packet); - } - // Only show failure to the player who attempted the dispel - if (dispelCasterGuid == playerGuid) { - 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"); - if (slot < NUM_TOTEM_SLOTS) { - activeTotemSlots_[slot].spellId = spellId; - activeTotemSlots_[slot].durationMs = duration; - activeTotemSlots_[slot].placedAt = std::chrono::steady_clock::now(); - } - 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(); - 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); - } - 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; - addUIError("Your party has been disbanded."); - 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(); - readyCheckResults_.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(); - } - // Track per-player result for live popup display - 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); - } - 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; - readyCheckResults_.clear(); - 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: { - // uint32 countdown in milliseconds (typically 3000 ms) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t ms = packet.readUInt32(); - duelCountdownMs_ = (ms > 0 && ms <= 30000) ? ms : 3000; - duelCountdownStartedAt_ = std::chrono::steady_clock::now(); - LOG_INFO("SMSG_DUEL_COUNTDOWN: ", duelCountdownMs_, " ms"); - } - 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: { - // uint8 count + count * uint64 guid — eligible recipients for master looter - masterLootCandidates_.clear(); - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t mlCount = packet.readUInt8(); - masterLootCandidates_.reserve(mlCount); - for (uint8_t i = 0; i < mlCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 8) break; - masterLootCandidates_.push_back(packet.readUInt64()); - } - LOG_INFO("SMSG_LOOT_MASTER_LIST: ", (int)masterLootCandidates_.size(), " candidates"); - 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; - 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); - } - } 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."); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playQuestActivate(); - } - 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) + ")"; - - addUIError(msg); - addSystemChatMessage(msg); - // Play error sound so the player notices the failure - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playError(); - } - 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; Classic/TBC/Turtle use the shorter format - size_t remaining = packet.getSize() - packet.getReadPos(); - 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.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); - watchedFactionId_ = factionId; - if (repChangeCallback_) repChangeCallback_(name, delta, standing); - } - LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing); - } - break; - } - case Opcode::SMSG_SET_FACTION_ATWAR: { - // uint32 repListId + uint8 set (1=set at-war, 0=clear at-war) - if (packet.getSize() - packet.getReadPos() < 5) { - packet.setReadPos(packet.getSize()); break; - } - 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; - LOG_DEBUG("SMSG_SET_FACTION_ATWAR: repListId=", repListId, - " atWar=", (int)setAtWar); - } - break; - } - case Opcode::SMSG_SET_FACTION_VISIBLE: { - // uint32 repListId + uint8 visible (1=show, 0=hide) - if (packet.getSize() - packet.getReadPos() < 5) { - packet.setReadPos(packet.getSize()); break; - } - 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; - LOG_DEBUG("SMSG_SET_FACTION_VISIBLE: repListId=", repListId, - " visible=", (int)visible); - } - break; - } - - case Opcode::SMSG_FEATURE_SYSTEM_STATUS: - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: - case Opcode::SMSG_SET_PCT_SPELL_MODIFIER: { - // WotLK format: one or more (uint8 groupIndex, uint8 modOp, int32 value) tuples - // Each tuple is 6 bytes; iterate until packet is consumed. - const bool isFlat = (*logicalOp == Opcode::SMSG_SET_FLAT_SPELL_MODIFIER); - auto& modMap = isFlat ? spellFlatMods_ : spellPctMods_; - while (packet.getSize() - packet.getReadPos() >= 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; - LOG_DEBUG(isFlat ? "SMSG_SET_FLAT_SPELL_MODIFIER" : "SMSG_SET_PCT_SPELL_MODIFIER", - ": group=", (int)groupIndex, " op=", (int)modOpRaw, " value=", value); - } - 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; - castTimeTotal += delaySec; // keep progress percentage correct - } - } 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 - std::string setName; - if (packet.getSize() - packet.getReadPos() >= 12) { - uint32_t setIndex = packet.readUInt32(); - uint64_t setGuid = packet.readUInt64(); - for (const auto& es : equipmentSets_) { - if (es.setGuid == setGuid || - (es.setGuid == 0 && es.setId == setIndex)) { - setName = es.name; - break; - } - } - (void)setIndex; - } - addSystemChatMessage(setName.empty() - ? std::string("Equipment set saved.") - : "Equipment set \"" + setName + "\" 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)+isCrit(1) = 21 bytes - const bool periodicWotlk = isActiveExpansion("wotlk"); - const size_t dotSz = periodicWotlk ? 21u : 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(); - 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.getSize() - packet.getReadPos() < 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.getSize() - packet.getReadPos() < 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.getSize() - packet.getReadPos() < 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.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"); - auto readEnergizeGuid = [&]() -> uint64_t { - if (energizeTbc) - return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; - return UpdateObjectParser::readPackedGuid(packet); - }; - if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u) - || (!energizeTbc && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t victimGuid = readEnergizeGuid(); - if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u) - || (!energizeTbc && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = readEnergizeGuid(); - if (packet.getSize() - packet.getReadPos() < 9) { - packet.setReadPos(packet.getSize()); break; - } - 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_ENVIRONMENTAL_DAMAGE_LOG: { - // 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.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) { - // 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.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: { - // 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.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 = 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; - } - 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. - // WotLK layout: uint32 newLevel + uint32 hpDelta + uint32 manaDelta + 5x uint32 statDeltas - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t newLevel = packet.readUInt32(); - if (newLevel > 0) { - // Parse stat deltas (WotLK layout has 7 more uint32s) - lastLevelUpDeltas_ = {}; - if (packet.getSize() - packet.getReadPos() >= 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_; - break; - } - } - if (newLevel > oldLevel) { - addSystemChatMessage("You have reached level " + std::to_string(newLevel) + "!"); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playLevelUp(); - } - if (levelUpCallback_) levelUpCallback_(newLevel); - } - } - } - 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()) { - addUIError(msg); - addSystemChatMessage(msg); - areaTriggerMsgs_.push_back(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); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playDropOnGround(); - } - } 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"; - addUIError(std::string("Sell failed: ") + msg); - addSystemChatMessage(std::string("Sell failed: ") + msg); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playError(); - } - 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); - 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."); - } - 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) + ")."; - addUIError(msg); - addSystemChatMessage(msg); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playError(); - } - } - } - 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; - } - addUIError(msg); - addSystemChatMessage(msg); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playError(); - } - } - 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(); - // 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); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playPickupBag(); - } - } - 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."); - barberShopOpen_ = false; - } 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); - } - 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(); - 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); - } - // 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); - } - } - 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 { - addUIError("Failed to socket gems."); - 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."; - addUIError(msg); - 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); - } - // animId == 0 is the fishing bobber splash ("fish hooked"). - // Detect by GO type 17 (FISHINGNODE) and notify the player so they - // know to click the bobber before the fish escapes. - 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) { // GO_TYPE_FISHINGNODE - addUIError("A fish is on your line!"); - addSystemChatMessage("A fish is on your line!"); - // Play a distinctive UI sound to alert the player - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) { - sfx->playQuestUpdate(); // Distinct "ping" sound - } - } - } - } - } - } - 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) { - // Fire toast callback before erasing - if (questCompleteCallback_) { - questCompleteCallback_(questId, it->title); - } - // Play quest-complete sound - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playQuestComplete(); - } - 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); - - if (questProgressCallback_) { - questProgressCallback_(quest.title, creatureName, count, reqCount); - } - - 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); - 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; - } - } - - 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). - addUIError("Your quest log is full."); - 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: { - // SMSG_GROUP_SET_LEADER: string leaderName (null-terminated) - if (packet.getSize() > packet.getReadPos()) { - std::string leaderName = packet.readString(); - // Update leaderGuid by name lookup in party members - 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."); - LOG_INFO("SMSG_GROUP_SET_LEADER: ", leaderName); - } - break; - } - - // ---- Teleport / Transfer ---- - case Opcode::MSG_MOVE_TELEPORT: - 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; - } - addUIError(abortMsg); - 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: - handleBattlefieldList(packet); - break; - case Opcode::SMSG_BATTLEFIELD_PORT_DENIED: - addUIError("Battlefield port denied."); - addSystemChatMessage("Battlefield port denied."); - break; - case Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS: { - bgPlayerPositions_.clear(); - for (int grp = 0; grp < 2; ++grp) { - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t count = packet.readUInt32(); - for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 16; ++i) { - BgPlayerPosition pos; - pos.guid = packet.readUInt64(); - pos.wowX = packet.readFloat(); - pos.wowY = packet.readFloat(); - pos.group = grp; - bgPlayerPositions_.push_back(pos); - } - } - 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: { - // SMSG_BATTLEGROUND_PLAYER_JOINED: uint64 guid - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t guid = packet.readUInt64(); - auto it = playerNameCache.find(guid); - std::string name = (it != playerNameCache.end()) ? it->second : ""; - if (!name.empty()) - addSystemChatMessage(name + " has entered the battleground."); - LOG_INFO("SMSG_BATTLEGROUND_PLAYER_JOINED: guid=0x", std::hex, guid, std::dec); - } - break; - } - case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT: { - // SMSG_BATTLEGROUND_PLAYER_LEFT: uint64 guid - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t guid = packet.readUInt64(); - auto it = playerNameCache.find(guid); - std::string name = (it != playerNameCache.end()) ? it->second : ""; - if (!name.empty()) - addSystemChatMessage(name + " has left the battleground."); - LOG_INFO("SMSG_BATTLEGROUND_PLAYER_LEFT: guid=0x", std::hex, guid, std::dec); - } - 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(); - std::string mapLabel = getMapName(mapId); - if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId); - // 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; - addSystemChatMessage(mapLabel + " will reset in " + - std::to_string(minutes) + " minute(s)."); - } else if (msgType == 2) { - addSystemChatMessage("You have been saved to " + mapLabel + "."); - } else if (msgType == 3) { - addSystemChatMessage("Welcome to " + mapLabel + "."); - } - 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()); - std::string mapLabel = getMapName(mapId); - if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId); - addSystemChatMessage(mapLabel + " 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* 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); - 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 ilMapId = packet.readUInt32(); - uint32_t ilDiff = packet.readUInt32(); - uint32_t ilTimeLeft = packet.readUInt32(); - packet.readUInt32(); // unk - uint8_t ilLocked = packet.readUInt8(); - // Notify player which instance is being entered/resumed - 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) { - uint32_t ilMins = ilTimeLeft / 60; - ilMsg += " — " + std::to_string(ilMins) + " min remaining."; - } else { - ilMsg += "."; - } - addSystemChatMessage(ilMsg); - // 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 mapId=", ilMapId, - " diff=", ilDiff, " timeLeft=", ilTimeLeft); - } - 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: { - // uint64 guid + uint8 ready + uint32 roles - if (packet.getSize() - packet.getReadPos() >= 13) { - uint64_t roleGuid = packet.readUInt64(); - uint8_t ready = packet.readUInt8(); - uint32_t roles = packet.readUInt32(); - // Build a descriptive message for group chat - std::string roleName; - if (roles & 0x02) roleName += "Tank "; - if (roles & 0x04) roleName += "Healer "; - if (roles & 0x08) roleName += "DPS "; - if (roleName.empty()) roleName = "None"; - // Find player name - 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); - LOG_DEBUG("SMSG_LFG_ROLE_CHOSEN: guid=", roleGuid, - " ready=", (int)ready, " roles=", roles); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_LFG_UPDATE_SEARCH: - case Opcode::SMSG_UPDATE_LFG_LIST: - case Opcode::SMSG_LFG_PLAYER_INFO: - case Opcode::SMSG_LFG_PARTY_INFO: - // Informational LFG packets not yet surfaced in UI — consume silently. - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_OPEN_LFG_DUNGEON_FINDER: - // Server requests client to open the dungeon finder UI - packet.setReadPos(packet.getSize()); // consume any payload - if (openLfgCallback_) openLfgCallback_(); - 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: - handleArenaTeamRoster(packet); - 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: - handlePvpLogData(packet); - break; - case Opcode::MSG_INSPECT_ARENA_TEAMS: { - // WotLK: uint64 playerGuid + uint8 teamCount + per-team fields - if (packet.getSize() - packet.getReadPos() < 9) { - packet.setReadPos(packet.getSize()); - break; - } - 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.getSize() - packet.getReadPos() < 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.getSize() - packet.getReadPos() < 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=", (int)teamCount); - 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; - - // ---- Broadcast speed changes (server→client, no ACK) ---- - // Format: PackedGuid (mover) + MovementInfo (variable) + float speed - // MovementInfo is complex (optional transport/fall/spline blocks based on flags). - // We consume the packet to suppress "Unhandled world opcode" warnings. - case Opcode::MSG_MOVE_SET_RUN_SPEED: - case Opcode::MSG_MOVE_SET_RUN_BACK_SPEED: - case Opcode::MSG_MOVE_SET_WALK_SPEED: - case Opcode::MSG_MOVE_SET_SWIM_SPEED: - case Opcode::MSG_MOVE_SET_SWIM_BACK_SPEED: - case Opcode::MSG_MOVE_SET_FLIGHT_SPEED: - case Opcode::MSG_MOVE_SET_FLIGHT_BACK_SPEED: - if (state == WorldState::IN_WORLD) { - handleMoveSetSpeed(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: entity manager > playerNameCache - auto entity = entityManager.getEntity(memberGuid); - std::string name; - if (entity) { - auto player = std::dynamic_pointer_cast(entity); - if (player && !player->getName().empty()) name = player->getName(); - } - if (name.empty()) { - auto nit = playerNameCache.find(memberGuid); - if (nit != playerNameCache.end()) name = nit->second; - } - if (name.empty()) name = "(unknown)"; - 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) + ... - // action: 0=sold/won, 1=expired, 2=bid placed on your auction - 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(); - ensureItemInfo(itemEntry); - auto* info = getItemInfo(itemEntry); - std::string rawName = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); - 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.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 rawName2 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); - uint32_t bidQuality = info ? info->quality : 1u; - std::string bidLink = buildItemLink(itemEntry, bidQuality, rawName2); - addSystemChatMessage("You have been outbid on " + bidLink + "."); - } - 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 rawName3 = info && !info->name.empty() ? info->name : ("Item #" + std::to_string(itemEntry)); - uint32_t remQuality = info ? info->quality : 1u; - std::string remLink = buildItemLink(itemEntry, remQuality, rawName3); - addSystemChatMessage("Your auction of " + remLink + " 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: { - // PackedGuid (player guid) + uint32 vehicleId - // vehicleId == 0 means the player left the vehicle - if (packet.getSize() - packet.getReadPos() >= 1) { - (void)UpdateObjectParser::readPackedGuid(packet); // player guid (unused) - } - if (packet.getSize() - packet.getReadPos() >= 4) { - vehicleId_ = packet.readUInt32(); - } else { - vehicleId_ = 0; - } - 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; - 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.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: - // 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.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) { addUIError("Failed to equip item set."); 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; - - // ---- LFG error/timeout states ---- - case Opcode::SMSG_LFG_TIMEDOUT: - // Server-side LFG invite timed out (no response within time limit) - addSystemChatMessage("Dungeon Finder: Invite timed out."); - if (openLfgCallback_) openLfgCallback_(); - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_LFG_OTHER_TIMEDOUT: - // 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.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_LFG_AUTOJOIN_FAILED: { - // uint32 result — LFG auto-join attempt failed (player selected auto-join at queue time) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t result = packet.readUInt32(); - (void)result; - } - addUIError("Dungeon Finder: Auto-join failed."); - addSystemChatMessage("Dungeon Finder: Auto-join failed."); - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER: - // 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.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_LFG_LEADER_IS_LFM: - // Party leader is currently set to Looking for More (LFM) mode - addSystemChatMessage("Your party leader is currently Looking for More."); - packet.setReadPos(packet.getSize()); - break; - - // ---- Meeting stone (Classic/TBC group-finding via summon stone) ---- - case Opcode::SMSG_MEETINGSTONE_SETQUEUE: { - // uint32 zoneId + uint8 level_min + uint8 level_max — player queued for meeting stone - if (packet.getSize() - packet.getReadPos() >= 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=", (int)levelMin, "-", (int)levelMax); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_MEETINGSTONE_COMPLETE: - // 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.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_MEETINGSTONE_IN_PROGRESS: - // Meeting stone search is still ongoing - addSystemChatMessage("Meeting Stone: Searching for group members..."); - LOG_DEBUG("SMSG_MEETINGSTONE_IN_PROGRESS"); - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_MEETINGSTONE_MEMBER_ADDED: { - // uint64 memberGuid — a player was added to your group via meeting stone - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t memberGuid = packet.readUInt64(); - auto nit = playerNameCache.find(memberGuid); - if (nit != playerNameCache.end() && !nit->second.empty()) { - addSystemChatMessage("Meeting Stone: " + nit->second + - " 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); - } - break; - } - case Opcode::SMSG_MEETINGSTONE_JOINFAILED: { - // 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.getSize() - packet.getReadPos() >= 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=", (int)reason); - } - break; - } - case Opcode::SMSG_MEETINGSTONE_LEAVE: - // 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.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: { - // 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.getSize() - packet.getReadPos() < 1) { packet.setReadPos(packet.getSize()); break; } - uint8_t gmStatus = packet.readUInt8(); - // Status 6 = GMTICKET_STATUS_HASTEXT — open ticket with text - if (gmStatus == 6 && packet.getSize() - packet.getReadPos() >= 1) { - gmTicketText_ = packet.readString(); - uint32_t ageSec = (packet.getSize() - packet.getReadPos() >= 4) ? packet.readUInt32() : 0; - /*uint32_t daysLeft =*/ (packet.getSize() - packet.getReadPos() >= 4) ? packet.readUInt32() : 0; - gmTicketWaitHours_ = (packet.getSize() - packet.getReadPos() >= 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=", (int)gmStatus, ")"); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_GMTICKET_SYSTEMSTATUS: { - // uint32 status: 1 = GM support available, 0 = offline/unavailable - if (packet.getSize() - packet.getReadPos() >= 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.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_SPELLDAMAGESHIELD: { - // 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.getSize() - packet.getReadPos(); }; - const size_t shieldMinSz = shieldTbc ? 24u : 2u; - if (packet.getSize() - packet.getReadPos() < shieldMinSz) { - packet.setReadPos(packet.getSize()); break; - } - if (!shieldTbc && (!hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t victimGuid = shieldTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (shieldTbc ? 8u : 1u) - || (!shieldTbc && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = shieldTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - const size_t shieldTailSize = shieldWotlkLike ? 16u : 12u; - if (shieldRem() < shieldTailSize) { - packet.setReadPos(packet.getSize()); break; - } - 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); - } - break; - } - case Opcode::SMSG_AURACASTLOG: - case Opcode::SMSG_SPELLBREAKLOG: - // These packets are not damage-shield events. Consume them without - // synthesizing reflected damage entries or misattributing GUIDs. - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_SPELLORDAMAGE_IMMUNE: { - // 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.getSize() - packet.getReadPos() < minSz) { - packet.setReadPos(packet.getSize()); break; - } - if (!immuneUsesFullGuid && !hasFullPackedGuid(packet)) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = immuneUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (immuneUsesFullGuid ? 8u : 2u) - || (!immuneUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t victimGuid = immuneUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 5) break; - 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); - } - break; - } - case Opcode::SMSG_SPELLDISPELLOG: { - // 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.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u) - || (!dispelUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t casterGuid = dispelUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u) - || (!dispelUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t victimGuid = dispelUsesFullGuid - ? 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(); - // 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.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELLSTEALLOG: { - // 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.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u) - || (!stealUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t stealVictim = stealUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u) - || (!stealUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t stealCaster = stealUsesFullGuid - ? 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(); - // 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.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: { - // 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.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; - return UpdateObjectParser::readPackedGuid(packet); - }; - if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u) - || (!procChanceUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t procTargetGuid = readProcChanceGuid(); - if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u) - || (!procChanceUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t procCasterGuid = readProcChanceGuid(); - if (packet.getSize() - packet.getReadPos() < 4) { - packet.setReadPos(packet.getSize()); break; - } - 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELLINSTAKILLLOG: { - // 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.getSize() - packet.getReadPos(); }; - if (ik_rem() < (ikUsesFullGuid ? 8u : 1u) - || (!ikUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t ikCaster = ikUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (ik_rem() < (ikUsesFullGuid ? 8u : 1u) - || (!ikUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t ikVictim = ikUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (ik_rem() < 4) { - packet.setReadPos(packet.getSize()); break; - } - 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_SPELLLOGEXECUTE: { - // 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.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)) { - packet.setReadPos(packet.getSize()); break; - } - if (!exeUsesFullGuid && !hasFullPackedGuid(packet)) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t exeCaster = exeUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) { - packet.setReadPos(packet.getSize()); break; - } - 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.getSize() - packet.getReadPos() < 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.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t drainTarget = exeUsesFullGuid - ? packet.readUInt64() - : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 12) { packet.setReadPos(packet.getSize()); 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.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t leechTarget = exeUsesFullGuid - ? packet.readUInt64() - : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 8) { packet.setReadPos(packet.getSize()); 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.getSize() - packet.getReadPos() < 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)); - loadSpellNameCache(); - auto spellIt = spellNameCache_.find(exeSpellId); - std::string spellName = (spellIt != spellNameCache_.end() && !spellIt->second.name.empty()) - ? spellIt->second.name : ""; - 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.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t icTarget = exeUsesFullGuid - ? packet.readUInt64() - : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 4) { packet.setReadPos(packet.getSize()); 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.getSize() - packet.getReadPos() < 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.setReadPos(packet.getSize()); - break; - } - } - packet.setReadPos(packet.getSize()); - break; - } - 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_ITEM_ENCHANT_TIME_UPDATE: { - // Format: uint64 itemGuid + uint32 slot + uint32 durationSec + uint64 playerGuid - // slot: 0=main-hand, 1=off-hand, 2=ranged - if (packet.getSize() - packet.getReadPos() < 24) { - packet.setReadPos(packet.getSize()); break; - } - /*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) { break; } - - 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"); - break; - } - case Opcode::SMSG_COMPLAIN_RESULT: { - // uint8 result: 0=success, 1=failed, 2=disabled - if (packet.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_ITEM_REFUND_INFO_RESPONSE: - case Opcode::SMSG_LOOT_LIST: - // Consume silently — informational, no UI action needed - 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; - s.interruptible = isSpellInterruptible(chanSpellId); - } - 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(); - std::string questTitle; - for (const auto& q : questLog_) - if (q.questId == questId && !q.title.empty()) { questTitle = q.title; break; } - 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); - } - 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: { - // 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 = UpdateObjectParser::readPackedGuid(packet); - if (targetGuid == playerGuid || targetGuid == 0) { - selfResAvailable_ = true; - LOG_INFO("SMSG_PRE_RESURRECT: self-resurrection available (guid=0x", - std::hex, targetGuid, std::dec, ")"); - } - break; - } - - // ---- Hearthstone bind error ---- - case Opcode::SMSG_PLAYERBINDERROR: { - if (packet.getSize() - packet.getReadPos() >= 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."); - } - } - break; - } - - // ---- Instance/raid errors ---- - case Opcode::SMSG_RAID_GROUP_ONLY: { - 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.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) { 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."); } - } - break; - } - case Opcode::SMSG_RESET_FAILED_NOTIFY: { - addUIError("Cannot reset instance: another player is still inside."); - 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 (group type, local player flags, leader) ---- - // Sent when the player's group configuration changes: group type, - // role/flags (assistant/MT/MA), or leader changes. - // Format: uint8 groupType | uint32 memberFlags | uint64 leaderGuid - case Opcode::SMSG_REAL_GROUP_UPDATE: { - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 1) break; - uint8_t newGroupType = packet.readUInt8(); - if (rem() < 4) break; - uint32_t newMemberFlags = packet.readUInt32(); - if (rem() < 8) break; - 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); - 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: - 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: 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; - case Opcode::SMSG_PLAY_SPELL_IMPACT: { - // uint64 targetGuid + uint32 visualId (same structure as SMSG_PLAY_SPELL_VISUAL) - if (packet.getSize() - packet.getReadPos() < 12) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t impTargetGuid = packet.readUInt64(); - uint32_t impVisualId = packet.readUInt32(); - if (impVisualId == 0) break; - auto* renderer = core::Application::getInstance().getRenderer(); - if (!renderer) break; - glm::vec3 spawnPos; - if (impTargetGuid == playerGuid) { - spawnPos = renderer->getCharacterPosition(); - } else { - auto entity = entityManager.getEntity(impTargetGuid); - if (!entity) break; - glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()); - spawnPos = core::coords::canonicalToRender(canonical); - } - renderer->playSpellVisual(impVisualId, spawnPos, /*useImpactKit=*/true); - break; - } - - // ---- Resistance/combat log ---- - case Opcode::SMSG_RESISTLOG: { - // 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.getSize() - packet.getReadPos(); }; - if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } - /*uint32_t hitInfo =*/ packet.readUInt32(); - if (rl_rem() < (rlUsesFullGuid ? 8u : 1u) - || (!rlUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t attackerGuid = rlUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (rl_rem() < (rlUsesFullGuid ? 8u : 1u) - || (!rlUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t victimGuid = rlUsesFullGuid - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } - 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.setReadPos(packet.getSize()); break; } - /*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.setReadPos(packet.getSize()); - break; - } - - // ---- Read item results ---- - case Opcode::SMSG_READ_ITEM_OK: - bookPages_.clear(); // fresh book for this item read - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_READ_ITEM_FAILED: - addUIError("You cannot read this item."); - 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: - addUIError("That creature can't talk to you right now."); - 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: - handlePetitionQueryResponse(packet); - break; - case Opcode::SMSG_PETITION_SHOW_SIGNATURES: - handlePetitionShowSignatures(packet); - break; - case Opcode::SMSG_PETITION_SIGN_RESULTS: - handlePetitionSignResults(packet); - 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); - 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); - } - 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: { - // 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.getSize() - packet.getReadPos() >= minSize) { - if (hasCount) /*uint8_t castCount =*/ packet.readUInt8(); - uint32_t spellId = packet.readUInt32(); - uint8_t reason = (packet.getSize() - packet.getReadPos() >= 1) - ? packet.readUInt8() : 0; - LOG_DEBUG("SMSG_PET_CAST_FAILED: spell=", spellId, - " reason=", (int)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.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: { - // uint64 petGuid + uint32 cost (copper) - if (packet.getSize() - packet.getReadPos() >= 12) { - petUnlearnGuid_ = packet.readUInt64(); - petUnlearnCost_ = packet.readUInt32(); - petUnlearnPending_ = true; - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_PET_UPDATE_COMBO_POINTS: - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_PET_RENAMEABLE: - // Server signals that the pet can now be named (first tame) - petRenameablePending_ = true; - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_PET_NAME_INVALID: - addUIError("That pet name is invalid. Please choose a different name."); - addSystemChatMessage("That pet name is invalid. Please choose a different name."); - packet.setReadPos(packet.getSize()); - break; - - // ---- Inspect (Classic 1.12 gear inspection) ---- - case Opcode::SMSG_INSPECT: { - // 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.getSize() - packet.getReadPos() < 2) { - packet.setReadPos(packet.getSize()); break; - } - uint64_t guid = UpdateObjectParser::readPackedGuid(packet); - if (guid == 0) { packet.setReadPos(packet.getSize()); break; } - - constexpr int kGearSlots = 19; - size_t needed = kGearSlots * sizeof(uint32_t); - if (packet.getSize() - packet.getReadPos() < needed) { - packet.setReadPos(packet.getSize()); break; - } - - 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"); - 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; - 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.setReadPos(packet.getSize()); - break; - } - - // ---- Misc consume (no state change needed) ---- - case Opcode::SMSG_SET_PLAYER_DECLINED_NAMES_RESULT: - case Opcode::SMSG_REDIRECT_CLIENT: - case Opcode::SMSG_PVP_QUEUE_STATS: - case Opcode::SMSG_NOTIFY_DEST_LOC_SPELL_CAST: - case Opcode::SMSG_PLAYER_SKINNED: - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_PROPOSE_LEVEL_GRANT: { - // Recruit-A-Friend: a mentor is offering to grant you a level - if (packet.getSize() - packet.getReadPos() >= 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()) { - auto nit = playerNameCache.find(mentorGuid); - if (nit != playerNameCache.end()) mentorName = nit->second; - } - addSystemChatMessage(mentorName.empty() - ? "A player is offering to grant you a level." - : (mentorName + " is offering to grant you a level.")); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_REFER_A_FRIEND_EXPIRED: - addSystemChatMessage("Your Recruit-A-Friend link has expired."); - packet.setReadPos(packet.getSize()); - break; - case Opcode::SMSG_REFER_A_FRIEND_FAILURE: { - if (packet.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_REPORT_PVP_AFK_RESULT: { - if (packet.getSize() - packet.getReadPos() >= 1) { - uint8_t result = packet.readUInt8(); - if (result == 0) - addSystemChatMessage("AFK report submitted."); - else - addSystemChatMessage("Cannot report that player as AFK right now."); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_RESPOND_INSPECT_ACHIEVEMENTS: - handleRespondInspectAchievements(packet); - break; - case Opcode::SMSG_QUEST_POI_QUERY_RESPONSE: - handleQuestPoiQueryResponse(packet); - break; - case Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA: - vehicleId_ = 0; // Vehicle ride cancelled; clear UI - packet.setReadPos(packet.getSize()); - break; - 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; - - // ---- Mirror image data (WotLK: Mage ability Mirror Image) ---- - case Opcode::SMSG_MIRRORIMAGE_DATA: { - // 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.getSize() - packet.getReadPos() < 8) break; - uint64_t mirrorGuid = packet.readUInt64(); - if (packet.getSize() - packet.getReadPos() < 4) break; - uint32_t displayId = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < 3) break; - /*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.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; - - // ---- Battlefield Manager (WotLK outdoor battlefields: Wintergrasp, Tol Barad) ---- - case Opcode::SMSG_BATTLEFIELD_MGR_ENTRY_INVITE: { - // uint64 battlefieldGuid + uint32 zoneId + uint64 expireUnixTime (seconds) - if (packet.getSize() - packet.getReadPos() < 20) { - packet.setReadPos(packet.getSize()); break; - } - 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); - break; - } - case Opcode::SMSG_BATTLEFIELD_MGR_ENTERED: { - // uint64 battlefieldGuid + uint8 isSafe (1=pvp zones enabled) + uint8 onQueue - if (packet.getSize() - packet.getReadPos() >= 8) { - uint64_t bfGuid2 = packet.readUInt64(); - (void)bfGuid2; - uint8_t isSafe = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; - uint8_t onQueue = (packet.getSize() - packet.getReadPos() >= 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=", (int)isSafe, " onQueue=", (int)onQueue); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_BATTLEFIELD_MGR_QUEUE_INVITE: { - // uint64 battlefieldGuid + uint32 battlefieldId + uint64 expireTime - if (packet.getSize() - packet.getReadPos() < 20) { - packet.setReadPos(packet.getSize()); break; - } - 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); - break; - } - case Opcode::SMSG_BATTLEFIELD_MGR_QUEUE_REQUEST_RESPONSE: { - // 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.getSize() - packet.getReadPos() < 11) { - packet.setReadPos(packet.getSize()); break; - } - 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=", (int)accepted, - " result=", (int)result); - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_BATTLEFIELD_MGR_EJECT_PENDING: { - // uint64 battlefieldGuid + uint8 remove - if (packet.getSize() - packet.getReadPos() >= 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=", (int)remove); - } - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_BATTLEFIELD_MGR_EJECTED: { - // uint64 battlefieldGuid + uint32 reason + uint32 battleStatus + uint8 relocated - if (packet.getSize() - packet.getReadPos() >= 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=", (int)relocated); - } - bfMgrActive_ = false; - bfMgrInvitePending_ = false; - packet.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_BATTLEFIELD_MGR_STATE_CHANGE: { - // uint32 oldState + uint32 newState - // States: 0=Waiting, 1=Starting, 2=InProgress, 3=Ending, 4=Cooldown - if (packet.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - - // ---- WotLK Calendar system (pending invites, event notifications, command results) ---- - case Opcode::SMSG_CALENDAR_SEND_NUM_PENDING: { - // uint32 numPending — number of unacknowledged calendar invites - if (packet.getSize() - packet.getReadPos() >= 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"); - } - break; - } - case Opcode::SMSG_CALENDAR_COMMAND_RESULT: { - // 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.getSize() - packet.getReadPos() < 5) { - packet.setReadPos(packet.getSize()); break; - } - /*uint32_t command =*/ packet.readUInt32(); - uint8_t result = packet.readUInt8(); - std::string info = (packet.getReadPos() < packet.getSize()) ? 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_CALENDAR_EVENT_INVITE_ALERT: { - // 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.getSize() - packet.getReadPos() < 9) { - packet.setReadPos(packet.getSize()); break; - } - /*uint64_t eventId =*/ packet.readUInt64(); - std::string title = (packet.getReadPos() < packet.getSize()) ? packet.readString() : ""; - packet.setReadPos(packet.getSize()); // 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, "'"); - break; - } - // Remaining calendar informational packets — parse title where possible and consume - case Opcode::SMSG_CALENDAR_EVENT_STATUS: { - // 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.getSize() - packet.getReadPos() < 31) { - packet.setReadPos(packet.getSize()); break; - } - /*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.getReadPos() < packet.getSize()) ? 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_CALENDAR_RAID_LOCKOUT_ADDED: { - // uint64 inviteId + uint64 eventId + uint32 mapId + uint32 difficulty + uint64 resetTime - if (packet.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_CALENDAR_RAID_LOCKOUT_REMOVED: { - // uint64 inviteId + uint64 eventId + uint32 mapId + uint32 difficulty - if (packet.getSize() - packet.getReadPos() >= 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.setReadPos(packet.getSize()); - break; - } - case Opcode::SMSG_CALENDAR_RAID_LOCKOUT_UPDATED: { - // Same format as LOCKOUT_ADDED; consume - packet.setReadPos(packet.getSize()); - break; - } - // Remaining calendar opcodes: safe consume — data surfaced via SEND_CALENDAR/SEND_EVENT - case Opcode::SMSG_CALENDAR_SEND_CALENDAR: - case Opcode::SMSG_CALENDAR_SEND_EVENT: - case Opcode::SMSG_CALENDAR_ARENA_TEAM: - case Opcode::SMSG_CALENDAR_FILTER_GUILD: - case Opcode::SMSG_CALENDAR_CLEAR_PENDING_ACTION: - case Opcode::SMSG_CALENDAR_EVENT_INVITE: - case Opcode::SMSG_CALENDAR_EVENT_INVITE_NOTES: - case Opcode::SMSG_CALENDAR_EVENT_INVITE_NOTES_ALERT: - case Opcode::SMSG_CALENDAR_EVENT_INVITE_REMOVED: - case Opcode::SMSG_CALENDAR_EVENT_INVITE_REMOVED_ALERT: - case Opcode::SMSG_CALENDAR_EVENT_INVITE_STATUS_ALERT: - case Opcode::SMSG_CALENDAR_EVENT_MODERATOR_STATUS_ALERT: - case Opcode::SMSG_CALENDAR_EVENT_REMOVED_ALERT: - case Opcode::SMSG_CALENDAR_EVENT_UPDATED_ALERT: - packet.setReadPos(packet.getSize()); - break; - - case Opcode::SMSG_SERVERTIME: { - // uint32 unixTime — server's current unix timestamp; use to sync gameTime_ - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t srvTime = packet.readUInt32(); - if (srvTime > 0) { - gameTime_ = static_cast(srvTime); - LOG_DEBUG("SMSG_SERVERTIME: serverTime=", srvTime); - } - } - break; - } - - case Opcode::SMSG_KICK_REASON: { - // uint64 kickerGuid + uint32 kickReasonType + null-terminated reason string - // kickReasonType: 0=other, 1=afk, 2=vote kick - if (!packetHasRemaining(packet, 12)) { - packet.setReadPos(packet.getSize()); - break; - } - uint64_t kickerGuid = packet.readUInt64(); - uint32_t reasonType = packet.readUInt32(); - std::string reason; - if (packet.getReadPos() < packet.getSize()) - 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, "'"); - break; - } - - case Opcode::SMSG_GROUPACTION_THROTTLED: { - // uint32 throttleMs — rate-limited group action; notify the player - if (packetHasRemaining(packet, 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); - } - break; - } - - case Opcode::SMSG_GMRESPONSE_RECEIVED: { - // WotLK 3.3.5a: uint32 ticketId + string subject + string body + uint32 count - // per count: string responseText - if (!packetHasRemaining(packet, 4)) { - packet.setReadPos(packet.getSize()); - break; - } - uint32_t ticketId = packet.readUInt32(); - std::string subject; - std::string body; - if (packet.getReadPos() < packet.getSize()) subject = packet.readString(); - if (packet.getReadPos() < packet.getSize()) body = packet.readString(); - uint32_t responseCount = 0; - if (packetHasRemaining(packet, 4)) - responseCount = packet.readUInt32(); - std::string responseText; - for (uint32_t i = 0; i < responseCount && i < 10; ++i) { - if (packet.getReadPos() < packet.getSize()) { - 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, "'"); - break; - } - - case Opcode::SMSG_GMRESPONSE_STATUS_UPDATE: { - // uint32 ticketId + uint8 status (1=open, 2=surveyed, 3=need_more_help) - if (packet.getSize() - packet.getReadPos() >= 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)); - } - break; - } - - // ---- Voice chat (WotLK built-in voice) — consume silently ---- - case Opcode::SMSG_VOICE_SESSION_ROSTER_UPDATE: - case Opcode::SMSG_VOICE_SESSION_LEAVE: - case Opcode::SMSG_VOICE_SESSION_ADJUST_PRIORITY: - case Opcode::SMSG_VOICE_SET_TALKER_MUTED: - case Opcode::SMSG_VOICE_SESSION_ENABLE: - case Opcode::SMSG_VOICE_PARENTAL_CONTROLS: - case Opcode::SMSG_AVAILABLE_VOICE_CHANNEL: - case Opcode::SMSG_VOICE_CHAT_STATUS: - packet.setReadPos(packet.getSize()); - break; - - // ---- Dance / custom emote system (WotLK) — consume silently ---- - case Opcode::SMSG_NOTIFY_DANCE: - case Opcode::SMSG_PLAY_DANCE: - case Opcode::SMSG_STOP_DANCE: - case Opcode::SMSG_DANCE_QUERY_RESPONSE: - case Opcode::SMSG_INVALIDATE_DANCE: - packet.setReadPos(packet.getSize()); - break; - - // ---- Commentator / spectator mode — consume silently ---- - case Opcode::SMSG_COMMENTATOR_STATE_CHANGED: - case Opcode::SMSG_COMMENTATOR_MAP_INFO: - case Opcode::SMSG_COMMENTATOR_GET_PLAYER_INFO: - case Opcode::SMSG_COMMENTATOR_PLAYER_INFO: - case Opcode::SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT1: - case Opcode::SMSG_COMMENTATOR_SKIRMISH_QUEUE_RESULT2: - packet.setReadPos(packet.getSize()); - break; - - // ---- Debug / cheat / GM-only opcodes — consume silently ---- - case Opcode::SMSG_DBLOOKUP: - case Opcode::SMSG_CHECK_FOR_BOTS: - case Opcode::SMSG_GODMODE: - case Opcode::SMSG_PETGODMODE: - case Opcode::SMSG_DEBUG_AISTATE: - case Opcode::SMSG_DEBUGAURAPROC: - case Opcode::SMSG_TEST_DROP_RATE_RESULT: - case Opcode::SMSG_COOLDOWN_CHEAT: - case Opcode::SMSG_GM_PLAYER_INFO: - case Opcode::SMSG_CHEAT_DUMP_ITEMS_DEBUG_ONLY_RESPONSE: - case Opcode::SMSG_CHEAT_DUMP_ITEMS_DEBUG_ONLY_RESPONSE_WRITE_FILE: - case Opcode::SMSG_CHEAT_PLAYER_LOOKUP: - case Opcode::SMSG_IGNORE_REQUIREMENTS_CHEAT: - case Opcode::SMSG_IGNORE_DIMINISHING_RETURNS_CHEAT: - case Opcode::SMSG_DEBUG_LIST_TARGETS: - case Opcode::SMSG_DEBUG_SERVER_GEO: - case Opcode::SMSG_DUMP_OBJECTS_DATA: - case Opcode::SMSG_AFK_MONITOR_INFO_RESPONSE: - case Opcode::SMSG_FORCEACTIONSHOW: - case Opcode::SMSG_MOVE_CHARACTER_CHEAT: - packet.setReadPos(packet.getSize()); - 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, @@ -8896,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)); } } @@ -9054,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); @@ -9066,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; } @@ -9083,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; @@ -9381,6 +8729,27 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { 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", {}); + } } } @@ -9887,7 +9256,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { 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=", (int)readLen, + " len=", static_cast(readLen), (strIdx ? " module=\"" + moduleName + "\"" : "")); if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { uint32_t now = static_cast( @@ -9906,9 +9275,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { 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 < (int)readLen; i++) { if (memBuf[i] != 0) { allZero = false; break; } } + for (int i = 0; i < static_cast(readLen); i++) { if (memBuf[i] != 0) { allZero = false; break; } } std::string hexDump; - for (int i = 0; i < (int)readLen; i++) { char hx[4]; snprintf(hx,4,"%02x ",memBuf[i]); hexDump += hx; } + 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) @@ -9968,7 +9337,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { 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=", (int)patLen, " found=", found ? "yes" : "no", + " patLen=", static_cast(patLen), " found=", found ? "yes" : "no", turtleFallback ? " (turtle-fallback)" : ""); pos += kPageSize; resultData.push_back(pageResult); @@ -10242,7 +9611,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { pos += 4; uint8_t readLen = decrypted[pos++]; LOG_WARNING("Warden: (sync) MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), - " len=", (int)readLen, + " len=", static_cast(readLen), moduleName.empty() ? "" : (" module=\"" + moduleName + "\"")); // Lazy-load WoW.exe PE image on first MEM_CHECK @@ -10336,7 +9705,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { 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=", (int)len2, + " 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", @@ -10578,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; } } @@ -10660,12 +10029,115 @@ void GameHandler::sendRequestVehicleExit() { 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 (state != WorldState::IN_WORLD) return; - // CMSG_EQUIPMENT_SET_USE: uint32 setId - network::Packet pkt(wireOpcode(Opcode::CMSG_EQUIPMENT_SET_USE)); - pkt.writeUInt32(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) { @@ -10740,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; } @@ -10761,7 +10233,7 @@ void GameHandler::sendMovement(Opcode opcode) { movementInfo.time = movementTime; if (opcode == Opcode::MSG_MOVE_SET_FACING && - (isClassicLikeExpansion() || isActiveExpansion("tbc"))) { + (isPreWotlk())) { const float facingDelta = core::coords::normalizeAngleRad( movementInfo.orientation - lastFacingSentOrientation_); const uint32_t sinceLastFacingMs = @@ -10773,6 +10245,13 @@ void GameHandler::sendMovement(Opcode opcode) { } } + // 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. @@ -10877,6 +10356,15 @@ 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; @@ -11459,11 +10947,32 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } 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 == 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); } + } 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); @@ -11483,6 +10992,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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; @@ -11512,7 +11023,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } unit->setMountDisplayId(val); - } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } + } } if (block.guid == playerGuid) { constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; @@ -11537,6 +11048,11 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem playerDead_ = true; 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); + } } } // Classic: rebuild playerAuras from UNIT_FIELD_AURAS on initial object create @@ -11578,6 +11094,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } LOG_DEBUG("[Classic] Rebuilt playerAuras from UNIT_FIELD_AURAS (CREATE_OBJECT)"); + fireAddonEvent("UNIT_AURA", {"player"}); } } } @@ -11781,6 +11298,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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); @@ -11811,8 +11330,19 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } 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); @@ -11829,7 +11359,12 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem // 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); @@ -11919,6 +11454,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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); @@ -11931,10 +11468,12 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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(); @@ -11956,6 +11495,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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); @@ -11963,11 +11503,15 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } else if (oldHealth == 0 && val > 0) { if (block.guid == playerGuid) { + bool wasGhost = releasedSpirit_; playerDead_ = false; - if (!releasedSpirit_) { + 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_) { @@ -11977,10 +11521,25 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } // 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 == 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}); + } } 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); @@ -12019,6 +11578,11 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } 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 && @@ -12039,6 +11603,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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) { @@ -12071,8 +11637,26 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem // 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", {}); + } + } } } @@ -12112,6 +11696,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } LOG_DEBUG("[Classic] Rebuilt playerAuras from UNIT_FIELD_AURAS (VALUES)"); + fireAddonEvent("UNIT_AURA", {"player"}); } } } @@ -12169,6 +11754,15 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem qsPkt.writeUInt64(block.guid); socket->send(qsPkt); } + // Fire UNIT_MODEL_CHANGED for addons that track model swaps + if (addonEventCallback_) { + std::string uid; + if (block.guid == targetGuid) uid = "target"; + else if (block.guid == focusGuid) uid = "focus"; + else if (block.guid == petGuid_) uid = "pet"; + if (!uid.empty()) + fireAddonEvent("UNIT_MODEL_CHANGED", {uid}); + } } } // Update XP / inventory slot / skill fields for player entity @@ -12207,6 +11801,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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); @@ -12232,6 +11828,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem if (key == ufPlayerXp) { playerXp_ = val; LOG_DEBUG("XP updated: ", val); + fireAddonEvent("PLAYER_XP_UPDATE", {std::to_string(val)}); } else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; @@ -12239,6 +11836,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } else if (ufPlayerRestedXpV != 0xFFFF && key == ufPlayerRestedXpV) { playerRestedXp_ = val; + fireAddonEvent("UPDATE_EXHAUSTION", {}); } else if (key == ufPlayerLevel) { serverPlayerLevel_ = val; @@ -12251,8 +11849,19 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } 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); @@ -12315,8 +11924,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem 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); } @@ -12347,7 +11958,10 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem // 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(); + if (slotsChanged) { + rebuildOnlineInventory(); + fireAddonEvent("PLAYER_EQUIPMENT_CHANGED", {}); + } extractSkillFields(lastPlayerFields_); extractExploredZoneFields(lastPlayerFields_); applyQuestStateFromFields(lastPlayerFields_); @@ -12452,6 +12066,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } if (inventoryChanged) { rebuildOnlineInventory(); + fireAddonEvent("BAG_UPDATE", {}); + fireAddonEvent("UNIT_INVENTORY_CHANGED", {"player"}); } } if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) { @@ -12608,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 @@ -12707,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; } @@ -12738,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; @@ -12777,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()) { @@ -12853,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); } @@ -12864,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"); @@ -12877,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); @@ -12910,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); @@ -12925,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); @@ -12933,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); @@ -13116,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); } @@ -13124,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; @@ -13142,6 +12786,7 @@ 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) { @@ -13150,10 +12795,7 @@ void GameHandler::setFocus(uint64_t guid) { if (unit && !unit->getName().empty()) { name = unit->getName(); } - if (name.empty()) { - auto nit = playerNameCache.find(guid); - if (nit != playerNameCache.end()) name = nit->second; - } + 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); @@ -13167,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 { @@ -13272,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; } @@ -13304,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; } @@ -13315,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; } @@ -13326,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; } @@ -13337,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; } @@ -13354,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; } @@ -13379,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; } @@ -13403,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; } @@ -13422,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; } @@ -13439,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; } @@ -13501,18 +13151,18 @@ void GameHandler::cancelLogout() { } 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; } @@ -13525,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; } @@ -13571,6 +13221,7 @@ 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() { @@ -13580,6 +13231,7 @@ void GameHandler::cancelFollow() { } followTargetGuid_ = 0; addSystemChatMessage("You stop following."); + fireAddonEvent("AUTOFOLLOW_END", {}); } void GameHandler::assistTarget() { @@ -13635,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; } @@ -13659,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; } @@ -13670,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; } @@ -13682,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; } @@ -13694,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; } @@ -13711,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; } @@ -13728,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; } @@ -13740,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; } @@ -13757,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; } @@ -13774,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; } @@ -13795,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; } @@ -13807,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(); @@ -13816,14 +13468,11 @@ 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()) { - auto nit = playerNameCache.find(duelChallengerGuid_); - if (nit != playerNameCache.end()) - duelChallengerName_ = nit->second; + duelChallengerName_ = lookupName(duelChallengerGuid_); } if (duelChallengerName_.empty()) { char tmp[32]; @@ -13834,16 +13483,14 @@ void GameHandler::handleDuelRequested(network::Packet& packet) { pendingDuelRequest_ = true; addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!"); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playTargetSelect(); - } + 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; @@ -13852,10 +13499,11 @@ void GameHandler::handleDuelComplete(network::Packet& packet) { 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; + 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(); @@ -13917,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; } @@ -13938,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; } @@ -13955,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; } @@ -13967,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; } @@ -13985,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; } @@ -14003,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; } @@ -14016,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; } @@ -14029,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" @@ -14052,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; } @@ -14064,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; } @@ -14081,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; } @@ -14098,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; } @@ -14147,6 +13795,9 @@ void GameHandler::releaseSpirit() { 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); } } @@ -14195,7 +13846,7 @@ void GameHandler::useSelfRes() { } 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); @@ -14313,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 + }); + } } // ============================================================ @@ -14334,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; @@ -14348,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); @@ -14357,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); @@ -14365,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 { @@ -14384,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) { @@ -14415,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}); + } } } @@ -14478,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; @@ -14550,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. @@ -14592,10 +14275,7 @@ void GameHandler::handleItemQueryResponse(network::Packet& packet) { std::string msg = "Received: " + link; if (it->count > 1) msg += " x" + std::to_string(it->count); addSystemChatMessage(msg); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playLootItem(); - } + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLootItem(); }); if (itemLootCallback_) itemLootCallback_(data.entry, it->count, data.quality, itemName); it = pendingItemPushNotifs_.erase(it); } else { @@ -14643,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; } @@ -14662,20 +14342,20 @@ 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 + 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; + if (!packet.hasRemaining(2)) break; uint16_t glyphId = packet.readUInt16(); if (gl < MAX_GLYPH_SLOTS) learnedGlyphs_[g][gl] = glyphId; } @@ -14693,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); @@ -14735,23 +14415,23 @@ 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 } @@ -14759,12 +14439,12 @@ void GameHandler::handleInspectResults(network::Packet& packet) { // Parse enchantment slot mask + enchant IDs std::array enchantIds{}; - bytesLeft = packet.getSize() - packet.getReadPos(); + 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; enchantIds[slot] = packet.readUInt16(); } @@ -14789,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 { @@ -15599,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); } @@ -15616,11 +15301,12 @@ void GameHandler::stopAutoAttack() { 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, uint8_t powerType, @@ -15661,6 +15347,34 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint 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) { @@ -15714,6 +15428,7 @@ void GameHandler::handleAttackStart(network::Packet& packet) { 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); @@ -15800,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(); @@ -15810,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) { @@ -15833,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); @@ -15893,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", @@ -15923,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); @@ -15953,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); @@ -15983,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); @@ -16013,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(); @@ -16034,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); @@ -16053,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(); @@ -16086,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); @@ -16128,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(); @@ -16137,37 +15852,37 @@ 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(); // Map BG type IDs to their names (stable across all three expansions) @@ -16209,21 +15924,21 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) { uint32_t avgWaitSec = 0, timeInQueueSec = 0; if (statusId == 1) { // STATUS_WAIT_QUEUE: avgWaitTime(4) + timeInQueue(4) - if (packet.getSize() - packet.getReadPos() >= 8) { + 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(); } @@ -16272,6 +15987,7 @@ 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) { @@ -16286,7 +16002,7 @@ void GameHandler::handleBattlefieldList(network::Packet& packet) { // WotLK 3.3.5a: // bgTypeId(4) isRegistered(1) isHoliday(1) minLevel(4) maxLevel(4) count(4) [instanceId(4)...] - if (packet.getSize() - packet.getReadPos() < 5) return; + if (!packet.hasRemaining(5)) return; AvailableBgInfo info; info.bgTypeId = packet.readUInt32(); @@ -16296,17 +16012,17 @@ void GameHandler::handleBattlefieldList(network::Packet& packet) { const bool isTbc = isActiveExpansion("tbc"); if (isTbc || isWotlk) { - if (packet.getSize() - packet.getReadPos() < 1) return; + if (!packet.hasRemaining(1)) return; info.isHoliday = packet.readUInt8() != 0; } if (isWotlk) { - if (packet.getSize() - packet.getReadPos() < 8) return; + if (!packet.hasRemaining(8)) return; info.minLevel = packet.readUInt32(); info.maxLevel = packet.readUInt32(); } - if (packet.getSize() - packet.getReadPos() < 4) return; + if (!packet.hasRemaining(4)) return; uint32_t count = packet.readUInt32(); // Sanity cap to avoid OOM from malformed packets @@ -16315,7 +16031,7 @@ void GameHandler::handleBattlefieldList(network::Packet& packet) { info.instanceIds.reserve(count); for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < 4) break; + if (!packet.hasRemaining(4)) break; info.instanceIds.push_back(packet.readUInt32()); } @@ -16439,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(); @@ -16447,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(); @@ -16470,7 +16186,7 @@ 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(); @@ -16542,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(); @@ -16570,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(); @@ -16590,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(); @@ -16639,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(); @@ -16664,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(); @@ -16674,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; @@ -16696,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; } @@ -16721,7 +16437,7 @@ void GameHandler::handleLfgUpdatePlayer(network::Packet& packet) { } void GameHandler::handleLfgPlayerReward(network::Packet& packet) { - if (!packetHasRemaining(packet, 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(); @@ -16744,9 +16460,9 @@ void GameHandler::handleLfgPlayerReward(network::Packet& packet) { std::string rewardMsg = std::string("Dungeon Finder reward: ") + moneyBuf + ", " + std::to_string(xp) + " XP"; - if (packetHasRemaining(packet, 4)) { + if (packet.hasRemaining(4)) { uint32_t rewardCount = packet.readUInt32(); - for (uint32_t i = 0; i < rewardCount && packetHasRemaining(packet, 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 @@ -16769,7 +16485,7 @@ void GameHandler::handleLfgPlayerReward(network::Packet& packet) { } void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { - if (!packetHasRemaining(packet, 7 + 4 + 4 + 4 + 4)) return; + if (!packet.hasRemaining(7 + 4 + 4 + 4 + 4)) return; bool inProgress = packet.readUInt8() != 0; /*bool myVote =*/ packet.readUInt8(); // whether local player has voted @@ -16785,9 +16501,9 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { lfgBootNeeded_ = votesNeeded; // Optional: reason string and target name (null-terminated) follow the fixed fields - if (packet.getReadPos() < packet.getSize()) + if (packet.hasData()) lfgBootReason_ = packet.readString(); - if (packet.getReadPos() < packet.getSize()) + if (packet.hasData()) lfgBootTargetName_ = packet.readString(); if (inProgress) { @@ -16813,7 +16529,7 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { } 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); @@ -16825,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); @@ -16853,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; @@ -16920,7 +16647,7 @@ void GameHandler::loadAreaTriggerDbc() { } void GameHandler::checkAreaTriggers() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; if (onTaxiFlight_ || taxiClientActive_) return; loadAreaTriggerDbc(); @@ -17010,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(); @@ -17029,11 +16756,11 @@ 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(); uint32_t teamType = 0; - if (packet.getSize() - packet.getReadPos() >= 4) + if (packet.hasRemaining(4)) teamType = packet.readUInt32(); LOG_INFO("Arena team query response: id=", teamId, " name=", teamName, " type=", teamType); @@ -17069,7 +16796,7 @@ void GameHandler::handleArenaTeamRoster(network::Packet& packet) { // uint32 personalRating // float modDay (unused here) // float modWeek (unused here) - if (packet.getSize() - packet.getReadPos() < 9) return; + if (!packet.hasRemaining(9)) return; uint32_t teamId = packet.readUInt32(); /*uint8_t unk =*/ packet.readUInt8(); @@ -17083,20 +16810,20 @@ void GameHandler::handleArenaTeamRoster(network::Packet& packet) { roster.members.reserve(memberCount); for (uint32_t i = 0; i < memberCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 12) break; + if (!packet.hasRemaining(12)) break; ArenaTeamMember m; m.guid = packet.readUInt64(); m.online = (packet.readUInt8() != 0); m.name = packet.readString(); - if (packet.getSize() - packet.getReadPos() < 20) break; + 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.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { packet.readFloat(); packet.readFloat(); } @@ -17125,12 +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(); // Read string params (up to 3) uint8_t strCount = 0; - if (packet.getSize() - packet.getReadPos() >= 1) { + if (packet.hasRemaining(1)) { strCount = packet.readUInt8(); } @@ -17176,14 +16903,14 @@ void GameHandler::handleArenaTeamEvent(network::Packet& packet) { break; } addSystemChatMessage(msg); - LOG_INFO("Arena team event: ", (int)event, " ", 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(); @@ -17220,7 +16947,7 @@ void GameHandler::requestArenaTeamRoster(uint32_t 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; @@ -17236,7 +16963,7 @@ void GameHandler::handleArenaError(network::Packet& packet) { } void GameHandler::requestPvpLog() { - if (state != WorldState::IN_WORLD || !socket) return; + 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); @@ -17244,7 +16971,7 @@ void GameHandler::requestPvpLog() { } void GameHandler::handlePvpLogData(network::Packet& packet) { - auto remaining = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto remaining = [&]() { return packet.getRemainingSize(); }; if (remaining() < 1) return; bgScoreboard_ = BgScoreboardData{}; @@ -17255,7 +16982,7 @@ void GameHandler::handlePvpLogData(network::Packet& packet) { // 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.setReadPos(packet.getSize()); return; } + if (remaining() < 20) { packet.skipAll(); return; } bgScoreboard_.arenaTeams[t].ratingChange = packet.readUInt32(); bgScoreboard_.arenaTeams[t].newRating = packet.readUInt32(); packet.readUInt32(); // unk1 @@ -17314,14 +17041,14 @@ void GameHandler::handlePvpLogData(network::Packet& packet) { if (bgScoreboard_.isArena) { LOG_INFO("Arena log: ", bgScoreboard_.players.size(), " players, hasWinner=", - bgScoreboard_.hasWinner, " winner=", (int)bgScoreboard_.winner, + bgScoreboard_.hasWinner, " winner=", static_cast(bgScoreboard_.winner), " team0='", bgScoreboard_.arenaTeams[0].teamName, - "' ratingChange=", (int32_t)bgScoreboard_.arenaTeams[0].ratingChange, + "' ratingChange=", static_cast(bgScoreboard_.arenaTeams[0].ratingChange), " team1='", bgScoreboard_.arenaTeams[1].teamName, - "' ratingChange=", (int32_t)bgScoreboard_.arenaTeams[1].ratingChange); + "' ratingChange=", static_cast(bgScoreboard_.arenaTeams[1].ratingChange)); } else { LOG_INFO("PvP log: ", bgScoreboard_.players.size(), " players, hasWinner=", - bgScoreboard_.hasWinner, " winner=", (int)bgScoreboard_.winner); + bgScoreboard_.hasWinner, " winner=", static_cast(bgScoreboard_.winner)); } } @@ -17330,13 +17057,13 @@ void GameHandler::handleMoveSetSpeed(network::Packet& packet) { // 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 = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool useFull = isPreWotlk(); uint64_t moverGuid = useFull - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + ? 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.getSize() - packet.getReadPos(); + const size_t remaining = packet.getRemainingSize(); if (remaining < 4) return; if (remaining > 4) { // Advance past all MovementInfo bytes (flags, time, position, optional blocks). @@ -17361,9 +17088,9 @@ void GameHandler::handleMoveSetSpeed(network::Packet& packet) { 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 } @@ -17390,7 +17117,7 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { uint64_t transportGuid = 0; float tLocalX = 0, tLocalY = 0, tLocalZ = 0, tLocalO = 0; if (onTransport) { - transportGuid = UpdateObjectParser::readPackedGuid(packet); + transportGuid = packet.readPackedGuid(); tLocalX = packet.readFloat(); tLocalY = packet.readFloat(); tLocalZ = packet.readFloat(); @@ -17881,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); + } + } } } @@ -17888,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(); @@ -17902,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)); @@ -17934,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(); @@ -17948,27 +17696,27 @@ 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) { @@ -17984,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(); @@ -18190,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()) { @@ -18255,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) { @@ -18287,6 +18029,13 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { 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()) { @@ -18299,7 +18048,7 @@ 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); @@ -18315,6 +18064,7 @@ void GameHandler::cancelCast() { craftQueueRemaining_ = 0; queuedSpellId_ = 0; queuedSpellTarget_ = 0; + fireAddonEvent("UNIT_SPELLCAST_STOP", {"player"}); } void GameHandler::startCraftQueue(uint32_t spellId, int count) { @@ -18330,7 +18080,7 @@ void GameHandler::cancelCraftQueue() { } void GameHandler::cancelAura(uint32_t spellId) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = CancelAuraPacket::build(spellId); socket->send(packet); } @@ -18349,7 +18099,7 @@ uint32_t GameHandler::getTempEnchantRemainingMs(uint32_t slot) const { } 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; @@ -18357,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; } @@ -18366,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); @@ -18405,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) { @@ -18438,7 +18192,7 @@ void GameHandler::togglePetSpellAutocast(uint32_t spellId) { petAutocastSpells_.insert(spellId); else petAutocastSpells_.erase(spellId); - LOG_DEBUG("togglePetSpellAutocast: spellId=", spellId, " autocast=", (int)newState); + LOG_DEBUG("togglePetSpellAutocast: spellId=", spellId, " autocast=", static_cast(newState)); } void GameHandler::renamePet(const std::string& newName) { @@ -18487,7 +18241,7 @@ void GameHandler::handleListStabledPets(network::Packet& packet) { // uint32 displayId // uint8 isActive (1 = active/summoned, 0 = stabled) constexpr size_t kMinHeader = 8 + 1 + 1; - if (packet.getSize() - packet.getReadPos() < kMinHeader) { + if (!packet.hasRemaining(kMinHeader)) { LOG_WARNING("MSG_LIST_STABLED_PETS: packet too short (", packet.getSize(), ")"); return; } @@ -18499,13 +18253,13 @@ void GameHandler::handleListStabledPets(network::Packet& packet) { stabledPets_.reserve(petCount); for (uint8_t i = 0; i < petCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 4 + 4 + 4) break; + 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.getSize() - packet.getReadPos() < 4 + 1) break; + if (!packet.hasRemaining(4) + 1) break; pet.displayId = packet.readUInt32(); pet.isActive = (packet.readUInt8() != 0); stabledPets_.push_back(std::move(pet)); @@ -18513,7 +18267,7 @@ void GameHandler::handleListStabledPets(network::Packet& packet) { stableWindowOpen_ = true; LOG_INFO("MSG_LIST_STABLED_PETS: stableMasterGuid=0x", std::hex, stableMasterGuid_, std::dec, - " petCount=", (int)petCount, " numSlots=", (int)stableNumSlots_); + " 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, @@ -18530,8 +18284,11 @@ 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 (state == WorldState::IN_WORLD && socket) { + if (isInWorld()) { const bool classic = isClassicLikeExpansion(); auto pkt = SetActionButtonPacket::build( static_cast(slot), @@ -18560,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; } } @@ -18584,6 +18343,19 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { 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; + } + } + } } } @@ -18593,6 +18365,10 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { 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) { @@ -18612,11 +18388,7 @@ void GameHandler::handleCastFailed(network::Packet& packet) { 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(); }); // Show failure reason in the UIError overlay and in chat int powerType = -1; @@ -18635,10 +18407,12 @@ void GameHandler::handleCastFailed(network::Packet& packet) { addLocalChatMessage(msg); // Play error sound for cast failure feedback - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playError(); - } + 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) { @@ -18659,6 +18433,7 @@ void GameHandler::handleSpellStart(network::Packet& packet) { if (data.casterUnit != playerGuid && data.castTime > 0) { auto& s = unitCastStates_[data.casterUnit]; s.casting = true; + s.isChannel = false; s.spellId = data.spellId; s.timeTotal = data.castTime / 1000.0f; s.timeRemaining = s.timeTotal; @@ -18684,17 +18459,14 @@ void GameHandler::handleSpellStart(network::Packet& packet) { currentCastSpellId = data.spellId; castTimeTotal = data.castTime / 1000.0f; castTimeRemaining = castTimeTotal; + fireAddonEvent("CURRENT_SPELL_CAST_CHANGED", {}); - // Play precast (channeling) sound with correct magic school - // Skip sound for profession/tradeskill spells (crafting should be silent) + // 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()) { - loadSpellNameCache(); - auto it = spellNameCache_.find(data.spellId); - auto school = (it != spellNameCache_.end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; + auto school = schoolMaskToMagicSchool(getSpellSchoolMask(data.spellId)); ssm->playPrecast(school, audio::SpellSoundManager::SpellPower::MEDIUM); } } @@ -18713,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) { @@ -18721,31 +18500,24 @@ void GameHandler::handleSpellGo(network::Packet& packet) { // Cast completed if (data.casterUnit == playerGuid) { - // Play cast-complete sound with correct magic school - // Skip sound for profession/tradeskill spells (crafting should be silent) + // 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()) { - 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); + 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. @@ -18789,6 +18561,9 @@ void GameHandler::handleSpellGo(network::Packet& packet) { spellCastAnimCallback_(playerGuid, 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_; @@ -18811,12 +18586,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) { if (targetsPlayer) { 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); + ssm->playCast(schoolMaskToMagicSchool(getSpellSchoolMask(data.spellId))); } } } @@ -18849,15 +18619,18 @@ void GameHandler::handleSpellGo(network::Packet& packet) { if (tgt == playerGuid) { playerIsHit = true; } if (data.casterUnit == playerGuid && tgt != playerGuid && tgt != 0) { playerHitEnemy = true; } } + // 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); } } } @@ -18868,16 +18641,16 @@ 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 @@ -18915,13 +18688,15 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) { } 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); @@ -18930,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) { @@ -18967,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) { @@ -18984,7 +18768,7 @@ 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. @@ -18996,19 +18780,32 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) { 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; } + // 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) { @@ -19025,10 +18822,11 @@ 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()) @@ -19053,7 +18851,7 @@ 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(); @@ -19083,7 +18881,10 @@ void GameHandler::handleSupercededSpell(network::Packet& packet) { LOG_DEBUG("Action bar slot upgraded: spell ", oldSpellId, " -> ", newSpellId); } } - if (barChanged) saveCharacterConfig(); + 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 @@ -19098,12 +18899,12 @@ void GameHandler::handleSupercededSpell(network::Packet& packet) { 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"); bool barChanged = false; - for (uint32_t i = 0; i < spellCount && packet.getSize() - packet.getReadPos() >= 4; ++i) { + for (uint32_t i = 0; i < spellCount && packet.hasRemaining(4); ++i) { uint32_t spellId = packet.readUInt32(); knownSpells.erase(spellId); LOG_INFO(" Unlearned spell: ", spellId); @@ -19134,13 +18935,13 @@ void GameHandler::handleTalentsInfo(network::Packet& packet) { // Per group: uint8 talentCount, [uint32 talentId + uint8 rank] × count, // uint8 glyphCount, [uint16 glyphId] × count - if (packet.getSize() - packet.getReadPos() < 1) return; + 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.getSize() - packet.getReadPos() < 6) { + if (!packet.hasRemaining(6)) { LOG_WARNING("handleTalentsInfo: packet too short for header"); return; } @@ -19156,20 +18957,20 @@ void GameHandler::handleTalentsInfo(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 + 1u; // wire sends 0-indexed; store 1-indexed } learnedGlyphs_[g].fill(0); - if (packet.getSize() - packet.getReadPos() < 1) break; + if (!packet.hasRemaining(1)) break; uint8_t glyphCount = packet.readUInt8(); for (uint8_t gl = 0; gl < glyphCount; ++gl) { - if (packet.getSize() - packet.getReadPos() < 2) break; + if (!packet.hasRemaining(2)) break; uint16_t glyphId = packet.readUInt16(); if (gl < MAX_GLYPH_SLOTS) learnedGlyphs_[g][gl] = glyphId; } @@ -19179,9 +18980,14 @@ void GameHandler::handleTalentsInfo(network::Packet& packet) { static_cast(unspentTalents > 255 ? 255 : unspentTalents); LOG_INFO("handleTalentsInfo: unspent=", unspentTalents, - " groups=", (int)talentGroupCount, " active=", (int)activeTalentGroup, + " groups=", static_cast(talentGroupCount), " active=", static_cast(activeTalentGroup), " learned=", learnedTalents_[activeTalentGroup].size()); + // Fire talent-related events for addons + fireAddonEvent("CHARACTER_POINTS_CHANGED", {}); + fireAddonEvent("ACTIVE_TALENT_GROUP_CHANGED", {}); + fireAddonEvent("PLAYER_TALENT_UPDATE", {}); + if (!talentsInitialized_) { talentsInitialized_ = true; if (unspentTalents > 0) { @@ -19192,7 +18998,7 @@ void GameHandler::handleTalentsInfo(network::Packet& packet) { } 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; } @@ -19205,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; } @@ -19219,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); @@ -19242,7 +19048,7 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) { void GameHandler::confirmPetUnlearn() { if (!petUnlearnPending_) return; petUnlearnPending_ = false; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // Respond with CMSG_PET_UNLEARN_TALENTS (no payload in 3.3.5a) network::Packet pkt(wireOpcode(Opcode::CMSG_PET_UNLEARN_TALENTS)); @@ -19257,7 +19063,7 @@ 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. @@ -19272,7 +19078,7 @@ void GameHandler::confirmTalentWipe() { } void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto pkt = AlterAppearancePacket::build(hairStyle, hairColor, facialHair); socket->send(pkt); LOG_INFO("sendAlterAppearance: hair=", hairStyle, " color=", hairColor, " facial=", facialHair); @@ -19283,14 +19089,14 @@ void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, ui // ============================================================ 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); @@ -19298,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); @@ -19306,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) { @@ -19323,10 +19131,8 @@ void GameHandler::handleGroupInvite(network::Packet& packet) { if (!data.inviterName.empty()) { addSystemChatMessage(data.inviterName + " has invited you to a group."); } - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playTargetSelect(); - } + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playTargetSelect(); }); + fireAddonEvent("PARTY_INVITE_REQUEST", {data.inviterName}); } void GameHandler::handleGroupDecline(network::Packet& packet) { @@ -19346,6 +19152,7 @@ void GameHandler::handleGroupList(network::Packet& packet) { 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. @@ -19362,6 +19169,21 @@ void GameHandler::handleGroupList(network::Packet& packet) { } 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", {}); + } } void GameHandler::handleGroupUninvite(network::Packet& packet) { @@ -19369,6 +19191,10 @@ 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; @@ -19418,7 +19244,7 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) { } 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. @@ -19435,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(); @@ -19448,7 +19274,7 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) { } } if (!member) { - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } @@ -19602,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}); + } + } } // ============================================================ @@ -19609,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); @@ -19652,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); @@ -19660,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 @@ -19681,7 +19541,7 @@ 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; @@ -19690,7 +19550,7 @@ void GameHandler::deleteGmTicket() { } void GameHandler::requestGmTicket() { - if (state != WorldState::IN_WORLD || !socket) return; + 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); @@ -19698,7 +19558,7 @@ void GameHandler::requestGmTicket() { } 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); @@ -19727,14 +19587,14 @@ uint32_t GameHandler::getEntityGuildId(uint64_t guid) const { } 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); @@ -19743,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"); @@ -19752,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); @@ -19779,7 +19639,7 @@ void GameHandler::handlePetitionQueryResponse(network::Packet& packet) { // 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.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 12) return; /*uint32_t entry =*/ packet.readUInt32(); @@ -19793,7 +19653,7 @@ void GameHandler::handlePetitionQueryResponse(network::Packet& packet) { } LOG_INFO("SMSG_PETITION_QUERY_RESPONSE: guid=", petGuid, " name=", guildName); - packet.setReadPos(packet.getSize()); // skip remaining fields + packet.skipAll(); // skip remaining fields } void GameHandler::handlePetitionShowSignatures(network::Packet& packet) { @@ -19805,7 +19665,7 @@ void GameHandler::handlePetitionShowSignatures(network::Packet& packet) { // For each signature: // uint64 playerGuid // uint32 unk (always 0) - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 21) return; petitionInfo_ = PetitionInfo{}; @@ -19834,7 +19694,7 @@ void GameHandler::handlePetitionShowSignatures(network::Packet& packet) { void GameHandler::handlePetitionSignResults(network::Packet& packet) { // SMSG_PETITION_SIGN_RESULTS (3.3.5a): // uint64 petitionGuid, uint64 playerGuid, uint32 result - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; if (rem() < 20) return; uint64_t petGuid = packet.readUInt64(); @@ -19916,6 +19776,7 @@ 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) { @@ -19941,8 +19802,10 @@ void GameHandler::handleGuildQueryResponse(network::Packet& packet) { guildRankNames_.push_back(data.rankNames[i]); } LOG_INFO("Guild name set to: ", guildName_); - if (wasUnknown && !guildName_.empty()) + if (wasUnknown && !guildName_.empty()) { addSystemChatMessage("Guild: <" + guildName_ + ">"); + fireAddonEvent("PLAYER_GUILD_UPDATE", {}); + } } else { LOG_INFO("Cached guild name: id=", data.guildId, " name=", data.guildName); } @@ -19992,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) @@ -20014,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: @@ -20038,6 +19924,7 @@ 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) { @@ -20118,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); } @@ -20132,11 +20019,12 @@ 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); } @@ -20144,7 +20032,7 @@ void GameHandler::closeLoot() { } void GameHandler::lootMasterGive(uint8_t lootSlot, uint64_t targetGuid) { - if (state != WorldState::IN_WORLD || !socket) return; + 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); @@ -20154,14 +20042,14 @@ void GameHandler::lootMasterGive(uint8_t lootSlot, uint64_t targetGuid) { } 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. @@ -20173,7 +20061,7 @@ void GameHandler::interactWithGameObject(uint64_t guid) { void GameHandler::performGameObjectInteractionNow(uint64_t guid) { if (guid == 0) return; - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // Rate-limit to prevent spamming the server static uint64_t lastInteractGuid = 0; static std::chrono::steady_clock::time_point lastInteractTime{}; @@ -20237,40 +20125,14 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } - LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec, - " entry=", goEntry, " type=", goType, - " name='", goName, "' dist=", entity ? std::sqrt( - (entity->getX() - movementInfo.x) * (entity->getX() - movementInfo.x) + - (entity->getY() - movementInfo.y) * (entity->getY() - movementInfo.y) + - (entity->getZ() - movementInfo.z) * (entity->getZ() - movementInfo.z)) : -1.0f); - auto packet = GameObjectUsePacket::build(guid); - socket->send(packet); - lastInteractedGoGuid_ = guid; - - // 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; } @@ -20283,40 +20145,49 @@ 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); } - // Some servers require CMSG_GAMEOBJ_REPORT_USE for lootable gameobjects. - // Only send it when the active opcode table actually supports it. - 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); + + 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); + 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); + } } } - if (shouldSendLoot) { - // Don't send CMSG_LOOT immediately — give the server time to process - // CMSG_GAMEOBJ_USE first (chests need to transition to lootable state, - // gathering nodes start a spell cast). A premature CMSG_LOOT can cause - // an empty SMSG_LOOT_RESPONSE that clears our gather-cast loot state. - pendingGameObjectLootOpens_.erase( - std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(), - [&](const PendingLootOpen& p) { return p.guid == guid; }), - pendingGameObjectLootOpens_.end()); - // Short delay for chests (server makes them lootable quickly after USE), - // plus a longer retry to catch slow state transitions. - pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.20f}); - pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.75f}); - } else { - // Non-lootable interaction (mailbox, door, button, etc.) — no CMSG_LOOT will be - // sent, and no SMSG_LOOT_RESPONSE will arrive to clear it. Clear the gather-loot - // guid now so a subsequent timed cast completion can't fire a spurious CMSG_LOOT. - lastInteractedGoGuid_ = 0; - } - // Don't retry CMSG_GAMEOBJ_USE — resending can toggle chest state on some - // servers (opening→closing the chest). The delayed CMSG_LOOT retries above - // handle the case where the first loot attempt arrives too early. } void GameHandler::selectGossipOption(uint32_t optionId) { @@ -20330,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 @@ -20371,7 +20242,7 @@ void GameHandler::selectGossipOption(uint32_t optionId) { 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=", (int)isVendor, " repair=", (int)isArmorer); + " vendor=", static_cast(isVendor), " repair=", static_cast(isArmorer)); } if (textLower.find("make this inn your home") != std::string::npos || @@ -20400,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. @@ -20426,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. @@ -20496,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(); @@ -20514,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(); @@ -20530,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) { @@ -20550,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)); } } @@ -20581,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 { @@ -20590,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); @@ -20611,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) { @@ -20779,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). @@ -20856,10 +20743,7 @@ void GameHandler::acceptQuest() { pendingQuestAcceptNpcGuids_[questId] = npcGuid; // Play quest-accept sound - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playQuestActivate(); - } + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestActivate(); }); questDetailsOpen = false; questDetailsOpenTime = std::chrono::steady_clock::time_point{}; @@ -20897,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); @@ -20908,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. @@ -20918,7 +20805,7 @@ void GameHandler::abandonQuest(uint32_t questId) { } void GameHandler::shareQuestWithParty(uint32_t questId) { - if (state != WorldState::IN_WORLD || !socket) { + if (!isInWorld()) { addSystemChatMessage("Cannot share quest: not in world."); return; } @@ -20930,13 +20817,9 @@ void GameHandler::shareQuestWithParty(uint32_t questId) { pkt.writeUInt32(questId); socket->send(pkt); // Local feedback: find quest title - for (const auto& q : questLog_) { - if (q.questId == questId && !q.title.empty()) { - addSystemChatMessage("Sharing quest: " + q.title); - return; - } - } - addSystemChatMessage("Quest shared."); + auto questTitle = getQuestTitle(questId); + addSystemChatMessage(questTitle.empty() ? std::string("Quest shared.") + : ("Sharing quest: " + questTitle)); } void GameHandler::handleQuestRequestItems(network::Packet& packet) { @@ -21022,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) @@ -21080,11 +20964,12 @@ void GameHandler::closeQuestOfferReward() { void GameHandler::closeGossip() { gossipWindowOpen = false; + fireAddonEvent("GOSSIP_CLOSED", {}); currentGossip = GossipMessageData{}; } void GameHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; if (itemGuid == 0 || questId == 0) { addSystemChatMessage("Cannot start quest right now."); return; @@ -21112,13 +20997,14 @@ uint64_t GameHandler::getBagItemGuid(int bagIndex, int slotIndex) const { } 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(); @@ -21127,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); @@ -21175,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); @@ -21185,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); @@ -21195,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); @@ -21249,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); @@ -21260,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)); @@ -21315,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) { @@ -21329,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); @@ -21338,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); } @@ -21364,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). @@ -21382,13 +21269,13 @@ 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 (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; if (count == 0) return; // Find a free slot for the split destination: try backpack first, then bags @@ -21396,8 +21283,8 @@ void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) { if (freeBp >= 0) { uint8_t dstBag = 0xFF; uint8_t dstSlot = static_cast(23 + freeBp); - LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot, - ") count=", (int)count, " -> dst(bag=0xFF slot=", (int)dstSlot, ")"); + 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; @@ -21409,9 +21296,9 @@ void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) { if (inventory.getBagSlot(b, s).empty()) { uint8_t dstBag = static_cast(19 + b); uint8_t dstSlot = static_cast(s); - LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot, - ") count=", (int)count, " -> dst(bag=", (int)dstBag, - " slot=", (int)dstSlot, ")"); + 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; @@ -21431,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)) { @@ -21476,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)) { @@ -21494,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) { @@ -21506,7 +21393,7 @@ 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 (state != WorldState::IN_WORLD || !socket) 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); @@ -21516,10 +21403,10 @@ 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 (state != WorldState::IN_WORLD || !socket) 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=", (int)wowBag, " slot=", slotIndex); + LOG_INFO("openItemInBag: CMSG_OPEN_ITEM bag=", static_cast(wowBag), " slot=", slotIndex); socket->send(packet); } @@ -21586,6 +21473,8 @@ void GameHandler::handleLootResponse(network::Packet& packet) { return; } lootWindowOpen = true; + 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(), @@ -21600,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); @@ -21617,7 +21506,7 @@ void GameHandler::handleLootResponse(network::Packet& packet) { } // Auto-loot items when enabled - if (autoLoot_ && state == WorldState::IN_WORLD && socket && !localLoot.itemAutoLootSent) { + if (autoLoot_ && isInWorld() && !localLoot.itemAutoLootSent) { for (const auto& item : currentLoot.items) { auto pkt = AutostoreLootItemPacket::build(item.slotIndex); socket->send(pkt); @@ -21630,6 +21519,7 @@ void GameHandler::handleLootReleaseResponse(network::Packet& packet) { (void)packet; localLootState_.erase(currentLoot.lootGuid); lootWindowOpen = false; + fireAddonEvent("LOOT_CLOSED", {}); currentLoot = LootResponseData{}; } @@ -21647,11 +21537,9 @@ void GameHandler::handleLootRemoved(network::Packet& packet) { std::string msgStr = "Looted: " + link; if (it->count > 1) msgStr += " x" + std::to_string(it->count); addSystemChatMessage(msgStr); - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playLootItem(); - } + withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLootItem(); }); currentLoot.items.erase(it); + fireAddonEvent("LOOT_SLOT_CLEARED", {std::to_string(slotIndex + 1)}); break; } } @@ -21663,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. @@ -21728,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(); @@ -21737,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 } @@ -21745,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(); } @@ -21755,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 { @@ -21776,6 +21665,7 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { currentGossip = std::move(data); gossipWindowOpen = true; + fireAddonEvent("GOSSIP_SHOW", {}); vendorWindowOpen = false; bool hasAvailableQuest = false; @@ -21826,6 +21716,7 @@ void GameHandler::handleGossipComplete(network::Packet& packet) { } gossipWindowOpen = false; + fireAddonEvent("GOSSIP_CLOSED", {}); currentGossip = GossipMessageData{}; } @@ -21848,6 +21739,7 @@ void GameHandler::handleListInventory(network::Packet& packet) { 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) { @@ -21953,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()); @@ -21969,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, ")"); } @@ -21983,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; } @@ -22010,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; @@ -22094,9 +21988,42 @@ void GameHandler::loadSpellNameCache() { 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"); } @@ -22121,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(); @@ -22277,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; @@ -22288,20 +22283,35 @@ const std::string& GameHandler::getSpellRank(uint32_t spellId) const { } const std::string& GameHandler::getSpellDescription(uint32_t spellId) const { - const_cast(this)->loadSpellNameCache(); + 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 { - const_cast(this)->loadSpellNameCache(); + 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; - const_cast(this)->loadSpellNameCache(); + 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) @@ -22310,7 +22320,7 @@ bool GameHandler::isSpellInterruptible(uint32_t spellId) const { uint32_t GameHandler::getSpellSchoolMask(uint32_t spellId) const { if (spellId == 0) return 0; - const_cast(this)->loadSpellNameCache(); + loadSpellNameCache(); auto it = spellNameCache_.find(spellId); return (it != spellNameCache_.end()) ? it->second.schoolMask : 0; } @@ -22390,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)}); } @@ -22409,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) { @@ -22428,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; } @@ -22481,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); @@ -22498,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; } @@ -22589,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(); @@ -22635,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", {}); } // ============================================================ @@ -23454,17 +23498,17 @@ void GameHandler::handleWho(network::Packet& packet) { } 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(); // Store structured entry @@ -23492,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(), @@ -23512,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; @@ -23533,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) { @@ -23548,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(); @@ -23574,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); } @@ -23590,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) { @@ -23613,8 +23660,7 @@ void GameHandler::handleFriendStatus(network::Packet& packet) { if (cit2 != contacts_.end() && !cit2->name.empty()) { playerName = cit2->name; } else { - auto it = playerNameCache.find(data.guid); - if (it != playerNameCache.end()) playerName = it->second; + playerName = lookupName(data.guid); } } @@ -23674,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) { @@ -23693,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 @@ -23731,7 +23774,8 @@ void GameHandler::handleLogoutResponse(network::Packet& packet) { 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."); @@ -23759,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; } } @@ -23890,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) { @@ -24002,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); } @@ -24055,6 +24121,21 @@ void GameHandler::loadCharacterConfig() { } 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_" @@ -24197,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() { @@ -24367,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; } @@ -24378,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; @@ -24418,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; } @@ -24485,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(); @@ -24502,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 @@ -24536,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() { @@ -24564,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(); @@ -24583,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 @@ -24669,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); @@ -24686,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, @@ -24768,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) { @@ -24866,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(); @@ -24875,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); @@ -24893,24 +24985,21 @@ 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()) { - auto nit = playerNameCache.find(sharedQuestSharerGuid_); - if (nit != playerNameCache.end()) - sharedQuestSharerName_ = nit->second; + sharedQuestSharerName_ = lookupName(sharedQuestSharerGuid_); } if (sharedQuestSharerName_.empty()) { char tmp[32]; @@ -24946,7 +25035,7 @@ 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(); @@ -24955,14 +25044,11 @@ void GameHandler::handleSummonRequest(network::Packet& packet) { 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()) { - auto nit = playerNameCache.find(summonerGuid_); - if (nit != playerNameCache.end()) - summonerName_ = nit->second; + summonerName_ = lookupName(summonerGuid_); } if (summonerName_.empty()) { char tmp[32]; @@ -24979,6 +25065,7 @@ void GameHandler::handleSummonRequest(network::Packet& packet) { addSystemChatMessage(msg); LOG_INFO("SMSG_SUMMON_REQUEST: summoner=", summonerName_, " zoneId=", zoneId, " timeout=", summonTimeoutSec_, "s"); + fireAddonEvent("CONFIRM_SUMMON", {}); } void GameHandler::acceptSummon() { @@ -25010,24 +25097,21 @@ 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()) { - auto nit = playerNameCache.find(tradePeerGuid_); - if (nit != playerNameCache.end()) - tradePeerName_ = nit->second; + tradePeerName_ = lookupName(tradePeerGuid_); } if (tradePeerName_.empty()) { char tmp[32]; @@ -25037,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 @@ -25046,22 +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 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) @@ -25145,7 +25235,7 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) { // Minimum: isSelf(1) + [tradeId(4)] + slotCount(4) = 5 or 9 bytes const bool isWotLK = isActiveExpansion("wotlk"); size_t minHdr = isWotLK ? 9u : 5u; - if (packet.getSize() - packet.getReadPos() < minHdr) return; + if (!packet.hasRemaining(minHdr)) return; uint8_t isSelf = packet.readUInt8(); if (isWotLK) { @@ -25160,20 +25250,20 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) { 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); } - 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; @@ -25188,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; @@ -25199,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_); } @@ -25214,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(); @@ -25254,8 +25344,7 @@ 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"; @@ -25301,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(); @@ -25319,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()) { @@ -25329,6 +25418,10 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { auto* info = getItemInfo(itemId); 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); @@ -25341,7 +25434,7 @@ void GameHandler::handleLootRollWon(network::Packet& packet) { } 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)); @@ -25362,7 +25455,7 @@ 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() { +void GameHandler::loadTitleNameCache() const { if (titleNameCacheLoaded_) return; titleNameCacheLoaded_ = true; @@ -25390,13 +25483,13 @@ void GameHandler::loadTitleNameCache() { } std::string GameHandler::getFormattedTitle(uint32_t bit) const { - const_cast(this)->loadTitleNameCache(); + 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"; - auto nameIt = playerNameCache.find(playerGuid); - const std::string& pName = (nameIt != playerNameCache.end()) ? nameIt->second : kUnknown; + const std::string& pName = ln2.empty() ? kUnknown : ln2; const std::string& fmt = it->second; size_t pos = fmt.find("%s"); @@ -25407,7 +25500,7 @@ std::string GameHandler::getFormattedTitle(uint32_t bit) const { } void GameHandler::sendSetTitle(int32_t bit) { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; auto packet = SetTitlePacket::build(bit); socket->send(packet); chosenTitleBit_ = bit; @@ -25450,7 +25543,7 @@ void GameHandler::loadAchievementNameCache() { } 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(); @@ -25475,25 +25568,17 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { earnedAchievements_.insert(achievementId); achievementDates_[achievementId] = earnDate; - if (auto* renderer = core::Application::getInstance().getRenderer()) { - if (auto* sfx = renderer->getUiSoundManager()) - sfx->playAchievementAlert(); - } + 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()) { - auto nit = playerNameCache.find(guid); - if (nit != playerNameCache.end()) - senderName = nit->second; - } + if (senderName.empty()) senderName = lookupName(guid); if (senderName.empty()) { char tmp[32]; std::snprintf(tmp, sizeof(tmp), "0x%llX", @@ -25514,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)}); } // --------------------------------------------------------------------------- @@ -25527,10 +25613,10 @@ void GameHandler::handleAllAchievementData(network::Packet& packet) { 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; + if (!packet.hasRemaining(4)) break; uint32_t date = packet.readUInt32(); earnedAchievements_.insert(id); achievementDates_[id] = date; @@ -25538,11 +25624,11 @@ void GameHandler::handleAllAchievementData(network::Packet& packet) { // 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 @@ -25566,31 +25652,31 @@ void GameHandler::handleRespondInspectAchievements(network::Packet& packet) { loadAchievementNameCache(); // Read the inspected player's packed guid - if (packet.getSize() - packet.getReadPos() < 1) return; - uint64_t inspectedGuid = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasRemaining(1)) return; + uint64_t inspectedGuid = packet.readPackedGuid(); if (inspectedGuid == 0) { - packet.setReadPos(packet.getSize()); + packet.skipAll(); return; } std::unordered_set achievements; // Achievement records: { uint32 id, uint32 packedDate } until 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; + 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.getSize() - packet.getReadPos() >= 4) { + while (packet.hasRemaining(4)) { uint32_t id = packet.readUInt32(); if (id == 0xFFFFFFFF) break; // counter(8) + date(4) + unk(4) = 16 bytes - if (packet.getSize() - packet.getReadPos() < 16) break; + if (!packet.hasRemaining(16)) break; packet.readUInt64(); // counter packet.readUInt32(); // date packet.readUInt32(); // unk @@ -25606,7 +25692,7 @@ void GameHandler::handleRespondInspectAchievements(network::Packet& packet) { // Faction name cache (lazily loaded from Faction.dbc) // --------------------------------------------------------------------------- -void GameHandler::loadFactionNameCache() { +void GameHandler::loadFactionNameCache() const { if (factionNameCacheLoaded_) return; factionNameCacheLoaded_ = true; @@ -25663,17 +25749,32 @@ void GameHandler::loadFactionNameCache() { } uint32_t GameHandler::getFactionIdByRepListId(uint32_t repListId) const { - const_cast(this)->loadFactionNameCache(); + loadFactionNameCache(); auto it = factionRepListToId_.find(repListId); return (it != factionRepListToId_.end()) ? it->second : 0u; } uint32_t GameHandler::getRepListIdByFactionId(uint32_t factionId) const { - const_cast(this)->loadFactionNameCache(); + 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 { auto it = factionNameCache_.find(factionId); if (it != factionNameCache_.end()) return it->second; @@ -25681,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; @@ -25692,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; @@ -25722,12 +25823,12 @@ 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() { +void GameHandler::loadMapNameCache() const { if (mapNameCacheLoaded_) return; mapNameCacheLoaded_ = true; @@ -25751,7 +25852,7 @@ void GameHandler::loadMapNameCache() { std::string GameHandler::getMapName(uint32_t mapId) const { if (mapId == 0) return {}; - const_cast(this)->loadMapNameCache(); + loadMapNameCache(); auto it = mapNameCache_.find(mapId); return (it != mapNameCache_.end()) ? it->second : std::string{}; } @@ -25760,7 +25861,7 @@ std::string GameHandler::getMapName(uint32_t mapId) const { // LFG dungeon name cache (WotLK: LFGDungeons.dbc) // --------------------------------------------------------------------------- -void GameHandler::loadLfgDungeonDbc() { +void GameHandler::loadLfgDungeonDbc() const { if (lfgDungeonNameCacheLoaded_) return; lfgDungeonNameCacheLoaded_ = true; @@ -25787,7 +25888,7 @@ void GameHandler::loadLfgDungeonDbc() { std::string GameHandler::getLfgDungeonName(uint32_t dungeonId) const { if (dungeonId == 0) return {}; - const_cast(this)->loadLfgDungeonDbc(); + loadLfgDungeonDbc(); auto it = lfgDungeonNameCache_.find(dungeonId); return (it != lfgDungeonNameCache_.end()) ? it->second : std::string{}; } @@ -25810,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(); @@ -25828,7 +25929,7 @@ 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)); @@ -25852,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); @@ -25894,7 +25995,7 @@ void GameHandler::declineBfMgrInvite() { // ---- WotLK Calendar ---- void GameHandler::requestCalendar() { - if (state != WorldState::IN_WORLD || !socket) return; + if (!isInWorld()) return; // CMSG_CALENDAR_GET_CALENDAR has no payload network::Packet pkt(wireOpcode(Opcode::CMSG_CALENDAR_GET_CALENDAR)); socket->send(pkt); diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index a6de6dcb..83fcc5fe 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -224,6 +224,92 @@ void Inventory::sortBags() { } } +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 { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 0d4d09e2..f758f317 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -9,22 +9,6 @@ namespace game { namespace { -bool hasFullPackedGuid(const network::Packet& packet) { - if (packet.getReadPos() >= packet.getSize()) { - 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.getSize() - packet.getReadPos() >= guidBytes; -} - std::string formatPacketBytes(const network::Packet& packet, size_t startPos) { const auto& rawData = packet.getData(); if (startPos >= rawData.size()) { @@ -45,17 +29,17 @@ std::string formatPacketBytes(const network::Packet& packet, size_t startPos) { } bool skipClassicSpellCastTargets(network::Packet& packet, uint64_t* primaryTargetGuid = nullptr) { - if (packet.getSize() - packet.getReadPos() < 2) { + if (!packet.hasRemaining(2)) { return false; } const uint16_t targetFlags = packet.readUInt16(); const auto readPackedTargetGuid = [&](bool capture) -> bool { - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { return false; } - const uint64_t guid = UpdateObjectParser::readPackedGuid(packet); + const uint64_t guid = packet.readPackedGuid(); if (capture && primaryTargetGuid && *primaryTargetGuid == 0) { *primaryTargetGuid = guid; } @@ -80,7 +64,7 @@ bool skipClassicSpellCastTargets(network::Packet& packet, uint64_t* primaryTarge } if ((targetFlags & 0x0020) != 0) { // SOURCE_LOCATION - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { return false; } (void)packet.readFloat(); @@ -88,7 +72,7 @@ bool skipClassicSpellCastTargets(network::Packet& packet, uint64_t* primaryTarge (void)packet.readFloat(); } if ((targetFlags & 0x0040) != 0) { // DEST_LOCATION - if (packet.getSize() - packet.getReadPos() < 12) { + if (!packet.hasRemaining(12)) { return false; } (void)packet.readFloat(); @@ -97,7 +81,7 @@ bool skipClassicSpellCastTargets(network::Packet& packet, uint64_t* primaryTarge } if ((targetFlags & 0x1000) != 0) { // TRADE_ITEM - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { return false; } (void)packet.readUInt8(); @@ -189,14 +173,14 @@ uint32_t classicWireMoveFlags(uint32_t internalFlags) { // Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate // ============================================================================ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; + 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_TRANSPORT = 0x02; const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04; @@ -227,7 +211,7 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo 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(); @@ -340,7 +324,7 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo // Current melee target as packed guid if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { if (rem() < 1) return false; - /*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + /*uint64_t meleeTargetGuid =*/ packet.readPackedGuid(); } // Transport progress / world time @@ -371,10 +355,10 @@ 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 (wireFlags & ClassicMoveFlags::ONTRANSPORT) { @@ -395,10 +379,10 @@ 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 @@ -406,7 +390,7 @@ void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const M // Pitch (Classic: only SWIMMING) if (wireFlags & ClassicMoveFlags::SWIMMING) { - packet.writeBytes(reinterpret_cast(&info.pitch), sizeof(float)); + packet.writeFloat(info.pitch); } // Fall time (always present) @@ -414,10 +398,10 @@ void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const M // Jump data (Classic JUMPING = 0x2000) if (wireFlags & 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)); + packet.writeFloat(info.jumpVelocity); + packet.writeFloat(info.jumpSinAngle); + packet.writeFloat(info.jumpCosAngle); + packet.writeFloat(info.jumpXYSpeed); } } @@ -505,20 +489,20 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) { data = SpellStartData{}; - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + auto rem = [&]() { return packet.getRemainingSize(); }; const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + data.casterUnit = packet.readPackedGuid(); // Vanilla/Turtle SMSG_SPELL_START does not include castCount here. // Layout after the two packed GUIDs is spellId(u32) + castFlags(u16) + castTime(u32). @@ -562,7 +546,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da // Always reset output to avoid stale targets when callers reuse buffers. data = SpellGoData{}; - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + 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) { @@ -584,10 +568,10 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da }; if (rem() < 2) return false; - if (!hasFullPackedGuid(packet)) return false; - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) 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(); // Vanilla/Turtle SMSG_SPELL_GO does not include castCount here. // Layout after the two packed GUIDs is spellId(u32) + castFlags(u16). @@ -613,13 +597,13 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } const uint8_t rawHitCount = packet.readUInt8(); if (rawHitCount > 128) { - LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); + 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=", (int)rawHitCount, + LOG_WARNING("[Classic] Spell go: invalid hitCount/remaining (hits=", static_cast(rawHitCount), " remaining=", rem(), " occurrence=", badHitCountTrunc, ")"); } traceFailure("invalid_hit_count", packet.getReadPos(), rawHitCount); @@ -635,10 +619,10 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da for (uint16_t i = 0; i < rawHitCount; ++i) { uint64_t targetGuid = 0; if (usePackedGuids) { - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { return false; } - targetGuid = UpdateObjectParser::readPackedGuid(packet); + targetGuid = packet.readPackedGuid(); } else { if (rem() < 8) { return false; @@ -654,7 +638,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da }; if (!parseHitList(false) && !parseHitList(true)) { - LOG_WARNING("[Classic] Spell go: truncated hit targets at index 0/", (int)rawHitCount); + 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; @@ -673,7 +657,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } const uint8_t rawMissCount = packet.readUInt8(); if (rawMissCount > 128) { - LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); + 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) { @@ -695,7 +679,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da static uint32_t badMissCountTrunc = 0; ++badMissCountTrunc; if (badMissCountTrunc <= 10 || (badMissCountTrunc % 100) == 0) { - LOG_WARNING("[Classic] Spell go: invalid missCount/remaining (misses=", (int)rawMissCount, + LOG_WARNING("[Classic] Spell go: invalid missCount/remaining (misses=", static_cast(rawMissCount), " remaining=", rem(), " occurrence=", badMissCountTrunc, ")"); } traceFailure("invalid_miss_count", packet.getReadPos(), rawMissCount); @@ -708,10 +692,10 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da bool truncatedMissTargets = false; const auto parseMissEntry = [&](SpellGoMissEntry& m, bool usePackedGuid) -> bool { if (usePackedGuid) { - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { return false; } - m.targetGuid = UpdateObjectParser::readPackedGuid(packet); + m.targetGuid = packet.readPackedGuid(); } else { if (rem() < 8) { return false; @@ -727,7 +711,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da packet.setReadPos(missEntryPos); if (!parseMissEntry(m, true)) { LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", i, - "/", (int)rawMissCount); + "/", static_cast(rawMissCount)); traceFailure("truncated_miss_target", packet.getReadPos(), i); truncatedMissTargets = true; break; @@ -735,7 +719,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } if (rem() < 1) { LOG_WARNING("[Classic] Spell go: missing missType at miss index ", i, - "/", (int)rawMissCount); + "/", static_cast(rawMissCount)); traceFailure("missing_miss_type", packet.getReadPos(), i); truncatedMissTargets = true; break; @@ -744,7 +728,7 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da if (m.missType == 11) { if (rem() < 1) { LOG_WARNING("[Classic] Spell go: truncated reflect payload at miss index ", i, - "/", (int)rawMissCount); + "/", static_cast(rawMissCount)); traceFailure("truncated_reflect", packet.getReadPos(), i); truncatedMissTargets = true; break; @@ -774,8 +758,8 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da // 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; } @@ -792,21 +776,21 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { data = AttackerStateUpdateData{}; - auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + 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(); - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (!hasFullPackedGuid(packet)) { + data.attackerGuid = packet.readPackedGuid(); // PackedGuid in Vanilla + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + data.targetGuid = packet.readPackedGuid(); // PackedGuid in Vanilla if (rem() < 5) { packet.setReadPos(startPos); @@ -862,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 || !hasFullPackedGuid(packet)) 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 || !hasFullPackedGuid(packet)) 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 @@ -897,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 || !hasFullPackedGuid(packet)) 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 || !hasFullPackedGuid(packet)) 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(); @@ -939,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; @@ -992,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 @@ -1011,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(); } @@ -1028,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; } @@ -1039,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; } @@ -1068,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); @@ -1084,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; @@ -1142,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); } @@ -1262,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(); } @@ -1388,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; } @@ -1402,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(); @@ -1435,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; @@ -1459,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; @@ -1477,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 @@ -1497,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; @@ -1559,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; @@ -1693,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; } @@ -1747,7 +1731,7 @@ 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; } @@ -1760,7 +1744,7 @@ 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; } @@ -1781,12 +1765,12 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ 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; } @@ -1813,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; } @@ -1831,14 +1815,14 @@ 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) { + 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 @@ -1849,7 +1833,7 @@ bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQ } // AmmoType + RangedModRange (2 fields, 8 bytes) - if (packet.getSize() - packet.getReadPos() >= 8) { + if (packet.hasRemaining(8)) { packet.readUInt32(); // AmmoType packet.readFloat(); // RangedModRange } @@ -1857,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 @@ -1867,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 @@ -1926,13 +1910,13 @@ namespace TurtleMoveFlags { } bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; + 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_TRANSPORT = 0x02; const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04; @@ -1964,7 +1948,7 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc 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(); @@ -2085,7 +2069,7 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { if (rem() < 1) return false; - /*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + /*uint64_t meleeTargetGuid =*/ packet.readPackedGuid(); } if (updateFlags & UPDATEFLAG_TRANSPORT) { @@ -2111,7 +2095,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec } if (withHasTransportByte) { - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { packet.setReadPos(start); return false; } @@ -2120,7 +2104,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec uint32_t remainingBlockCount = out.blockCount; - 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) { @@ -2128,7 +2112,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec return false; } --remainingBlockCount; - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { packet.setReadPos(start); return false; } @@ -2138,11 +2122,11 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec return false; } for (uint32_t i = 0; i < count; ++i) { - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { packet.setReadPos(start); return false; } - out.outOfRangeGuids.push_back(UpdateObjectParser::readPackedGuid(packet)); + out.outOfRangeGuids.push_back(packet.readPackedGuid()); } } else { packet.setReadPos(packet.getReadPos() - 1); @@ -2152,7 +2136,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec out.blockCount = remainingBlockCount; out.blocks.reserve(out.blockCount); for (uint32_t i = 0; i < out.blockCount; ++i) { - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { packet.setReadPos(start); return false; } @@ -2182,8 +2166,8 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec return true; case UpdateType::CREATE_OBJECT: case UpdateType::CREATE_OBJECT2: - block.guid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getReadPos() >= packet.getSize()) return false; + 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; @@ -2196,7 +2180,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec switch (updateType) { case UpdateType::VALUES: - block.guid = UpdateObjectParser::readPackedGuid(packet); + block.guid = packet.readPackedGuid(); ok = UpdateObjectParser::parseUpdateFields(packet, block); break; case UpdateType::MOVEMENT: @@ -2298,7 +2282,7 @@ bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveD auto looksLikeWotlkMonsterMove = [&](network::Packet& probe) -> bool { const size_t probeStart = probe.getReadPos(); - uint64_t guid = UpdateObjectParser::readPackedGuid(probe); + uint64_t guid = probe.readPackedGuid(); if (guid == 0) { probe.setReadPos(probeStart); return false; @@ -2414,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 9d68879f..8d86a808 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -30,14 +30,14 @@ namespace TbcMoveFlags { // - Flag 0x08 (HIGH_GUID) reads 2 u32s (Classic: 1 u32) // ============================================================================ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { - auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); }; + 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 @@ -78,7 +78,7 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& 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(); @@ -184,7 +184,7 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& // Target GUID if (updateFlags & UPDATEFLAG_HAS_TARGET) { if (rem() < 1) return false; - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + /*uint64_t targetGuid =*/ packet.readPackedGuid(); } // Transport time @@ -228,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) { @@ -252,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); @@ -266,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) @@ -276,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); } } @@ -317,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); @@ -333,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; @@ -391,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); } @@ -430,7 +430,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa uint32_t remainingBlockCount = out.blockCount; - 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) { @@ -438,7 +438,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa return false; } --remainingBlockCount; - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { packet.setReadPos(start); return false; } @@ -452,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 { @@ -479,7 +479,7 @@ 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; } @@ -490,7 +490,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa } 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; @@ -544,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) @@ -564,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; @@ -581,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 @@ -602,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; @@ -632,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; @@ -656,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; @@ -693,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(); @@ -710,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; } @@ -801,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; } @@ -810,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(); @@ -835,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(); @@ -849,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, "'"); @@ -912,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(); @@ -928,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; } @@ -938,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(); @@ -982,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; } @@ -992,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(); @@ -1004,7 +1004,7 @@ 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; } @@ -1017,7 +1017,7 @@ 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; } @@ -1038,7 +1038,7 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery 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 } @@ -1050,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; } @@ -1074,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; } @@ -1091,13 +1091,13 @@ 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) { + 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 @@ -1108,14 +1108,14 @@ bool TbcPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQuery } // 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 @@ -1125,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 @@ -1133,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 @@ -1158,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(); @@ -1247,7 +1247,7 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector bool { if (!(targetFlags & flag)) return true; - if (packet.getSize() - packet.getReadPos() < 12) return false; + if (!packet.hasRemaining(12)) return false; (void)packet.readFloat(); (void)packet.readFloat(); (void)packet.readFloat(); return true; }; @@ -1306,7 +1306,7 @@ static bool skipTbcSpellCastTargets(network::Packet& packet, uint64_t* primaryTa // ============================================================================ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) { data = SpellStartData{}; - if (packet.getSize() - packet.getReadPos() < 22) return false; + if (!packet.hasRemaining(22)) return false; data.casterGuid = packet.readUInt64(); // full GUID (object) data.casterUnit = packet.readUInt64(); // full GUID (caster unit) @@ -1344,7 +1344,7 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) const size_t startPos = packet.getReadPos(); // Fixed header before hit/miss lists: // casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32) - if (packet.getSize() - packet.getReadPos() < 25) return false; + if (!packet.hasRemaining(25)) return false; data.casterGuid = packet.readUInt64(); // full GUID in TBC data.casterUnit = packet.readUInt64(); // full GUID in TBC @@ -1361,15 +1361,15 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) const uint8_t rawHitCount = packet.readUInt8(); if (rawHitCount > 128) { - LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); + LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", static_cast(rawHitCount), ")"); } 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.getReadPos() + 8 > packet.getSize()) { + if (!packet.hasRemaining(8)) { LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", i, - "/", (int)rawHitCount); + "/", static_cast(rawHitCount)); truncatedTargets = true; break; } @@ -1392,14 +1392,14 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) const uint8_t rawMissCount = packet.readUInt8(); if (rawMissCount > 128) { - LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); + 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.getReadPos() + 9 > packet.getSize()) { + if (!packet.hasRemaining(9)) { LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i, - "/", (int)rawMissCount); + "/", static_cast(rawMissCount)); truncatedTargets = true; break; } @@ -1407,9 +1407,9 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) m.targetGuid = packet.readUInt64(); // full GUID in TBC m.missType = packet.readUInt8(); if (m.missType == 11) { // SPELL_MISS_REFLECT - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i, - "/", (int)rawMissCount); + "/", static_cast(rawMissCount)); truncatedTargets = true; break; } @@ -1429,8 +1429,8 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) // any subsequent fields are not misaligned for ground-targeted AoE spells. skipTbcSpellCastTargets(packet, &data.targetGuid); - LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, - " misses=", (int)data.missCount); + LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", static_cast(data.hitCount), + " misses=", static_cast(data.missCount)); return true; } @@ -1443,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; @@ -1459,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; } @@ -1478,7 +1478,7 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke data = AttackerStateUpdateData{}; const size_t startPos = packet.getReadPos(); - auto rem = [&]() { return packet.getSize() - 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 @@ -1543,7 +1543,7 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL // = 43 bytes // Some servers append additional trailing fields; consume the canonical minimum // and leave any extension bytes unread. - if (packet.getSize() - packet.getReadPos() < 43) return false; + if (!packet.hasRemaining(43)) return false; data = SpellDamageLogData{}; @@ -1578,7 +1578,7 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) { // Fixed payload is 28 bytes; many cores append crit flag (1 byte). // targetGuid(8) + casterGuid(8) + spellId(4) + heal(4) + overheal(4) - if (packet.getSize() - packet.getReadPos() < 28) return false; + if (!packet.hasRemaining(28)) return false; data = SpellHealLogData{}; @@ -1761,7 +1761,7 @@ bool TbcPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, Gam return true; } - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { LOG_ERROR("TBC SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")"); return false; } @@ -1779,7 +1779,7 @@ bool TbcPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, Gam packet.readString(); // castBarCaption // 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(); @@ -1828,7 +1828,7 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData data.motd = packet.readString(); data.guildInfo = packet.readString(); - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("TBC GuildRoster: truncated before rankCount"); data.ranks.clear(); data.members.clear(); @@ -1844,19 +1844,19 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData data.ranks.resize(rankCount); for (uint32_t i = 0; i < rankCount; ++i) { - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("TBC GuildRoster: 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 (guild banks added in TBC 2.3) 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 } @@ -1864,7 +1864,7 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData data.members.resize(numMembers); for (uint32_t i = 0; i < numMembers; ++i) { - if (packet.getReadPos() + 9 > packet.getSize()) { + if (!packet.hasRemaining(9)) { LOG_WARNING("TBC GuildRoster: truncated member at index ", i); break; } @@ -1878,7 +1878,7 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData m.name = packet.readString(); } - if (packet.getReadPos() + 1 > packet.getSize()) { + if (!packet.hasRemaining(1)) { m.rankIndex = 0; m.level = 1; m.classId = 0; @@ -1886,7 +1886,7 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData m.zoneId = 0; } else { m.rankIndex = packet.readUInt32(); - if (packet.getReadPos() + 2 > packet.getSize()) { + if (!packet.hasRemaining(2)) { m.level = 1; m.classId = 0; } else { @@ -1895,7 +1895,7 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData } // TBC: NO gender byte (WotLK added it) m.gender = 0; - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { m.zoneId = 0; } else { m.zoneId = packet.readUInt32(); @@ -1903,7 +1903,7 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData } if (!m.online) { - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { m.lastOnline = 0.0f; } else { m.lastOnline = packet.readFloat(); diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 649c9923..58cb6a79 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -179,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) @@ -259,16 +259,16 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float } // 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); @@ -403,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 @@ -480,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) diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index 6a736546..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}, @@ -74,6 +75,8 @@ static const UFNameEntry kUFNames[] = { {"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_memory.cpp b/src/game/warden_memory.cpp index 33127e2c..d57586bb 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -861,7 +861,7 @@ void WardenMemory::verifyWardenScanEntries() { bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], uint8_t patternLen, bool imageOnly, uint32_t hintOffset, bool hintOnly) const { - if (!loaded_ || patternLen == 0 || patternLen > 255) return false; + if (!loaded_ || patternLen == 0) return false; // Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1) std::string cacheKey(26, '\0'); @@ -949,7 +949,7 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect 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=", (int)patternLen); + std::hex, hintOffset, std::dec, " patLen=", static_cast(patternLen)); size_t totalPositions = 0; for (const auto& r : ranges) { diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index eea0f0ee..bf44c26e 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -74,8 +74,7 @@ bool WardenModule::load(const std::vector& moduleData, // Step 3: Verify RSA signature if (!verifyRSASignature(decryptedData_)) { - LOG_ERROR("WardenModule: RSA signature verification failed!"); - // Note: Currently returns true (skipping verification) due to placeholder modulus + // Expected with placeholder modulus — verification is skipped gracefully } // Step 4: Strip RSA signature (last 256 bytes) then zlib decompress @@ -126,7 +125,7 @@ bool WardenModule::load(const std::vector& moduleData, return true; } -bool WardenModule::processCheckRequest(const std::vector& checkData, +bool WardenModule::processCheckRequest([[maybe_unused]] const std::vector& checkData, [[maybe_unused]] std::vector& responseOut) { if (!loaded_) { LOG_ERROR("WardenModule: Module not loaded, cannot process checks"); @@ -427,12 +426,11 @@ bool WardenModule::verifyRSASignature(const std::vector& data) { } } - LOG_ERROR("WardenModule: RSA signature verification FAILED (hash mismatch)"); - LOG_ERROR("WardenModule: NOTE: Using placeholder modulus - extract real modulus from WoW.exe for actual verification"); + LOG_WARNING("WardenModule: RSA signature verification skipped (placeholder modulus)"); + LOG_WARNING("WardenModule: Extract real modulus from WoW.exe for actual verification"); // For development, return true to proceed (since we don't have real modulus) // TODO: Set to false once real modulus is extracted - LOG_WARNING("WardenModule: Skipping RSA verification (placeholder modulus)"); return true; // TEMPORARY - change to false for production } @@ -705,7 +703,7 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize); } relocDataOffset_ = 0; - LOG_ERROR("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback"); + LOG_WARNING("WardenModule: Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback"); return true; } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index e20c2d09..7a156f8e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -21,7 +21,7 @@ namespace { } bool hasFullPackedGuid(const wowee::network::Packet& packet) { - if (packet.getReadPos() >= packet.getSize()) { + if (!packet.hasData()) { return false; } @@ -33,7 +33,7 @@ namespace { ++guidBytes; } } - return packet.getSize() - packet.getReadPos() >= guidBytes; + return packet.hasRemaining(guidBytes); } const char* updateTypeName(wowee::game::UpdateType type) { @@ -200,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; } @@ -249,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; } @@ -420,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; } @@ -455,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); @@ -474,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; } @@ -483,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()); @@ -512,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(); @@ -526,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; @@ -548,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 @@ -574,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; @@ -587,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(); @@ -595,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); } @@ -661,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) { @@ -682,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; @@ -715,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; } @@ -760,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 @@ -793,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); @@ -832,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). @@ -847,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); } } @@ -859,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 @@ -888,32 +826,12 @@ 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.getSize() - packet.getReadPos(); }; + auto rem = [&]() -> size_t { return packet.getRemainingSize(); }; if (rem() < 2) return false; // Update flags (3.3.5a uses 2 bytes for flags) @@ -982,7 +900,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock 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(); @@ -994,26 +912,27 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec, " offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")"); - if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT + if (moveFlags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT if (rem() < 4) return false; /*uint32_t tTime2 =*/ packet.readUInt32(); } } // Swimming/flying pitch - // WotLK 3.3.5a movement flags relevant here: + // WotLK 3.3.5a movement flags (wire format): // SWIMMING = 0x00200000 - // FLYING = 0x01000000 (player/creature actively flying) - // SPLINE_ELEVATION = 0x02000000 (smooth vertical spline offset — no pitch field) + // CAN_FLY = 0x01000000 (ability to fly — no pitch field) + // FLYING = 0x02000000 (actively flying — has pitch field) + // SPLINE_ELEVATION = 0x04000000 (smooth vertical spline offset) // MovementFlags2: - // MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0010 + // MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0020 // // Pitch is present when SWIMMING or FLYING are set, or the always-allow flag is set. - // The original code checked 0x02000000 (SPLINE_ELEVATION) which neither covers SWIMMING - // nor FLYING, causing misaligned reads for swimming/flying entities in SMSG_UPDATE_OBJECT. + // Note: CAN_FLY (0x01000000) does NOT gate pitch; only FLYING (0x02000000) does. + // (TBC uses 0x01000000 for FLYING — see TbcMoveFlags in packet_parsers_tbc.cpp.) if ((moveFlags & 0x00200000) /* SWIMMING */ || - (moveFlags & 0x01000000) /* FLYING */ || - (moveFlags2 & 0x0010) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) { + (moveFlags & 0x02000000) /* FLYING */ || + (moveFlags2 & 0x0020) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) { if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } @@ -1054,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); @@ -1077,33 +996,49 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // 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. - const size_t legacyStart = packet.getReadPos(); 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(); const size_t afterSplineId = packet.getReadPos(); - // Helper: try to parse uncompressed spline points from current read position. - auto tryParseUncompressedSpline = [&](const char* tag) -> bool { + // 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 needed = static_cast(pc) * 12ull + 13ull; - if (!bytesAvailable(needed)) return false; - for (uint32_t i = 0; i < pc; i++) { - packet.readFloat(); packet.readFloat(); packet.readFloat(); + 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 { + // 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.readUInt8(); // splineMode packet.readFloat(); packet.readFloat(); packet.readFloat(); // endPoint - LOG_DEBUG(" Spline pointCount=", pc, " (", tag, ")"); + LOG_DEBUG(" Spline pointCount=", pc, " compressed=", compressed, " (", tag, ")"); return true; }; - // --- Try 1: Classic format (pointCount immediately after splineId) --- - bool splineParsed = tryParseUncompressedSpline("classic"); + // --- Try 1: Classic format (uncompressed points immediately after splineId) --- + bool splineParsed = tryParseSplinePoints(false, "classic"); - // --- Try 2: WotLK format (durationMod+durationModNext+conditional+pointCount) --- + // --- Try 2: WotLK format (durationMod+durationModNext+conditional+compressed points) --- if (!splineParsed) { packet.setReadPos(afterSplineId); bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext @@ -1115,64 +1050,28 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock else { packet.readUInt8(); packet.readUInt32(); } } } - if (wotlkOk && (splineFlags & 0x00000800)) { // SPLINEFLAG_PARABOLIC + // AzerothCore/ChromieCraft always writes verticalAcceleration(float) + // + effectStartTime(uint32) unconditionally — NOT gated by PARABOLIC flag. + if (wotlkOk) { if (!bytesAvailable(8)) { wotlkOk = false; } - else { packet.readFloat(); packet.readUInt32(); } + else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); } } if (wotlkOk) { - splineParsed = tryParseUncompressedSpline("wotlk"); - } - } - - // --- Try 3: Compact layout (compressed points) as final recovery --- - if (!splineParsed) { - 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 invalid (classic+wotlk+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; - } else { - compactPayloadBytes = 12ull; - if (compactPointCount > 1) { - compactPayloadBytes += static_cast(compactPointCount - 1) * 4ull; + // 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 compact fallback } } else if (updateFlags & UPDATEFLAG_POSITION) { // Transport position update (UPDATEFLAG_POSITION = 0x0100) if (rem() < 1) return false; - uint64_t transportGuid = readPackedGuid(packet); + uint64_t transportGuid = packet.readPackedGuid(); if (rem() < 32) return false; // 8 floats block.x = packet.readFloat(); block.y = packet.readFloat(); @@ -1215,7 +1114,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Target GUID (for units with target) if (updateFlags & UPDATEFLAG_HAS_TARGET) { if (rem() < 1) return false; - /*uint64_t targetGuid =*/ readPackedGuid(packet); + /*uint64_t targetGuid =*/ packet.readPackedGuid(); } // Transport time @@ -1255,7 +1154,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) { size_t startPos = packet.getReadPos(); - if (packet.getReadPos() >= packet.getSize()) return false; + if (!packet.hasData()) return false; // Read number of blocks (each block is 32 fields = 32 bits) uint8_t blockCount = packet.readUInt8(); @@ -1264,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. @@ -1274,7 +1191,7 @@ 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()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i, " type=", updateTypeName(block.updateType), " objectType=", static_cast(block.objectType), @@ -1309,7 +1226,7 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& highestSetBit = fieldIndex; } // Validate 4 bytes available before reading field value - if (packet.getReadPos() + 4 > packet.getSize()) { + if (!packet.hasRemaining(4)) { LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex, " type=", updateTypeName(block.updateType), " objectType=", static_cast(block.objectType), @@ -1344,19 +1261,19 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& } bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) { - if (packet.getReadPos() >= packet.getSize()) return false; + 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 - if (packet.getReadPos() >= packet.getSize()) return false; - 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); @@ -1364,7 +1281,7 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::MOVEMENT: { // Movement update - if (packet.getReadPos() + 8 > packet.getSize()) return false; + if (!packet.hasRemaining(8)) return false; block.guid = packet.readUInt64(); LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec); @@ -1374,15 +1291,15 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::CREATE_OBJECT: case UpdateType::CREATE_OBJECT2: { // Create new object with full data - if (packet.getReadPos() >= packet.getSize()) return false; - 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.getReadPos() >= packet.getSize()) return false; + 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); @@ -1407,7 +1324,7 @@ 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; } } @@ -1433,7 +1350,7 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) 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)) { @@ -1451,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); } @@ -1504,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; @@ -1587,7 +1504,7 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { packet.setReadPos(start); return false; } - if ((packet.getSize() - packet.getReadPos()) < (static_cast(len) + minTrailingBytes)) { + if (!packet.hasRemaining(static_cast(len) + minTrailingBytes)) { packet.setReadPos(start); return false; } @@ -1738,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; } @@ -1794,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; @@ -1846,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; @@ -1854,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(); } @@ -1907,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; } @@ -1928,14 +1845,14 @@ network::Packet RequestPlayedTimePacket::build(bool sendToChat) { bool PlayedTimeParser::parse(network::Packet& packet, PlayedTimeData& data) { // Classic/Turtle may omit the trailing trigger-message byte and send only // totalTime(4) + levelTime(4). Later expansions append triggerMsg(1). - if (packet.getSize() - packet.getReadPos() < 8) { + 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.getSize() - packet.getReadPos() >= 1) && (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; } @@ -1989,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; } @@ -1998,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; } @@ -2041,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; } @@ -2059,7 +1976,7 @@ 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; } @@ -2084,8 +2001,8 @@ network::Packet SetActionButtonPacket::build(uint8_t button, uint8_t type, uint3 packet.writeUInt16(static_cast(id)); packet.writeUInt8(classicType); packet.writeUInt8(0); // misc - LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (Classic): button=", (int)button, - " id=", id, " type=", (int)classicType); + 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 @@ -2093,7 +2010,7 @@ network::Packet SetActionButtonPacket::build(uint8_t button, uint8_t type, uint3 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=", (int)button, + LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (TBC/WotLK): button=", static_cast(button), " packed=0x", std::hex, packed, std::dec); } return packet; @@ -2292,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(); } @@ -2319,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; @@ -2328,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 { @@ -2337,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; @@ -2353,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); @@ -2392,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(); @@ -2411,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 } @@ -2432,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; } @@ -2441,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; @@ -2456,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; @@ -2465,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(); @@ -2474,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(); @@ -2482,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(); @@ -2508,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; } @@ -2592,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; } @@ -2637,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; } @@ -2703,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; } @@ -2729,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; } @@ -2747,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 @@ -2757,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; @@ -2769,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; } @@ -2809,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; @@ -2859,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; } @@ -2879,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(); @@ -2909,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; @@ -2930,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"; @@ -2974,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; } @@ -3000,7 +2917,7 @@ 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; } @@ -3022,7 +2939,7 @@ 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; } @@ -3042,7 +2959,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa 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 } @@ -3060,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; } @@ -3080,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 } @@ -3115,7 +3032,7 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa // 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 @@ -3125,15 +3042,15 @@ 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 @@ -3173,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) { @@ -3206,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): @@ -3230,24 +3147,24 @@ 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; @@ -3267,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(); @@ -3285,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(); @@ -3311,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) { @@ -3326,37 +3243,37 @@ 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; @@ -3371,7 +3288,7 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d if (pointCount > 1) { requiredBytes += static_cast(pointCount - 1) * 4ull; } - if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; + if (!packet.hasRemaining(requiredBytes)) return false; // First float[3] is destination. data.destX = packet.readFloat(); @@ -3388,7 +3305,7 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d } 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; @@ -3409,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); @@ -3420,23 +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(); - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { + data.attackerGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + 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; } @@ -3449,7 +3366,7 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda // (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.getSize() - packet.getReadPos(); + size_t remaining = packet.getRemainingSize(); size_t maxFit = remaining / 20; if (data.subDamageCount > maxFit) { data.subDamageCount = static_cast(std::min(maxFit, 64)); @@ -3462,7 +3379,7 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda 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) { + if (!packet.hasRemaining(20)) { data.subDamageCount = i; break; } @@ -3476,7 +3393,7 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda } // Validate victimState + overkill fields (8 bytes) - if (packet.getSize() - packet.getReadPos() < 8) { + if (!packet.hasRemaining(8)) { data.victimState = 0; data.overkill = 0; return !data.subDamages.empty(); @@ -3485,7 +3402,7 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda data.victimState = 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.getSize() - packet.getReadPos(); }; + 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; @@ -3508,22 +3425,22 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da // 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.getSize() - packet.getReadPos() < 33) return false; + if (!packet.hasRemaining(33)) return false; size_t startPos = packet.getReadPos(); - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { + data.targetGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); + 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; } @@ -3537,7 +3454,7 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da // Remaining fields are required for a complete event. // Reject truncated packets so we do not emit partial/incorrect combat entries. - if (packet.getSize() - packet.getReadPos() < 10) { + if (!packet.hasRemaining(10)) { packet.setReadPos(startPos); return false; } @@ -3558,22 +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(); - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { + data.targetGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); + 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; } @@ -3596,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; } @@ -3607,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) @@ -3627,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; } @@ -3635,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"); @@ -3651,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; } @@ -3670,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 @@ -3678,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"); @@ -3691,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; } @@ -3777,12 +3698,12 @@ 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; } @@ -3791,21 +3712,21 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { // 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.getSize() - packet.getReadPos() < 15) return false; + if (!packet.hasRemaining(15)) return false; size_t startPos = packet.getReadPos(); - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { return false; } - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + data.casterUnit = packet.readPackedGuid(); // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 13 bytes) - if (packet.getSize() - packet.getReadPos() < 13) { + if (!packet.hasRemaining(13)) { packet.setReadPos(startPos); return false; } @@ -3816,7 +3737,7 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { data.castTime = packet.readUInt32(); // SpellCastTargets starts with target flags and is mandatory. - if (packet.getSize() - packet.getReadPos() < 4) { + if (!packet.hasRemaining(4)) { LOG_WARNING("Spell start: missing targetFlags"); packet.setReadPos(startPos); return false; @@ -3828,15 +3749,15 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { uint32_t targetFlags = packet.readUInt32(); auto readPackedTarget = [&](uint64_t* out) -> bool { - if (!hasFullPackedGuid(packet)) return false; - uint64_t g = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) return false; + uint64_t g = packet.readPackedGuid(); if (out) *out = g; return true; }; auto skipPackedAndFloats3 = [&]() -> bool { - if (!hasFullPackedGuid(packet)) return false; - UpdateObjectParser::readPackedGuid(packet); // transport GUID (may be zero) - if (packet.getSize() - packet.getReadPos() < 12) return false; + 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; }; @@ -3859,7 +3780,7 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } // STRING: null-terminated if (targetFlags & 0x0200u) { - while (packet.getReadPos() < packet.getSize() && packet.readUInt8() != 0) {} + while (packet.hasData() && packet.readUInt8() != 0) {} } LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); @@ -3872,21 +3793,21 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { // Packed GUIDs are variable-length, so only require the smallest possible // shape up front: 2 GUID masks + fixed fields through hitCount. - if (packet.getSize() - packet.getReadPos() < 16) return false; + if (!packet.hasRemaining(16)) return false; size_t startPos = packet.getReadPos(); - if (!hasFullPackedGuid(packet)) { + if (!packet.hasFullPackedGuid()) { return false; } - data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) { + data.casterGuid = packet.readPackedGuid(); + if (!packet.hasFullPackedGuid()) { packet.setReadPos(startPos); return false; } - data.casterUnit = UpdateObjectParser::readPackedGuid(packet); + 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; } @@ -3899,7 +3820,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { const uint8_t rawHitCount = packet.readUInt8(); if (rawHitCount > 128) { - LOG_WARNING("Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); + LOG_WARNING("Spell go: hitCount capped (requested=", static_cast(rawHitCount), ")"); } const uint8_t storedHitLimit = std::min(rawHitCount, 128); @@ -3908,8 +3829,8 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { 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.getSize() - packet.getReadPos() < 8) { - LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount); + if (!packet.hasRemaining(8)) { + LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", static_cast(rawHitCount)); truncatedTargets = true; break; } @@ -3925,7 +3846,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.hitCount = static_cast(data.hitTargets.size()); // missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. - if (packet.getSize() - packet.getReadPos() < 1) { + if (!packet.hasRemaining(1)) { LOG_WARNING("Spell go: missing missCount after hit target list"); packet.setReadPos(startPos); return false; @@ -3946,16 +3867,16 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { if (i == missCountPos - 1) hexCtx += "["; if (i == missCountPos) hexCtx += "] "; } - LOG_WARNING("Spell go: suspect missCount=", (int)rawMissCount, - " spell=", data.spellId, " hits=", (int)data.hitCount, + 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); } if (rawMissCount > 128) { - LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount, - ") spell=", data.spellId, " hits=", (int)data.hitCount, - " remaining=", packet.getSize() - packet.getReadPos()); + 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); @@ -3963,9 +3884,9 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { 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.getSize() - packet.getReadPos() < 9) { // 8 GUID + 1 missType - LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount, - " spell=", data.spellId, " hits=", (int)data.hitCount); + 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; } @@ -3973,8 +3894,8 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { m.targetGuid = packet.readUInt64(); m.missType = packet.readUInt8(); if (m.missType == 11) { // SPELL_MISS_REFLECT - if (packet.getSize() - packet.getReadPos() < 1) { - LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount); + if (!packet.hasRemaining(1)) { + LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", static_cast(rawMissCount)); truncatedTargets = true; break; } @@ -3990,28 +3911,28 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& 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 ", (int)data.hitCount, " hits despite miss truncation"); - packet.setReadPos(packet.getSize()); // consume remaining bytes + LOG_DEBUG("Spell go: salvaging ", static_cast(data.hitCount), " hits despite miss truncation"); + packet.skipAll(); // consume remaining bytes return true; } // 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.getReadPos() < packet.getSize()) { - if (packet.getSize() - packet.getReadPos() >= 4) { + if (packet.hasData()) { + if (packet.hasRemaining(4)) { uint32_t targetFlags = packet.readUInt32(); auto readPackedTarget = [&](uint64_t* out) -> bool { - if (!hasFullPackedGuid(packet)) return false; - uint64_t g = UpdateObjectParser::readPackedGuid(packet); + if (!packet.hasFullPackedGuid()) return false; + uint64_t g = packet.readPackedGuid(); if (out) *out = g; return true; }; auto skipPackedAndFloats3 = [&]() -> bool { - if (!hasFullPackedGuid(packet)) return false; - UpdateObjectParser::readPackedGuid(packet); // transport GUID - if (packet.getSize() - packet.getReadPos() < 12) return false; + if (!packet.hasFullPackedGuid()) return false; + packet.readPackedGuid(); // transport GUID + if (!packet.hasRemaining(12)) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); return true; }; @@ -4034,29 +3955,29 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { } // STRING: null-terminated if (targetFlags & 0x0200u) { - while (packet.getReadPos() < packet.getSize() && packet.readUInt8() != 0) {} + while (packet.hasData() && packet.readUInt8() != 0) {} } } } - LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, - " misses=", (int)data.missCount); + 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; } @@ -4070,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; @@ -4083,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; @@ -4105,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); @@ -4122,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"); } @@ -4133,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(); @@ -4142,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"); } @@ -4171,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; } @@ -4179,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; } @@ -4195,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(); @@ -4279,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); @@ -4389,7 +4310,7 @@ network::Packet LootReleasePacket::build(uint64_t lootGuid) { bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat) { data = LootResponseData{}; - size_t avail = packet.getSize() - packet.getReadPos(); + 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, @@ -4404,9 +4325,9 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, data.lootType = packet.readUInt8(); // Short failure packet — no gold/item data follows. - avail = packet.getSize() - packet.getReadPos(); + avail = packet.getRemainingSize(); if (avail < 5) { - LOG_DEBUG("LootResponseParser: lootType=", (int)data.lootType, " (empty/failure response)"); + LOG_DEBUG("LootResponseParser: lootType=", static_cast(data.lootType), " (empty/failure response)"); return false; } @@ -4419,7 +4340,7 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, 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; } @@ -4446,7 +4367,7 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, // Quest item section only present in WotLK 3.3.5a uint8_t questItemCount = 0; - if (isWotlkFormat && packet.getSize() - packet.getReadPos() >= 1) { + if (isWotlkFormat && packet.hasRemaining(1)) { questItemCount = packet.readUInt8(); data.items.reserve(data.items.size() + questItemCount); if (!parseLootItemList(questItemCount, true)) { @@ -4455,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; } @@ -4523,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; } @@ -4534,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(); @@ -4551,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(); @@ -4567,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, "'"); @@ -4578,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(); @@ -4597,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; } @@ -4612,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; } @@ -4630,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; } @@ -4669,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; } @@ -4692,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(); @@ -4710,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; @@ -4723,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; @@ -4758,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; } @@ -4795,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(); @@ -4826,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(); @@ -4844,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; @@ -4863,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; @@ -4966,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; } @@ -4982,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; } @@ -4998,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; @@ -5014,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; } @@ -5028,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(); @@ -5043,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; } @@ -5074,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 { @@ -5135,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); @@ -5146,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; } @@ -5154,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 } @@ -5167,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; } @@ -5189,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; } @@ -5250,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; @@ -5259,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) { @@ -5271,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) { @@ -5379,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(); @@ -5457,7 +5378,7 @@ 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(); @@ -5610,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; @@ -5620,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; @@ -5629,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; } @@ -5646,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; } @@ -5654,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; } @@ -5677,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; @@ -5685,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; @@ -5777,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 @@ -5792,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(); @@ -5821,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(); } @@ -5829,11 +5750,11 @@ 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; 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 271fc0e9..4482e3f3 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -332,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"; } @@ -360,13 +360,8 @@ 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(); @@ -418,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, @@ -588,11 +577,9 @@ void WorldSocket::pumpNetworkIO() { } // 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) { @@ -668,7 +655,7 @@ void WorldSocket::tryParsePackets() { 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, 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/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/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 6b4e00b8..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); @@ -554,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]; }; @@ -576,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]; }; @@ -2648,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(); @@ -3315,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 40ffd4b1..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,7 @@ 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 --- @@ -579,7 +618,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout .setLayout(ribbonPipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha()); @@ -746,6 +785,9 @@ void M2Renderer::shutdown() { if (ribbonPipelineLayout_) { vkDestroyPipelineLayout(device, ribbonPipelineLayout_, nullptr); ribbonPipelineLayout_ = VK_NULL_HANDLE; } // Destroy descriptor pools and layouts + if (dummyBoneBuffer_) { vmaDestroyBuffer(alloc, dummyBoneBuffer_, dummyBoneAlloc_); dummyBoneBuffer_ = VK_NULL_HANDLE; } + // dummyBoneSet_ is freed implicitly when boneDescPool_ is destroyed + dummyBoneSet_ = VK_NULL_HANDLE; if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; } if (boneDescPool_) { vkDestroyDescriptorPool(device, boneDescPool_, nullptr); boneDescPool_ = VK_NULL_HANDLE; } if (materialSetLayout_) { vkDestroyDescriptorSetLayout(device, materialSetLayout_, nullptr); materialSetLayout_ = VK_NULL_HANDLE; } @@ -812,7 +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; } @@ -822,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; } @@ -955,15 +1005,6 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { 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); @@ -976,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; @@ -1152,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; @@ -1235,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) { @@ -1303,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. @@ -1359,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", @@ -1508,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); @@ -1579,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); @@ -1615,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, @@ -1737,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)"); @@ -2490,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; @@ -2505,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); @@ -2514,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]; @@ -2521,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; @@ -2683,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); } @@ -2771,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++; } @@ -2785,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_; @@ -2803,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; @@ -2896,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); } @@ -3141,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(); @@ -3956,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; @@ -4142,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(); @@ -4962,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); @@ -4997,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()); @@ -5030,7 +4876,7 @@ void M2Renderer::recreatePipelines() { .setLayout(smokePipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); } // --- Ribbon pipelines --- @@ -5064,7 +4910,7 @@ void M2Renderer::recreatePipelines() { .setLayout(ribbonPipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx_->getPipelineCache()); }; ribbonPipeline_ = buildRibbonPipeline(PipelineBuilder::blendAlpha()); diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index cce494d9..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(); @@ -270,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(); 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 d6119e74..67f9f7fa 100644 --- a/src/rendering/performance_hud.cpp +++ b/src/rendering/performance_hud.cpp @@ -23,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() { } @@ -456,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: Strafe 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 2d942c23..36e404cb 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -33,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" @@ -343,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; } @@ -597,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() { @@ -1214,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); @@ -1296,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{}; @@ -1433,18 +1438,22 @@ void Renderer::endFrame() { renderFSRUpscale(); } - // ImGui rendering — must respect subpass contents mode - // Parallel recording only applies when no post-process pass is active. - if (!fsr_.enabled && !fsr2_.enabled && !fxaa_.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); } @@ -1574,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) { @@ -1598,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; @@ -1614,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; @@ -1689,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); @@ -2860,16 +2869,21 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition 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 @@ -2880,6 +2894,7 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition } 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); @@ -2892,16 +2907,21 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId); return; } - activeSpellVisuals_.push_back({instanceId, 0.0f}); + // 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, - " model=", modelPath); + " 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 >= SPELL_VISUAL_DURATION) { + if (it->elapsed >= it->duration) { m2Renderer->removeInstance(it->instanceId); it = activeSpellVisuals_.erase(it); } else { @@ -3006,6 +3026,14 @@ void Renderer::resetCombatVisualState() { 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 { @@ -3173,7 +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::RAIN); // thunderstorm — use rain particles + else if (wType == 3) weather->setWeatherType(Weather::Type::STORM); else weather->setWeatherType(Weather::Type::NONE); weather->setIntensity(wInt); } else { @@ -3465,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) { @@ -3800,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(); @@ -3912,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(); @@ -4037,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; @@ -4124,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(); @@ -4151,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); @@ -4330,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. @@ -4372,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{}; @@ -4648,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(); @@ -4729,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]); @@ -5249,7 +5283,8 @@ bool Renderer::initFXAAResources() { 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, &fxaa_.sceneSampler) != VK_SUCCESS) { + fxaa_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo); + if (fxaa_.sceneSampler == VK_NULL_HANDLE) { LOG_ERROR("FXAA: failed to create sampler"); destroyFXAAResources(); return false; @@ -5333,7 +5368,7 @@ bool Renderer::initFXAAResources() { .setLayout(fxaa_.pipelineLayout) .setRenderPass(vkCtx->getImGuiRenderPass()) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device); + .build(device, vkCtx->getPipelineCache()); vertMod.destroy(); fragMod.destroy(); @@ -5359,7 +5394,7 @@ void Renderer::destroyFXAAResources() { 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; } - if (fxaa_.sceneSampler) { vkDestroySampler(device, fxaa_.sceneSampler, nullptr); fxaa_.sceneSampler = 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); 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 f380cc65..50a12d0d 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -179,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; @@ -562,7 +562,17 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { // Pre-load WMO doodads (M2 models inside WMO) if (!workerRunning.load()) return nullptr; - if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { + + // Skip WMO doodads if this placement was already prepared by another tile's worker. + // This prevents 15+ copies of Stormwind's ~6000 doodads from being parsed + // simultaneously, which was the primary cause of OOM during world load. + bool wmoAlreadyPrepared = false; + if (placement.uniqueId != 0) { + std::lock_guard lock(preparedWmoUniqueIdsMutex_); + wmoAlreadyPrepared = !preparedWmoUniqueIds_.insert(placement.uniqueId).second; + } + + if (!wmoAlreadyPrepared && !wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { glm::mat4 wmoMatrix(1.0f); wmoMatrix = glm::translate(wmoMatrix, pos); wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1)); @@ -575,6 +585,7 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { setsToLoad.push_back(placement.doodadSet); } std::unordered_set loadedDoodadIndices; + std::unordered_set wmoPreparedModelIds; // within-WMO model dedup for (uint32_t setIdx : setsToLoad) { const auto& doodadSet = wmoModel.doodadSets[setIdx]; for (uint32_t di = 0; di < doodadSet.count; di++) { @@ -599,15 +610,16 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); - // Skip file I/O if model already uploaded from a previous tile + // Skip file I/O if model already uploaded or already prepared within this WMO bool modelAlreadyUploaded = false; { std::lock_guard lock(uploadedM2IdsMutex_); modelAlreadyUploaded = uploadedM2Ids_.count(doodadModelId) > 0; } + bool modelAlreadyPreparedInWmo = !wmoPreparedModelIds.insert(doodadModelId).second; pipeline::M2Model m2Model; - if (!modelAlreadyUploaded) { + if (!modelAlreadyUploaded && !modelAlreadyPreparedInWmo) { std::vector m2Data = assetManager->readFile(m2Path); if (m2Data.empty()) continue; @@ -1211,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 @@ -1404,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; } @@ -1425,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 @@ -1509,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(); @@ -1561,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. @@ -2292,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 775881d3..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"); @@ -932,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(); diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index 51781a3c..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; } @@ -82,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(); @@ -121,6 +191,46 @@ void VkContext::runDeferredCleanup(uint32_t frameIndex) { 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; @@ -182,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; @@ -215,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()); @@ -229,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"); @@ -267,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}; @@ -367,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; } @@ -854,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); @@ -889,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; @@ -898,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; } @@ -1360,7 +1657,7 @@ 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; } @@ -1401,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]; @@ -1420,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; } @@ -1490,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() { @@ -1500,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; } @@ -1520,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; @@ -1540,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_) { @@ -1560,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) { @@ -1567,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 { @@ -1577,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 6dd0b26f..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, "]"); @@ -1718,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; } @@ -1848,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); @@ -2092,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 c15bad3f..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"); @@ -1679,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(); @@ -2177,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]; }; @@ -2200,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]; }; @@ -2677,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)); @@ -3273,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 @@ -3578,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; } @@ -3678,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), @@ -3692,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), @@ -3706,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), @@ -3720,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 cc278b5f..03da7972 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -165,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(); @@ -363,10 +363,10 @@ 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); } } } @@ -1096,6 +1096,41 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi } } + // 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); diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 777285cf..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(); } @@ -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 96b53dd0..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" @@ -173,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(); @@ -462,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(); @@ -518,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 811ef73e..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,7 +27,6 @@ #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" @@ -48,6 +47,18 @@ #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) { @@ -75,21 +86,6 @@ namespace { // 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). - void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { - bool any = false; - if (g > 0) { - ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g); - any = true; - } - if (s > 0 || g > 0) { - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s); - any = true; - } - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c); - } - // 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. @@ -267,6 +263,7 @@ static std::string evaluateMacroConditionals(const std::string& rawArg, 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) { @@ -495,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; } } @@ -594,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); @@ -731,6 +685,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderBgInvitePopup(gameHandler); renderBfMgrInvitePopup(gameHandler); renderLfgProposalPopup(gameHandler); + renderLfgRoleCheckPopup(gameHandler); renderGuildRoster(gameHandler); renderSocialFrame(gameHandler); renderBuffBar(gameHandler); @@ -1146,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(); @@ -1205,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"); @@ -1276,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; } @@ -1315,7 +1270,12 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { if (i > 0 && i < static_cast(chatTabUnread_.size()) && chatTabUnread_[i] > 0) { tabLabel += " (" + std::to_string(chatTabUnread_[i]) + ")"; } - // Use ImGuiTabItemFlags_NoPushId so label changes don't break tab identity + // 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; @@ -1325,6 +1285,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { } ImGui::EndTabItem(); } + if (hasUnread) ImGui::PopStyleColor(); } ImGui::EndTabBar(); } @@ -1411,15 +1372,7 @@ 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) @@ -1473,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) { @@ -1856,11 +1809,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { 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::TextDisabled("Sell:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + renderCoinsFromCopper(info->sellPrice); } if (ImGui::GetIO().KeyShift && info->inventoryType > 0) { @@ -2579,12 +2529,12 @@ 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 @@ -2620,23 +2570,36 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { for (auto& ch : lowerWord) ch = static_cast(std::tolower(static_cast(ch))); static const std::vector kCmds = { - "/afk", "/away", "/cancelaura", "/cancelform", "/cancelshapeshift", - "/cast", "/chathelp", "/clear", - "/dance", "/do", "/dnd", "/e", "/emote", - "/cl", "/combatlog", "/dismount", "/equip", "/follow", - "/g", "/guild", "/guildinfo", - "/gmticket", "/grouploot", "/i", "/instance", - "/invite", "/j", "/join", "/kick", - "/l", "/leave", "/local", "/me", + "/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", - "/r", "/raid", - "/raidwarning", "/random", "/reply", "/roll", - "/s", "/say", "/setloot", "/shout", "/sit", "/stand", - "/startattack", "/stopattack", "/stopfollow", "/stopcasting", - "/t", "/target", "/time", - "/trade", "/uninvite", "/use", "/w", "/whisper", - "/who", "/wts", "/wtb", "/y", "/yell", "/zone" + "/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 @@ -2665,6 +2628,107 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { 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; } @@ -2778,6 +2842,18 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { 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_) { @@ -3250,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 @@ -3285,7 +3361,7 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { // Derive class color via shared helper ImVec4 classColor = activeChar ? classColorVec4(static_cast(activeChar->characterClass)) - : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + : kColorBrightGreen; // Name in class color — clickable for self-target, right-click for menu ImGui::PushStyleColor(ImGuiCol_Text, classColor); @@ -3383,7 +3459,7 @@ void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) { float pct = static_cast(playerHp) / static_cast(playerMaxHp); ImVec4 hpColor; if (isDead) { - hpColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + hpColor = kColorDarkGray; } else if (pct > 0.5f) { hpColor = ImVec4(0.2f, 0.8f, 0.2f, 1.0f); // green } else if (pct > 0.2f) { @@ -3980,16 +4056,16 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { ImGui::Text("%s", nm.c_str()); } ImGui::TextColored(autocastOn - ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) - : ImVec4(0.6f, 0.6f, 0.6f, 1.0f), + ? kColorGreen + : kColorGray, "Autocast: %s (right-click to toggle)", autocastOn ? "On" : "Off"); if (petOnCd) { if (petCd >= 60.0f) - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + ImGui::TextColored(kColorRed, "Cooldown: %d min %d sec", static_cast(petCd) / 60, static_cast(petCd) % 60); else - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", petCd); } ImGui::EndTooltip(); @@ -4108,12 +4184,18 @@ 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(); @@ -4123,7 +4205,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { } else { 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 + 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) { @@ -4131,11 +4213,12 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { } else if (diff >= -2) { hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even } else { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy + hostileColor = kColorBrightGreen; // Green - easy } } + } // end tapped else } else { - hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly + hostileColor = kColorBrightGreen; // Friendly } } @@ -4248,7 +4331,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) ImGui::SetTooltip("Has a quest available"); } else if (qgs == QGS::AVAILABLE_LOW) { ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "!"); + 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); @@ -4256,7 +4339,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest ready to turn in"); } else if (qgs == QGS::INCOMPLETE) { ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "?"); + ImGui::TextColored(kColorGray, "?"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest incomplete"); } } @@ -4352,7 +4435,7 @@ 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 ??"); @@ -4371,7 +4454,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) ImGui::SetTooltip("Rare Elite — uncommon spawn, group recommended"); } else if (rank == 3) { ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "[Boss]"); + ImGui::TextColored(kColorRed, "[Boss]"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Boss — raid / dungeon boss"); } else if (rank == 4) { ImGui::SameLine(0, 4); @@ -4543,7 +4626,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (totGuid == gameHandler.getPlayerGuid()) { auto playerEnt = gameHandler.getEntityManager().getEntity(totGuid); totName = playerEnt ? getEntityName(playerEnt) : "You"; - totColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + totColor = kColorBrightGreen; } else if (totEnt) { totName = getEntityName(totEnt); uint8_t cid = entityClassId(totEnt.get()); @@ -4775,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(); } @@ -5007,7 +5090,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { 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(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db); + ImGui::TextColored(ui::colors::kLightGray, "%s", db); } ImGui::EndTooltip(); } @@ -5052,12 +5135,18 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { if (focus->getType() == game::ObjectType::PLAYER) { // Use class color for player focus targets uint8_t cid = entityClassId(focus.get()); - focusColor = (cid != 0) ? classColorVec4(cid) : ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + 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(); if (mobLv == 0) { @@ -5065,7 +5154,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { } else { 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); + focusColor = kColorGray; else if (diff >= 10) focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); else if (diff >= 5) @@ -5073,10 +5162,11 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { 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); + focusColor = kColorBrightGreen; } + } // end tapped else } else { - focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + focusColor = kColorBrightGreen; } } @@ -5177,13 +5267,13 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "!"); } else if (qgs == QGS::AVAILABLE_LOW) { ImGui::SameLine(0, 4); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "!"); + 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(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "?"); + ImGui::TextColored(kColorGray, "?"); } } @@ -5471,7 +5561,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { 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(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db); + ImGui::TextColored(ui::colors::kLightGray, "%s", db); } ImGui::EndTooltip(); } @@ -5501,7 +5591,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { ImVec4 fofColor(0.7f, 0.7f, 0.7f, 1.0f); if (fofGuid == gameHandler.getPlayerGuid()) { fofName = "You"; - fofColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + fofColor = kColorBrightGreen; } else if (fofEnt) { fofName = getEntityName(fofEnt); uint8_t fcid = entityClassId(fofEnt.get()); @@ -5668,12 +5758,17 @@ static std::string evaluateMacroConditionals(const std::string& rawArg, 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, @mouseover, @target + // @target specifiers: @player, @focus, @pet, @mouseover, @target if (!c.empty() && c[0] == '@') { std::string spec = c.substr(1); - if (spec == "player") tgt = gameHandler.getPlayerGuid(); + 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; @@ -5684,9 +5779,14 @@ static std::string evaluateMacroConditionals(const std::string& rawArg, // target=X specifiers if (c.rfind("target=", 0) == 0) { std::string spec = c.substr(7); - if (spec == "player") tgt = gameHandler.getPlayerGuid(); + 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; @@ -5742,6 +5842,61 @@ static std::string evaluateMacroConditionals(const std::string& rawArg, 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()) @@ -5929,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(); @@ -5942,6 +6148,30 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { 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. @@ -6091,25 +6321,53 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { 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 /cast /duel /pvp", - " /forfeit /follow /stopfollow /assist", - "Items: /use /equip /equipset [name]", - "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 /zone /loc /afk [msg] /dnd [msg] /inspect", - " /helm /cloak /trade /join /leave ", - " /score /unstuck /logout /ticket /screenshot /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; @@ -6963,6 +7221,12 @@ 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") { @@ -8012,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: @@ -8020,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: @@ -8038,11 +8302,11 @@ 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: @@ -8067,12 +8331,12 @@ ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const { case game::ChatType::BG_SYSTEM_ALLIANCE: return ImVec4(0.3f, 0.6f, 1.0f, 1.0f); // Blue case game::ChatType::BG_SYSTEM_HORDE: - return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red + 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 } } @@ -8419,6 +8683,19 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) { 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; @@ -8556,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. @@ -8602,37 +8954,63 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { const auto& slot = bar[absSlot]; bool onCooldown = !slot.isReady(); - const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty(); - // Out-of-range check: red tint when a targeted spell cannot reach the current target. - // Only applies to SPELL slots with a known max range (>5 yd) and an active target. - // Item range is checked below after barItemDef is populated. - bool outOfRange = false; - if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 - && !onCooldown && gameHandler.hasTarget()) { - uint32_t maxRange = spellbookScreen.getSpellMaxRange(slot.id, assetMgr); - if (maxRange > 5) { // >5 yd = not melee/self - 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(); - float dist = std::sqrt(dx * dx + dy * dy + dz * dz); - if (dist > static_cast(maxRange)) - outOfRange = true; + // 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; } } } - // Insufficient-power check: orange tint when player doesn't have enough power to cast. - // Only applies to SPELL slots with a known power cost and when not already on cooldown. + 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; - if (!slot.isEmpty() && slot.type == game::ActionBarSlot::SPELL && slot.id != 0 - && !onCooldown) { + { + 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; - spellbookScreen.getSpellPowerInfo(slot.id, assetMgr, spellCost, spellPowerType); + 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 || @@ -8693,17 +9071,22 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } // 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 line + // 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))); - if (cl.rfind("/cast ", 0) != 0 && cl != "/cast") continue; + 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); @@ -8712,9 +9095,18 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { size_t ce = showArg.find(']'); if (ce != std::string::npos) showArg = showArg.substr(ce + 1); } - // Take first alternative before ';' - size_t semi = showArg.find(';'); - if (semi != std::string::npos) showArg = showArg.substr(0, semi); + // 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); @@ -8741,6 +9133,18 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { 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; + } + } + } } } } @@ -8819,8 +9223,14 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } // Error-flash overlay: red fade on spell cast failure (~0.5 s). - if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) { - auto flashIt = actionFlashEndTimes_.find(slot.id); + // 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; @@ -8957,21 +9367,67 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { 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(); - 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)"); + // 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) { @@ -8990,10 +9446,10 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { 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(); } @@ -9008,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; @@ -9026,10 +9485,10 @@ 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 if (cd >= 5.0f) 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; @@ -9097,7 +9556,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } } if (totalCount > 0) { - char countStr[8]; + char countStr[16]; snprintf(countStr, sizeof(countStr), "%d", totalCount); ImVec2 btnMax = ImGui::GetItemRectMax(); ImVec2 tsz = ImGui::CalcTextSize(countStr); @@ -9199,6 +9658,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { 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(); @@ -10152,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; @@ -10259,14 +10719,14 @@ void GameScreen::renderCooldownTracker(game::GameHandler& gameHandler) { // Name (truncated) + remaining time char timeStr[16]; if (cd.remaining >= 60.0f) - snprintf(timeStr, sizeof(timeStr), "%dm%ds", (int)cd.remaining / 60, (int)cd.remaining % 60); + 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 ? ImVec4(1.0f, 0.3f, 0.3f, 1.0f) : + ImVec4 cdColor = cd.remaining > 30.0f ? kColorRed : cd.remaining > 10.0f ? ImVec4(1.0f, 0.6f, 0.2f, 1.0f) : - cd.remaining > 5.0f ? ImVec4(1.0f, 1.0f, 0.3f, 1.0f) : + cd.remaining > 5.0f ? kColorYellow : ImVec4(0.5f, 1.0f, 0.5f, 1.0f); // Truncate name to fit @@ -10404,7 +10864,7 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { // 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 ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) + ImVec4 objColor = objDone ? kColorGreen : ImVec4(0.75f, 0.75f, 0.75f, 1.0f); std::string name = gameHandler.getCachedCreatureName(entry); if (name.empty()) { @@ -10426,7 +10886,7 @@ void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) { auto reqIt = q.requiredItemCounts.find(itemId); if (reqIt != q.requiredItemCounts.end()) required = reqIt->second; bool objDone = (count >= required); - ImVec4 objColor = objDone ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) + 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; @@ -11202,8 +11662,16 @@ 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); @@ -12177,7 +12645,7 @@ void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) { snprintf(hpText, sizeof(hpText), "OOR"); } else if (maxHp >= 10000) { snprintf(hpText, sizeof(hpText), "%dk/%dk", - (int)hp / 1000, (int)maxHp / 1000); + static_cast(hp) / 1000, static_cast(maxHp) / 1000); } else { snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp); } @@ -12197,7 +12665,7 @@ 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), ""); @@ -12592,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); @@ -12611,7 +13079,7 @@ 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); } } @@ -12654,26 +13122,26 @@ void GameScreen::renderQuestCompleteToasts(float deltaTime) { ImVec2 br(toastX + toastW, toastY + toastH); // Background + gold border (quest completion) - draw->AddRectFilled(tl, br, IM_COL32(20, 18, 8, (int)(alpha * 210)), 5.0f); - draw->AddRect(tl, br, IM_COL32(220, 180, 30, (int)(alpha * 230)), 5.0f, 0, 1.5f); + 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, (int)(alpha * 230))); - draw->AddCircle (ImVec2(iconCx, iconCy), 7.0f, IM_COL32(255, 220, 50, (int)(alpha * 200))); + 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, (int)(alpha * 240)), header); + 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, (int)(alpha * 220)), titleStr); + IM_COL32(220, 215, 195, static_cast(alpha * 220)), titleStr); } } @@ -12722,16 +13190,16 @@ void GameScreen::renderZoneToasts(float deltaTime) { ImVec2 tl(toastX, toastY); ImVec2 br(toastX + toastW, toastY + toastH); - draw->AddRectFilled(tl, br, IM_COL32(10, 10, 16, (int)(alpha * 200)), 6.0f); - draw->AddRect(tl, br, IM_COL32(160, 140, 80, (int)(alpha * 220)), 6.0f, 0, 1.2f); + 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, (int)(alpha * 200)), header); + 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, (int)(alpha * 240)), e.zoneName.c_str()); + IM_COL32(255, 230, 140, static_cast(alpha * 240)), e.zoneName.c_str()); } } @@ -12786,18 +13254,18 @@ void GameScreen::renderAreaTriggerToasts(float deltaTime, game::GameHandler& gam ImVec2 tl(toastX, toastY); ImVec2 br(toastX + toastW, toastY + toastH); - draw->AddRectFilled(tl, br, IM_COL32(8, 12, 22, (int)(alpha * 190)), 5.0f); - draw->AddRect(tl, br, IM_COL32(100, 160, 220, (int)(alpha * 200)), 5.0f, 0, 1.0f); + 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, (int)(alpha * 180)), t.text.c_str()); + 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, (int)(alpha * 240)), t.text.c_str()); + IM_COL32(180, 220, 255, static_cast(alpha * 240)), t.text.c_str()); } } @@ -13063,7 +13531,7 @@ void GameScreen::renderBossFrames(game::GameHandler& gameHandler) { 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(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", db); + ImGui::TextColored(ui::colors::kLightGray, "%s", db); } ImGui::EndTooltip(); } @@ -13099,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(); @@ -13123,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(); @@ -13224,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(); @@ -13254,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) { @@ -13282,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(); @@ -13315,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; @@ -13472,20 +13940,10 @@ 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) - ImVec4(0.90f, 0.80f, 0.50f, 1.0f),// 6=artifact (light gold) - ImVec4(0.90f, 0.80f, 0.50f, 1.0f),// 7=heirloom (light gold) - }; uint8_t q = roll.itemQuality; - ImVec4 col = (q < 8) ? kQualityColors[q] : kQualityColors[1]; + ImVec4 col = ui::getQualityColor(static_cast(q)); // Countdown bar { @@ -13531,7 +13989,7 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { ? rollInfo->name.c_str() : roll.itemName.c_str(); if (rollInfo && rollInfo->valid) - col = (rollInfo->quality < 8) ? kQualityColors[rollInfo->quality] : kQualityColors[1]; + col = ui::getQualityColor(static_cast(rollInfo->quality)); ImGui::TextColored(col, "[%s]", displayName); if (ImGui::IsItemHovered() && rollInfo && rollInfo->valid) { inventoryScreen.renderItemTooltip(*rollInfo); @@ -13584,7 +14042,7 @@ void GameScreen::renderLootRollPopup(game::GameHandler& gameHandler) { 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 - ImVec4(0.5f, 0.5f, 0.5f, 1.0f), // Pass — gray + kColorDarkGray, // Pass — gray }; auto rollTypeIndex = [](uint8_t t) -> int { if (t == 0) return 0; @@ -13628,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()); @@ -13655,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!"); @@ -13856,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(); @@ -13885,6 +14343,71 @@ 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 (!chatInputActive && !ImGui::GetIO().WantTextInput && @@ -13918,11 +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::TextDisabled("Cost:"); ImGui::SameLine(0, 4); - renderCoinsText(gold, silver, copper); + renderCoinsFromCopper(cost); ImGui::Spacing(); ImGui::Text("Guild Name:"); ImGui::InputText("##petitionname", petitionNameBuffer_, sizeof(petitionNameBuffer_)); @@ -14036,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(); @@ -14064,7 +14584,7 @@ void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { 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(); @@ -14243,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; @@ -14296,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()); @@ -14612,7 +15132,7 @@ void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) { const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str(); ImVec4 nameCol = c.isOnline() ? classColorVec4(static_cast(c.classId)) - : ImVec4(0.5f, 0.5f, 0.5f, 1.0f); + : kColorDarkGray; ImGui::TextColored(nameCol, "%s", displayName); if (c.isOnline() && c.level > 0) { @@ -15128,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(); } @@ -15527,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)); @@ -15536,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 = "?"; @@ -15545,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 = "!"; @@ -15553,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; } @@ -15684,16 +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; ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4); - renderCoinsText(gold, silver, copper); + 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); } @@ -15799,11 +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::TextDisabled("Required money:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + renderCoinsFromCopper(quest.requiredMoney); } // Complete / Cancel buttons @@ -15978,11 +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; ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + renderCoinsFromCopper(quest.rewardMoney); } } @@ -16020,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) // ============================================================ @@ -16039,11 +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::TextDisabled("Your money:"); ImGui::SameLine(0, 4); - renderCoinsText(mg, ms, mc); + renderCoinsFromCopper(money); if (vendor.canRepair) { ImGui::SameLine(); @@ -16081,7 +16644,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { } 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(); @@ -16171,7 +16734,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (canAfford) { renderCoinsText(g, s, c); } else { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c); + ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c); } ImGui::TableSetColumnIndex(3); if (!canAfford) ImGui::BeginDisabled(); @@ -16272,8 +16835,9 @@ 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; @@ -16282,7 +16846,14 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { if (canAfford) { renderCoinsText(g, s, c); } else { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c); + 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()); + } } } @@ -16290,7 +16861,7 @@ void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) { 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 { @@ -16404,11 +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::TextDisabled("Your money:"); ImGui::SameLine(0, 4); - renderCoinsText(mg, ms, mc); + renderCoinsFromCopper(money); // Filter controls static bool showUnavailable = false; @@ -16527,8 +17095,8 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); if (!name.empty()) { - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "%s", name.c_str()); - if (!rank.empty()) ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s", rank.c_str()); + ImGui::TextColored(kColorYellow, "%s", name.c_str()); + if (!rank.empty()) ImGui::TextColored(kColorGray, "%s", rank.c_str()); } const std::string& spDesc = gameHandler.getSpellDescription(spell->spellId); if (!spDesc.empty()) { @@ -16540,7 +17108,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { } 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); @@ -16548,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 @@ -16574,7 +17142,7 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { if (canAfford) { renderCoinsText(g, s, c); } else { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%ug %us %uc", g, s, c); + ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c); } } else { ImGui::TextColored(color, "Free"); @@ -16596,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, @@ -16793,6 +17361,12 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) { 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(); } } @@ -17002,7 +17576,7 @@ void GameScreen::renderStableWindow(game::GameHandler& gameHandler) { bool open = true; if (!ImGui::Begin("Pet Stable", &open, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + kDialogFlags)) { ImGui::End(); if (!open) { // User closed the window; clear stable state @@ -17184,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(); @@ -17629,6 +18203,594 @@ void GameScreen::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) { // 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; @@ -18023,114 +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)"); - - 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(); + renderSettingsInterfaceTab(); ImGui::EndTabItem(); } @@ -18138,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(); } @@ -18286,167 +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"); - 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 = kDefaultMouseSensitivity; - pendingInvertMouse = kDefaultInvertMouse; - 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(); - } - + renderSettingsGameplayTab(); ImGui::EndTabItem(); } @@ -18454,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(); } @@ -18556,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(); } @@ -18611,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(); } @@ -20521,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(); @@ -20819,12 +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::TextDisabled("Your money:"); ImGui::SameLine(0, 4); - renderCoinsText(mg, ms, mc); + renderCoinsFromCopper(gameHandler.getMoneyCopper()); ImGui::SameLine(ImGui::GetWindowWidth() - 100); if (ImGui::Button("Compose")) { mailRecipientBuffer_[0] = '\0'; @@ -20870,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]"); @@ -20930,10 +21520,10 @@ void GameScreen::renderMailWindow(game::GameHandler& gameHandler) { const char* mname = kMon[tmExp->tm_mon]; int daysLeft = static_cast(secsLeft / 86400.0f); if (secsLeft <= 0.0f) { - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.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(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), + ImGui::TextColored(kColorRed, "Expires: %s %d, %d (%d day%s!)", mname, tmExp->tm_mday, 1900 + tmExp->tm_year, daysLeft, daysLeft == 1 ? "" : "s"); @@ -20953,11 +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::TextDisabled("Money:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + renderCoinsFromCopper(mail.money); ImGui::SameLine(); if (ImGui::SmallButton("Take Money")) { gameHandler.mailTakeMoney(mail.messageId); @@ -20969,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); } @@ -21131,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) @@ -21163,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 { @@ -21202,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); @@ -21690,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 @@ -21736,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) { @@ -21795,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); @@ -21837,13 +22433,12 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(3); { uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid; - renderCoinsText(bid / 10000, (bid / 100) % 100, bid % 100); + renderCoinsFromCopper(bid); } ImGui::TableSetColumnIndex(4); if (auction.buyoutPrice > 0) { - renderCoinsText(auction.buyoutPrice / 10000, - (auction.buyoutPrice / 100) % 100, auction.buyoutPrice % 100); + renderCoinsFromCopper(auction.buyoutPrice); } else { ImGui::TextDisabled("--"); } @@ -21988,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); @@ -22000,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) @@ -22017,10 +22626,10 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(1); ImGui::Text("%u", a.stackCount); ImGui::TableSetColumnIndex(2); - renderCoinsText(a.currentBid / 10000, (a.currentBid / 100) % 100, a.currentBid % 100); + renderCoinsFromCopper(a.currentBid); ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) - renderCoinsText(a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + renderCoinsFromCopper(a.buyoutPrice); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); @@ -22063,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(); @@ -22075,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); @@ -22093,11 +22712,11 @@ void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) { ImGui::TableSetColumnIndex(2); { uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid; - renderCoinsText(bid / 10000, (bid / 100) % 100, bid % 100); + renderCoinsFromCopper(bid); } ImGui::TableSetColumnIndex(3); if (a.buyoutPrice > 0) - renderCoinsText(a.buyoutPrice / 10000, (a.buyoutPrice / 100) % 100, a.buyoutPrice % 100); + renderCoinsFromCopper(a.buyoutPrice); else ImGui::TextDisabled("--"); ImGui::TableSetColumnIndex(4); @@ -22173,10 +22792,10 @@ 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 || @@ -22196,7 +22815,7 @@ void GameScreen::renderDingEffect() { if (dingManaDelta_ > 0) written += snprintf(statBuf + written, sizeof(statBuf) - written, "+%u Mana ", dingManaDelta_); - for (int i = 0; i < 5 && written < (int)sizeof(statBuf) - 1; ++i) { + 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]); @@ -22209,9 +22828,9 @@ void GameScreen::renderDingEffect() { 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, (int)(alpha * 160)), statBuf); + IM_COL32(0, 0, 0, static_cast(alpha * 160)), statBuf); draw->AddText(font, smallSize, ImVec2(stx, yOff), - IM_COL32(100, 220, 100, (int)(alpha * 230)), statBuf); + IM_COL32(100, 220, 100, static_cast(alpha * 230)), statBuf); } } } @@ -22262,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(); @@ -22273,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]; @@ -22289,7 +22908,7 @@ 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); } // --------------------------------------------------------------------------- @@ -22346,22 +22965,22 @@ void GameScreen::renderDiscoveryToast() { // "Discovered!" 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); // Area name in white draw->AddText(font, nameSize, ImVec2(nameX + 1, nameY + 1), - IM_COL32(0, 0, 0, (int)(alpha * 160)), discoveryToastName_.c_str()); + IM_COL32(0, 0, 0, static_cast(alpha * 160)), discoveryToastName_.c_str()); draw->AddText(font, nameSize, ImVec2(nameX, nameY), - IM_COL32(255, 255, 255, (int)(alpha * 255)), discoveryToastName_.c_str()); + 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, (int)(alpha * 140)), xpBuf); + IM_COL32(0, 0, 0, static_cast(alpha * 140)), xpBuf); draw->AddText(font, xpSize, ImVec2(xpX, xpY), - IM_COL32(100, 220, 100, (int)(alpha * 230)), xpBuf); + IM_COL32(100, 220, 100, static_cast(alpha * 230)), xpBuf); } } @@ -22908,15 +23527,15 @@ void GameScreen::renderZoneText(game::GameHandler& gameHandler) { // "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()); } // --------------------------------------------------------------------------- @@ -23081,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..."); @@ -23114,7 +23733,7 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { 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: { std::string dName = gameHandler.getCurrentLfgDungeonName(); @@ -23161,7 +23780,7 @@ 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()) { @@ -23255,14 +23874,14 @@ void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) { // 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)) { uint8_t lastCat = 255; - 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].cat != lastCat && kCatHeaders[kDungeons[i].cat]) { if (lastCat != 255) ImGui::Separator(); ImGui::TextDisabled("%s", kCatHeaders[kDungeons[i].cat]); @@ -23324,7 +23943,7 @@ 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 { auto difficultyLabel = [](uint32_t diff) -> const char* { switch (diff) { @@ -23381,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 @@ -23441,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; @@ -23746,14 +24367,14 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { 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 = ImVec4(0.4f, 1.0f, 0.4f, 1.0f); + 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 = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + color = kColorBrightGreen; break; case T::PERIODIC_HEAL: if (spell) @@ -23958,8 +24579,8 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { : ImVec4(1.0f, 0.15f, 0.15f, 1.0f); break; default: - snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", (int)e.type, e.amount); - color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", static_cast(e.type), e.amount); + color = ui::colors::kLightGray; break; } @@ -24017,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."); @@ -24081,7 +24702,7 @@ 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; }; @@ -24214,7 +24835,7 @@ void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { // Show existing open ticket if any if (gameHandler.hasActiveGmTicket()) { - ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "You have an open GM ticket."); + 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()); @@ -24340,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]; @@ -24675,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(); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 366e9fa0..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" @@ -74,20 +75,6 @@ const game::ItemSlot* findComparableEquipped(const game::Inventory& inventory, u } } -void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { - bool any = false; - if (g > 0) { - ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g); - any = true; - } - if (s > 0 || g > 0) { - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s); - any = true; - } - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c); -} } // namespace InventoryScreen::~InventoryScreen() { @@ -96,17 +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 - case game::ItemQuality::ARTIFACT: return ImVec4(0.90f, 0.80f, 0.50f, 1.0f); // Light gold - case game::ItemQuality::HEIRLOOM: return ImVec4(0.90f, 0.80f, 0.50f, 1.0f); // Light gold - default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - } + return ui::getQualityColor(quality); } // ============================================================ @@ -710,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; - } } } @@ -941,6 +914,14 @@ void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t m 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(); @@ -979,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; @@ -1068,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(); @@ -1128,12 +1115,30 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, ImGui::Spacing(); ImGui::Separator(); - // Sort Bags button — client-side reorder by quality/type - if (ImGui::SmallButton("Sort Bags")) { - inventory.sortBags(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Sort all bag slots by quality (highest first),\nthen by item ID, then by stack size."); + // 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) { @@ -1249,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(); } @@ -1422,32 +1443,54 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { ImGui::EndTabItem(); } - // Equipment Sets tab (WotLK only) - const auto& eqSets = gameHandler.getEquipmentSets(); - if (!eqSets.empty()) { - if (ImGui::BeginTabItem("Outfits")) { - ImGui::Spacing(); - ImGui::TextDisabled("Saved Equipment Sets"); - ImGui::Separator(); + // 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)); - // Icon placeholder or name const char* displayName = es.name.empty() ? "(Unnamed)" : es.name.c_str(); ImGui::Text("%s", displayName); - if (!es.iconName.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled("(%s)", es.iconName.c_str()); - } - ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60.0f); + 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::EndTabItem(); } ImGui::EndTabBar(); @@ -1529,9 +1572,9 @@ void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) { ImGui::TextColored(tier.color, "[%s]", tier.name); ImGui::SameLine(90.0f); if (atWar) { - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", displayName); + ImGui::TextColored(ui::colors::kRed, "%s", displayName); ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "(At War)"); + 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(); @@ -2222,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(); } @@ -2370,12 +2413,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite // 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) { @@ -2389,11 +2432,11 @@ 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, + "' inventoryType=", static_cast(item.inventoryType), " itemId=", item.itemId, " startQuestId=", item.startQuestId); if (item.startQuestId != 0) { @@ -2413,7 +2456,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite } } else if (kind == SlotKind::BACKPACK && isBagSlot) { LOG_INFO("Right-click bag item: name='", item.name, - "' inventoryType=", (int)item.inventoryType, + "' inventoryType=", static_cast(item.inventoryType), " bagIndex=", bagIndex, " slotIndex=", bagSlotIndex, " startQuestId=", item.startQuestId); if (item.startQuestId != 0) { @@ -2546,7 +2589,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I } 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"); } @@ -2584,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."); } } } @@ -2614,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 @@ -3017,11 +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::TextDisabled("Sell:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + renderCoinsFromCopper(item.sellPrice); } // Shift-hover comparison with currently equipped equivalent. @@ -3047,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); } @@ -3062,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); } }; @@ -3242,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."); } } @@ -3259,7 +3324,7 @@ 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); @@ -3666,11 +3731,8 @@ 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::TextDisabled("Sell:"); ImGui::SameLine(0, 4); - renderCoinsText(g, s, c); + renderCoinsFromCopper(info.sellPrice); } // Shift-hover: compare with currently equipped item diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index fe5cd2cb..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" @@ -215,20 +216,6 @@ std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) { return s; } -void renderCoinsText(uint32_t g, uint32_t s, uint32_t c) { - bool any = false; - if (g > 0) { - ImGui::TextColored(ImVec4(1.00f, 0.82f, 0.00f, 1.0f), "%ug", g); - any = true; - } - if (s > 0 || g > 0) { - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.80f, 0.80f, 0.80f, 1.0f), "%us", s); - any = true; - } - if (any) ImGui::SameLine(0, 3); - ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.20f, 1.0f), "%uc", c); -} } // anonymous namespace void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& invScreen) { 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 3d2ceeed..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" @@ -585,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 @@ -597,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 5f87712f..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" @@ -141,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(); @@ -227,8 +228,8 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab // 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, (int)talent->row); - maxCol = std::max(maxCol, (int)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); @@ -239,8 +240,8 @@ void TalentScreen::renderTalentTree(game::GameHandler& gameHandler, uint32_t tab const float iconSize = 40.0f; const float spacing = 8.0f; const float cellSize = iconSize + spacing; - const float gridWidth = (float)(maxCol + 1) * cellSize + spacing; - const float gridHeight = (float)(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; @@ -552,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()); } } @@ -581,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); } @@ -590,7 +591,7 @@ 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(); @@ -748,7 +749,7 @@ void TalentScreen::renderGlyphs(game::GameHandler& gameHandler) { if (!name.empty()) { ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", name.c_str()); } else { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "Glyph #%u", (uint32_t)glyphId); + ImGui::TextColored(ui::colors::kLightGray, "Glyph #%u", static_cast(glyphId)); } };