diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index a80af5d2c9..7116f311cf 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -66,6 +66,30 @@ #include "..\Minecraft.World\GenericStats.h" #endif +namespace +{ + char mapIconToFrame(char iconSlot) + { + if (iconSlot >= 8) return iconSlot - 4; + return iconSlot; + } + + // Same hash as getRandomPlayerMapIcon in MapItemSavedData.cpp, returning + // the Iggy/SWF frame index (0-7) instead of the raw icon slot. + char computePlayerMapFrame(int entityId, int playerIndex) + { + static const char PLAYER_MAP_ICON_SLOTS[] = { 0, 1, 2, 3, 8, 9, 10, 11 }; + unsigned int seed = static_cast(entityId); + seed ^= static_cast(playerIndex * 0x9E3779B9u); + seed ^= (seed >> 16); + seed *= 0x7FEB352Du; + seed ^= (seed >> 15); + seed *= 0x846CA68Bu; + seed ^= (seed >> 16); + return mapIconToFrame(PLAYER_MAP_ICON_SLOTS[seed % 8]); + } +} + ClientConnection::ClientConnection(Minecraft *minecraft, const wstring& ip, int port) { // 4J Stu - No longer used as we use the socket version below. @@ -377,6 +401,7 @@ void ClientConnection::handleLogin(shared_ptr packet) BYTE networkSmallId = getSocket()->getSmallId(); app.UpdatePlayerInfo(networkSmallId, packet->m_playerIndex, packet->m_uiGamePrivileges); + app.SetPlayerMapIcon(minecraft->player->getName().c_str(), computePlayerMapFrame(packet->clientVersion, packet->m_playerIndex)); minecraft->player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, packet->m_uiGamePrivileges); // Assume all privileges are on, so that the first message we see only indicates things that have been turned off @@ -447,6 +472,7 @@ void ClientConnection::handleLogin(shared_ptr packet) BYTE networkSmallId = getSocket()->getSmallId(); app.UpdatePlayerInfo(networkSmallId, packet->m_playerIndex, packet->m_uiGamePrivileges); + app.SetPlayerMapIcon(player->getName().c_str(), computePlayerMapFrame(packet->clientVersion, packet->m_playerIndex)); player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, packet->m_uiGamePrivileges); // Assume all privileges are on, so that the first message we see only indicates things that have been turned off @@ -917,6 +943,36 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) } } + // Client-side registration: if we still have no IQNet entry for this remote + // player, create one so they appear in the Tab player list. + // Find the first available IQNet slot (customData == 0, skip slot 0 which + // is the host). We can't use packet->m_playerIndex directly because on + // dedicated servers the game-level player index starts at 0 for real + // players, conflicting with the IQNet host slot. + if (matchedQNetPlayer == nullptr) + { + for (int s = 1; s < MINECRAFT_NET_MAX_PLAYERS; ++s) + { + IQNetPlayer* qp = &IQNet::m_player[s]; + if (qp->GetCustomDataValue() == 0 && qp->m_gamertag[0] == 0) + { + BYTE smallId = static_cast(s); + qp->m_smallId = smallId; + qp->m_isRemote = true; + qp->m_isHostPlayer = false; + qp->m_resolvedXuid = pktXuid; + wcsncpy_s(qp->m_gamertag, 32, packet->name.c_str(), _TRUNCATE); + if (smallId >= IQNet::s_playerCount) + IQNet::s_playerCount = smallId + 1; + + extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager; + g_pPlatformNetworkManager->NotifyPlayerJoined(qp); + matchedQNetPlayer = qp; + break; + } + } + } + if (matchedQNetPlayer != nullptr) { // Store packet-authoritative XUID on this network slot so later lookups by XUID @@ -946,6 +1002,7 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) player->setPlayerIndex( packet->m_playerIndex ); player->setCustomSkin( packet->m_skinId ); player->setCustomCape( packet->m_capeId ); + app.SetPlayerMapIcon(packet->name.c_str(), computePlayerMapFrame(packet->id, packet->m_playerIndex)); player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, packet->m_uiGamePrivileges); if (!player->customTextureUrl.empty() && player->customTextureUrl.substr(0, 3).compare(L"def") != 0 && !app.IsFileInMemoryTextures(player->customTextureUrl)) @@ -1088,28 +1145,27 @@ void ClientConnection::handleRemoveEntity(shared_ptr packe for (int i = 0; i < packet->ids.length; i++) { shared_ptr entity = getEntity(packet->ids[i]); - if (entity != nullptr && entity->GetType() == eTYPE_PLAYER) + if (entity != nullptr) { shared_ptr player = dynamic_pointer_cast(entity); if (player != nullptr) { - PlayerUID xuid = player->getXuid(); - INetworkPlayer* np = g_NetworkManager.GetPlayerByXuid(xuid); - if (np != nullptr) + // Match by gamertag in the IQNet array (XUID may be 0 on dedicated servers) + for (int s = 1; s < MINECRAFT_NET_MAX_PLAYERS; ++s) { - NetworkPlayerXbox* npx = (NetworkPlayerXbox*)np; - IQNetPlayer* qp = npx->GetQNetPlayer(); - if (qp != nullptr) + IQNetPlayer* qp = &IQNet::m_player[s]; + if (qp->GetCustomDataValue() != 0 && + _wcsicmp(qp->m_gamertag, player->getName().c_str()) == 0) { extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager; g_pPlatformNetworkManager->NotifyPlayerLeaving(qp); qp->m_smallId = 0; qp->m_isRemote = false; qp->m_isHostPlayer = false; - // Clear resolved id to avoid stale XUID -> player matches after disconnect. qp->m_resolvedXuid = INVALID_XUID; qp->m_gamertag[0] = 0; qp->SetCustomDataValue(0); + break; } } } diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 0a2fd159a4..c98837890d 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -187,6 +187,7 @@ CMinecraftApp::CMinecraftApp() #endif ZeroMemory(m_playerColours,MINECRAFT_NET_MAX_PLAYERS); + ZeroMemory(m_playerMapIcons,MINECRAFT_NET_MAX_PLAYERS); m_iDLCOfferC=0; m_bAllDLCContentRetrieved=true; @@ -8463,6 +8464,39 @@ short CMinecraftApp::GetPlayerColour(BYTE networkSmallId) return index; } +void CMinecraftApp::SetPlayerMapIcon(const wchar_t* name, char icon) +{ + if (name == nullptr) return; + // Update existing entry or use first empty slot + int emptySlot = -1; + for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; ++i) + { + if (m_playerMapIcons[i].name[0] != 0 && _wcsicmp(m_playerMapIcons[i].name, name) == 0) + { + m_playerMapIcons[i].icon = icon; + return; + } + if (emptySlot < 0 && m_playerMapIcons[i].name[0] == 0) + emptySlot = i; + } + if (emptySlot >= 0) + { + wcsncpy_s(m_playerMapIcons[emptySlot].name, 32, name, _TRUNCATE); + m_playerMapIcons[emptySlot].icon = icon; + } +} + +char CMinecraftApp::GetPlayerMapIconByName(const wchar_t* name) +{ + if (name == nullptr) return 0; + for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; ++i) + { + if (m_playerMapIcons[i].name[0] != 0 && _wcsicmp(m_playerMapIcons[i].name, name) == 0) + return m_playerMapIcons[i].icon; + } + return 0; +} + unsigned int CMinecraftApp::GetPlayerPrivileges(BYTE networkSmallId) { diff --git a/Minecraft.Client/Common/Consoles_App.h b/Minecraft.Client/Common/Consoles_App.h index 0c1c261efd..ff7aa8e88d 100644 --- a/Minecraft.Client/Common/Consoles_App.h +++ b/Minecraft.Client/Common/Consoles_App.h @@ -747,10 +747,14 @@ class CMinecraftApp private: BYTE m_playerColours[MINECRAFT_NET_MAX_PLAYERS]; // An array of QNet small-id's unsigned int m_playerGamePrivileges[MINECRAFT_NET_MAX_PLAYERS]; + struct PlayerMapIconEntry { wchar_t name[32]; char icon; }; + PlayerMapIconEntry m_playerMapIcons[MINECRAFT_NET_MAX_PLAYERS]; public: void UpdatePlayerInfo(BYTE networkSmallId, SHORT playerColourIndex, unsigned int playerGamePrivileges); short GetPlayerColour(BYTE networkSmallId); + void SetPlayerMapIcon(const wchar_t* name, char icon); + char GetPlayerMapIconByName(const wchar_t* name); unsigned int GetPlayerPrivileges(BYTE networkSmallId); wstring getEntityName(eINSTANCEOF type); diff --git a/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp b/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp index 338d1905cd..d3a149df59 100644 --- a/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp @@ -27,8 +27,15 @@ UIScene_InGameInfoMenu::UIScene_InGameInfoMenu(int iPad, void *initData, UILayer { PlayerInfo *info = BuildPlayerInfo(player); + // Skip the dedicated server's phantom host entry (slot 0, empty name) + if (info->m_smallId == 0 && info->m_name.empty()) + { + delete info; + continue; + } + m_players.push_back(info); - m_playerList.addItem(info->m_name, info->m_colorState, info->m_voiceStatus); + m_playerList.addItem(info->m_name, info->m_colorState, info->m_voiceStatus); } } @@ -174,8 +181,15 @@ void UIScene_InGameInfoMenu::handleReload() { PlayerInfo *info = BuildPlayerInfo(player); + // Skip the dedicated server's phantom host entry (slot 0, empty name) + if (info->m_smallId == 0 && info->m_name.empty()) + { + delete info; + continue; + } + m_players.push_back(info); - m_playerList.addItem(info->m_name, info->m_colorState, info->m_voiceStatus); + m_playerList.addItem(info->m_name, info->m_colorState, info->m_voiceStatus); } } @@ -202,23 +216,22 @@ void UIScene_InGameInfoMenu::tick() { UIScene::tick(); - // Update players by index + // Update players by their stored smallId (not sequential index, which can mismatch + // when entries like the dedicated server host are filtered from the UI list) for(DWORD i = 0; i < m_players.size(); ++i) { - INetworkPlayer *player = g_NetworkManager.GetPlayerByIndex( i ); + INetworkPlayer *player = g_NetworkManager.GetPlayerBySmallId( m_players[i]->m_smallId ); if(player != nullptr) { PlayerInfo *info = BuildPlayerInfo(player); - m_players[i]->m_smallId = info->m_smallId; - if(info->m_voiceStatus != m_players[i]->m_voiceStatus) { m_players[i]->m_voiceStatus = info->m_voiceStatus; m_playerList.setVOIPIcon(i, info->m_voiceStatus); } - + if(info->m_colorState != m_players[i]->m_colorState) { m_players[i]->m_colorState = info->m_colorState; @@ -424,11 +437,19 @@ void UIScene_InGameInfoMenu::OnPlayerChanged(void *callbackParam, INetworkPlayer // If the player is joining if(!leaving) { + PlayerInfo *info = scene->BuildPlayerInfo(pPlayer); + + // Skip the dedicated server's phantom host entry (slot 0, empty name) + if (pPlayer->GetSmallId() == 0 && info->m_name.empty()) + { + delete info; + return; + } + app.DebugPrintf(" Player \"%ls\" not found, adding\n", pPlayer->GetOnlineName()); - PlayerInfo *info = scene->BuildPlayerInfo(pPlayer); scene->m_players.push_back(info); - + // Note that the tick updates buttons every tick so it's only really important that we // add the button (not the order or content) scene->m_playerList.addItem(info->m_name, info->m_colorState, info->m_voiceStatus); @@ -491,7 +512,7 @@ UIScene_InGameInfoMenu::PlayerInfo *UIScene_InGameInfoMenu::BuildPlayerInfo(INet } info->m_voiceStatus = voiceStatus; - info->m_colorState = app.GetPlayerColour(info->m_smallId); + info->m_colorState = app.GetPlayerMapIconByName(player->GetOnlineName()); info->m_name = playerName; return info; diff --git a/Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp b/Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp index 017af93ef6..d8390d3734 100644 --- a/Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp @@ -193,12 +193,12 @@ void UIScene_TeleportMenu::tick() { m_players[i] = player->GetSmallId(); - short icon = app.GetPlayerColour( m_players[i] ); + short icon = static_cast(app.GetPlayerMapIconByName(player->GetOnlineName())); if(icon != m_playersColourState[i]) { m_playersColourState[i] = icon; - m_playerList.setPlayerIcon( i, (int)app.GetPlayerColour( m_players[i] ) ); + m_playerList.setPlayerIcon( i, (int)icon ); } wstring playerName = L""; diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index ba82ec6acd..8a6b531a57 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -496,6 +496,50 @@ void PlayerList::add(shared_ptr player) } } + // Send AddPlayerPackets so all players appear in each other's Tab list + // regardless of render distance. The entity tracking system will send + // another AddPlayerPacket when they enter range, which is handled + // gracefully by putEntity replacing the old entity. + { + PlayerUID xuid = INVALID_XUID; + PlayerUID onlineXuid = INVALID_XUID; +#ifndef MINECRAFT_SERVER_BUILD + xuid = player->getXuid(); + onlineXuid = player->getOnlineXuid(); +#endif + int xp = Mth::floor(player->x * 32.0); + int yp = Mth::floor(player->y * 32.0); + int zp = Mth::floor(player->z * 32.0); + int yRotp = Mth::floor(player->yRot * 256.0f / 360.0f); + int xRotp = Mth::floor(player->xRot * 256.0f / 360.0f); + int yHeadRotp = Mth::floor(player->yHeadRot * 256.0f / 360.0f); + + // Broadcast the new player to all existing players + broadcastAll(std::make_shared(player, xuid, onlineXuid, xp, yp, zp, yRotp, xRotp, yHeadRotp)); + + // Send all existing players to the new player + for (size_t i = 0; i < players.size(); i++) + { + shared_ptr op = players.at(i); + if (op != player && op->connection->getNetworkPlayer()) + { + PlayerUID opXuid = INVALID_XUID; + PlayerUID opOnlineXuid = INVALID_XUID; +#ifndef MINECRAFT_SERVER_BUILD + opXuid = op->getXuid(); + opOnlineXuid = op->getOnlineXuid(); +#endif + int oxp = Mth::floor(op->x * 32.0); + int oyp = Mth::floor(op->y * 32.0); + int ozp = Mth::floor(op->z * 32.0); + int oyRotp = Mth::floor(op->yRot * 256.0f / 360.0f); + int oxRotp = Mth::floor(op->xRot * 256.0f / 360.0f); + int oyHeadRotp = Mth::floor(op->yHeadRot * 256.0f / 360.0f); + player->connection->send(std::make_shared(op, opXuid, opOnlineXuid, oxp, oyp, ozp, oyRotp, oxRotp, oyHeadRotp)); + } + } + } + if(level->isAtLeastOnePlayerSleeping()) { shared_ptr firstSleepingPlayer = nullptr; @@ -528,6 +572,14 @@ if (player->riding != nullptr) level->removeEntityImmediately(player->riding); app.DebugPrintf("removing player mount"); } + // Notify all clients to remove this player entity, not just those who + // had the player in tracking range. This ensures players added to the + // Tab list via the AddPlayerPacket broadcast are properly cleaned up. + { + intArray ids(1); + ids[0] = player->entityId; + broadcastAll(std::make_shared(ids)); + } level->getTracker()->removeEntity(player); level->removeEntity(player); level->getChunkMap()->remove(player);