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

@@ -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