fix: chat prefix, hostile faction display, and game object looting

- Add BG_SYSTEM_NEUTRAL/ALLIANCE/HORDE chat types (0x52-0x54) and reclassify
  them as SYSTEM in the parser — prevents bogus [Say] prefix on arena/BG
  system messages
- Remove fallback [TypeName] bracket for sender-less SAY/YELL/WHISPER messages;
  only group-channel types (Party/Guild/Raid/BG) show brackets without a sender
- Remove factionTemplate != 0 guard — units with FT=0 now get setHostile() like
  any other unit (defaulting to hostile from the map default), fixing NPCs that
  appeared friendly due to unset faction template
- Enable CMSG_LOOT for WotLK type=3 (chest) game objects in addition to
  CMSG_GAMEOBJ_USE — fixes Milly's Harvest and other quest gather objects on
  AzerothCore WotLK servers
This commit is contained in:
Kelsi 2026-03-10 15:32:04 -07:00
parent 942df21c66
commit 1a370fef76
4 changed files with 52 additions and 19 deletions

View file

@ -615,7 +615,11 @@ enum class ChatType : uint8_t {
MONSTER_WHISPER = 42, MONSTER_WHISPER = 42,
RAID_BOSS_WHISPER = 43, RAID_BOSS_WHISPER = 43,
RAID_BOSS_EMOTE = 44, RAID_BOSS_EMOTE = 44,
MONSTER_PARTY = 50 MONSTER_PARTY = 50,
// BG/Arena system messages (WoW 3.3.5a — no sender, treated as SYSTEM in display)
BG_SYSTEM_NEUTRAL = 82,
BG_SYSTEM_ALLIANCE = 83,
BG_SYSTEM_HORDE = 84
}; };
/** /**

View file

@ -7930,10 +7930,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if (ghostStateCallback_) ghostStateCallback_(true); if (ghostStateCallback_) ghostStateCallback_(true);
} }
} }
// Determine hostility from faction template for online creatures // Determine hostility from faction template for online creatures.
if (unit->getFactionTemplate() != 0) { // Always call isHostileFaction — factionTemplate=0 defaults to hostile
unit->setHostile(isHostileFaction(unit->getFactionTemplate())); // in the lookup rather than silently staying at the struct default (false).
} unit->setHostile(isHostileFaction(unit->getFactionTemplate()));
// Trigger creature spawn callback for units/players with displayId // Trigger creature spawn callback for units/players with displayId
if (block.objectType == ObjectType::UNIT && unit->getDisplayId() == 0) { if (block.objectType == ObjectType::UNIT && unit->getDisplayId() == 0) {
LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec, LOG_WARNING("[Spawn] UNIT guid=0x", std::hex, block.guid, std::dec,
@ -14287,8 +14287,9 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
// animation/sound and expects the client to request the mail list. // animation/sound and expects the client to request the mail list.
bool isMailbox = false; bool isMailbox = false;
bool chestLike = false; bool chestLike = false;
// Stock-like behavior: GO use opens GO loot context. Keep eager CMSG_LOOT only // Chest-type game objects (type=3): on all expansions, also send CMSG_LOOT so
// as Classic/Turtle fallback behavior. // the server opens the loot response. Other harvestable/interactive types rely
// on the server auto-sending SMSG_LOOT_RESPONSE after CMSG_GAMEOBJ_USE.
bool shouldSendLoot = isActiveExpansion("classic") || isActiveExpansion("turtle"); bool shouldSendLoot = isActiveExpansion("classic") || isActiveExpansion("turtle");
if (entity && entity->getType() == ObjectType::GAMEOBJECT) { if (entity && entity->getType() == ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<GameObject>(entity); auto go = std::static_pointer_cast<GameObject>(entity);
@ -14305,6 +14306,8 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
refreshMailList(); refreshMailList();
} else if (info && info->type == 3) { } else if (info && info->type == 3) {
chestLike = true; chestLike = true;
// Type-3 chests require CMSG_LOOT on all expansions (AzerothCore WotLK included)
shouldSendLoot = true;
} else if (turtleMode) { } else if (turtleMode) {
// Turtle compatibility: keep eager loot open behavior. // Turtle compatibility: keep eager loot open behavior.
shouldSendLoot = true; shouldSendLoot = true;
@ -14315,21 +14318,19 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
std::transform(lower.begin(), lower.end(), lower.begin(), std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); }); [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
chestLike = (lower.find("chest") != std::string::npos); chestLike = (lower.find("chest") != std::string::npos);
if (chestLike) shouldSendLoot = true;
} }
// For WotLK chest-like gameobjects, report use but let server open loot. // For WotLK chest-like gameobjects, also send CMSG_GAMEOBJ_REPORT_USE.
if (!isMailbox && chestLike) { if (!isMailbox && chestLike && isActiveExpansion("wotlk")) {
if (isActiveExpansion("wotlk")) { network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE));
network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); reportUse.writeUInt64(guid);
reportUse.writeUInt64(guid); socket->send(reportUse);
socket->send(reportUse);
}
} }
if (shouldSendLoot) { if (shouldSendLoot) {
lootTarget(guid); lootTarget(guid);
} }
// Retry use briefly to survive packet loss/order races. Keep loot retries only // Retry use briefly to survive packet loss/order races.
// when we intentionally use eager loot-open mode. const bool retryLoot = shouldSendLoot;
const bool retryLoot = shouldSendLoot && (turtleMode || isActiveExpansion("classic"));
const bool retryUse = turtleMode || isActiveExpansion("classic"); const bool retryUse = turtleMode || isActiveExpansion("classic");
if (retryUse || retryLoot) { if (retryUse || retryLoot) {
pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot}); pendingGameObjectLootRetries_.push_back(PendingLootRetry{guid, 0.15f, 2, retryLoot});

View file

@ -1422,6 +1422,14 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
break; break;
} }
case ChatType::BG_SYSTEM_NEUTRAL:
case ChatType::BG_SYSTEM_ALLIANCE:
case ChatType::BG_SYSTEM_HORDE:
// BG/Arena system messages — no sender GUID or name field, just message.
// Reclassify as SYSTEM for consistent display.
data.type = ChatType::SYSTEM;
break;
default: default:
// SAY, GUILD, PARTY, YELL, WHISPER, WHISPER_INFORM, RAID, etc. // SAY, GUILD, PARTY, YELL, WHISPER, WHISPER_INFORM, RAID, etc.
// All have receiverGuid (typically senderGuid repeated) // All have receiverGuid (typically senderGuid repeated)

View file

@ -1251,8 +1251,25 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
renderTextWithLinks(fullMsg, color); renderTextWithLinks(fullMsg, color);
} }
} else { } else {
std::string fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + processedMessage; // No sender name. For group/channel types show a bracket prefix;
renderTextWithLinks(fullMsg, color); // for sender-specific types (SAY, YELL, WHISPER, etc.) just show the
// raw message — these are server-side announcements without a speaker.
bool isGroupType =
msg.type == game::ChatType::PARTY ||
msg.type == game::ChatType::GUILD ||
msg.type == game::ChatType::OFFICER ||
msg.type == game::ChatType::RAID ||
msg.type == game::ChatType::RAID_LEADER ||
msg.type == game::ChatType::RAID_WARNING ||
msg.type == game::ChatType::BATTLEGROUND ||
msg.type == game::ChatType::BATTLEGROUND_LEADER;
if (isGroupType) {
std::string fullMsg = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + processedMessage;
renderTextWithLinks(fullMsg, color);
} else {
// SAY, YELL, WHISPER, unknown BG_SYSTEM_* types, etc. — no prefix
renderTextWithLinks(tsPrefix + processedMessage, color);
}
} }
} }
@ -3421,6 +3438,9 @@ const char* GameScreen::getChatTypeName(game::ChatType type) const {
case game::ChatType::ACHIEVEMENT: return "Achievement"; case game::ChatType::ACHIEVEMENT: return "Achievement";
case game::ChatType::DND: return "DND"; case game::ChatType::DND: return "DND";
case game::ChatType::AFK: return "AFK"; case game::ChatType::AFK: return "AFK";
case game::ChatType::BG_SYSTEM_NEUTRAL:
case game::ChatType::BG_SYSTEM_ALLIANCE:
case game::ChatType::BG_SYSTEM_HORDE: return "System";
default: return "Unknown"; default: return "Unknown";
} }
} }