diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 158c4274..d67bd0b2 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1300,7 +1300,7 @@ public: // Barber shop bool isBarberShopOpen() const { return barberShopOpen_; } - void closeBarberShop() { barberShopOpen_ = false; } + void closeBarberShop() { barberShopOpen_ = false; if (addonEventCallback_) addonEventCallback_("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) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 12e7f8fb..6148ddd0 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -268,7 +268,17 @@ static int lua_UnitExists(lua_State* L) { static int lua_UnitIsDead(lua_State* L) { const char* uid = luaL_optstring(L, 1, "player"); auto* unit = resolveUnit(L, uid); - lua_pushboolean(L, unit && unit->getHealth() == 0); + 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); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + 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; } @@ -413,6 +423,324 @@ static int lua_UnitPlayerControlled(lua_State* L) { 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) { lua_pushboolean(L, 0); return 1; } + 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) { lua_pushboolean(L, 0); return 1; } + 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) { lua_pushboolean(L, 0); return 1; } + 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) { lua_pushnumber(L, 0); return 1; } + const char* uid = luaL_optstring(L, 1, "player"); + const char* mobUid = luaL_optstring(L, 2, nullptr); + std::string uidStr(uid); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + uint64_t playerUnitGuid = resolveUnitGuid(gh, uidStr); + if (playerUnitGuid == 0) { lua_pushnumber(L, 0); return 1; } + // If no mob specified, check general combat threat against current target + uint64_t mobGuid = 0; + if (mobUid && *mobUid) { + std::string mStr(mobUid); + for (char& c : mStr) c = static_cast(std::tolower(static_cast(c))); + 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); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + uint64_t unitGuid = resolveUnitGuid(gh, uidStr); + bool isTanking = false; + int status = 0; + if (unitGuid != 0 && mobUid && *mobUid) { + std::string mStr(mobUid); + for (char& c : mStr) c = static_cast(std::tolower(static_cast(c))); + 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); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + 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) { lua_pushboolean(L, 0); return 1; } + const char* uid = luaL_checkstring(L, 1); + int distIdx = static_cast(luaL_optnumber(L, 2, 4)); + std::string uidStr(uid); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushboolean(L, 0); return 1; } + auto targetEnt = gh->getEntityManager().getEntity(guid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { lua_pushboolean(L, 0); return 1; } + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + 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) { lua_pushnil(L); return 1; } + 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); + for (char& c : nameLow) c = static_cast(std::tolower(static_cast(c))); + for (uint32_t sid : gh->getKnownSpells()) { + std::string sn = gh->getSpellName(sid); + for (char& c : sn) c = static_cast(std::tolower(static_cast(c))); + if (sn == nameLow) { spellId = sid; break; } + } + } + if (spellId == 0) { lua_pushnil(L); return 1; } + + // Get spell max range from DBC + auto data = gh->getSpellData(spellId); + if (data.maxRange <= 0.0f) { lua_pushnil(L); return 1; } + + // Resolve target position + std::string uidStr(uid); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushnil(L); return 1; } + auto targetEnt = gh->getEntityManager().getEntity(guid); + auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid()); + if (!targetEnt || !playerEnt) { lua_pushnil(L); return 1; } + + float dx = playerEnt->getX() - targetEnt->getX(); + float dy = playerEnt->getY() - targetEnt->getY(); + float dz = playerEnt->getZ() - targetEnt->getZ(); + float dist = std::sqrt(dx*dx + dy*dy + dz*dz); + lua_pushnumber(L, dist <= data.maxRange ? 1 : 0); + return 1; +} + +// 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); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + 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) { lua_pushboolean(L, 0); return 1; } + const char* uid1 = luaL_checkstring(L, 1); + const char* uid2 = luaL_checkstring(L, 2); + std::string u1(uid1), u2(uid2); + for (char& c : u1) c = static_cast(std::tolower(static_cast(c))); + for (char& c : u2) c = static_cast(std::tolower(static_cast(c))); + uint64_t g1 = resolveUnitGuid(gh, u1); + uint64_t g2 = resolveUnitGuid(gh, u2); + if (g1 == 0 || g2 == 0 || g1 == g2) { lua_pushboolean(L, 0); return 1; } + // 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) { lua_pushboolean(L, 0); return 1; } + (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) { lua_pushboolean(L, 0); return 1; } + 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) { lua_pushnil(L); return 1; } + const char* uid = luaL_optstring(L, 1, "target"); + std::string uidStr(uid); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + uint64_t guid = resolveUnitGuid(gh, uidStr); + if (guid == 0) { lua_pushnil(L); return 1; } + auto entity = gh->getEntityManager().getEntity(guid); + if (!entity || entity->getType() == game::ObjectType::PLAYER) { lua_pushnil(L); return 1; } + auto unit = std::dynamic_pointer_cast(entity); + if (!unit) { lua_pushnil(L); return 1; } + uint32_t family = gh->getCreatureFamily(unit->getEntry()); + if (family == 0) { lua_pushnil(L); return 1; } + 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) { lua_pushboolean(L, 0); return 1; } + std::string uidStr(uid); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + 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"); @@ -502,10 +830,22 @@ static int lua_UnitRace(lua_State* L) { 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) { - lua_pushnumber(L, unit->getPowerType()); - static const char* kPowerNames[] = {"MANA","RAGE","FOCUS","ENERGY","HAPPINESS","","RUNIC_POWER"}; 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); + for (char& c : uidStr) c = static_cast(std::tolower(static_cast(c))); + 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; } @@ -1011,6 +1351,27 @@ static int lua_SetRaidTarget(lua_State* L) { 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) { @@ -3077,6 +3438,17 @@ void LuaEngine::registerCoreAPI() { {"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}, @@ -3090,6 +3462,10 @@ void LuaEngine::registerCoreAPI() { {"CastSpellByName", lua_CastSpellByName}, {"IsSpellKnown", lua_IsSpellKnown}, {"GetSpellCooldown", lua_GetSpellCooldown}, + {"GetSpellPowerCost", lua_GetSpellPowerCost}, + {"IsSpellInRange", lua_IsSpellInRange}, + {"UnitDistanceSquared", lua_UnitDistanceSquared}, + {"CheckInteractDistance", lua_CheckInteractDistance}, {"HasTarget", lua_HasTarget}, {"TargetUnit", lua_TargetUnit}, {"ClearTarget", lua_ClearTarget}, @@ -3542,13 +3918,22 @@ void LuaEngine::registerCoreAPI() { "function StaticPopup_Show() end\n" "function StaticPopup_Hide() end\n" // CreateTexture/CreateFontString are now C frame methods in the metatable - "RAID_CLASS_COLORS = {\n" - " WARRIOR={r=0.78,g=0.61,b=0.43}, PALADIN={r=0.96,g=0.55,b=0.73},\n" - " HUNTER={r=0.67,g=0.83,b=0.45}, ROGUE={r=1.0,g=0.96,b=0.41},\n" - " PRIEST={r=1.0,g=1.0,b=1.0}, DEATHKNIGHT={r=0.77,g=0.12,b=0.23},\n" - " SHAMAN={r=0.0,g=0.44,b=0.87}, MAGE={r=0.41,g=0.80,b=0.94},\n" - " WARLOCK={r=0.58,g=0.51,b=0.79}, DRUID={r=1.0,g=0.49,b=0.04},\n" - "}\n" + "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" // Money formatting utility "function GetCoinTextureString(copper)\n" " if not copper or copper == 0 then return '0c' end\n" @@ -3663,6 +4048,21 @@ void LuaEngine::registerCoreAPI() { "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" ); diff --git a/src/core/application.cpp b/src/core/application.cpp index 63b2c2f9..c5e7dccb 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3933,7 +3933,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 @@ -6464,7 +6464,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x }; uint16_t geosetGloves = pickGeoset(401, 4); // Bare gloves/forearms (group 4) - uint16_t geosetBoots = pickGeoset(502, 5); // Bare boots/shins (group 5) + 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 @@ -7276,7 +7276,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 @@ -7385,7 +7385,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) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 164e9d52..f81f0ef0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2078,6 +2078,8 @@ void GameHandler::handlePacket(network::Packet& packet) { // XP is updated via PLAYER_XP update fields from the server. if (areaDiscoveryCallback_) areaDiscoveryCallback_(areaName, xpGained); + if (addonEventCallback_) + addonEventCallback_("CHAT_MSG_COMBAT_XP_GAIN", {msg, std::to_string(xpGained)}); } } break; @@ -2211,6 +2213,8 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t value = packet.readUInt32(); worldStates_[field] = value; LOG_DEBUG("SMSG_UPDATE_WORLD_STATE: field=", field, " value=", value); + if (addonEventCallback_) + addonEventCallback_("UPDATE_WORLD_STATES", {}); break; } case Opcode::SMSG_WORLD_STATE_UI_TIMER_UPDATE: { @@ -2302,6 +2306,8 @@ void GameHandler::handlePacket(network::Packet& packet) { uint8_t paused = packet.readUInt8(); if (type < 3) { mirrorTimers_[type].paused = (paused != 0); + if (addonEventCallback_) + addonEventCallback_("MIRROR_TIMER_PAUSE", {paused ? "1" : "0"}); } break; } @@ -2772,7 +2778,27 @@ void GameHandler::handlePacket(network::Packet& packet) { // Sent by server when player sits in barber chair — triggers barber shop UI LOG_INFO("SMSG_ENABLE_BARBER_SHOP: barber shop available"); barberShopOpen_ = true; + if (addonEventCallback_) addonEventCallback_("BARBER_SHOP_OPEN", {}); break; + case Opcode::MSG_CORPSE_QUERY: { + // Server response: uint8 found + (if found) uint32 mapId + float x + float y + float z + uint32 corpseMapId + if (packet.getSize() - packet.getReadPos() < 1) break; + uint8_t found = packet.readUInt8(); + if (found && packet.getSize() - packet.getReadPos() >= 20) { + /*uint32_t mapId =*/ packet.readUInt32(); + float cx = packet.readFloat(); + float cy = packet.readFloat(); + float cz = packet.readFloat(); + uint32_t corpseMapId = packet.readUInt32(); + // Server coords: x=west, y=north (opposite of canonical) + corpseX_ = cx; + corpseY_ = cy; + corpseZ_ = cz; + corpseMapId_ = corpseMapId; + LOG_INFO("MSG_CORPSE_QUERY: corpse at (", cx, ",", cy, ",", cz, ") map=", corpseMapId); + } + break; + } case Opcode::SMSG_FEIGN_DEATH_RESISTED: addUIError("Your Feign Death was resisted."); addSystemChatMessage("Your Feign Death attempt was resisted."); @@ -2845,6 +2871,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // All threat dropped on the local player (e.g. Vanish, Feign Death) threatLists_.clear(); LOG_DEBUG("SMSG_THREAT_CLEAR: threat wiped"); + if (addonEventCallback_) addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {}); break; case Opcode::SMSG_THREAT_REMOVE: { // packed_guid (unit) + packed_guid (victim whose threat was removed) @@ -2888,6 +2915,8 @@ void GameHandler::handlePacket(network::Packet& packet) { std::sort(list.begin(), list.end(), [](const ThreatEntry& a, const ThreatEntry& b){ return a.threat > b.threat; }); threatLists_[unitGuid] = std::move(list); + if (addonEventCallback_) + addonEventCallback_("UNIT_THREAT_LIST_UPDATE", {}); break; } @@ -4119,6 +4148,10 @@ void GameHandler::handlePacket(network::Packet& packet) { if (auto* sfx = renderer->getUiSoundManager()) sfx->playQuestActivate(); } + if (addonEventCallback_) { + addonEventCallback_("TRAINER_UPDATE", {}); + addonEventCallback_("SPELLS_CHANGED", {}); + } break; } case Opcode::SMSG_TRAINER_BUY_FAILED: { @@ -4258,8 +4291,10 @@ void GameHandler::handlePacket(network::Packet& packet) { addSystemChatMessage(buf); watchedFactionId_ = factionId; if (repChangeCallback_) repChangeCallback_(name, delta, standing); - if (addonEventCallback_) + if (addonEventCallback_) { addonEventCallback_("UPDATE_FACTION", {}); + addonEventCallback_("CHAT_MSG_COMBAT_FACTION_CHANGE", {std::string(buf)}); + } } LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing); } @@ -5125,6 +5160,7 @@ void GameHandler::handlePacket(network::Packet& packet) { if (result == 0) { addSystemChatMessage("Hairstyle changed."); barberShopOpen_ = false; + if (addonEventCallback_) addonEventCallback_("BARBER_SHOP_CLOSE", {}); } else { const char* msg = (result == 1) ? "Not enough money for new hairstyle." : (result == 2) ? "You are not at a barber shop." @@ -5407,11 +5443,14 @@ void GameHandler::handlePacket(network::Packet& packet) { } questLog_.erase(it); LOG_INFO(" Removed quest ", questId, " from quest log"); + if (addonEventCallback_) + addonEventCallback_("QUEST_TURNED_IN", {std::to_string(questId)}); break; } } } - if (addonEventCallback_) addonEventCallback_("QUEST_LOG_UPDATE", {}); + if (addonEventCallback_) + addonEventCallback_("QUEST_LOG_UPDATE", {}); // Re-query all nearby quest giver NPCs so markers refresh if (socket) { for (const auto& [guid, entity] : entityManager.getEntities()) { @@ -11900,11 +11939,42 @@ 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_) { + std::string uid; + if (block.guid == playerGuid) uid = "player"; + else if (block.guid == targetGuid) uid = "target"; + else if (block.guid == focusGuid) uid = "focus"; + if (!uid.empty()) + addonEventCallback_("UNIT_FACTION", {uid}); + } + } + else if (key == ufFlags) { + unit->setUnitFlags(val); + if (addonEventCallback_) { + std::string uid; + if (block.guid == playerGuid) uid = "player"; + else if (block.guid == targetGuid) uid = "target"; + else if (block.guid == focusGuid) uid = "focus"; + if (!uid.empty()) + addonEventCallback_("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_) { + std::string uid; + if (block.guid == playerGuid) uid = "player"; + else if (block.guid == targetGuid) uid = "target"; + else if (block.guid == focusGuid) uid = "focus"; + else if (block.guid == petGuid_) uid = "pet"; + if (!uid.empty()) + addonEventCallback_("UNIT_MODEL_CHANGED", {uid}); + } + } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } else if (key == ufDynFlags) { unit->setDynamicFlags(val); @@ -11924,6 +11994,8 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem uint32_t old = currentMountDisplayId_; currentMountDisplayId_ = val; if (val != old && mountCallback_) mountCallback_(val); + if (val != old && addonEventCallback_) + addonEventCallback_("UNIT_MODEL_CHANGED", {"player"}); if (old == 0 && val != 0) { // Just mounted — find the mount aura (indefinite duration, self-cast) mountAuraSpellId_ = 0; @@ -11978,6 +12050,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 @@ -14095,6 +14172,7 @@ void GameHandler::followTarget() { addSystemChatMessage("Now following " + targetName + "."); LOG_INFO("Following target: ", targetName, " (GUID: 0x", std::hex, targetGuid, std::dec, ")"); + if (addonEventCallback_) addonEventCallback_("AUTOFOLLOW_BEGIN", {}); } void GameHandler::cancelFollow() { @@ -14104,6 +14182,7 @@ void GameHandler::cancelFollow() { } followTargetGuid_ = 0; addSystemChatMessage("You stop following."); + if (addonEventCallback_) addonEventCallback_("AUTOFOLLOW_END", {}); } void GameHandler::assistTarget() { @@ -14673,6 +14752,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); } } @@ -24592,6 +24674,7 @@ void GameHandler::handleLogoutResponse(network::Packet& packet) { logoutCountdown_ = 20.0f; } LOG_INFO("Logout response: success, instant=", (int)data.instant); + if (addonEventCallback_) addonEventCallback_("PLAYER_LOGOUT", {}); } else { // Failure addSystemChatMessage("Cannot logout right now."); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 4daf51ff..b74a78b5 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4237,6 +4237,12 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { if (u->getHealth() == 0 && u->getMaxHealth() > 0) { hostileColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); } 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 = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); // Grey — tapped by other + } else { // WoW level-based color for hostile mobs uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); @@ -4257,6 +4263,7 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - easy } } + } // end tapped else } else { hostileColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Friendly } @@ -5181,6 +5188,12 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { if (u->getHealth() == 0 && u->getMaxHealth() > 0) { focusColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); } 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 = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); + } else { uint32_t playerLv = gameHandler.getPlayerLevel(); uint32_t mobLv = u->getLevel(); if (mobLv == 0) { @@ -5198,6 +5211,7 @@ void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) { else focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); } + } // end tapped else } else { focusColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); } @@ -11695,8 +11709,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);