Fix vanilla M2 animations, movement packets, and DBC locale

- Parse vanilla M2 animation tracks (flat arrays with M2Range indices)
  instead of skipping them, fixing T-pose on all vanilla models
- Use C4Quaternion (float[4]) for vanilla bone rotations instead of
  CompressedQuat (int16[4]) which produced garbage transforms
- Fix vanilla M2 attachment struct size (48 bytes, not 40) so weapons
  attach to correct bones instead of model origin
- Route movement packets through expansion-specific packet parsers
  instead of hardcoded WotLK format, fixing server-side position sync
- Fix Spell.dbc field indices for classic/turtle (Name=120, Rank=129,
  IconID=117) - were pointing to Portuguese locale column (+7 offset)
- Change guild roster keybind from J to O (WoW default)
- Add guild opcodes for all expansions
This commit is contained in:
Kelsi 2026-02-13 21:39:48 -08:00
parent 60c93fa1e3
commit 22728b461f
16 changed files with 951 additions and 26 deletions

View file

@ -170,6 +170,8 @@ void GameScreen::render(game::GameHandler& gameHandler) {
renderCombatText(gameHandler);
renderPartyFrames(gameHandler);
renderGroupInvitePopup(gameHandler);
renderGuildInvitePopup(gameHandler);
renderGuildRoster(gameHandler);
renderBuffBar(gameHandler);
renderLootWindow(gameHandler);
renderGossipWindow(gameHandler);
@ -1589,6 +1591,24 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
return;
}
// /gkick command
if (cmdLower == "gkick" || cmdLower == "guildkick") {
if (spacePos != std::string::npos) {
std::string playerName = command.substr(spacePos + 1);
gameHandler.kickGuildMember(playerName);
chatInputBuffer[0] = '\0';
return;
}
game::MessageChatData msg;
msg.type = game::ChatType::SYSTEM;
msg.language = game::ChatLanguage::UNIVERSAL;
msg.message = "Usage: /gkick <player>";
gameHandler.addLocalChatMessage(msg);
chatInputBuffer[0] = '\0';
return;
}
// /readycheck command
if (cmdLower == "readycheck" || cmdLower == "rc") {
gameHandler.initiateReadyCheck();
@ -3181,6 +3201,135 @@ void GameScreen::renderGroupInvitePopup(game::GameHandler& gameHandler) {
ImGui::End();
}
void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) {
if (!gameHandler.hasPendingGuildInvite()) return;
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
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)) {
ImGui::TextWrapped("%s has invited you to join %s.",
gameHandler.getPendingGuildInviterName().c_str(),
gameHandler.getPendingGuildInviteGuildName().c_str());
ImGui::Spacing();
if (ImGui::Button("Accept", ImVec2(155, 30))) {
gameHandler.acceptGuildInvite();
}
ImGui::SameLine();
if (ImGui::Button("Decline", ImVec2(155, 30))) {
gameHandler.declineGuildInvite();
}
}
ImGui::End();
}
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
// O key toggle (WoW default Social/Guild keybind)
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
showGuildRoster_ = !showGuildRoster_;
if (showGuildRoster_) {
if (!gameHandler.isInGuild()) {
gameHandler.addLocalChatMessage(game::MessageChatData{
game::ChatType::SYSTEM, game::ChatLanguage::UNIVERSAL, 0, "", 0, "", "You are not in a guild.", "", 0});
showGuildRoster_ = false;
return;
}
gameHandler.requestGuildRoster();
}
}
if (!showGuildRoster_) 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 - 300, screenH / 2 - 250), ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Once);
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Roster") : "Guild Roster";
bool open = showGuildRoster_;
if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
if (!gameHandler.hasGuildRoster()) {
ImGui::Text("Loading roster...");
} else {
const auto& roster = gameHandler.getGuildRoster();
// MOTD
if (!roster.motd.empty()) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str());
ImGui::Separator();
}
// Count online
int onlineCount = 0;
for (const auto& m : roster.members) {
if (m.online) ++onlineCount;
}
ImGui::Text("%d members (%d online)", (int)roster.members.size(), onlineCount);
ImGui::Separator();
// Table
if (ImGui::BeginTable("GuildRoster", 6,
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
ImGuiTableFlags_Sortable)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
ImGui::TableSetupColumn("Rank");
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f);
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Note");
ImGui::TableHeadersRow();
// Online members first, then offline
auto sortedMembers = roster.members;
std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) {
if (a.online != b.online) return a.online > b.online;
return a.name < b.name;
});
static const char* classNames[] = {
"Unknown", "Warrior", "Paladin", "Hunter", "Rogue",
"Priest", "Death Knight", "Shaman", "Mage", "Warlock",
"", "Druid"
};
for (const auto& m : sortedMembers) {
ImGui::TableNextRow();
ImVec4 textColor = m.online ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.name.c_str());
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%u", m.rankIndex);
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%u", m.level);
ImGui::TableNextColumn();
const char* className = (m.classId < 12) ? classNames[m.classId] : "Unknown";
ImGui::TextColored(textColor, "%s", className);
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%u", m.zoneId);
ImGui::TableNextColumn();
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
}
ImGui::EndTable();
}
}
}
ImGui::End();
showGuildRoster_ = open;
}
// ============================================================
// Buff/Debuff Bar (Phase 3)
// ============================================================