From ad74d443001ff17c5ab10e4438952e2805e05f38 Mon Sep 17 00:00:00 2001 From: MrTheShy <49885496+MrTheShy@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:32:18 +0100 Subject: Fix joining servers in split screen, splitscreen fixes (#1031) * Fix split-screen join failing when connecting to a remote host via UI When a non-host client connected to a remote server through the in-game UI (as opposed to the -ip/-port command line flags), the global variables g_Win64MultiplayerIP and g_Win64MultiplayerPort were never updated from their defaults ("127.0.0.1" and the default port). JoinSplitScreen() relies on these globals to open a second TCP connection for the local split-screen pad, so it would always attempt to connect to localhost, failing immediately on any remote session. Fix: update g_Win64MultiplayerIP and g_Win64MultiplayerPort inside JoinGame() once the primary connection is established. This ensures subsequent JoinSplitScreen() calls always reach the correct host regardless of how the session was joined. Additionally, guard PushFreeSmallId() against recycling smallIds in the range [0, XUSER_MAX_COUNT), which are permanently reserved for the host's local controller slots. Previously, if a host-side local pad disconnected its smallId could re-enter the free pool and be handed to an incoming remote client, causing that client's IQNetPlayer slot to collide with a local pad slot on the non-host machine. * Fix tutorial popup positioning in split-screen viewports Replace the manual switch-case that computed viewport origin with the shared GetViewportRect/Fit16x9 helpers (from UISplitScreenHelpers.h). This ensures the tutorial popup is positioned and scaled consistently with the rest of the split-screen UI, fitting a 16:9 box inside each viewport and applying safezone offsets correctly. Also adds missing default:break to safezone switch statements to silence compiler warnings. Made-with: Cursor * Prevent split-screen join when game window is not focused Add g_KBMInput.IsWindowFocused() guard to the tryJoin condition so that gamepad input from background windows does not accidentally trigger a split-screen player join. This avoids phantom joins when the user is interacting with another application. * Open debug overlay in fullscreen UI group during split-screen Pass eUIGroup_Fullscreen to NavigateToScene when opening the debug overlay, so it spans the entire window instead of being confined to a single split-screen viewport. This makes the debug info readable regardless of the current split-screen layout. * 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. * Fix fullscreen progress screen swallowing input before load completes Two issues in UIScene_FullscreenProgress::handleInput: 1. The touchpad/button press that triggers movie skip or input forwarding had no guard on m_threadCompleted, so pressing a button during the loading phase would fire the skip/send logic before the background thread finished. Added the m_threadCompleted check so that path is only reachable once the load is actually done. 2. The `handled = true` assignment was missing from that branch, so input events were not being consumed and could fall through to other handlers. Added it unconditionally at the end of the block. * Update player count decrement logic in PlatformNetworkManagerStub Refactor the condition for decrementing the player count in CPlatformNetworkManagerStub::DoWork. The previous check was replaced with a while loop to ensure that the player count is only decremented when there are more than one player and the last player's custom data value is zero. This change improves the handling of player connections in the network manager. * Refactor safe zone calculations in UI components for consistency Updated the safe zone calculations across multiple UI components to ensure symmetry in split viewports. Removed unnecessary assignments and added comments for clarity. Modified the repositionHud function to include an additional parameter for better handling of HUD positioning in split-screen scenarios. * Gui.cpp: fix F3 debug overlay in splitscreen + minor perf cleanup The F3 debug screen was badly broken in splitscreen: it used the GUI coordinate space which gets distorted by the splitscreen scaling, so text appeared stretched, misaligned or completely off-screen depending on the viewport layout. Fixed by setting up a dedicated projection matrix using physical pixel coordinates (g_rScreenWidth / g_rScreenHeight) each time the overlay is drawn, completely decoupled from whatever transform the HUD is using. The viewport dimensions are now computed per screen section so the ortho projection matches the actual pixel area of each player's quadrant. Version and branch strings are only shown for player 0 (iPad == 0) to avoid repeating them across every splitscreen pane. Also removed a few redundant calculations that were being done twice in the same frame (atan for xRot, health halves, air supply scaled value). These are minor and have negligible real-world impact; more substantial per-frame caching work (safe zone calculations etc.) will follow in a separate commit. --- Minecraft.Client/ClientConnection.cpp | 104 +++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 21 deletions(-) (limited to 'Minecraft.Client/ClientConnection.cpp') diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index ba40b43e..325e949b 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(); -- cgit v1.2.3