From a58aaf2662f10fda9e4ad5c50999d729bdf6d0c8 Mon Sep 17 00:00:00 2001 From: MrTheShy <49885496+MrTheShy@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:15:03 +0100 Subject: [PATCH] Fix non-host split-screen connections missing world updates Previously, secondary (non-host) split-screen connections used isPrimaryConnection() to gate nearly all world update packets, meaning the second local player would never receive tile updates, entity movement, sounds, particles, explosions, etc. The fix introduces per-connection tracking of which entities and chunks each ClientConnection has loaded, and uses that information to decide whether a secondary connection needs to process a given packet or if the primary connection already handled it. New members in ClientConnection: - m_trackedEntityIds: set of entity IDs this connection has received AddEntity/AddMob/AddPlayer etc. for - m_visibleChunks: set of chunk coordinates (packed into int64) this connection has marked visible - Both sets are cleared on close(), respawn (dimension change), and destructor New helpers: - findPrimaryConnection(): walks the MultiPlayerLevel connection list to find the connection on the primary pad - shouldProcessForEntity(id): secondary connection skips the packet only if the primary is already tracking that entity - shouldProcessForPosition(x, z): secondary connection skips the packet only if the primary already has that chunk visible - anyOtherConnectionHasChunk(x, z): used when a chunk becomes invisible to avoid hiding it from the level if another connection still needs it - isTrackingEntity(id): public accessor used by shouldProcessForEntity on the primary connection Packet handler changes: - handleMoveEntity, handleMoveEntitySmall, handleSetEntityMotion, handleTakeItemEntity: replaced isPrimaryConnection() with shouldProcessForEntity() so secondary connections still process movement for entities they know about - handleExplosion, handleLevelEvent: replaced isPrimaryConnection() with shouldProcessForPosition() so block destruction and level events fire for the correct connection based on chunks - handleChunkTilesUpdate, handleBlockRegionUpdate, handleTileUpdate, handleSignUpdate, handleTileEntityData, handleTileEvent, handleTileDestruction, handleComplexItemData, handleSoundEvent, handleParticleEvent: removed the isPrimaryConnection() guard entirely -- these are world-state updates that all connections must process regardless of which pad is primary - handleChunkVisibilityArea / handleChunkVisibility: now populate m_visibleChunks; on visibility=false, setChunkVisible(false) is only called on the level if no other connection still has that chunk loaded - handleAddEntity, handleAddExperienceOrb, handleAddPainting, handleAddPlayer, handleAddMob: now insert into m_trackedEntityIds on arrival - handleRemoveEntity: now erases from m_trackedEntityIds on removal - handleLevelEvent: removed a duplicate levelEvent() call that was always firing regardless of the isPrimaryConnection() check above it (latent bug) MultiPlayerLevel: added friend class ClientConnection to allow access to the connections list without exposing it publicly. --- Minecraft.Client/ClientConnection.cpp | 104 ++++++++++++++++++++------ Minecraft.Client/ClientConnection.h | 15 ++++ Minecraft.Client/MultiPlayerLevel.h | 1 + 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index ba40b43ee..325e949bb 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -149,8 +149,56 @@ bool ClientConnection::isPrimaryConnection() const return g_NetworkManager.IsHost() || m_userIndex == ProfileManager.GetPrimaryPad(); } +ClientConnection* ClientConnection::findPrimaryConnection() const +{ + if (level == nullptr) return nullptr; + int primaryPad = ProfileManager.GetPrimaryPad(); + MultiPlayerLevel* mpLevel = (MultiPlayerLevel*)level; + for (ClientConnection* conn : mpLevel->connections) + { + if (conn != this && conn->m_userIndex == primaryPad) + return conn; + } + return nullptr; +} + +bool ClientConnection::shouldProcessForEntity(int entityId) const +{ + if (g_NetworkManager.IsHost()) return true; + if (m_userIndex == ProfileManager.GetPrimaryPad()) return true; + + ClientConnection* primary = findPrimaryConnection(); + if (primary == nullptr) return true; + return !primary->isTrackingEntity(entityId); +} + +bool ClientConnection::shouldProcessForPosition(int blockX, int blockZ) const +{ + if (g_NetworkManager.IsHost()) return true; + if (m_userIndex == ProfileManager.GetPrimaryPad()) return true; + + ClientConnection* primary = findPrimaryConnection(); + if (primary == nullptr) return true; + return !primary->m_visibleChunks.count(chunkKey(blockX >> 4, blockZ >> 4)); +} + +bool ClientConnection::anyOtherConnectionHasChunk(int x, int z) const +{ + if (level == nullptr) return false; + MultiPlayerLevel* mpLevel = (MultiPlayerLevel*)level; + int64_t key = chunkKey(x, z); + for (ClientConnection* conn : mpLevel->connections) + { + if (conn != this && conn->m_visibleChunks.count(key)) + return true; + } + return false; +} + ClientConnection::~ClientConnection() { + m_trackedEntityIds.clear(); + m_visibleChunks.clear(); delete connection; delete random; delete savedDataStorage; @@ -664,6 +712,7 @@ void ClientConnection::handleAddEntity(shared_ptr packet) } e->entityId = packet->id; level->putEntity(packet->id, e); + m_trackedEntityIds.insert(packet->id); if (packet->data > -1) // 4J - changed "no data" value to be -1, we can have a valid entity id of 0 { @@ -712,6 +761,7 @@ void ClientConnection::handleAddExperienceOrb(shared_ptr e->xRot = 0; e->entityId = packet->id; level->putEntity(packet->id, e); + m_trackedEntityIds.insert(packet->id); } void ClientConnection::handleAddGlobalEntity(shared_ptr packet) @@ -738,13 +788,13 @@ void ClientConnection::handleAddPainting(shared_ptr packet) { shared_ptr painting = std::make_shared(level, packet->x, packet->y, packet->z, packet->dir, packet->motive); level->putEntity(packet->id, painting); + m_trackedEntityIds.insert(packet->id); } void ClientConnection::handleSetEntityMotion(shared_ptr packet) { - if (!isPrimaryConnection()) + if (!shouldProcessForEntity(packet->id)) { - // Secondary connection: only accept motion for our own local player (knockback) if (minecraft->localplayers[m_userIndex] == NULL || packet->id != minecraft->localplayers[m_userIndex]->entityId) return; @@ -939,6 +989,7 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) app.DebugPrintf("Custom cape for player %ls is %ls\n",player->name.c_str(),player->customTextureUrl2.c_str()); level->putEntity(packet->id, player); + m_trackedEntityIds.insert(packet->id); vector > *unpackedData = packet->getUnpackedData(); if (unpackedData != nullptr) @@ -979,7 +1030,7 @@ void ClientConnection::handleSetCarriedItem(shared_ptr pac void ClientConnection::handleMoveEntity(shared_ptr packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForEntity(packet->id)) return; shared_ptr e = getEntity(packet->id); if (e == nullptr) return; e->xp += packet->xa; @@ -1009,7 +1060,7 @@ void ClientConnection::handleRotateMob(shared_ptr packet) void ClientConnection::handleMoveEntitySmall(shared_ptr packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForEntity(packet->id)) return; shared_ptr e = getEntity(packet->id); if (e == nullptr) return; e->xp += packet->xa; @@ -1068,6 +1119,7 @@ void ClientConnection::handleRemoveEntity(shared_ptr packe #endif for (int i = 0; i < packet->ids.length; i++) { + m_trackedEntityIds.erase(packet->ids[i]); level->removeEntity(packet->ids[i]); } } @@ -1136,19 +1188,35 @@ void ClientConnection::handleChunkVisibilityArea(shared_ptrm_minZ; z <= packet->m_maxZ; ++z) + { for(int x = packet->m_minX; x <= packet->m_maxX; ++x) + { + m_visibleChunks.insert(chunkKey(x, z)); level->setChunkVisible(x, z, true); + } + } } void ClientConnection::handleChunkVisibility(shared_ptr packet) { if (level == NULL) return; - level->setChunkVisible(packet->x, packet->z, packet->visible); + if (packet->visible) + { + m_visibleChunks.insert(chunkKey(packet->x, packet->z)); + level->setChunkVisible(packet->x, packet->z, true); + } + else + { + m_visibleChunks.erase(chunkKey(packet->x, packet->z)); + if (!anyOtherConnectionHasChunk(packet->x, packet->z)) + { + level->setChunkVisible(packet->x, packet->z, false); + } + } } void ClientConnection::handleChunkTilesUpdate(shared_ptr packet) { - if (!isPrimaryConnection()) return; // 4J - changed to encode level in packet MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->levels[packet->levelIdx]; if( dimensionLevel ) @@ -1218,7 +1286,6 @@ void ClientConnection::handleChunkTilesUpdate(shared_ptr void ClientConnection::handleBlockRegionUpdate(shared_ptr packet) { - if (!isPrimaryConnection()) return; // 4J - changed to encode level in packet MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->levels[packet->levelIdx]; if( dimensionLevel ) @@ -1279,7 +1346,6 @@ void ClientConnection::handleBlockRegionUpdate(shared_ptr packet) { - if (!isPrimaryConnection()) return; // 4J added - using a block of 255 to signify that this is a packet for destroying a tile, where we need to inform the level renderer that we are about to do so. // This is used in creative mode as the point where a tile is first destroyed at the client end of things. Packets formed like this are potentially sent from // ServerPlayerGameMode::destroyBlock @@ -1394,7 +1460,7 @@ void ClientConnection::send(shared_ptr packet) void ClientConnection::handleTakeItemEntity(shared_ptr packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForEntity(packet->itemId)) return; shared_ptr from = getEntity(packet->itemId); shared_ptr to = dynamic_pointer_cast(getEntity(packet->playerId)); @@ -2414,6 +2480,8 @@ void ClientConnection::close() // If it's already done, then we don't need to do anything here. And in fact trying to do something could cause a crash if(done) return; done = true; + m_trackedEntityIds.clear(); + m_visibleChunks.clear(); connection->flush(); connection->close(DisconnectPacket::eDisconnect_Closed); } @@ -2453,6 +2521,7 @@ void ClientConnection::handleAddMob(shared_ptr packet) mob->yd = packet->yd / 8000.0f; mob->zd = packet->zd / 8000.0f; level->putEntity(packet->id, mob); + m_trackedEntityIds.insert(packet->id); vector > *unpackedData = packet->getUnpackedData(); if (unpackedData != nullptr) @@ -2792,6 +2861,9 @@ void ClientConnection::handleRespawn(shared_ptr packet) // so it doesn't leak into the new dimension level->playStreamingMusic(L"", 0, 0, 0); + m_trackedEntityIds.clear(); + m_visibleChunks.clear(); + // Remove client connection from this level level->removeClientConnection(this, false); @@ -2899,8 +2971,7 @@ void ClientConnection::handleRespawn(shared_ptr packet) void ClientConnection::handleExplosion(shared_ptr packet) { - // World modification (block destruction) must only happen once - if (isPrimaryConnection()) + if (shouldProcessForPosition((int)packet->x, (int)packet->z)) { if(!packet->m_bKnockbackOnly) { @@ -3244,7 +3315,6 @@ void ClientConnection::handleTileEditorOpen(shared_ptr pac void ClientConnection::handleSignUpdate(shared_ptr packet) { - if (!isPrimaryConnection()) return; app.DebugPrintf("ClientConnection::handleSignUpdate - "); if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z)) { @@ -3278,7 +3348,6 @@ void ClientConnection::handleSignUpdate(shared_ptr packet) void ClientConnection::handleTileEntityData(shared_ptr packet) { - if (!isPrimaryConnection()) return; if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z)) { shared_ptr te = minecraft->level->getTileEntity(packet->x, packet->y, packet->z); @@ -3331,7 +3400,6 @@ void ClientConnection::handleContainerClose(shared_ptr pac void ClientConnection::handleTileEvent(shared_ptr packet) { - if (!isPrimaryConnection()) return; PIXBeginNamedEvent(0,"Handle tile event\n"); minecraft->level->tileEvent(packet->x, packet->y, packet->z, packet->tile, packet->b0, packet->b1); PIXEndNamedEvent(); @@ -3339,7 +3407,6 @@ void ClientConnection::handleTileEvent(shared_ptr packet) void ClientConnection::handleTileDestruction(shared_ptr packet) { - if (!isPrimaryConnection()) return; minecraft->level->destroyTileProgress(packet->getEntityId(), packet->getX(), packet->getY(), packet->getZ(), packet->getState()); } @@ -3421,7 +3488,6 @@ void ClientConnection::handleGameEvent(shared_ptr gameEventPack void ClientConnection::handleComplexItemData(shared_ptr packet) { - if (!isPrimaryConnection()) return; if (packet->itemType == Item::map->id) { MapItem::getSavedData(packet->itemId, minecraft->level)->handleComplexItemData(packet->data); @@ -3436,7 +3502,7 @@ void ClientConnection::handleComplexItemData(shared_ptr p void ClientConnection::handleLevelEvent(shared_ptr packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForPosition(packet->x, packet->z)) return; if (packet->type == LevelEvent::SOUND_DRAGON_DEATH) { for(unsigned int i = 0; i < XUSER_MAX_COUNT; ++i) @@ -3456,8 +3522,6 @@ void ClientConnection::handleLevelEvent(shared_ptr packet) { minecraft->level->levelEvent(packet->type, packet->x, packet->y, packet->z, packet->data); } - - minecraft->level->levelEvent(packet->type, packet->x, packet->y, packet->z, packet->data); } void ClientConnection::handleAwardStat(shared_ptr packet) @@ -3660,7 +3724,6 @@ void ClientConnection::handlePlayerAbilities(shared_ptr p void ClientConnection::handleSoundEvent(shared_ptr packet) { - if (!isPrimaryConnection()) return; minecraft->level->playLocalSound(packet->getX(), packet->getY(), packet->getZ(), packet->getSound(), packet->getVolume(), packet->getPitch(), false); } @@ -3973,7 +4036,6 @@ void ClientConnection::handleSetPlayerTeamPacket(shared_ptr void ClientConnection::handleParticleEvent(shared_ptr packet) { - if (!isPrimaryConnection()) return; for (int i = 0; i < packet->getCount(); i++) { double xVarience = random->nextGaussian() * packet->getXDist(); diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index f13b93e7f..3448496d0 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -1,4 +1,5 @@ #pragma once +#include #include "..\Minecraft.World\net.minecraft.network.h" class Minecraft; class MultiPlayerLevel; @@ -44,6 +45,20 @@ public: private: DWORD m_userIndex; // 4J Added bool isPrimaryConnection() const; + + std::unordered_set m_trackedEntityIds; + std::unordered_set m_visibleChunks; + + static int64_t chunkKey(int x, int z) { return ((int64_t)x << 32) | ((int64_t)z & 0xFFFFFFFF); } + + ClientConnection* findPrimaryConnection() const; + bool shouldProcessForEntity(int entityId) const; + bool shouldProcessForPosition(int blockX, int blockZ) const; + bool anyOtherConnectionHasChunk(int x, int z) const; + +public: + bool isTrackingEntity(int entityId) const { return m_trackedEntityIds.count(entityId) > 0; } + public: SavedDataStorage *savedDataStorage; ClientConnection(Minecraft *minecraft, const wstring& ip, int port); diff --git a/Minecraft.Client/MultiPlayerLevel.h b/Minecraft.Client/MultiPlayerLevel.h index a552fc2b1..b7f1640a3 100644 --- a/Minecraft.Client/MultiPlayerLevel.h +++ b/Minecraft.Client/MultiPlayerLevel.h @@ -12,6 +12,7 @@ using namespace std; class MultiPlayerLevel : public Level { + friend class ClientConnection; private: static const int TICKS_BEFORE_RESET = 20 * 4;