feat: handle SMSG_BATTLEFIELD_MGR_* and SMSG_CALENDAR_* opcodes

Implements WotLK Outdoor Battlefield Manager (Wintergrasp/Tol Barad):
- Parse SMSG_BATTLEFIELD_MGR_ENTRY_INVITE, ENTERED, QUEUE_INVITE,
  QUEUE_REQUEST_RESPONSE, EJECT_PENDING, EJECTED, STATE_CHANGE
- Store bfMgrInvitePending_/bfMgrActive_/bfMgrZoneId_ state
- Send CMSG_BATTLEFIELD_MGR_ENTRY_INVITE_RESPONSE via acceptBfMgrInvite() /
  declineBfMgrInvite() accessors
- Add renderBfMgrInvitePopup() UI dialog with Enter/Decline buttons;
  recognises Wintergrasp (zone 4197) and Tol Barad (zone 5095) by name

Implements WotLK Calendar notifications:
- SMSG_CALENDAR_SEND_NUM_PENDING: track pending invite count
- SMSG_CALENDAR_COMMAND_RESULT: map 15 error codes to friendly messages
- SMSG_CALENDAR_EVENT_INVITE_ALERT: notify player of new event invite with title
- SMSG_CALENDAR_EVENT_STATUS: show per-event RSVP status changes (9 statuses)
- SMSG_CALENDAR_RAID_LOCKOUT_ADDED/REMOVED: log raid lockout calendar entries
- Remaining SMSG_CALENDAR_* packets safely consumed
- requestCalendar() sends CMSG_CALENDAR_GET_CALENDAR + GET_NUM_PENDING
This commit is contained in:
Kelsi 2026-03-12 22:25:46 -07:00
parent dd38026b23
commit c5a6979d69
4 changed files with 415 additions and 0 deletions

View file

@ -687,6 +687,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderGuildInvitePopup(gameHandler);
renderReadyCheckPopup(gameHandler);
renderBgInvitePopup(gameHandler);
renderBfMgrInvitePopup(gameHandler);
renderLfgProposalPopup(gameHandler);
renderGuildRoster(gameHandler);
renderSocialFrame(gameHandler);
@ -10947,6 +10948,63 @@ void GameScreen::renderBgInvitePopup(game::GameHandler& gameHandler) {
ImGui::PopStyleColor(3);
}
void GameScreen::renderBfMgrInvitePopup(game::GameHandler& gameHandler) {
// Only shown on WotLK servers (outdoor battlefields like Wintergrasp use the BF Manager)
if (!gameHandler.hasBfMgrInvite()) return;
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 190.0f, screenH / 2.0f - 55.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(380.0f, 0.0f), ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.10f, 0.20f, 0.96f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.15f, 0.15f, 0.45f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
const ImGuiWindowFlags flags =
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse;
if (ImGui::Begin("Battlefield", nullptr, flags)) {
// Resolve zone name for Wintergrasp (zoneId 4197)
uint32_t zoneId = gameHandler.getBfMgrZoneId();
const char* zoneName = nullptr;
if (zoneId == 4197) zoneName = "Wintergrasp";
else if (zoneId == 5095) zoneName = "Tol Barad";
if (zoneName) {
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", zoneName);
} else {
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "Outdoor Battlefield");
}
ImGui::Spacing();
ImGui::TextWrapped("You are invited to join the outdoor battlefield. Do you want to enter?");
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.5f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
if (ImGui::Button("Enter Battlefield", ImVec2(178, 28))) {
gameHandler.acceptBfMgrInvite();
}
ImGui::PopStyleColor(2);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.15f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.0f));
if (ImGui::Button("Decline", ImVec2(175, 28))) {
gameHandler.declineBfMgrInvite();
}
ImGui::PopStyleColor(2);
}
ImGui::End();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
}
void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
using LfgState = game::GameHandler::LfgState;
if (gameHandler.getLfgState() != LfgState::Proposal) return;