From 00d1d23cca2d407a26d7c9fd4b4e257d7b200bda Mon Sep 17 00:00:00 2001 From: itsRevela Date: Thu, 26 Mar 2026 22:25:40 -0500 Subject: [PATCH 1/3] Fix player list not showing all players on dedicated servers Register remote players in the client's IQNet array when their AddPlayerPacket arrives, so they appear in the Tab player list. Previously only the host and local player were registered. Also filter the dedicated server's phantom host entry (slot 0, empty gamertag) from the UI, fix tick() to update entries by smallId instead of sequential index, and fix player removal to use gamertag matching since XUIDs are 0 on dedicated servers. --- Minecraft.Client/ClientConnection.cpp | 45 +++++++++++++++---- .../Common/UI/UIScene_InGameInfoMenu.cpp | 39 ++++++++++++---- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index a80af5d2c9..df2ee62736 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -917,6 +917,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 @@ -1088,28 +1118,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/UI/UIScene_InGameInfoMenu.cpp b/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp index 338d1905cd..b8e1844456 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); From 75eb646becfbae8090f76b44dd690b88194a4ce6 Mon Sep 17 00:00:00 2001 From: itsRevela Date: Thu, 26 Mar 2026 22:25:58 -0500 Subject: [PATCH 2/3] Fix player list map icon colors to match map markers The tab player list and teleport menu now show the correct map marker color for each player. The icon is computed using the same hash as the map renderer (getRandomPlayerMapIcon) and stored by player name, bypassing the unreliable small-ID lookup that produced wrong colors on dedicated servers. --- Minecraft.Client/ClientConnection.cpp | 27 +++++++++++++++ Minecraft.Client/Common/Consoles_App.cpp | 34 +++++++++++++++++++ Minecraft.Client/Common/Consoles_App.h | 4 +++ .../Common/UI/UIScene_InGameInfoMenu.cpp | 2 +- .../Common/UI/UIScene_TeleportMenu.cpp | 4 +-- 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index df2ee62736..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 @@ -976,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)) 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 b8e1844456..d3a149df59 100644 --- a/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp @@ -512,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""; From 898d4352a3eedb9788d20f8ec03fcbfa0b192f6b Mon Sep 17 00:00:00 2001 From: itsRevela Date: Thu, 26 Mar 2026 23:54:51 -0500 Subject: [PATCH 3/3] Send AddPlayerPacket for all players on join and RemoveEntitiesPacket on disconnect Players now appear in each other's Tab list immediately on join, regardless of render distance. Previously, players only appeared when they entered entity tracking range because AddPlayerPacket was only sent through the TrackedEntity system. On disconnect, a RemoveEntitiesPacket is broadcast to all clients so players added via the join broadcast are properly cleaned up, not just those within tracking range. --- Minecraft.Client/PlayerList.cpp | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) 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);