Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 64 additions & 8 deletions Minecraft.Client/ClientConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned int>(entityId);
seed ^= static_cast<unsigned int>(playerIndex * 0x9E3779B9u);
seed ^= (seed >> 16);
seed *= 0x7FEB352Du;
Copy link
Copy Markdown
Collaborator

@sylvessa sylvessa Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do these magic numbers come from? why is it done like this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think these magic numbers should be documented

Copy link
Copy Markdown
Contributor Author

@itsRevela itsRevela Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not my code, it is currently being used upstream in y'alls MapItemSavedData.cpp:

static char getRandomPlayerMapIcon(const shared_ptr<Player>& player)
{
    // use seed bit shift random
    unsigned int seed = static_cast<unsigned int>(player->entityId);
    seed ^= static_cast<unsigned int>(player->getPlayerIndex() * 0x9E3779B9u);
    seed ^= (seed >> 16);
    seed *= 0x7FEB352Du;
    seed ^= (seed >> 15);
    seed *= 0x846CA68Bu;
    seed ^= (seed >> 16);

    return PLAYER_MAP_ICON_SLOTS[seed % (sizeof(PLAYER_MAP_ICON_SLOTS) / sizeof(PLAYER_MAP_ICON_SLOTS[0]))];
}

It picks a random color for each player's map icon, but it has to be the same color everytime for the same player. So instead of actual randomness, it scramble the player's ID into a number between 0-7 (8 available colors)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting, i never saw this. i just thought it was weird you were using some random seed lol. then again i havent really messed with map stuff

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.
Expand Down Expand Up @@ -377,6 +401,7 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> 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
Expand Down Expand Up @@ -447,6 +472,7 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> 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
Expand Down Expand Up @@ -917,6 +943,36 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> 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<BYTE>(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
Expand Down Expand Up @@ -946,6 +1002,7 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> 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))
Expand Down Expand Up @@ -1088,28 +1145,27 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packe
for (int i = 0; i < packet->ids.length; i++)
{
shared_ptr<Entity> entity = getEntity(packet->ids[i]);
if (entity != nullptr && entity->GetType() == eTYPE_PLAYER)
if (entity != nullptr)
{
shared_ptr<Player> player = dynamic_pointer_cast<Player>(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;
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions Minecraft.Client/Common/Consoles_App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down
4 changes: 4 additions & 0 deletions Minecraft.Client/Common/Consoles_App.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@
void RemoveMemoryTPDFile(int iConfig);
bool IsFileInTPD(int iConfig);
void GetTPD(int iConfig,PBYTE *ppbData,DWORD *pdwBytes);
int GetTPDSize() {return m_MEM_TPD.size();}

Check warning on line 365 in Minecraft.Client/Common/Consoles_App.h

View workflow job for this annotation

GitHub Actions / build

'return': conversion from 'size_t' to 'int', possible loss of data
#ifndef __PS3__
int GetTPConfigVal(WCHAR *pwchDataFile);
#endif
Expand Down Expand Up @@ -747,10 +747,14 @@
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);
Expand Down
41 changes: 31 additions & 10 deletions Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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;
Expand Down Expand Up @@ -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("<UIScene_InGameInfoMenu::OnPlayerChanged> 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);
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions Minecraft.Client/Common/UI/UIScene_TeleportMenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,12 @@ void UIScene_TeleportMenu::tick()
{
m_players[i] = player->GetSmallId();

short icon = app.GetPlayerColour( m_players[i] );
short icon = static_cast<short>(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"";
Expand Down
52 changes: 52 additions & 0 deletions Minecraft.Client/PlayerList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,50 @@ void PlayerList::add(shared_ptr<ServerPlayer> 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<AddPlayerPacket>(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<ServerPlayer> 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<AddPlayerPacket>(op, opXuid, opOnlineXuid, oxp, oyp, ozp, oyRotp, oxRotp, oyHeadRotp));
}
}
}

if(level->isAtLeastOnePlayerSleeping())
{
shared_ptr<ServerPlayer> firstSleepingPlayer = nullptr;
Expand Down Expand Up @@ -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<RemoveEntitiesPacket>(ids));
}
level->getTracker()->removeEntity(player);
level->removeEntity(player);
level->getChunkMap()->remove(player);
Expand Down
Loading