diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index afa6ec75..b95bbea0 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -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 p void ClientConnection::handleRemoveEntity(shared_ptr 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 packe shared_ptr player = dynamic_pointer_cast(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 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(sizeof(wchar_t)); + wstring leavingName(reinterpret_cast(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) diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index 3448496d..e2abb82a 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -48,6 +48,7 @@ private: std::unordered_set m_trackedEntityIds; std::unordered_set 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); } diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 153f8f45..d45b0bcc 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -290,6 +290,12 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr playerConnection->send(std::make_shared(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::FORK_HELLO_CHANNEL, byteArray())); + updateEntireScoreboard(reinterpret_cast(level->getScoreboard()), player); sendLevelInfo(player, level); @@ -654,7 +660,17 @@ if (player->riding != nullptr) { players.erase(it); } - //broadcastAll(shared_ptr( 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(name.size() * sizeof(wchar_t))); + memcpy(payload.data, name.c_str(), payload.length); + broadcastAll(std::make_shared( + CustomPayloadPacket::FORK_PLAYER_LEAVE_CHANNEL, payload)); + } removePlayerFromReceiving(player); player->connection = nullptr; // Must remove reference to connection, or else there is a circular dependency diff --git a/Minecraft.World/CustomPayloadPacket.cpp b/Minecraft.World/CustomPayloadPacket.cpp index 2ef505cc..208e272b 100644 --- a/Minecraft.World/CustomPayloadPacket.cpp +++ b/Minecraft.World/CustomPayloadPacket.cpp @@ -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) { diff --git a/Minecraft.World/CustomPayloadPacket.h b/Minecraft.World/CustomPayloadPacket.h index d6b7ef35..7445bdf8 100644 --- a/Minecraft.World/CustomPayloadPacket.h +++ b/Minecraft.World/CustomPayloadPacket.h @@ -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; diff --git a/README.md b/README.md index 5e952532..206a53dd 100644 --- a/README.md +++ b/README.md @@ -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