diff options
| author | MrTheShy <49885496+MrTheShy@users.noreply.github.com> | 2026-03-13 07:32:18 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-13 01:32:18 -0500 |
| commit | ad74d443001ff17c5ab10e4438952e2805e05f38 (patch) | |
| tree | 135ccaa323d4877f0b2480b764585a82af26b516 | |
| parent | 714462b130f98558744a06f8dbc4fa6f2f2b3c62 (diff) | |
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.
| -rw-r--r-- | Minecraft.Client/ClientConnection.cpp | 104 | ||||
| -rw-r--r-- | Minecraft.Client/ClientConnection.h | 15 | ||||
| -rw-r--r-- | Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp | 2 | ||||
| -rw-r--r-- | Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp | 20 | ||||
| -rw-r--r-- | Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp | 37 | ||||
| -rw-r--r-- | Minecraft.Client/Common/UI/UIScene.cpp | 21 | ||||
| -rw-r--r-- | Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp | 3 | ||||
| -rw-r--r-- | Minecraft.Client/Common/UI/UIScene_HUD.cpp | 28 | ||||
| -rw-r--r-- | Minecraft.Client/Common/UI/UIScene_HUD.h | 2 | ||||
| -rw-r--r-- | Minecraft.Client/Gui.cpp | 402 | ||||
| -rw-r--r-- | Minecraft.Client/Minecraft.cpp | 6 | ||||
| -rw-r--r-- | Minecraft.Client/MultiPlayerLevel.h | 1 | ||||
| -rw-r--r-- | Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp | 10 |
13 files changed, 369 insertions, 282 deletions
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<AddEntityPacket> 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<AddExperienceOrbPacket> e->xRot = 0; e->entityId = packet->id; level->putEntity(packet->id, e); + m_trackedEntityIds.insert(packet->id); } void ClientConnection::handleAddGlobalEntity(shared_ptr<AddGlobalEntityPacket> packet) @@ -738,13 +788,13 @@ void ClientConnection::handleAddPainting(shared_ptr<AddPaintingPacket> packet) { shared_ptr<Painting> painting = std::make_shared<Painting>(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<SetEntityMotionPacket> 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<AddPlayerPacket> 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<shared_ptr<SynchedEntityData::DataItem> > *unpackedData = packet->getUnpackedData(); if (unpackedData != nullptr) @@ -979,7 +1030,7 @@ void ClientConnection::handleSetCarriedItem(shared_ptr<SetCarriedItemPacket> pac void ClientConnection::handleMoveEntity(shared_ptr<MoveEntityPacket> packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForEntity(packet->id)) return; shared_ptr<Entity> e = getEntity(packet->id); if (e == nullptr) return; e->xp += packet->xa; @@ -1009,7 +1060,7 @@ void ClientConnection::handleRotateMob(shared_ptr<RotateHeadPacket> packet) void ClientConnection::handleMoveEntitySmall(shared_ptr<MoveEntityPacketSmall> packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForEntity(packet->id)) return; shared_ptr<Entity> e = getEntity(packet->id); if (e == nullptr) return; e->xp += packet->xa; @@ -1068,6 +1119,7 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> 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_ptr<ChunkVisibilityAreaP { if (level == NULL) return; for(int z = packet->m_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<ChunkVisibilityPacket> 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<ChunkTilesUpdatePacket> 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<ChunkTilesUpdatePacket> void ClientConnection::handleBlockRegionUpdate(shared_ptr<BlockRegionUpdatePacket> 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<BlockRegionUpdatePacke void ClientConnection::handleTileUpdate(shared_ptr<TileUpdatePacket> 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> packet) void ClientConnection::handleTakeItemEntity(shared_ptr<TakeItemEntityPacket> packet) { - if (!isPrimaryConnection()) return; + if (!shouldProcessForEntity(packet->itemId)) return; shared_ptr<Entity> from = getEntity(packet->itemId); shared_ptr<LivingEntity> to = dynamic_pointer_cast<LivingEntity>(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<AddMobPacket> packet) mob->yd = packet->yd / 8000.0f; mob->zd = packet->zd / 8000.0f; level->putEntity(packet->id, mob); + m_trackedEntityIds.insert(packet->id); vector<shared_ptr<SynchedEntityData::DataItem> > *unpackedData = packet->getUnpackedData(); if (unpackedData != nullptr) @@ -2792,6 +2861,9 @@ void ClientConnection::handleRespawn(shared_ptr<RespawnPacket> 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<RespawnPacket> packet) void ClientConnection::handleExplosion(shared_ptr<ExplodePacket> 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<TileEditorOpenPacket> pac void ClientConnection::handleSignUpdate(shared_ptr<SignUpdatePacket> 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<SignUpdatePacket> packet) void ClientConnection::handleTileEntityData(shared_ptr<TileEntityDataPacket> packet) { - if (!isPrimaryConnection()) return; if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z)) { shared_ptr<TileEntity> te = minecraft->level->getTileEntity(packet->x, packet->y, packet->z); @@ -3331,7 +3400,6 @@ void ClientConnection::handleContainerClose(shared_ptr<ContainerClosePacket> pac void ClientConnection::handleTileEvent(shared_ptr<TileEventPacket> 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<TileEventPacket> packet) void ClientConnection::handleTileDestruction(shared_ptr<TileDestructionPacket> 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<GameEventPacket> gameEventPack void ClientConnection::handleComplexItemData(shared_ptr<ComplexItemDataPacket> 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<ComplexItemDataPacket> p void ClientConnection::handleLevelEvent(shared_ptr<LevelEventPacket> 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<LevelEventPacket> 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<AwardStatPacket> packet) @@ -3660,7 +3724,6 @@ void ClientConnection::handlePlayerAbilities(shared_ptr<PlayerAbilitiesPacket> p void ClientConnection::handleSoundEvent(shared_ptr<LevelSoundPacket> 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<SetPlayerTeamPacket> void ClientConnection::handleParticleEvent(shared_ptr<LevelParticlesPacket> 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 f13b93e7..3448496d 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -1,4 +1,5 @@ #pragma once +#include <unordered_set> #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<int> m_trackedEntityIds; + std::unordered_set<int64_t> 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/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index b32cc934..7340a7e0 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -240,7 +240,7 @@ void CPlatformNetworkManagerStub::DoWork() qnetPlayer->m_resolvedXuid = INVALID_XUID; qnetPlayer->m_gamertag[0] = 0; qnetPlayer->SetCustomDataValue(0); - if (IQNet::s_playerCount > 1) + while (IQNet::s_playerCount > 1 && IQNet::m_player[IQNet::s_playerCount - 1].GetCustomDataValue() == 0) IQNet::s_playerCount--; } // NOTE: Do NOT call PushFreeSmallId here. The old PlayerConnection's diff --git a/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp b/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp index 418546b7..4f60de5f 100644 --- a/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp +++ b/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp @@ -93,18 +93,22 @@ void UIComponent_Tooltips::updateSafeZone() case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: safeTop = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); + break; case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - safeBottom = getSafeZoneHalfHeight(); + safeTop = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); + break; case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: - safeLeft = getSafeZoneHalfWidth(); + safeTop = getSafeZoneHalfHeight(); safeBottom = getSafeZoneHalfHeight(); + safeLeft = getSafeZoneHalfWidth(); break; case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: - safeRight = getSafeZoneHalfWidth(); + safeTop = getSafeZoneHalfHeight(); safeBottom = getSafeZoneHalfHeight(); + break; case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT: safeTop = getSafeZoneHalfHeight(); @@ -112,22 +116,22 @@ void UIComponent_Tooltips::updateSafeZone() break; case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: safeTop = getSafeZoneHalfHeight(); - safeRight = getSafeZoneHalfWidth(); + break; case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: - safeBottom = getSafeZoneHalfHeight(); + safeTop = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); break; case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: - safeBottom = getSafeZoneHalfHeight(); - safeRight = getSafeZoneHalfWidth(); + safeTop = getSafeZoneHalfHeight(); + break; case C4JRender::VIEWPORT_TYPE_FULLSCREEN: default: safeTop = getSafeZoneHalfHeight(); safeBottom = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); - safeRight = getSafeZoneHalfWidth(); + break; } setSafeZone(safeTop, safeBottom, safeLeft, safeRight); diff --git a/Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp b/Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp index fcbd17f3..76d3babf 100644 --- a/Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp +++ b/Minecraft.Client/Common/UI/UIComponent_TutorialPopup.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIComponent_TutorialPopup.h" +#include "UISplitScreenHelpers.h" #include "..\..\Common\Tutorial\Tutorial.h" #include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\MultiplayerLocalPlayer.h" @@ -474,27 +475,17 @@ void UIComponent_TutorialPopup::render(S32 width, S32 height, C4JRender::eViewpo { if(viewport != C4JRender::VIEWPORT_TYPE_FULLSCREEN) { - S32 xPos = 0; - S32 yPos = 0; - switch( viewport ) - { - case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - xPos = static_cast<S32>(ui.getScreenWidth() / 2); - yPos = static_cast<S32>(ui.getScreenHeight() / 2); - break; - case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: - yPos = static_cast<S32>(ui.getScreenHeight() / 2); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: - case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: - case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: - xPos = static_cast<S32>(ui.getScreenWidth() / 2); - break; - case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: - xPos = static_cast<S32>(ui.getScreenWidth() / 2); - yPos = static_cast<S32>(ui.getScreenHeight() / 2); - break; - } + // Derive the viewport origin and fit a 16:9 box inside it (same as UIScene::render), + // then apply safezone nudges so the popup stays clear of screen edges. + F32 originX, originY, viewW, viewH; + GetViewportRect(ui.getScreenWidth(), ui.getScreenHeight(), viewport, originX, originY, viewW, viewH); + + S32 fitW, fitH, offsetX, offsetY; + Fit16x9(viewW, viewH, fitW, fitH, offsetX, offsetY); + + S32 xPos = static_cast<S32>(originX) + offsetX; + S32 yPos = static_cast<S32>(originY) + offsetY; + //Adjust for safezone switch( viewport ) { @@ -505,6 +496,7 @@ void UIComponent_TutorialPopup::render(S32 width, S32 height, C4JRender::eViewpo case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: yPos += getSafeZoneHalfHeight(); break; + default: break; } switch( viewport ) { @@ -515,10 +507,11 @@ void UIComponent_TutorialPopup::render(S32 width, S32 height, C4JRender::eViewpo case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: xPos -= getSafeZoneHalfWidth(); break; + default: break; } ui.setupRenderPosition(xPos, yPos); - IggyPlayerSetDisplaySize( getMovie(), width, height ); + IggyPlayerSetDisplaySize( getMovie(), fitW, fitH ); IggyPlayerDraw( getMovie() ); } else diff --git a/Minecraft.Client/Common/UI/UIScene.cpp b/Minecraft.Client/Common/UI/UIScene.cpp index 0088f43d..303897a7 100644 --- a/Minecraft.Client/Common/UI/UIScene.cpp +++ b/Minecraft.Client/Common/UI/UIScene.cpp @@ -172,15 +172,22 @@ void UIScene::updateSafeZone() { case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: safeTop = getSafeZoneHalfHeight(); + safeLeft = getSafeZoneHalfWidth(); + break; case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - safeBottom = getSafeZoneHalfHeight(); + // safeTop mirrors SPLIT_TOP for visual symmetry. safeBottom omitted. + safeTop = getSafeZoneHalfHeight(); + safeLeft = getSafeZoneHalfWidth(); + break; case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: + safeTop = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); break; case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: - safeRight = getSafeZoneHalfWidth(); + safeTop = getSafeZoneHalfHeight(); + break; case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT: safeTop = getSafeZoneHalfHeight(); @@ -188,22 +195,22 @@ void UIScene::updateSafeZone() break; case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: safeTop = getSafeZoneHalfHeight(); - safeRight = getSafeZoneHalfWidth(); + break; case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: - safeBottom = getSafeZoneHalfHeight(); + safeTop = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); break; case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: - safeBottom = getSafeZoneHalfHeight(); - safeRight = getSafeZoneHalfWidth(); + safeTop = getSafeZoneHalfHeight(); + break; case C4JRender::VIEWPORT_TYPE_FULLSCREEN: default: safeTop = getSafeZoneHalfHeight(); safeBottom = getSafeZoneHalfHeight(); safeLeft = getSafeZoneHalfWidth(); - safeRight = getSafeZoneHalfWidth(); + break; } setSafeZone(safeTop, safeBottom, safeLeft, safeRight); diff --git a/Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp b/Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp index e89c0626..6a4ea096 100644 --- a/Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp +++ b/Minecraft.Client/Common/UI/UIScene_FullscreenProgress.cpp @@ -278,7 +278,7 @@ void UIScene_FullscreenProgress::handleInput(int iPad, int key, bool repeat, boo #ifdef __ORBIS__ case ACTION_MENU_TOUCHPAD_PRESS: #endif - if(pressed) + if(pressed && m_threadCompleted) { sendInputToMovie(key, repeat, pressed, released); } @@ -292,6 +292,7 @@ void UIScene_FullscreenProgress::handleInput(int iPad, int key, bool repeat, boo } break; } + handled = true; } } diff --git a/Minecraft.Client/Common/UI/UIScene_HUD.cpp b/Minecraft.Client/Common/UI/UIScene_HUD.cpp index 0d8adcb2..213caa8d 100644 --- a/Minecraft.Client/Common/UI/UIScene_HUD.cpp +++ b/Minecraft.Client/Common/UI/UIScene_HUD.cpp @@ -65,22 +65,26 @@ void UIScene_HUD::updateSafeZone() case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:
safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
- safeRight = getSafeZoneHalfWidth();
+
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM:
- safeBottom = getSafeZoneHalfHeight();
+ // safeTop mirrors SPLIT_TOP so both players have the same vertical inset
+ // from their viewport's top edge (split divider), keeping GUI symmetrical.
+ // safeBottom is intentionally omitted: it would shift m_Hud.y upward in
+ // ActionScript, placing the hotbar too high relative to SPLIT_TOP.
+ safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
- safeRight = getSafeZoneHalfWidth();
+
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT:
- safeLeft = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
+ safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT:
- safeRight = getSafeZoneHalfWidth();
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
+
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT:
safeTop = getSafeZoneHalfHeight();
@@ -88,22 +92,22 @@ void UIScene_HUD::updateSafeZone() break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT:
safeTop = getSafeZoneHalfHeight();
- safeRight = getSafeZoneHalfWidth();
+
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT:
- safeBottom = getSafeZoneHalfHeight();
+ safeTop = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
break;
case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT:
- safeBottom = getSafeZoneHalfHeight();
- safeRight = getSafeZoneHalfWidth();
+ safeTop = getSafeZoneHalfHeight();
+
break;
case C4JRender::VIEWPORT_TYPE_FULLSCREEN:
default:
safeTop = getSafeZoneHalfHeight();
safeBottom = getSafeZoneHalfHeight();
safeLeft = getSafeZoneHalfWidth();
- safeRight = getSafeZoneHalfWidth();
+
break;
}
setSafeZone(safeTop, safeBottom, safeLeft, safeRight);
@@ -734,7 +738,7 @@ void UIScene_HUD::render(S32 width, S32 height, C4JRender::eViewportType viewpor IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) );
- repositionHud(tileWidth, tileHeight, scale);
+ repositionHud(tileWidth, tileHeight, scale, needsYTile);
m_renderWidth = tileWidth;
m_renderHeight = tileHeight;
@@ -805,7 +809,7 @@ void UIScene_HUD::handleTimerComplete(int id) //setVisible(anyVisible);
}
-void UIScene_HUD::repositionHud(S32 tileWidth, S32 tileHeight, F32 scale)
+void UIScene_HUD::repositionHud(S32 tileWidth, S32 tileHeight, F32 scale, bool needsYTile)
{
if(!m_bSplitscreen) return;
diff --git a/Minecraft.Client/Common/UI/UIScene_HUD.h b/Minecraft.Client/Common/UI/UIScene_HUD.h index 569b5234..04468c8e 100644 --- a/Minecraft.Client/Common/UI/UIScene_HUD.h +++ b/Minecraft.Client/Common/UI/UIScene_HUD.h @@ -176,5 +176,5 @@ protected: #endif private: - void repositionHud(S32 tileWidth, S32 tileHeight, F32 scale); + void repositionHud(S32 tileWidth, S32 tileHeight, F32 scale, bool needsYTile); }; diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index 43b41998..f0d44319 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -443,7 +443,8 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) double maxHealth = minecraft->localplayers[iPad]->getAttribute(SharedMonsterAttributes.MAX_HEALTH); double totalAbsorption = minecraft->localplayers[iPad]->getAbsorptionAmount(); - int numHealthRows = Mth.ceil((maxHealth + totalAbsorption) / 2 / (float) NUM_HEARTS_PER_ROW); + const double healthHalves = (maxHealth + totalAbsorption) / 2.0; + int numHealthRows = Mth.ceil(healthHalves / (float) NUM_HEARTS_PER_ROW); int healthRowHeight = Math.max(10 - (numHealthRows - 2), 3); int yLine2 = yLine1 - (numHealthRows - 1) * healthRowHeight - 10; absorption = totalAbsorption; @@ -469,7 +470,7 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) } //minecraft.profiler.popPush("health"); - for (int i = Mth.ceil((maxHealth + totalAbsorption) / 2) - 1; i >= 0; i--) + for (int i = (int)Mth.ceil(healthHalves) - 1; i >= 0; i--) { int healthTexBaseX = 16; if (minecraft.player.hasEffect(MobEffect.poison)) @@ -607,8 +608,11 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) // render air bubbles if (minecraft->player->isUnderLiquid(Material::water)) { - int count = (int) ceil((minecraft->player->getAirSupply() - 2) * 10.0f / Player::TOTAL_AIR_SUPPLY); - int extra = (int) ceil((minecraft->player->getAirSupply()) * 10.0f / Player::TOTAL_AIR_SUPPLY) - count; + const int airSupply = minecraft->player->getAirSupply(); + const float airScale = 10.0f / Player::TOTAL_AIR_SUPPLY; + const float airSupplyScaled = airSupply * airScale; + int count = (int) ceil((airSupply - 2) * airScale); + int extra = (int) ceil(airSupplyScaled) - count; for (int i = 0; i < count + extra; i++) { // Air bubbles @@ -725,7 +729,8 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) Lighting::turnOn(); glRotatef(-45 - 90, 0, 1, 0); - glRotatef(-(float) atan(yd / 40.0f ) * 20, 1, 0, 0); + const float xRotAngle = -(float) atan(yd / 40.0f) * 20; + glRotatef(xRotAngle, 1, 0, 0); float bodyRot = (minecraft->player->yBodyRotO + (minecraft->player->yBodyRot - minecraft->player->yBodyRotO)); // Fixed rotation angle of degrees, adjusted by bodyRot to negate the rotation that occurs in the renderer // bodyRot in the rotation below is a simplification of "180 - (180 - bodyRot)" where the first 180 is EntityRenderDispatcher::instance->playerRotY that we set below @@ -736,7 +741,7 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) // Set head rotation to body rotation to make head static minecraft->player->yRot = bodyRot; minecraft->player->yRotO = minecraft->player->yRot; - minecraft->player->xRot = -(float) atan(yd / 40.0f) * 20; + minecraft->player->xRot = xRotAngle; minecraft->player->onFire = 0; minecraft->player->setSharedFlag(Entity::FLAG_ONFIRE, false); @@ -849,207 +854,6 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) // font.draw(str, x + 1, y, 0xffffff); // } -#ifndef _FINAL_BUILD - MemSect(31); - - // temporarily render overlay at all times so version is more obvious in bug reports - // we can turn this off once things stabilize - if (true)// minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr) - { - const int debugLeft = 1; - const int debugTop = 1; - const float maxContentWidth = 1200.f; - const float maxContentHeight = 420.f; - float scale = static_cast<float>(screenWidth - debugLeft - 8) / maxContentWidth; - float scaleV = static_cast<float>(screenHeight - debugTop - 80) / maxContentHeight; - if (scaleV < scale) scale = scaleV; - if (scale > 1.f) scale = 1.f; - if (scale < 0.5f) scale = 0.5f; - glPushMatrix(); - glTranslatef(static_cast<float>(debugLeft), static_cast<float>(debugTop), 0.f); - glScalef(scale, scale, 1.f); - glTranslatef(static_cast<float>(-debugLeft), static_cast<float>(-debugTop), 0.f); - - vector<wstring> lines; - - lines.push_back(ClientConstants::VERSION_STRING); - lines.push_back(ClientConstants::BRANCH_STRING); - if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr) - { - lines.push_back(minecraft->fpsString); - lines.push_back(L"E: " + std::to_wstring(minecraft->level->getAllEntities().size())); // Could maybe use entity::shouldRender to work out how many are rendered but thats like expensive - // TODO Add server information with packet counts - once multiplayer is more stable - int renderDistance = app.GetGameSettings(iPad, eGameSetting_RenderDistance); - // Calculate the chunk sections using 16 * (2n + 1)^2 - lines.push_back(L"C: " + std::to_wstring(16 * (2 * renderDistance + 1) * (2 * renderDistance + 1)) + L" D: " + std::to_wstring(renderDistance)); - lines.push_back(minecraft->gatherStats4()); // Chunk Cache - - // Dimension - wstring dimension = L"unknown"; - switch (minecraft->player->dimension) - { - case -1: - dimension = L"minecraft:the_nether"; - break; - case 0: - dimension = L"minecraft:overworld"; - break; - case 1: - dimension = L"minecraft:the_end"; - break; - } - lines.push_back(dimension); - - lines.push_back(L""); // Spacer - - // Players block pos - int xBlockPos = Mth::floor(minecraft->player->x); - int yBlockPos = Mth::floor(minecraft->player->y); - int zBlockPos = Mth::floor(minecraft->player->z); - - // Chunk player is in - int xChunkPos = xBlockPos >> 4; - int yChunkPos = yBlockPos >> 4; - int zChunkPos = zBlockPos >> 4; - - // Players offset within the chunk - int xChunkOffset = xBlockPos & 15; - int yChunkOffset = yBlockPos & 15; - int zChunkOffset = zBlockPos & 15; - - // Format the position like java with limited decumal places - WCHAR posString[44]; // Allows upto 7 digit positions (+-9_999_999) - swprintf(posString, 44, L"%.3f / %.5f / %.3f", minecraft->player->x, minecraft->player->y, minecraft->player->z); - - lines.push_back(L"XYZ: " + std::wstring(posString)); - lines.push_back(L"Block: " + std::to_wstring(static_cast<int>(xBlockPos)) + L" " + std::to_wstring(static_cast<int>(yBlockPos)) + L" " + std::to_wstring(static_cast<int>(zBlockPos))); - lines.push_back(L"Chunk: " + std::to_wstring(xChunkOffset) + L" " + std::to_wstring(yChunkOffset) + L" " + std::to_wstring(zChunkOffset) + L" in " + std::to_wstring(xChunkPos) + L" " + std::to_wstring(yChunkPos) + L" " + std::to_wstring(zChunkPos)); - - // Wrap the yRot to 360 then adjust to (-180 to 180) range to match java - float yRotDisplay = fmod(minecraft->player->yRot, 360.0f); - if (yRotDisplay > 180.0f) - { - yRotDisplay -= 360.0f; - } - if (yRotDisplay < -180.0f) - { - yRotDisplay += 360.0f; - } - // Generate the angle string in the format "yRot / xRot" with one decimal place, similar to java edition - WCHAR angleString[16]; - swprintf(angleString, 16, L"%.1f / %.1f", yRotDisplay, minecraft->player->xRot); - - // Work out the named direction - int direction = Mth::floor(minecraft->player->yRot * 4.0f / 360.0f + 0.5) & 0x3; - wstring cardinalDirection; - switch (direction) - { - case 0: - cardinalDirection = L"south"; - break; - case 1: - cardinalDirection = L"west"; - break; - case 2: - cardinalDirection = L"north"; - break; - case 3: - cardinalDirection = L"east"; - break; - } - - lines.push_back(L"Facing: " + cardinalDirection + L" (" + angleString + L")"); - - // We have to limit y to 256 as we don't get any information past that - if (minecraft->level != NULL && minecraft->level->hasChunkAt(xBlockPos, fmod(yBlockPos, 256), zBlockPos)) - { - LevelChunk *chunkAt = minecraft->level->getChunkAt(xBlockPos, zBlockPos); - if (chunkAt != NULL) - { - int skyLight = chunkAt->getBrightness(LightLayer::Sky, xChunkOffset, yChunkOffset, zChunkOffset); - int blockLight = chunkAt->getBrightness(LightLayer::Block, xChunkOffset, yChunkOffset, zChunkOffset); - int maxLight = fmax(skyLight, blockLight); - lines.push_back(L"Light: " + std::to_wstring(maxLight) + L" (" + std::to_wstring(skyLight) + L" sky, " + std::to_wstring(blockLight) + L" block)"); - - lines.push_back(L"CH S: " + std::to_wstring(chunkAt->getHeightmap(xChunkOffset, zChunkOffset))); - - Biome *biome = chunkAt->getBiome(xChunkOffset, zChunkOffset, minecraft->level->getBiomeSource()); - lines.push_back(L"Biome: " + biome->m_name + L" (" + std::to_wstring(biome->id) + L")"); - - lines.push_back(L"Difficulty: " + std::to_wstring(minecraft->level->difficulty) + L" (Day " + std::to_wstring(minecraft->level->getGameTime() / Level::TICKS_PER_DAY) + L")"); - } - } - - // This is all LCE only stuff, it was never on java - lines.push_back(L""); // Spacer - lines.push_back(L"Seed: " + std::to_wstring(minecraft->level->getLevelData()->getSeed())); - lines.push_back(minecraft->gatherStats1()); // Time to autosave - lines.push_back(minecraft->gatherStats2()); // Empty currently - CPlatformNetworkManagerStub::GatherStats() - lines.push_back(minecraft->gatherStats3()); // RTT - } - -#ifdef _DEBUG // Only show terrain features in debug builds not release - // TERRAIN FEATURES - if (minecraft->level->dimension->id == 0) - { - wstring wfeature[eTerrainFeature_Count]; - - wfeature[eTerrainFeature_Stronghold] = L"Stronghold: "; - wfeature[eTerrainFeature_Mineshaft] = L"Mineshaft: "; - wfeature[eTerrainFeature_Village] = L"Village: "; - wfeature[eTerrainFeature_Ravine] = L"Ravine: "; - - float maxW = static_cast<float>(screenWidth - debugLeft - 8) / scale; - float maxWForContent = maxW - static_cast<float>(font->width(L"...")); - bool truncated[eTerrainFeature_Count] = {}; - - for (size_t i = 0; i < app.m_vTerrainFeatures.size(); i++) - { - FEATURE_DATA *pFeatureData = app.m_vTerrainFeatures[i]; - int type = pFeatureData->eTerrainFeature; - if (type < eTerrainFeature_Stronghold || type > eTerrainFeature_Ravine) - { - continue; - } - if (truncated[type]) - { - continue; - } - - wstring itemInfo = L"[" + std::to_wstring(pFeatureData->x * 16) + L", " + std::to_wstring(pFeatureData->z * 16) + L"] "; - if (font->width(wfeature[type] + itemInfo) <= maxWForContent) - { - wfeature[type] += itemInfo; - } - else - { - wfeature[type] += L"..."; - truncated[type] = true; - } - } - - lines.push_back(L""); // Add a spacer line - for (int i = eTerrainFeature_Stronghold; i <= static_cast<int>(eTerrainFeature_Ravine); i++) - { - lines.push_back(wfeature[i]); - } - lines.push_back(L""); - } -#endif - - // Loop through the lines and draw them all on screen - int yPos = debugTop; - for (const auto &line : lines) - { - drawString(font, line, debugLeft, yPos, 0xffffff); - yPos += 10; - } - - glPopMatrix(); - } - MemSect(0); -#endif - lastTickA = a; // 4J Stu - This is now displayed in a xui scene #if 0 @@ -1203,6 +1007,190 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) glPopMatrix(); } +#ifndef _FINAL_BUILD + MemSect(31); + if (true) + { + // Real window dimensions updated on every WM_SIZE — always current + extern int g_rScreenWidth; + extern int g_rScreenHeight; + + // Set up a fresh projection using physical pixel coordinates so the debug + // text is never distorted regardless of aspect ratio, splitscreen layout, + // or menu state. 1 coordinate unit = 1 physical pixel. + // Compute the actual viewport dimensions for this player's screen section. + // glOrtho must match the viewport exactly for 1 unit = 1 physical pixel. + int vpW = g_rScreenWidth; + int vpH = g_rScreenHeight; + switch (minecraft->player->m_iScreenSection) + { + case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: + case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: + vpH /= 2; + break; + case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: + case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: + vpW /= 2; + break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT: + case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: + case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: + case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: + vpW /= 2; + vpH /= 2; + break; + default: // VIEWPORT_TYPE_FULLSCREEN + break; + } + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(0, vpW, vpH, 0, 1000, 3000); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + glTranslatef(0, 0, -2000); + + // Font was designed for guiScale px/unit; scale up so characters appear + // at the same physical size as the rest of the HUD at 0.5x. + const float fontScale = static_cast<float>(guiScale) * 1.0f; + const int debugLeft = 1; + const int debugTop = 1; + + glTranslatef(static_cast<float>(debugLeft), static_cast<float>(debugTop), 0.f); + glScalef(fontScale, fontScale, 1.f); + glTranslatef(static_cast<float>(-debugLeft), static_cast<float>(-debugTop), 0.f); + + vector<wstring> lines; + + // Only show version/branch for player 0 to avoid cluttering each splitscreen viewport + if (iPad == 0) + { + lines.push_back(ClientConstants::VERSION_STRING); + lines.push_back(ClientConstants::BRANCH_STRING); + } + if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr) + { + lines.push_back(minecraft->fpsString); + lines.push_back(L"E: " + std::to_wstring(minecraft->level->getAllEntities().size())); + int renderDistance = app.GetGameSettings(iPad, eGameSetting_RenderDistance); + lines.push_back(L"C: " + std::to_wstring(16 * (2 * renderDistance + 1) * (2 * renderDistance + 1)) + L" D: " + std::to_wstring(renderDistance)); + lines.push_back(minecraft->gatherStats4()); + + wstring dimension = L"unknown"; + switch (minecraft->player->dimension) + { + case -1: dimension = L"minecraft:the_nether"; break; + case 0: dimension = L"minecraft:overworld"; break; + case 1: dimension = L"minecraft:the_end"; break; + } + lines.push_back(dimension); + lines.push_back(L""); + + int xBlockPos = Mth::floor(minecraft->player->x); + int yBlockPos = Mth::floor(minecraft->player->y); + int zBlockPos = Mth::floor(minecraft->player->z); + int xChunkPos = xBlockPos >> 4; + int yChunkPos = yBlockPos >> 4; + int zChunkPos = zBlockPos >> 4; + int xChunkOffset = xBlockPos & 15; + int yChunkOffset = yBlockPos & 15; + int zChunkOffset = zBlockPos & 15; + + WCHAR posString[44]; + swprintf(posString, 44, L"%.3f / %.5f / %.3f", minecraft->player->x, minecraft->player->y, minecraft->player->z); + + lines.push_back(L"XYZ: " + std::wstring(posString)); + lines.push_back(L"Block: " + std::to_wstring(xBlockPos) + L" " + std::to_wstring(yBlockPos) + L" " + std::to_wstring(zBlockPos)); + lines.push_back(L"Chunk: " + std::to_wstring(xChunkOffset) + L" " + std::to_wstring(yChunkOffset) + L" " + std::to_wstring(zChunkOffset) + L" in " + std::to_wstring(xChunkPos) + L" " + std::to_wstring(yChunkPos) + L" " + std::to_wstring(zChunkPos)); + + float yRotDisplay = fmod(minecraft->player->yRot, 360.0f); + if (yRotDisplay > 180.0f) yRotDisplay -= 360.0f; + if (yRotDisplay < -180.0f) yRotDisplay += 360.0f; + WCHAR angleString[16]; + swprintf(angleString, 16, L"%.1f / %.1f", yRotDisplay, minecraft->player->xRot); + + int direction = Mth::floor(minecraft->player->yRot * 4.0f / 360.0f + 0.5) & 0x3; + const wchar_t* cardinals[] = { L"south", L"west", L"north", L"east" }; + lines.push_back(L"Facing: " + std::wstring(cardinals[direction]) + L" (" + angleString + L")"); + + if (minecraft->level != NULL && minecraft->level->hasChunkAt(xBlockPos, fmod(yBlockPos, 256), zBlockPos)) + { + LevelChunk *chunkAt = minecraft->level->getChunkAt(xBlockPos, zBlockPos); + if (chunkAt != NULL) + { + int skyLight = chunkAt->getBrightness(LightLayer::Sky, xChunkOffset, yChunkOffset, zChunkOffset); + int blockLight = chunkAt->getBrightness(LightLayer::Block, xChunkOffset, yChunkOffset, zChunkOffset); + int maxLight = fmax(skyLight, blockLight); + lines.push_back(L"Light: " + std::to_wstring(maxLight) + L" (" + std::to_wstring(skyLight) + L" sky, " + std::to_wstring(blockLight) + L" block)"); + lines.push_back(L"CH S: " + std::to_wstring(chunkAt->getHeightmap(xChunkOffset, zChunkOffset))); + Biome *biome = chunkAt->getBiome(xChunkOffset, zChunkOffset, minecraft->level->getBiomeSource()); + lines.push_back(L"Biome: " + biome->m_name + L" (" + std::to_wstring(biome->id) + L")"); + lines.push_back(L"Difficulty: " + std::to_wstring(minecraft->level->difficulty) + L" (Day " + std::to_wstring(minecraft->level->getGameTime() / Level::TICKS_PER_DAY) + L")"); + } + } + + lines.push_back(L""); + lines.push_back(L"Seed: " + std::to_wstring(minecraft->level->getLevelData()->getSeed())); + lines.push_back(minecraft->gatherStats1()); + lines.push_back(minecraft->gatherStats2()); + lines.push_back(minecraft->gatherStats3()); + } + +#ifdef _DEBUG + if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr && minecraft->level->dimension->id == 0) + { + wstring wfeature[eTerrainFeature_Count]; + wfeature[eTerrainFeature_Stronghold] = L"Stronghold: "; + wfeature[eTerrainFeature_Mineshaft] = L"Mineshaft: "; + wfeature[eTerrainFeature_Village] = L"Village: "; + wfeature[eTerrainFeature_Ravine] = L"Ravine: "; + + // maxW in font units: physical width divided by font scale + float maxW = (static_cast<float>(g_rScreenWidth) - debugLeft - 8) / fontScale; + float maxWForContent = maxW - static_cast<float>(font->width(L"...")); + bool truncated[eTerrainFeature_Count] = {}; + + for (size_t i = 0; i < app.m_vTerrainFeatures.size(); i++) + { + FEATURE_DATA *pFeatureData = app.m_vTerrainFeatures[i]; + int type = pFeatureData->eTerrainFeature; + if (type < eTerrainFeature_Stronghold || type > eTerrainFeature_Ravine) continue; + if (truncated[type]) continue; + wstring itemInfo = L"[" + std::to_wstring(pFeatureData->x * 16) + L", " + std::to_wstring(pFeatureData->z * 16) + L"] "; + if (font->width(wfeature[type] + itemInfo) <= maxWForContent) + wfeature[type] += itemInfo; + else + { + wfeature[type] += L"..."; + truncated[type] = true; + } + } + + lines.push_back(L""); + for (int i = eTerrainFeature_Stronghold; i <= static_cast<int>(eTerrainFeature_Ravine); i++) + lines.push_back(wfeature[i]); + lines.push_back(L""); + } +#endif + + int yPos = debugTop; + for (const auto &line : lines) + { + drawString(font, line, debugLeft, yPos, 0xffffff); + yPos += 10; + } + + glMatrixMode(GL_MODELVIEW); + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + glMatrixMode(GL_MODELVIEW); + } + MemSect(0); +#endif + glColor4f(1, 1, 1, 1); glDisable(GL_BLEND); glEnable(GL_ALPHA_TEST); diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index aa8fa1fa..11fd81a0 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -1629,7 +1629,7 @@ void Minecraft::run_middle() s_prevXButtons[i] = xCurButtons; } bool startJustPressed = s_startPressLatch[i] > 0; - bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0; + bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0 && g_KBMInput.IsWindowFocused(); #else bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && RenderManager.IsHiDef() && InputManager.ButtonPressed(i); #endif @@ -3706,7 +3706,9 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) app.EnableDebugOverlay(options->renderDebug,iPad); #else // 4J Stu - The xbox uses a completely different way of navigating to this scene - ui.NavigateToScene(0, eUIScene_DebugOverlay, nullptr, eUILayer_Debug); + // Always open in the fullscreen group so the overlay spans the full window + // regardless of split-screen viewport configuration. + ui.NavigateToScene(0, eUIScene_DebugOverlay, nullptr, eUILayer_Debug, eUIGroup_Fullscreen); #endif #endif } diff --git a/Minecraft.Client/MultiPlayerLevel.h b/Minecraft.Client/MultiPlayerLevel.h index a552fc2b..b7f1640a 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; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 28d29504..9d73eda8 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -392,6 +392,11 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port) } s_localSmallId = assignedSmallId; + // Save the host IP and port so JoinSplitScreen can connect to the same host + // regardless of how the connection was initiated (UI vs command line). + strncpy_s(g_Win64MultiplayerIP, sizeof(g_Win64MultiplayerIP), ip, _TRUNCATE); + g_Win64MultiplayerPort = port; + app.DebugPrintf("Win64 LAN: Connected to %s:%d, assigned smallId=%d\n", ip, port, s_localSmallId); s_active = true; @@ -733,6 +738,11 @@ bool WinsockNetLayer::PopDisconnectedSmallId(BYTE* outSmallId) void WinsockNetLayer::PushFreeSmallId(BYTE smallId) { + // SmallIds 0..(XUSER_MAX_COUNT-1) are permanently reserved for the host's + // local pads and must never be recycled to remote clients. + if (smallId < (BYTE)XUSER_MAX_COUNT) + return; + EnterCriticalSection(&s_freeSmallIdLock); // Guard against double-recycle: the reconnect path (queueSmallIdForRecycle) and // the DoWork disconnect path can both push the same smallId. If we allow duplicates, |
