fix: player list no longer limited by render distance

The previous IQNet cleanup in handleRemoveEntity fired on every entity
despawn, which happens both when a player goes out of tracking range
and when they disconnect.  This caused players to vanish from the Tab
list whenever they moved beyond render distance.

Introduce two custom payload channels (MC|ForkHello, MC|ForkPLeave) so
the client can distinguish "out of range" from "actually left":

- Server sends MC|ForkHello during login to identify itself as a fork
- Server sends MC|ForkPLeave with the player's gamertag on disconnect
- Client skips IQNet cleanup in handleRemoveEntity on fork servers
- Client cleans up IQNet only when MC|ForkPLeave arrives

Fully backwards-compatible: no existing packet wire formats changed.
Upstream clients ignore the unknown channels, fork clients on upstream
servers fall back to the old entity-tracking-based cleanup.
This commit is contained in:
itsRevela 2026-04-01 16:01:56 -05:00
parent 8e2b475317
commit 463bf2b93f
6 changed files with 76 additions and 4 deletions

View file

@ -135,6 +135,7 @@ ClientConnection::ClientConnection(Minecraft *minecraft, Socket *socket, int iUs
started = false;
savedDataStorage = new SavedDataStorage(nullptr);
maxPlayers = 20;
m_isForkServer = false;
this->minecraft = minecraft;
@ -1141,7 +1142,11 @@ void ClientConnection::handleMoveEntitySmall(shared_ptr<MoveEntityPacketSmall> p
void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packet)
{
#ifdef _WINDOWS64
if (!g_NetworkManager.IsHost())
// On fork servers, IQNet cleanup is handled by the MC|ForkPLeave custom
// payload so players stay in Tab regardless of render distance. On
// upstream servers (no MC|ForkHello received), fall back to the old
// behaviour of cleaning up IQNet here.
if (!m_isForkServer && !g_NetworkManager.IsHost())
{
for (int i = 0; i < packet->ids.length; i++)
{
@ -1151,7 +1156,6 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packe
shared_ptr<Player> player = dynamic_pointer_cast<Player>(entity);
if (player != 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)
{
IQNetPlayer* qp = &IQNet::m_player[s];
@ -3900,6 +3904,44 @@ void ClientConnection::handleCustomPayload(shared_ptr<CustomPayloadPacket> custo
}
return;
}
// Fork server identification: enables render-distance-independent player list
if (CustomPayloadPacket::FORK_HELLO_CHANNEL.compare(customPayloadPacket->identifier) == 0)
{
m_isForkServer = true;
app.DebugPrintf("Client: Connected to fork server\n");
return;
}
// Fork server player leave: clean up IQNet slot so player leaves Tab list
if (CustomPayloadPacket::FORK_PLAYER_LEAVE_CHANNEL.compare(customPayloadPacket->identifier) == 0)
{
if (customPayloadPacket->data.data != nullptr && customPayloadPacket->length > 0)
{
int nameLen = customPayloadPacket->length / static_cast<int>(sizeof(wchar_t));
wstring leavingName(reinterpret_cast<const wchar_t*>(customPayloadPacket->data.data), nameLen);
for (int s = 1; s < MINECRAFT_NET_MAX_PLAYERS; ++s)
{
IQNetPlayer* qp = &IQNet::m_player[s];
if (qp->GetCustomDataValue() != 0 &&
_wcsicmp(qp->m_gamertag, leavingName.c_str()) == 0)
{
extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager;
g_pPlatformNetworkManager->NotifyPlayerLeaving(qp);
qp->m_smallId = 0;
qp->m_isRemote = false;
qp->m_isHostPlayer = false;
qp->m_resolvedXuid = INVALID_XUID;
qp->m_gamertag[0] = 0;
qp->SetCustomDataValue(0);
app.DebugPrintf("Client: Player \"%ls\" left fork server, cleared IQNet slot %d\n", leavingName.c_str(), s);
break;
}
}
}
return;
}
#endif
if (CustomPayloadPacket::TRADER_LIST_PACKET.compare(customPayloadPacket->identifier) == 0)

View file

@ -48,6 +48,7 @@ private:
std::unordered_set<int> m_trackedEntityIds;
std::unordered_set<int64_t> m_visibleChunks;
bool m_isForkServer; // true when connected to a fork server (received MC|ForkHello)
static int64_t chunkKey(int x, int z) { return ((int64_t)x << 32) | ((int64_t)z & 0xFFFFFFFF); }

View file

@ -290,6 +290,12 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
playerConnection->send(std::make_shared<SetCarriedItemPacket>(player->inventory->selected));
delete spawnPos;
// Identify this server as a fork so the client can enable extended
// features (e.g. render-distance-independent player list). Upstream
// clients will silently ignore the unknown channel.
playerConnection->send(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::FORK_HELLO_CHANNEL, byteArray()));
updateEntireScoreboard(reinterpret_cast<ServerScoreboard *>(level->getScoreboard()), player);
sendLevelInfo(player, level);
@ -654,7 +660,17 @@ if (player->riding != nullptr)
{
players.erase(it);
}
//broadcastAll(shared_ptr<PlayerInfoPacket>( new PlayerInfoPacket(player->name, false, 9999) ) );
// Notify fork clients that this player has left the server so they can
// clean up IQNet/Tab list entries. Uses a custom payload channel so the
// wire format of existing packets is unchanged (upstream clients simply
// ignore the unknown channel).
{
const wstring& name = player->getName();
byteArray payload(static_cast<int>(name.size() * sizeof(wchar_t)));
memcpy(payload.data, name.c_str(), payload.length);
broadcastAll(std::make_shared<CustomPayloadPacket>(
CustomPayloadPacket::FORK_PLAYER_LEAVE_CHANNEL, payload));
}
removePlayerFromReceiving(player);
player->connection = nullptr; // Must remove reference to connection, or else there is a circular dependency

View file

@ -22,6 +22,9 @@ const wstring CustomPayloadPacket::IDENTITY_TOKEN_ISSUE = L"MC|CTIssue";
const wstring CustomPayloadPacket::IDENTITY_TOKEN_CHALLENGE = L"MC|CTChallenge";
const wstring CustomPayloadPacket::IDENTITY_TOKEN_RESPONSE = L"MC|CTResponse";
const wstring CustomPayloadPacket::FORK_HELLO_CHANNEL = L"MC|ForkHello";
const wstring CustomPayloadPacket::FORK_PLAYER_LEAVE_CHANNEL = L"MC|ForkPLeave";
CustomPayloadPacket::CustomPayloadPacket()
: length(0)
{

View file

@ -27,6 +27,10 @@ public:
static const wstring IDENTITY_TOKEN_CHALLENGE; // server->client: request stored token
static const wstring IDENTITY_TOKEN_RESPONSE; // client->server: present stored token
// Fork extensions: server capability and player lifecycle
static const wstring FORK_HELLO_CHANNEL; // server->client: identifies fork server (empty payload)
static const wstring FORK_PLAYER_LEAVE_CHANNEL; // server->client: player disconnected (payload: UTF gamertag)
wstring identifier;
int length;
byteArray data;

View file

@ -14,6 +14,12 @@ This project is based on source code of Minecraft Legacy Console Edition v1.6.05
## Latest:
### Render-Distance-Independent Player List
- The Tab player list now shows all players on the server regardless of render distance. Previously, the player list was tied to entity tracking -- players only appeared in Tab when their entity was within render distance, and disappeared when they moved out of range
- The fix uses custom payload channels (`MC|ForkHello`, `MC|ForkPLeave`) to signal server identity and player disconnects, so the client can distinguish "player went out of range" from "player left the server." Entity despawns no longer clear the IQNet slot; only an explicit disconnect signal does
- Fully backwards-compatible: no existing packet wire formats are changed. Upstream clients ignore the unknown channels, and fork clients connecting to upstream servers fall back to the old entity-tracking-based behavior
### Graphics Settings Menu Fixes
- Fixed keyboard/gamepad navigation skipping VSync, Fullscreen, and Render Distance options. The SWF focus chain only linked the original console controls; added C++ post-init rewiring of `m_objNavDown`/`m_objNavUp` via Iggy so all controls are reachable
@ -120,7 +126,7 @@ proxy-protocol=true
- The Tab player list now correctly shows all connected players on dedicated servers. Previously only the local player was visible because remote players were never registered in the client's network player tracking when their `AddPlayerPacket` arrived
- The dedicated server's phantom host entry (slot 0, empty name) is now filtered from the list
- Players are properly removed from the list when they disconnect, using gamertag matching since dedicated server XUIDs are not available on the client
- ~~Players are properly removed from the list when they disconnect, using gamertag matching since dedicated server XUIDs are not available on the client~~ This initial implementation incorrectly tied IQNet cleanup to entity removal (`RemoveEntitiesPacket`), which is sent both when a player goes out of render distance and when they disconnect. This caused players to disappear from the Tab list whenever they moved out of tracking range. Fixed in the "Render-Distance-Independent Player List" update above
### SRV Record Support and Async Join Refactor