From 993052409a2ff6c997aea77f8f8fe143d08f5993 Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Tue, 24 Mar 2026 04:04:07 -0500 Subject: [PATCH 01/13] Fix XUID logging (#1395) * pass invalid_xuid to other players * actually more simple fix --- Minecraft.Client/TrackedEntity.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Minecraft.Client/TrackedEntity.cpp b/Minecraft.Client/TrackedEntity.cpp index 3aa33248d..380750449 100644 --- a/Minecraft.Client/TrackedEntity.cpp +++ b/Minecraft.Client/TrackedEntity.cpp @@ -653,11 +653,12 @@ shared_ptr TrackedEntity::getAddEntityPacket() PlayerUID xuid = INVALID_XUID; PlayerUID OnlineXuid = INVALID_XUID; - if( player != nullptr ) - { - xuid = player->getXuid(); - OnlineXuid = player->getOnlineXuid(); - } + // do not pass xuid/onlinxuid to cleints + //if( player != nullptr ) + //{ + // xuid = player->getXuid(); + // OnlineXuid = player->getOnlineXuid(); + //} // 4J Added yHeadRotp param to fix #102563 - TU12: Content: Gameplay: When one of the Players is idle for a few minutes his head turns 180 degrees. return std::make_shared(player, xuid, OnlineXuid, xp, yp, zp, yRotp, xRotp, yHeadRotp); } From a24318eedc44af2f5967c4727b7ebf578b8e233a Mon Sep 17 00:00:00 2001 From: ModMaker101 <119018978+ModMaker101@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:25:18 -0400 Subject: [PATCH 02/13] Memory leak fix: Make chunks unload properly (#1406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix chunk unload and cleanup logic, fixes #1347 * Applying formatting to code I edited 😝 --- Minecraft.Client/MultiPlayerChunkCache.cpp | 25 ++++++---- Minecraft.Client/PlayerChunkMap.cpp | 25 ++++++++++ Minecraft.Client/PlayerList.cpp | 11 ++++- Minecraft.Client/ServerChunkCache.cpp | 53 +++++++--------------- 4 files changed, 68 insertions(+), 46 deletions(-) diff --git a/Minecraft.Client/MultiPlayerChunkCache.cpp b/Minecraft.Client/MultiPlayerChunkCache.cpp index 62361ce36..03c47fcca 100644 --- a/Minecraft.Client/MultiPlayerChunkCache.cpp +++ b/Minecraft.Client/MultiPlayerChunkCache.cpp @@ -139,19 +139,26 @@ bool MultiPlayerChunkCache::reallyHasChunk(int x, int z) return hasData[idx]; } -void MultiPlayerChunkCache::drop(int x, int z) +void MultiPlayerChunkCache::drop(const int x, const int z) { - // 4J Stu - We do want to drop any entities in the chunks, especially for the case when a player is dead as they will - // not get the RemoveEntity packet if an entity is removed. - LevelChunk *chunk = getChunk(x, z); - if (!chunk->isEmpty()) + const int ix = x + XZOFFSET; + const int iz = z + XZOFFSET; + if ((ix < 0) || (ix >= XZSIZE)) return; + if ((iz < 0) || (iz >= XZSIZE)) return; + const int idx = ix * XZSIZE + iz; + LevelChunk* chunk = cache[idx]; + + if (chunk != nullptr && !chunk->isEmpty()) { - // Added parameter here specifies that we don't want to delete tile entities, as they won't get recreated unless they've got update packets - // The tile entities are in general only created on the client by virtue of the chunk rebuild + // Unload chunk but keep tile entities chunk->unload(false); - // 4J - We just want to clear out the entities in the chunk, but everything else should be valid - chunk->loaded = true; + const auto it = std::find(loadedChunkList.begin(), loadedChunkList.end(), chunk); + if (it != loadedChunkList.end()) loadedChunkList.erase(it); + + cache[idx] = nullptr; + hasData[idx] = false; + chunk->loaded = false; } } diff --git a/Minecraft.Client/PlayerChunkMap.cpp b/Minecraft.Client/PlayerChunkMap.cpp index bcc3f6ba0..ddf2bae2f 100644 --- a/Minecraft.Client/PlayerChunkMap.cpp +++ b/Minecraft.Client/PlayerChunkMap.cpp @@ -792,6 +792,14 @@ void PlayerChunkMap::setRadius(int newRadius) int xc = static_cast(player->x) >> 4; int zc = static_cast(player->z) >> 4; + for (auto it = addRequests.begin(); it != addRequests.end(); ) + { + if (it->player == player) + it = addRequests.erase(it); + else + ++it; + } + for (int x = xc - newRadius; x <= xc + newRadius; x++) for (int z = zc - newRadius; z <= zc + newRadius; z++) { @@ -801,9 +809,26 @@ void PlayerChunkMap::setRadius(int newRadius) getChunkAndAddPlayer(x, z, player); } } + + // Remove chunks that are outside the new radius + for (int x = xc - radius; x <= xc + radius; x++) + { + for (int z = zc - radius; z <= zc + radius; z++) + { + if (x < xc - newRadius || x > xc + newRadius || z < zc - newRadius || z > zc + newRadius) + { + getChunkAndRemovePlayer(x, z, player); + } + } + } } } + if (newRadius < radius) + { + level->cache->dropAll(); + } + assert(radius <= MAX_VIEW_DISTANCE); assert(radius >= MIN_VIEW_DISTANCE); this->radius = newRadius; diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index ba82ec6ac..331539cb4 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -1690,7 +1690,16 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) } // AP added for Vita so the range can be increased once the level starts -void PlayerList::setViewDistance(int newViewDistance) +void PlayerList::setViewDistance(const int newViewDistance) { viewDistance = newViewDistance; + + for (size_t i = 0; i < server->levels.length; i++) + { + ServerLevel* level = server->levels[i]; + if (level != nullptr) + { + level->getChunkMap()->setRadius(newViewDistance); + } + } } diff --git a/Minecraft.Client/ServerChunkCache.cpp b/Minecraft.Client/ServerChunkCache.cpp index c7d70c7d3..54312ffa1 100644 --- a/Minecraft.Client/ServerChunkCache.cpp +++ b/Minecraft.Client/ServerChunkCache.cpp @@ -80,54 +80,31 @@ vector *ServerChunkCache::getLoadedChunkList() return &m_loadedChunkList; } -void ServerChunkCache::drop(int x, int z) +void ServerChunkCache::drop(const int x, const int z) { - // 4J - we're not dropping things anymore now that we have a fixed sized cache -#ifdef _LARGE_WORLDS + const int ix = x + XZOFFSET; + const int iz = z + XZOFFSET; + if ((ix < 0) || (ix >= XZSIZE)) return; + if ((iz < 0) || (iz >= XZSIZE)) return; + const int idx = ix * XZSIZE + iz; + LevelChunk* chunk = cache[idx]; - bool canDrop = false; -// if (level->dimension->mayRespawn()) -// { -// Pos *spawnPos = level->getSharedSpawnPos(); -// int xd = x * 16 + 8 - spawnPos->x; -// int zd = z * 16 + 8 - spawnPos->z; -// delete spawnPos; -// int r = 128; -// if (xd < -r || xd > r || zd < -r || zd > r) -// { -// canDrop = true; -//} -// } -// else + if (chunk != nullptr) { - canDrop = true; - } - if(canDrop) - { - int ix = x + XZOFFSET; - int iz = z + XZOFFSET; - // Check we're in range of the stored level - if( ( ix < 0 ) || ( ix >= XZSIZE ) ) return; - if( ( iz < 0 ) || ( iz >= XZSIZE ) ) return; - int idx = ix * XZSIZE + iz; - LevelChunk *chunk = cache[idx]; + const auto it = std::find(m_loadedChunkList.begin(), m_loadedChunkList.end(), chunk); + if (it != m_loadedChunkList.end()) m_loadedChunkList.erase(it); - if(chunk) - { - m_toDrop.push_back(chunk); - } + cache[idx] = nullptr; + chunk->loaded = false; } -#endif } void ServerChunkCache::dropAll() { -#ifdef _LARGE_WORLDS for (LevelChunk *chunk : m_loadedChunkList) { drop(chunk->x, chunk->z); -} -#endif + } } // 4J - this is the original (and virtual) interface to create @@ -957,6 +934,10 @@ bool ServerChunkCache::tick() m_unloadedCache[idx] = chunk; cache[idx] = nullptr; } + else + { + continue; + } } m_toDrop.pop_front(); } From dee559bd16e5fc4fb1d8cdd16e7e3924666b01c9 Mon Sep 17 00:00:00 2001 From: Loki Rautio Date: Thu, 26 Mar 2026 01:37:23 -0500 Subject: [PATCH 03/13] Revert "Memory leak fix: Make chunks unload properly (#1406)" This reverts commit a24318eedc44af2f5967c4727b7ebf578b8e233a. This fix introduces broken behavior for dedicated servers. It will be merged back in once the related issue is fixed --- Minecraft.Client/MultiPlayerChunkCache.cpp | 25 ++++------ Minecraft.Client/PlayerChunkMap.cpp | 25 ---------- Minecraft.Client/PlayerList.cpp | 11 +---- Minecraft.Client/ServerChunkCache.cpp | 55 +++++++++++++++------- 4 files changed, 47 insertions(+), 69 deletions(-) diff --git a/Minecraft.Client/MultiPlayerChunkCache.cpp b/Minecraft.Client/MultiPlayerChunkCache.cpp index 03c47fcca..62361ce36 100644 --- a/Minecraft.Client/MultiPlayerChunkCache.cpp +++ b/Minecraft.Client/MultiPlayerChunkCache.cpp @@ -139,26 +139,19 @@ bool MultiPlayerChunkCache::reallyHasChunk(int x, int z) return hasData[idx]; } -void MultiPlayerChunkCache::drop(const int x, const int z) +void MultiPlayerChunkCache::drop(int x, int z) { - const int ix = x + XZOFFSET; - const int iz = z + XZOFFSET; - if ((ix < 0) || (ix >= XZSIZE)) return; - if ((iz < 0) || (iz >= XZSIZE)) return; - const int idx = ix * XZSIZE + iz; - LevelChunk* chunk = cache[idx]; - - if (chunk != nullptr && !chunk->isEmpty()) + // 4J Stu - We do want to drop any entities in the chunks, especially for the case when a player is dead as they will + // not get the RemoveEntity packet if an entity is removed. + LevelChunk *chunk = getChunk(x, z); + if (!chunk->isEmpty()) { - // Unload chunk but keep tile entities + // Added parameter here specifies that we don't want to delete tile entities, as they won't get recreated unless they've got update packets + // The tile entities are in general only created on the client by virtue of the chunk rebuild chunk->unload(false); - const auto it = std::find(loadedChunkList.begin(), loadedChunkList.end(), chunk); - if (it != loadedChunkList.end()) loadedChunkList.erase(it); - - cache[idx] = nullptr; - hasData[idx] = false; - chunk->loaded = false; + // 4J - We just want to clear out the entities in the chunk, but everything else should be valid + chunk->loaded = true; } } diff --git a/Minecraft.Client/PlayerChunkMap.cpp b/Minecraft.Client/PlayerChunkMap.cpp index ddf2bae2f..bcc3f6ba0 100644 --- a/Minecraft.Client/PlayerChunkMap.cpp +++ b/Minecraft.Client/PlayerChunkMap.cpp @@ -792,14 +792,6 @@ void PlayerChunkMap::setRadius(int newRadius) int xc = static_cast(player->x) >> 4; int zc = static_cast(player->z) >> 4; - for (auto it = addRequests.begin(); it != addRequests.end(); ) - { - if (it->player == player) - it = addRequests.erase(it); - else - ++it; - } - for (int x = xc - newRadius; x <= xc + newRadius; x++) for (int z = zc - newRadius; z <= zc + newRadius; z++) { @@ -809,26 +801,9 @@ void PlayerChunkMap::setRadius(int newRadius) getChunkAndAddPlayer(x, z, player); } } - - // Remove chunks that are outside the new radius - for (int x = xc - radius; x <= xc + radius; x++) - { - for (int z = zc - radius; z <= zc + radius; z++) - { - if (x < xc - newRadius || x > xc + newRadius || z < zc - newRadius || z > zc + newRadius) - { - getChunkAndRemovePlayer(x, z, player); - } - } - } } } - if (newRadius < radius) - { - level->cache->dropAll(); - } - assert(radius <= MAX_VIEW_DISTANCE); assert(radius >= MIN_VIEW_DISTANCE); this->radius = newRadius; diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 331539cb4..ba82ec6ac 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -1690,16 +1690,7 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) } // AP added for Vita so the range can be increased once the level starts -void PlayerList::setViewDistance(const int newViewDistance) +void PlayerList::setViewDistance(int newViewDistance) { viewDistance = newViewDistance; - - for (size_t i = 0; i < server->levels.length; i++) - { - ServerLevel* level = server->levels[i]; - if (level != nullptr) - { - level->getChunkMap()->setRadius(newViewDistance); - } - } } diff --git a/Minecraft.Client/ServerChunkCache.cpp b/Minecraft.Client/ServerChunkCache.cpp index 54312ffa1..c7d70c7d3 100644 --- a/Minecraft.Client/ServerChunkCache.cpp +++ b/Minecraft.Client/ServerChunkCache.cpp @@ -80,31 +80,54 @@ vector *ServerChunkCache::getLoadedChunkList() return &m_loadedChunkList; } -void ServerChunkCache::drop(const int x, const int z) +void ServerChunkCache::drop(int x, int z) { - const int ix = x + XZOFFSET; - const int iz = z + XZOFFSET; - if ((ix < 0) || (ix >= XZSIZE)) return; - if ((iz < 0) || (iz >= XZSIZE)) return; - const int idx = ix * XZSIZE + iz; - LevelChunk* chunk = cache[idx]; + // 4J - we're not dropping things anymore now that we have a fixed sized cache +#ifdef _LARGE_WORLDS - if (chunk != nullptr) + bool canDrop = false; +// if (level->dimension->mayRespawn()) +// { +// Pos *spawnPos = level->getSharedSpawnPos(); +// int xd = x * 16 + 8 - spawnPos->x; +// int zd = z * 16 + 8 - spawnPos->z; +// delete spawnPos; +// int r = 128; +// if (xd < -r || xd > r || zd < -r || zd > r) +// { +// canDrop = true; +//} +// } +// else { - const auto it = std::find(m_loadedChunkList.begin(), m_loadedChunkList.end(), chunk); - if (it != m_loadedChunkList.end()) m_loadedChunkList.erase(it); - - cache[idx] = nullptr; - chunk->loaded = false; + canDrop = true; } + if(canDrop) + { + int ix = x + XZOFFSET; + int iz = z + XZOFFSET; + // Check we're in range of the stored level + if( ( ix < 0 ) || ( ix >= XZSIZE ) ) return; + if( ( iz < 0 ) || ( iz >= XZSIZE ) ) return; + int idx = ix * XZSIZE + iz; + LevelChunk *chunk = cache[idx]; + + if(chunk) + { + m_toDrop.push_back(chunk); + } + } +#endif } void ServerChunkCache::dropAll() { +#ifdef _LARGE_WORLDS for (LevelChunk *chunk : m_loadedChunkList) { drop(chunk->x, chunk->z); - } +} +#endif } // 4J - this is the original (and virtual) interface to create @@ -934,10 +957,6 @@ bool ServerChunkCache::tick() m_unloadedCache[idx] = chunk; cache[idx] = nullptr; } - else - { - continue; - } } m_toDrop.pop_front(); } From 1a50770647c582c5ce194e5741e3014bb1c1e8b2 Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:15:11 -0500 Subject: [PATCH 04/13] Add asynchronous server joining (#1408) --- .../Common/Network/GameNetworkManager.h | 3 +- .../Network/PlatformNetworkManagerStub.cpp | 63 +++-- .../Network/PlatformNetworkManagerStub.h | 6 + .../Common/UI/UIScene_ConnectingProgress.cpp | 127 +++++++++ .../Common/UI/UIScene_ConnectingProgress.h | 5 + .../Common/UI/UIScene_JoinMenu.cpp | 18 ++ .../Windows64/Network/WinsockNetLayer.cpp | 249 ++++++++++++++++++ .../Windows64/Network/WinsockNetLayer.h | 30 +++ 8 files changed, 480 insertions(+), 21 deletions(-) diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.h b/Minecraft.Client/Common/Network/GameNetworkManager.h index 3357b3cdd..22d588077 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.h +++ b/Minecraft.Client/Common/Network/GameNetworkManager.h @@ -47,7 +47,8 @@ public: { JOINGAME_SUCCESS, JOINGAME_FAIL_GENERAL, - JOINGAME_FAIL_SERVER_FULL + JOINGAME_FAIL_SERVER_FULL, + JOINGAME_PENDING } eJoinGameResult; void Initialise(); diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index 1e625098b..430f2c111 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -173,6 +173,11 @@ bool CPlatformNetworkManagerStub::Initialise(CGameNetworkManager *pGameNetworkMa m_bSearchPending = false; m_bIsOfflineGame = false; +#ifdef _WINDOWS64 + m_bJoinPending = false; + m_joinLocalUsersMask = 0; + m_joinHostName[0] = 0; +#endif m_pSearchParam = nullptr; m_SessionsUpdatedCallback = nullptr; @@ -282,6 +287,38 @@ void CPlatformNetworkManagerStub::DoWork() m_bLeaveGameOnTick = false; } } + + if (m_bJoinPending) + { + WinsockNetLayer::eJoinState state = WinsockNetLayer::GetJoinState(); + if (state == WinsockNetLayer::eJoinState_Success) + { + WinsockNetLayer::FinalizeJoin(); + + BYTE localSmallId = WinsockNetLayer::GetLocalSmallId(); + + IQNet::m_player[localSmallId].m_smallId = localSmallId; + IQNet::m_player[localSmallId].m_isRemote = false; + IQNet::m_player[localSmallId].m_isHostPlayer = false; + IQNet::m_player[localSmallId].m_resolvedXuid = Win64Xuid::ResolvePersistentXuid(); + + Minecraft* pMinecraft = Minecraft::GetInstance(); + wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str()); + IQNet::s_playerCount = localSmallId + 1; + + NotifyPlayerJoined(&IQNet::m_player[0]); + NotifyPlayerJoined(&IQNet::m_player[localSmallId]); + + m_pGameNetworkManager->StateChange_AnyToStarting(); + m_bJoinPending = false; + } + else if (state == WinsockNetLayer::eJoinState_Failed || + state == WinsockNetLayer::eJoinState_Rejected || + state == WinsockNetLayer::eJoinState_Cancelled) + { + m_bJoinPending = false; + } + } #endif } @@ -511,36 +548,22 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l IQNet::m_player[0].m_smallId = 0; IQNet::m_player[0].m_isRemote = true; IQNet::m_player[0].m_isHostPlayer = true; - // Remote host still maps to legacy host XUID in mixed old/new sessions. IQNet::m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid(); wcsncpy_s(IQNet::m_player[0].m_gamertag, 32, searchResult->data.hostName, _TRUNCATE); WinsockNetLayer::StopDiscovery(); - if (!WinsockNetLayer::JoinGame(hostIP, hostPort)) + wcsncpy_s(m_joinHostName, 32, searchResult->data.hostName, _TRUNCATE); + m_joinLocalUsersMask = localUsersMask; + + if (!WinsockNetLayer::BeginJoinGame(hostIP, hostPort)) { app.DebugPrintf("Win64 LAN: Failed to connect to %s:%d\n", hostIP, hostPort); return CGameNetworkManager::JOINGAME_FAIL_GENERAL; } - BYTE localSmallId = WinsockNetLayer::GetLocalSmallId(); - - IQNet::m_player[localSmallId].m_smallId = localSmallId; - IQNet::m_player[localSmallId].m_isRemote = false; - IQNet::m_player[localSmallId].m_isHostPlayer = false; - // Local non-host identity is the persistent uid.dat XUID. - IQNet::m_player[localSmallId].m_resolvedXuid = Win64Xuid::ResolvePersistentXuid(); - - Minecraft* pMinecraft = Minecraft::GetInstance(); - wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str()); - IQNet::s_playerCount = localSmallId + 1; - - NotifyPlayerJoined(&IQNet::m_player[0]); - NotifyPlayerJoined(&IQNet::m_player[localSmallId]); - - m_pGameNetworkManager->StateChange_AnyToStarting(); - - return CGameNetworkManager::JOINGAME_SUCCESS; + m_bJoinPending = true; + return CGameNetworkManager::JOINGAME_PENDING; #else return CGameNetworkManager::JOINGAME_SUCCESS; #endif diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h index 4a3f4068d..dffa39531 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h @@ -77,6 +77,12 @@ private: bool m_bIsPrivateGame; int m_flagIndexSize; +#ifdef _WINDOWS64 + bool m_bJoinPending; + int m_joinLocalUsersMask; + wchar_t m_joinHostName[32]; +#endif + // This is only maintained by the host, and is not valid on client machines GameSessionData m_hostGameSessionData; CGameNetworkManager *m_pGameNetworkManager; diff --git a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp index e10a5a62a..40557cd5c 100644 --- a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp +++ b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp @@ -2,6 +2,16 @@ #include "UI.h" #include "UIScene_ConnectingProgress.h" #include "..\..\Minecraft.h" +#ifdef _WINDOWS64 +#include "..\..\Windows64\Network\WinsockNetLayer.h" +#include "..\..\..\Minecraft.World\DisconnectPacket.h" + +static int ConnectingProgress_OnRejectedDialogOK(LPVOID, int iPad, const C4JStorage::EMessageResult) +{ + ui.NavigateBack(iPad); + return 0; +} +#endif UIScene_ConnectingProgress::UIScene_ConnectingProgress(int iPad, void *_initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { @@ -43,6 +53,12 @@ UIScene_ConnectingProgress::UIScene_ConnectingProgress(int iPad, void *_initData m_cancelFuncParam = param->cancelFuncParam; m_removeLocalPlayer = false; m_showingButton = false; + +#ifdef _WINDOWS64 + WinsockNetLayer::eJoinState initState = WinsockNetLayer::GetJoinState(); + m_asyncJoinActive = (initState != WinsockNetLayer::eJoinState_Idle && initState != WinsockNetLayer::eJoinState_Cancelled); + m_asyncJoinFailed = false; +#endif } UIScene_ConnectingProgress::~UIScene_ConnectingProgress() @@ -53,6 +69,18 @@ UIScene_ConnectingProgress::~UIScene_ConnectingProgress() void UIScene_ConnectingProgress::updateTooltips() { +#ifdef _WINDOWS64 + if (m_asyncJoinActive) + { + ui.SetTooltips( m_iPad, -1, IDS_TOOLTIPS_BACK); + return; + } + if (m_asyncJoinFailed) + { + ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT, -1); + return; + } +#endif // 4J-PB - removing the option of cancel join, since it didn't work anyway //ui.SetTooltips( m_iPad, -1, m_showTooltips?IDS_TOOLTIPS_CANCEL_JOIN:-1); ui.SetTooltips( m_iPad, -1, -1); @@ -62,6 +90,85 @@ void UIScene_ConnectingProgress::tick() { UIScene::tick(); +#ifdef _WINDOWS64 + if (m_asyncJoinActive) + { + WinsockNetLayer::eJoinState state = WinsockNetLayer::GetJoinState(); + if (state == WinsockNetLayer::eJoinState_Connecting) + { + // connecting............. + int attempt = WinsockNetLayer::GetJoinAttempt(); + int maxAttempts = WinsockNetLayer::GetJoinMaxAttempts(); + char buf[128]; + if (attempt <= 1) + sprintf_s(buf, "Connecting..."); + else + sprintf_s(buf, "Connecting failed, trying again (%d/%d)", attempt, maxAttempts); + wchar_t wbuf[128]; + mbstowcs(wbuf, buf, 128); + m_labelTitle.setLabel(wstring(wbuf)); + } + else if (state == WinsockNetLayer::eJoinState_Success) + { + m_asyncJoinActive = false; + // go go go + } + else if (state == WinsockNetLayer::eJoinState_Cancelled) + { + // cancel + m_asyncJoinActive = false; + navigateBack(); + } + else if (state == WinsockNetLayer::eJoinState_Rejected) + { + // server full and banned are passed differently compared to other disconnects it seems + m_asyncJoinActive = false; + DisconnectPacket::eDisconnectReason reason = WinsockNetLayer::GetJoinRejectReason(); + int exitReasonStringId; + switch (reason) + { + case DisconnectPacket::eDisconnect_ServerFull: + exitReasonStringId = IDS_DISCONNECTED_SERVER_FULL; + break; + case DisconnectPacket::eDisconnect_Banned: + exitReasonStringId = IDS_DISCONNECTED_KICKED; + break; + default: + exitReasonStringId = IDS_CONNECTION_LOST_SERVER; + break; + } + UINT uiIDA[1]; + uiIDA[0] = IDS_CONFIRM_OK; + ui.RequestErrorMessage(IDS_CONNECTION_FAILED, exitReasonStringId, uiIDA, 1, ProfileManager.GetPrimaryPad(), ConnectingProgress_OnRejectedDialogOK, nullptr, nullptr); + } + else if (state == WinsockNetLayer::eJoinState_Failed) + { + // FAIL + m_asyncJoinActive = false; + m_asyncJoinFailed = true; + + int maxAttempts = WinsockNetLayer::GetJoinMaxAttempts(); + char buf[256]; + sprintf_s(buf, "Failed to connect after %d attempts. The server may be unavailable.", maxAttempts); + wchar_t wbuf[256]; + mbstowcs(wbuf, buf, 256); + + // TIL that these exist + // not going to use a actual popup due to it requiring messing with strings which can really mess things up + // i dont trust myself with that + // these need to be touched up later as teh button is a bit offset + m_labelTitle.setLabel(L"Unable to connect to server"); + m_progressBar.setLabel(wstring(wbuf)); + m_progressBar.showBar(false); + m_progressBar.setVisible(true); + m_buttonConfirm.setVisible(true); + m_showingButton = true; + m_controlTimer.setVisible(false); + } + return; + } +#endif + if( m_removeLocalPlayer ) { m_removeLocalPlayer = false; @@ -94,6 +201,8 @@ void UIScene_ConnectingProgress::handleGainFocus(bool navBack) void UIScene_ConnectingProgress::handleLoseFocus() { + if (!m_runFailTimer) return; + int millisecsLeft = getTimer(0)->targetTime - System::currentTimeMillis(); int millisecsTaken = getTimer(0)->duration - millisecsLeft; app.DebugPrintf("\n"); @@ -208,6 +317,17 @@ void UIScene_ConnectingProgress::handleInput(int iPad, int key, bool repeat, boo switch(key) { // 4J-PB - Removed the option to cancel join - it didn't work anyway +#ifdef _WINDOWS64 + case ACTION_MENU_CANCEL: + if (pressed && m_asyncJoinActive) + { + m_asyncJoinActive = false; + WinsockNetLayer::CancelJoinGame(); + navigateBack(); + handled = true; + } + break; +#endif // case ACTION_MENU_CANCEL: // { // if(m_cancelFunc != nullptr) @@ -250,6 +370,13 @@ void UIScene_ConnectingProgress::handlePress(F64 controlId, F64 childId) case eControl_Confirm: if(m_showingButton) { +#ifdef _WINDOWS64 + if (m_asyncJoinFailed) + { + navigateBack(); + } + else +#endif if( m_iPad != ProfileManager.GetPrimaryPad() && g_NetworkManager.IsInSession() ) { // The connection failed if we see the button, so the temp player should be removed and the viewports updated again diff --git a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.h b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.h index 2c52284c4..eaaea7f61 100644 --- a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.h +++ b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.h @@ -13,6 +13,11 @@ private: void (*m_cancelFunc)(LPVOID param); LPVOID m_cancelFuncParam; +#ifdef _WINDOWS64 + bool m_asyncJoinActive; + bool m_asyncJoinFailed; +#endif + enum EControls { eControl_Confirm diff --git a/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp index 417c1700c..5b83ea7c5 100644 --- a/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp @@ -583,6 +583,24 @@ void UIScene_JoinMenu::JoinGame(UIScene_JoinMenu* pClass) // Alert the app the we no longer want to be informed of ethernet connections app.SetLiveLinkRequired( false ); +#ifdef _WINDOWS64 + if (result == CGameNetworkManager::JOINGAME_PENDING) + { + pClass->m_bIgnoreInput = false; + + ConnectionProgressParams *param = new ConnectionProgressParams(); + param->iPad = ProfileManager.GetPrimaryPad(); + param->stringId = -1; + param->showTooltips = true; + param->setFailTimer = false; + param->timerTime = 0; + param->cancelFunc = nullptr; + param->cancelFuncParam = nullptr; + ui.NavigateToScene(ProfileManager.GetPrimaryPad(), eUIScene_ConnectingProgress, param); + return; + } +#endif + if( result != CGameNetworkManager::JOINGAME_SUCCESS ) { int exitReasonStringId = -1; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 981ab3ab1..acc043e5f 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -67,6 +67,16 @@ SOCKET WinsockNetLayer::s_splitScreenSocket[XUSER_MAX_COUNT] = { INVALID_SOCKET, BYTE WinsockNetLayer::s_splitScreenSmallId[XUSER_MAX_COUNT] = { 0xFF, 0xFF, 0xFF, 0xFF }; HANDLE WinsockNetLayer::s_splitScreenRecvThread[XUSER_MAX_COUNT] = {nullptr, nullptr, nullptr, nullptr}; +// async stuff +HANDLE WinsockNetLayer::s_joinThread = nullptr; +volatile WinsockNetLayer::eJoinState WinsockNetLayer::s_joinState = WinsockNetLayer::eJoinState_Idle; +volatile int WinsockNetLayer::s_joinAttempt = 0; +volatile bool WinsockNetLayer::s_joinCancel = false; +char WinsockNetLayer::s_joinIP[256] = {}; +int WinsockNetLayer::s_joinPort = 0; +BYTE WinsockNetLayer::s_joinAssignedSmallId = 0; +DisconnectPacket::eDisconnectReason WinsockNetLayer::s_joinRejectReason = DisconnectPacket::eDisconnect_Quitting; + bool g_Win64MultiplayerHost = false; bool g_Win64MultiplayerJoin = false; int g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; @@ -114,6 +124,15 @@ void WinsockNetLayer::Shutdown() StopAdvertising(); StopDiscovery(); + s_joinCancel = true; + if (s_joinThread != nullptr) + { + WaitForSingleObject(s_joinThread, 5000); + CloseHandle(s_joinThread); + s_joinThread = nullptr; + } + s_joinState = eJoinState_Idle; + s_active = false; s_connected = false; @@ -421,6 +440,215 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port) return true; } +bool WinsockNetLayer::BeginJoinGame(const char* ip, int port) +{ + if (!s_initialized && !Initialize()) return false; + + // if there isnt any cleanup it sometime caused issues. Oops + CancelJoinGame(); + if (s_joinThread != nullptr) + { + WaitForSingleObject(s_joinThread, 5000); + CloseHandle(s_joinThread); + s_joinThread = nullptr; + } + + s_isHost = false; + s_hostSmallId = 0; + s_connected = false; + s_active = false; + + if (s_hostConnectionSocket != INVALID_SOCKET) + { + closesocket(s_hostConnectionSocket); + s_hostConnectionSocket = INVALID_SOCKET; + } + + if (s_clientRecvThread != nullptr) + { + WaitForSingleObject(s_clientRecvThread, 5000); + CloseHandle(s_clientRecvThread); + s_clientRecvThread = nullptr; + } + + strncpy_s(s_joinIP, sizeof(s_joinIP), ip, _TRUNCATE); + s_joinPort = port; + s_joinAttempt = 0; + s_joinCancel = false; + s_joinAssignedSmallId = 0; + s_joinRejectReason = DisconnectPacket::eDisconnect_Quitting; + s_joinState = eJoinState_Connecting; + + s_joinThread = CreateThread(nullptr, 0, JoinThreadProc, nullptr, 0, nullptr); + if (s_joinThread == nullptr) + { + s_joinState = eJoinState_Failed; + return false; + } + return true; +} + +DWORD WINAPI WinsockNetLayer::JoinThreadProc(LPVOID param) +{ + struct addrinfo hints = {}; + struct addrinfo* result = nullptr; + + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + char portStr[16]; + sprintf_s(portStr, "%d", s_joinPort); + + int iResult = getaddrinfo(s_joinIP, portStr, &hints, &result); + if (iResult != 0) + { + app.DebugPrintf("getaddrinfo failed for %s:%d - %d\n", s_joinIP, s_joinPort, iResult); + s_joinState = eJoinState_Failed; + return 0; + } + + bool connected = false; + BYTE assignedSmallId = 0; + SOCKET sock = INVALID_SOCKET; + + for (int attempt = 0; attempt < JOIN_MAX_ATTEMPTS; ++attempt) + { + if (s_joinCancel) + { + freeaddrinfo(result); + s_joinState = eJoinState_Cancelled; + return 0; + } + + s_joinAttempt = attempt + 1; + + sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (sock == INVALID_SOCKET) + { + app.DebugPrintf("socket() failed: %d\n", WSAGetLastError()); + break; + } + + int noDelay = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char*)&noDelay, sizeof(noDelay)); + + iResult = connect(sock, result->ai_addr, static_cast(result->ai_addrlen)); + if (iResult == SOCKET_ERROR) + { + int err = WSAGetLastError(); + app.DebugPrintf("connect() to %s:%d failed (attempt %d/%d): %d\n", s_joinIP, s_joinPort, attempt + 1, JOIN_MAX_ATTEMPTS, err); + closesocket(sock); + sock = INVALID_SOCKET; + for (int w = 0; w < 4 && !s_joinCancel; w++) + Sleep(50); + continue; + } + + BYTE assignBuf[1]; + int bytesRecv = recv(sock, (char*)assignBuf, 1, 0); + if (bytesRecv != 1) + { + app.DebugPrintf("failed to receive small id assignment from host (attempt %d/%d)\n", attempt + 1, JOIN_MAX_ATTEMPTS); + closesocket(sock); + sock = INVALID_SOCKET; + for (int w = 0; w < 4 && !s_joinCancel; w++) + Sleep(50); + continue; + } + + if (assignBuf[0] == WIN64_SMALLID_REJECT) + { + BYTE rejectBuf[5]; + if (!RecvExact(sock, rejectBuf, 5)) + { + app.DebugPrintf("failed to receive reject reason from host (?)\n"); + closesocket(sock); + sock = INVALID_SOCKET; + for (int w = 0; w < 4 && !s_joinCancel; w++) + Sleep(50); + continue; + } + int reason = ((rejectBuf[1] & 0xff) << 24) | ((rejectBuf[2] & 0xff) << 16) | + ((rejectBuf[3] & 0xff) << 8) | (rejectBuf[4] & 0xff); + s_joinRejectReason = (DisconnectPacket::eDisconnectReason)reason; + closesocket(sock); + freeaddrinfo(result); + s_joinState = eJoinState_Rejected; + return 0; + } + + assignedSmallId = assignBuf[0]; + connected = true; + break; + } + freeaddrinfo(result); + + if (s_joinCancel) + { + if (sock != INVALID_SOCKET) closesocket(sock); + s_joinState = eJoinState_Cancelled; + return 0; + } + + if (!connected) + { + s_joinState = eJoinState_Failed; + return 0; + } + + s_hostConnectionSocket = sock; + s_joinAssignedSmallId = assignedSmallId; + s_joinState = eJoinState_Success; + return 0; +} + +void WinsockNetLayer::CancelJoinGame() +{ + if (s_joinState == eJoinState_Connecting) + { + s_joinCancel = true; + } + else if (s_joinState == eJoinState_Success) + { + // fix a race cond + if (s_hostConnectionSocket != INVALID_SOCKET) + { + closesocket(s_hostConnectionSocket); + s_hostConnectionSocket = INVALID_SOCKET; + } + s_joinState = eJoinState_Cancelled; + } +} + +bool WinsockNetLayer::FinalizeJoin() +{ + if (s_joinState != eJoinState_Success) + return false; + + s_localSmallId = s_joinAssignedSmallId; + + strncpy_s(g_Win64MultiplayerIP, sizeof(g_Win64MultiplayerIP), s_joinIP, _TRUNCATE); + g_Win64MultiplayerPort = s_joinPort; + + app.DebugPrintf("connected to %s:%d, assigned smallId=%d\n", s_joinIP, s_joinPort, s_localSmallId); + + s_active = true; + s_connected = true; + + s_clientRecvThread = CreateThread(nullptr, 0, ClientRecvThreadProc, nullptr, 0, nullptr); + + if (s_joinThread != nullptr) + { + WaitForSingleObject(s_joinThread, 2000); + CloseHandle(s_joinThread); + s_joinThread = nullptr; + } + + s_joinState = eJoinState_Idle; + return true; +} + bool WinsockNetLayer::SendOnSocket(SOCKET sock, const void* data, int dataSize) { if (sock == INVALID_SOCKET || dataSize <= 0 || dataSize > WIN64_NET_MAX_PACKET_SIZE) return false; @@ -1334,4 +1562,25 @@ DWORD WINAPI WinsockNetLayer::DiscoveryThreadProc(LPVOID param) return 0; } +// some lazy helper funcs +WinsockNetLayer::eJoinState WinsockNetLayer::GetJoinState() +{ + return s_joinState; +} + +int WinsockNetLayer::GetJoinAttempt() +{ + return s_joinAttempt; +} + +int WinsockNetLayer::GetJoinMaxAttempts() +{ + return JOIN_MAX_ATTEMPTS; +} + +DisconnectPacket::eDisconnectReason WinsockNetLayer::GetJoinRejectReason() +{ + return s_joinRejectReason; +} + #endif diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index afccbd66e..8a11e391d 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -21,6 +21,8 @@ class Socket; +#include "..\..\..\Minecraft.World\DisconnectPacket.h" + #pragma pack(push, 1) struct Win64LANBroadcast { @@ -69,6 +71,23 @@ public: static bool HostGame(int port, const char* bindIp = nullptr); static bool JoinGame(const char* ip, int port); + enum eJoinState + { + eJoinState_Idle, + eJoinState_Connecting, + eJoinState_Success, + eJoinState_Failed, + eJoinState_Rejected, + eJoinState_Cancelled + }; + static bool BeginJoinGame(const char* ip, int port); + static void CancelJoinGame(); + static eJoinState GetJoinState(); + static int GetJoinAttempt(); + static int GetJoinMaxAttempts(); + static DisconnectPacket::eDisconnectReason GetJoinRejectReason(); + static bool FinalizeJoin(); + static bool SendToSmallId(BYTE targetSmallId, const void* data, int dataSize); static bool SendOnSocket(SOCKET sock, const void* data, int dataSize); @@ -112,6 +131,17 @@ private: static DWORD WINAPI SplitScreenRecvThreadProc(LPVOID param); static DWORD WINAPI AdvertiseThreadProc(LPVOID param); static DWORD WINAPI DiscoveryThreadProc(LPVOID param); + static DWORD WINAPI JoinThreadProc(LPVOID param); + + static HANDLE s_joinThread; + static volatile eJoinState s_joinState; + static volatile int s_joinAttempt; + static volatile bool s_joinCancel; + static char s_joinIP[256]; + static int s_joinPort; + static BYTE s_joinAssignedSmallId; + static DisconnectPacket::eDisconnectReason s_joinRejectReason; + static const int JOIN_MAX_ATTEMPTS = 4; static SOCKET s_listenSocket; static SOCKET s_hostConnectionSocket; From c96a8ee524f6fdc7bd7a7c33024f3483c40b7876 Mon Sep 17 00:00:00 2001 From: Sylvessa <225480449+sylvessa@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:19:20 -0500 Subject: [PATCH 05/13] fix splitscreen xuids (#1413) --- Minecraft.Client/TrackedEntity.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Minecraft.Client/TrackedEntity.cpp b/Minecraft.Client/TrackedEntity.cpp index 380750449..3ecd66786 100644 --- a/Minecraft.Client/TrackedEntity.cpp +++ b/Minecraft.Client/TrackedEntity.cpp @@ -653,12 +653,14 @@ shared_ptr TrackedEntity::getAddEntityPacket() PlayerUID xuid = INVALID_XUID; PlayerUID OnlineXuid = INVALID_XUID; - // do not pass xuid/onlinxuid to cleints - //if( player != nullptr ) - //{ - // xuid = player->getXuid(); - // OnlineXuid = player->getOnlineXuid(); - //} + // do not pass xuid/onlinexuid to clients if dedicated server +#ifndef MINECRAFT_SERVER_BUILD + if( player != nullptr ) + { + xuid = player->getXuid(); + OnlineXuid = player->getOnlineXuid(); + } +#endif // 4J Added yHeadRotp param to fix #102563 - TU12: Content: Gameplay: When one of the Players is idle for a few minutes his head turns 180 degrees. return std::make_shared(player, xuid, OnlineXuid, xp, yp, zp, yRotp, xRotp, yHeadRotp); } From 4f370c45e3e1717885e4b4c81f1e9287cfb38c72 Mon Sep 17 00:00:00 2001 From: Revela Date: Thu, 26 Mar 2026 15:16:15 -0500 Subject: [PATCH 06/13] Fix pistons permanently breaking server-wide on dedicated servers (#1420) triggerEvent() set ignoreUpdate to true at the start but three early return paths skipped the reset at the end. Once any of these paths was hit, the TLS flag stayed true permanently, blocking all piston neighbor updates for the rest of the server session. --- Minecraft.World/PistonBaseTile.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Minecraft.World/PistonBaseTile.cpp b/Minecraft.World/PistonBaseTile.cpp index e8e2a7138..530cbf73b 100644 --- a/Minecraft.World/PistonBaseTile.cpp +++ b/Minecraft.World/PistonBaseTile.cpp @@ -218,10 +218,12 @@ bool PistonBaseTile::triggerEvent(Level *level, int x, int y, int z, int param1, if (extend && param1 == TRIGGER_CONTRACT) { level->setData(x, y, z, facing | EXTENDED_BIT, UPDATE_CLIENTS); + ignoreUpdate(false); return false; } else if (!extend && param1 == TRIGGER_EXTEND) { + ignoreUpdate(false); return false; } } @@ -247,6 +249,7 @@ bool PistonBaseTile::triggerEvent(Level *level, int x, int y, int z, int param1, } else { + ignoreUpdate(false); return false; } PIXEndNamedEvent(); From 73d713878ce6860074705fa164403b331b6a7723 Mon Sep 17 00:00:00 2001 From: 666uvu <198883746+666uvu@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:55:56 +0000 Subject: [PATCH 07/13] fix redstone tick persistence on chunk unload (#1423) --- Minecraft.Client/MinecraftServer.cpp | 1 + Minecraft.Client/ServerLevel.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 1e3ed74ef..27ee68b65 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -1795,6 +1795,7 @@ void MinecraftServer::run(int64_t seed, void *lpParameter) chunkPacketManagement_PostTick(); } + lastTime = getCurrentTimeMillis(); // int64_t afterall = System::currentTimeMillis(); // PIXReportCounter(L"Server time all",(float)(afterall-beforeall)); // PIXReportCounter(L"Server ticks",(float)tickcount); diff --git a/Minecraft.Client/ServerLevel.cpp b/Minecraft.Client/ServerLevel.cpp index a2596911d..9d5ec9089 100644 --- a/Minecraft.Client/ServerLevel.cpp +++ b/Minecraft.Client/ServerLevel.cpp @@ -678,7 +678,7 @@ bool ServerLevel::tickPendingTicks(bool force) } else { - addToTickNextTick(td.x, td.y, td.z, td.tileId, 0); + forceAddTileTick(td.x, td.y, td.z, td.tileId, 0, td.priorityTilt); } } From 0d4874dea5ce5cfd31a67f6b1d5e38c271a4308b Mon Sep 17 00:00:00 2001 From: Loki Date: Fri, 27 Mar 2026 00:29:08 -0500 Subject: [PATCH 08/13] Clarify Android support in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e011c1eac..303728adf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you're looking for Dedicated Server software, download its [Nightly Build her - **Windows**: Supported for building and running the project - **macOS / Linux**: The Windows nightly build will run through Wine or CrossOver based on community reports, but this is unofficial and not currently tested by the maintainers when pushing updates -- **Android**: The Windows nightly build does run but has stability / frametime pacing issues frequently reported +- **Android**: VIA x86 EMULATORS (like GameNative) ONLY! The Windows nightly build does run but has stability / frametime pacing issues frequently reported - **iOS**: No current support - **All Consoles**: Console support remains in the code, but maintainers are not currently verifying console functionality / porting UI Changes to the console builds at this time. From 3c1166c45e3be1dc44f0ea83accb0408a24b2751 Mon Sep 17 00:00:00 2001 From: Sestain <35299377+sestain@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:59:35 +0200 Subject: [PATCH 09/13] Added support for Big-Endian DLCs (#1291) * Added support for Big-Endian DLCs * Remove unused variable * Remove the things made for other PR --- Minecraft.Client/Common/DLC/DLCManager.cpp | 43 +++++++++++++++++----- Minecraft.Client/Common/DLC/DLCManager.h | 24 ++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/Minecraft.Client/Common/DLC/DLCManager.cpp b/Minecraft.Client/Common/DLC/DLCManager.cpp index 931b0e1d9..88a7756e5 100644 --- a/Minecraft.Client/Common/DLC/DLCManager.cpp +++ b/Minecraft.Client/Common/DLC/DLCManager.cpp @@ -387,22 +387,34 @@ bool DLCManager::processDLCDataFile(DWORD &dwFilesProcessed, PBYTE pbData, DWORD // // unsigned long, p = number of parameters // // p * DLC_FILE_PARAM describing each parameter for this file // // ulFileSize bytes of data blob of the file added - unsigned int uiVersion=*(unsigned int *)pbData; + unsigned int uiVersion=readUInt32(pbData, false); uiCurrentByte+=sizeof(int); - if(uiVersion < CURRENT_DLC_VERSION_NUM) - { - if(pbData!=nullptr) delete [] pbData; - app.DebugPrintf("DLC version of %d is too old to be read\n", uiVersion); + bool bSwapEndian = false; + unsigned int uiVersionSwapped = SwapInt32(uiVersion); + if (uiVersion >= 0 && uiVersion <= CURRENT_DLC_VERSION_NUM) { + bSwapEndian = false; + } else if (uiVersionSwapped >= 0 && uiVersionSwapped <= CURRENT_DLC_VERSION_NUM) { + bSwapEndian = true; + } else { + if(pbData!=nullptr) delete [] pbData; + app.DebugPrintf("Unknown DLC version of %d\n", uiVersion); return false; } pack->SetDataPointer(pbData); - unsigned int uiParameterCount=*(unsigned int *)&pbData[uiCurrentByte]; + unsigned int uiParameterCount=readUInt32(&pbData[uiCurrentByte], bSwapEndian); uiCurrentByte+=sizeof(int); C4JStorage::DLC_FILE_PARAM *pParams = (C4JStorage::DLC_FILE_PARAM *)&pbData[uiCurrentByte]; //DWORD dwwchCount=0; for(unsigned int i=0;idwType = bSwapEndian ? SwapInt32(pParams->dwType) : pParams->dwType; + pParams->dwWchCount = bSwapEndian ? SwapInt32(pParams->dwWchCount) : pParams->dwWchCount; + char16_t* wchData = reinterpret_cast(pParams->wchData); + if (bSwapEndian) { + SwapUTF16Bytes(wchData, pParams->dwWchCount); + } + // Map DLC strings to application strings, then store the DLC index mapping to application index wstring parameterName(static_cast(pParams->wchData)); EDLCParameterType type = getParameterType(parameterName); @@ -414,14 +426,14 @@ bool DLCManager::processDLCDataFile(DWORD &dwFilesProcessed, PBYTE pbData, DWORD pParams = (C4JStorage::DLC_FILE_PARAM *)&pbData[uiCurrentByte]; } //ulCurrentByte+=ulParameterCount * sizeof(C4JStorage::DLC_FILE_PARAM); - - unsigned int uiFileCount=*(unsigned int *)&pbData[uiCurrentByte]; + unsigned int uiFileCount=readUInt32(&pbData[uiCurrentByte], bSwapEndian); uiCurrentByte+=sizeof(int); C4JStorage::DLC_FILE_DETAILS *pFile = (C4JStorage::DLC_FILE_DETAILS *)&pbData[uiCurrentByte]; DWORD dwTemp=uiCurrentByte; for(unsigned int i=0;idwWchCount = bSwapEndian ? SwapInt32(pFile->dwWchCount) : pFile->dwWchCount; dwTemp+=sizeof(C4JStorage::DLC_FILE_DETAILS)+pFile->dwWchCount*sizeof(WCHAR); pFile = (C4JStorage::DLC_FILE_DETAILS *)&pbData[dwTemp]; } @@ -430,6 +442,13 @@ bool DLCManager::processDLCDataFile(DWORD &dwFilesProcessed, PBYTE pbData, DWORD for(unsigned int i=0;idwType = bSwapEndian ? SwapInt32(pFile->dwType) : pFile->dwType; + pFile->uiFileSize = bSwapEndian ? SwapInt32(pFile->uiFileSize) : pFile->uiFileSize; + char16_t* wchFile = reinterpret_cast(pFile->wchFile); + if (bSwapEndian) { + SwapUTF16Bytes(wchFile, pFile->dwWchCount); + } + EDLCType type = static_cast(pFile->dwType); DLCFile *dlcFile = nullptr; @@ -445,12 +464,18 @@ bool DLCManager::processDLCDataFile(DWORD &dwFilesProcessed, PBYTE pbData, DWORD } // Params - uiParameterCount=*(unsigned int *)pbTemp; + uiParameterCount=readUInt32(pbTemp, bSwapEndian); pbTemp+=sizeof(int); pParams = (C4JStorage::DLC_FILE_PARAM *)pbTemp; for(unsigned int j=0;jdwType = bSwapEndian ? SwapInt32(pParams->dwType) : pParams->dwType; + pParams->dwWchCount = bSwapEndian ? SwapInt32(pParams->dwWchCount) : pParams->dwWchCount; + char16_t* wchData = reinterpret_cast(pParams->wchData); + if (bSwapEndian) { + SwapUTF16Bytes(wchData, pParams->dwWchCount); + } auto it = parameterMapping.find(pParams->dwType); diff --git a/Minecraft.Client/Common/DLC/DLCManager.h b/Minecraft.Client/Common/DLC/DLCManager.h index d4dd2508e..7191ab0b6 100644 --- a/Minecraft.Client/Common/DLC/DLCManager.h +++ b/Minecraft.Client/Common/DLC/DLCManager.h @@ -94,6 +94,30 @@ public: bool readDLCDataFile(DWORD &dwFilesProcessed, const string &path, DLCPack *pack, bool fromArchive = false); DWORD retrievePackIDFromDLCDataFile(const string &path, DLCPack *pack); + static unsigned short SwapInt16(unsigned short value) { + return (value >> 8) | (value << 8); + } + + static unsigned int SwapInt32(unsigned int value) { + return ((value & 0xFF) << 24) | + ((value & 0xFF00) << 8) | + ((value & 0xFF0000) >> 8) | + ((value & 0xFF000000) >> 24); + } + + static void SwapUTF16Bytes(char16_t* buffer, size_t count) { + for (size_t i = 0; i < count; ++i) { + char16_t& c = buffer[i]; + c = (c >> 8) | (c << 8); + } + } + + static unsigned int readUInt32(unsigned char* ptr, bool endian) { + unsigned int val = *(unsigned int*)ptr; + if (endian) val = SwapInt32(val); + return val; + } + private: bool processDLCDataFile(DWORD &dwFilesProcessed, PBYTE pbData, DWORD dwLength, DLCPack *pack); From 7447fabe0d8d97b42a6c9fb58ac121d2bb04372a Mon Sep 17 00:00:00 2001 From: Sestain <35299377+sestain@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:11:27 +0200 Subject: [PATCH 10/13] Fix game crashing if DLC has XMLVERSION paramater (#1285) * Fix game crashing if DLC has XMLVERSION paramater * Better implementation of XMLVersion check * Forgot to add type name to the list * Removed extra newline --- Minecraft.Client/Common/DLC/DLCManager.cpp | 13 +++++++++++++ Minecraft.Client/Common/DLC/DLCManager.h | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/Common/DLC/DLCManager.cpp b/Minecraft.Client/Common/DLC/DLCManager.cpp index 88a7756e5..c363becf4 100644 --- a/Minecraft.Client/Common/DLC/DLCManager.cpp +++ b/Minecraft.Client/Common/DLC/DLCManager.cpp @@ -10,6 +10,7 @@ const WCHAR *DLCManager::wchTypeNamesA[]= { + L"XMLVERSION", L"DISPLAYNAME", L"THEMENAME", L"FREE", @@ -405,6 +406,7 @@ bool DLCManager::processDLCDataFile(DWORD &dwFilesProcessed, PBYTE pbData, DWORD unsigned int uiParameterCount=readUInt32(&pbData[uiCurrentByte], bSwapEndian); uiCurrentByte+=sizeof(int); C4JStorage::DLC_FILE_PARAM *pParams = (C4JStorage::DLC_FILE_PARAM *)&pbData[uiCurrentByte]; + bool bXMLVersion = false; //DWORD dwwchCount=0; for(unsigned int i=0;idwType] = type; + + if (type == e_DLCParamType_XMLVersion) + { + bXMLVersion = true; + } } uiCurrentByte+= sizeof(C4JStorage::DLC_FILE_PARAM)+(pParams->dwWchCount*sizeof(WCHAR)); pParams = (C4JStorage::DLC_FILE_PARAM *)&pbData[uiCurrentByte]; } //ulCurrentByte+=ulParameterCount * sizeof(C4JStorage::DLC_FILE_PARAM); + + if (bXMLVersion) + { + uiCurrentByte += sizeof(int); + } + unsigned int uiFileCount=readUInt32(&pbData[uiCurrentByte], bSwapEndian); uiCurrentByte+=sizeof(int); C4JStorage::DLC_FILE_DETAILS *pFile = (C4JStorage::DLC_FILE_DETAILS *)&pbData[uiCurrentByte]; diff --git a/Minecraft.Client/Common/DLC/DLCManager.h b/Minecraft.Client/Common/DLC/DLCManager.h index 7191ab0b6..f114bd075 100644 --- a/Minecraft.Client/Common/DLC/DLCManager.h +++ b/Minecraft.Client/Common/DLC/DLCManager.h @@ -31,7 +31,8 @@ public: { e_DLCParamType_Invalid = -1, - e_DLCParamType_DisplayName = 0, + e_DLCParamType_XMLVersion = 0, + e_DLCParamType_DisplayName, e_DLCParamType_ThemeName, e_DLCParamType_Free, // identify free skins e_DLCParamType_Credit, // legal credits for DLC From 277d74716e9a2c937500e16273727a24f49508ee Mon Sep 17 00:00:00 2001 From: Botch Date: Sat, 28 Mar 2026 13:44:27 -0600 Subject: [PATCH 11/13] Render custom skin boxes on viewmodel (#1415) * Update PlayerRenderer.cpp * Fix fatal bug where skins with no additional boxes would crash the game --- Minecraft.Client/PlayerRenderer.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/PlayerRenderer.cpp b/Minecraft.Client/PlayerRenderer.cpp index a9b945440..23dff77f8 100644 --- a/Minecraft.Client/PlayerRenderer.cpp +++ b/Minecraft.Client/PlayerRenderer.cpp @@ -519,6 +519,29 @@ void PlayerRenderer::renderHand() { humanoidModel->arm0->render(1 / 16.0f,true); } + + + //Render custom skin boxes on viewmodel - Botch + vector* additionalModelParts = Minecraft::GetInstance()->player->GetAdditionalModelParts(); + if (!additionalModelParts) return; //If there are no custom boxes, return. This fixes bug where the game will crash if you select a skin with no additional boxes. + vector armchildren = humanoidModel->arm0->children; + std::unordered_set additionalModelPartSet(additionalModelParts->begin(), additionalModelParts->end()); + for (const auto& x : armchildren) { + if (x) { + if (additionalModelPartSet.find(x) != additionalModelPartSet.end()) { //This is to verify box is still actually on current skin - Botch + glPushMatrix(); + //We need to transform to match offset of arm - Botch + glTranslatef(-5 * 0.0625f, 2 * 0.0625f, 0); + glRotatef(0.1 * (180.0f / PI), 0, 0, 1); + x->visible = true; + x->render(1.0f / 16.0f, true); + x->visible = false; + glPopMatrix(); + } + } + } + + } void PlayerRenderer::setupPosition(shared_ptr _mob, double x, double y, double z) @@ -580,4 +603,4 @@ ResourceLocation *PlayerRenderer::getTextureLocation(shared_ptr entity) { shared_ptr player = dynamic_pointer_cast(entity); return new ResourceLocation(static_cast<_TEXTURE_NAME>(player->getTexture())); -} \ No newline at end of file +} From 38d58f2d8bb8af5516671b42940279d4e582d9c7 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Sun, 29 Mar 2026 05:35:25 +0100 Subject: [PATCH 12/13] Update actions workflows and add clang format check for PRs (#1418) * Add clang-format workflow for pull request checks * Modify push paths in nightly workflow Updated paths for push event to include all files except specified ones. * Update paths for nightly-server workflow triggers * Modify paths for pull request triggers Update pull request workflow to include specific paths. * Tidy up clang-format installation in workflow --- .github/workflows/clang-format.yml | 48 ++++++++++++++++++++++++++++ .github/workflows/nightly-server.yml | 11 ++++--- .github/workflows/nightly.yml | 11 ++++--- .github/workflows/pull-request.yml | 10 +++--- 4 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/clang-format.yml diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 000000000..a01bada4e --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,48 @@ +name: Check formatting + +on: + pull_request: + paths: + - '**' + - '!.gitignore' + - '!*.md' + - '!.github/**' + - '.github/workflows/clang-format.yml' + +permissions: + contents: read + pull-requests: write + +jobs: + format-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Fetch base commit + run: git fetch origin ${{ github.event.pull_request.base.sha }} + + - name: Install clang-format-20 + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" + sudo apt-get install -y -qq clang-format-20 + + - uses: reviewdog/action-setup@v1 + + - name: Check formatting on changed lines + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git clang-format-20 --binary clang-format-20 \ + --diff ${{ github.event.pull_request.base.sha }} -- \ + '*.c' '*.cpp' '*.cc' '*.h' '*.hpp' \ + | reviewdog \ + -name="clang-format" \ + -f=diff \ + -reporter=github-pr-check \ + -fail-level=error \ + -filter-mode=added diff --git a/.github/workflows/nightly-server.yml b/.github/workflows/nightly-server.yml index 5450de9ac..0fc20eb13 100644 --- a/.github/workflows/nightly-server.yml +++ b/.github/workflows/nightly-server.yml @@ -5,11 +5,12 @@ on: push: branches: - 'main' - paths-ignore: - - '.gitignore' - - '*.md' - - '.github/**' - - '!.github/workflows/nightly-server.yml' + paths: + - '**' + - '!.gitignore' + - '!*.md' + - '!.github/**' + - '.github/workflows/nightly-server.yml' permissions: contents: write diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 789db3e84..a5b53be08 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,11 +5,12 @@ on: push: branches: - 'main' - paths-ignore: - - '.gitignore' - - '*.md' - - '.github/**' - - '!.github/workflows/nightly.yml' + paths: + - '**' + - '!.gitignore' + - '!*.md' + - '!.github/**' + - '.github/workflows/nightly.yml' permissions: contents: write diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9d57f4b4b..3b5398a00 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,10 +4,12 @@ on: workflow_dispatch: pull_request: types: [opened, reopened, synchronize] - paths-ignore: - - '.gitignore' - - '*.md' - - '.github/*.md' + paths: + - '**' + - '!.gitignore' + - '!*.md' + - '!.github/**' + - '.github/workflows/pull-request.yml' jobs: build: From d3412aaae731c219e5b41574ea1ba924b003b3a8 Mon Sep 17 00:00:00 2001 From: blongm <33197955+blongm@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:05:32 +0000 Subject: [PATCH 13/13] Fixed issue with world seeds not saving correctly (#1119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fix issue where typing in a short seed on world creation doesn't save the seed correctly ## Changes ### Previous Behavior Typing in a seed on the world creation menu that's less than 8 characters long will result in garbage data being saved as the seed. Happens with controller and KBM. You can see this in-game - if you exit the world options menu and go back in, the seed will show up as boxes □□□. Weirdly, if you type a seed again, it behaves as expected. ### Root Cause For some reason, assigning `m_params->seed` to the seed text points it to garbage data, when it's 7 characters or less. ### New Behavior Seed entry behaves as expected. ### Fix Implementation - Added `static_cast` before assignment to `m_params->seed`. - Also replaced `(wchar_t *)` with `reinterpret_cast` in the functions. ### AI Use Disclosure No AI was used --- .../Common/UI/UIScene_LaunchMoreOptionsMenu.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp index b2981ebf6..1832e40cb 100644 --- a/Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp @@ -557,8 +557,8 @@ int UIScene_LaunchMoreOptionsMenu::KeyboardCompleteSeedCallback(LPVOID lpParam,b uint16_t pchText[128]; ZeroMemory(pchText, 128 * sizeof(uint16_t)); Win64_GetKeyboardText(pchText, 128); - pClass->m_editSeed.setLabel((wchar_t *)pchText); - pClass->m_params->seed = (wchar_t *)pchText; + pClass->m_editSeed.setLabel(reinterpret_cast(pchText)); + pClass->m_params->seed = static_cast(reinterpret_cast(pchText)); #else #ifdef __PSVITA__ uint16_t pchText[2048]; @@ -584,7 +584,7 @@ void UIScene_LaunchMoreOptionsMenu::getDirectEditInputs(vectorseed = input->getEditBuffer(); + m_params->seed = static_cast(input->getEditBuffer()); } #endif