diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..628eacdec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: MinecraftConsoles Community Discord + url: https://discord.gg/jrum7HhegA + about: If you need help, please ask for it in our Discord! You will get assistance much faster there, including help getting the project to compile. diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 63ee763a3..ba40b43ee 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -142,6 +142,13 @@ ClientConnection::ClientConnection(Minecraft *minecraft, Socket *socket, int iUs deferredEntityLinkPackets = vector(); } +bool ClientConnection::isPrimaryConnection() const +{ + // On host, all connections are primary (server is authoritative). + // On non-host, only the primary pad processes shared entity state. + return g_NetworkManager.IsHost() || m_userIndex == ProfileManager.GetPrimaryPad(); +} + ClientConnection::~ClientConnection() { delete connection; @@ -304,6 +311,10 @@ void ClientConnection::handleLogin(shared_ptr packet) level->isClientSide = true; minecraft->setLevel(level); } + else + { + level = (MultiPlayerLevel *)dimensionLevel; + } minecraft->player->setPlayerIndex( packet->m_playerIndex ); minecraft->player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) ); @@ -427,6 +438,7 @@ void ClientConnection::handleAddEntity(shared_ptr packet) { case AddEntityPacket::MINECART: e = Minecart::createMinecart(level, x, y, z, packet->data); + break; case AddEntityPacket::FISH_HOOK: { // 4J Stu - Brought forward from 1.4 to be able to drop XP from fishing @@ -704,6 +716,7 @@ void ClientConnection::handleAddExperienceOrb(shared_ptr void ClientConnection::handleAddGlobalEntity(shared_ptr packet) { + if (!isPrimaryConnection()) return; double x = packet->x / 32.0; double y = packet->y / 32.0; double z = packet->z / 32.0; @@ -729,6 +742,13 @@ void ClientConnection::handleAddPainting(shared_ptr packet) void ClientConnection::handleSetEntityMotion(shared_ptr packet) { + if (!isPrimaryConnection()) + { + // 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; + } shared_ptr e = getEntity(packet->id); if (e == nullptr) return; e->lerpMotion(packet->xa / 8000.0, packet->ya / 8000.0, packet->za / 8000.0); @@ -959,6 +979,7 @@ void ClientConnection::handleSetCarriedItem(shared_ptr pac void ClientConnection::handleMoveEntity(shared_ptr packet) { + if (!isPrimaryConnection()) return; shared_ptr e = getEntity(packet->id); if (e == nullptr) return; e->xp += packet->xa; @@ -988,6 +1009,7 @@ void ClientConnection::handleRotateMob(shared_ptr packet) void ClientConnection::handleMoveEntitySmall(shared_ptr packet) { + if (!isPrimaryConnection()) return; shared_ptr e = getEntity(packet->id); if (e == nullptr) return; e->xp += packet->xa; @@ -1112,6 +1134,7 @@ void ClientConnection::handleMovePlayer(shared_ptr packet) // 4J Added void ClientConnection::handleChunkVisibilityArea(shared_ptr packet) { + 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) level->setChunkVisible(x, z, true); @@ -1119,11 +1142,13 @@ void ClientConnection::handleChunkVisibilityArea(shared_ptr packet) { + if (level == NULL) return; level->setChunkVisible(packet->x, packet->z, packet->visible); } void ClientConnection::handleChunkTilesUpdate(shared_ptr packet) { + if (!isPrimaryConnection()) return; // 4J - changed to encode level in packet MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->levels[packet->levelIdx]; if( dimensionLevel ) @@ -1193,16 +1218,29 @@ void ClientConnection::handleChunkTilesUpdate(shared_ptr void ClientConnection::handleBlockRegionUpdate(shared_ptr packet) { + if (!isPrimaryConnection()) return; // 4J - changed to encode level in packet MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->levels[packet->levelIdx]; if( dimensionLevel ) { PIXBeginNamedEvent(0,"Handle block region update"); + if(packet->bIsFullChunk && packet->ys == 0) + { + app.DebugPrintf("[BRUP-CLIENT] *** EMPTY FULL CHUNK received at (%d,%d)! Buffer length=%d\n", + packet->x>>4, packet->z>>4, packet->buffer.length); + } + int y1 = packet->y + packet->ys; if(packet->bIsFullChunk) { y1 = Level::maxBuildHeight; + + // Ensure the chunk exists in the cache before writing data. + // The ChunkVisibilityAreaPacket that creates chunks can arrive AFTER the first BRUP, + // causing getChunk() to return EmptyLevelChunk (whose setBlocksAndData is a no-op). + dimensionLevel->setChunkVisible(packet->x >> 4, packet->z >> 4, true); + if(packet->buffer.length > 0) { PIXBeginNamedEvent(0, "Reordering to XZY"); @@ -1241,6 +1279,7 @@ void ClientConnection::handleBlockRegionUpdate(shared_ptr packet) { + if (!isPrimaryConnection()) return; // 4J added - using a block of 255 to signify that this is a packet for destroying a tile, where we need to inform the level renderer that we are about to do so. // This is used in creative mode as the point where a tile is first destroyed at the client end of things. Packets formed like this are potentially sent from // ServerPlayerGameMode::destroyBlock @@ -1355,6 +1394,7 @@ void ClientConnection::send(shared_ptr packet) void ClientConnection::handleTakeItemEntity(shared_ptr packet) { + if (!isPrimaryConnection()) return; shared_ptr from = getEntity(packet->itemId); shared_ptr to = dynamic_pointer_cast(getEntity(packet->playerId)); @@ -2859,31 +2899,34 @@ void ClientConnection::handleRespawn(shared_ptr packet) void ClientConnection::handleExplosion(shared_ptr packet) { - if(!packet->m_bKnockbackOnly) + // World modification (block destruction) must only happen once + if (isPrimaryConnection()) { - //app.DebugPrintf("Received ExplodePacket with explosion data\n"); - PIXBeginNamedEvent(0,"Handling explosion"); - Explosion *e = new Explosion(minecraft->level, nullptr, packet->x, packet->y, packet->z, packet->r); - PIXBeginNamedEvent(0,"Finalizing"); + if(!packet->m_bKnockbackOnly) + { + //app.DebugPrintf("Received ExplodePacket with explosion data\n"); + PIXBeginNamedEvent(0,"Handling explosion"); + Explosion *e = new Explosion(minecraft->level, nullptr, packet->x, packet->y, packet->z, packet->r); + PIXBeginNamedEvent(0,"Finalizing"); - // Fix for #81758 - TCR 006 BAS Non-Interactive Pause: TU9: Performance: Gameplay: After detonating bunch of TNT, game enters unresponsive state for couple of seconds. - // The changes we are making here have been decided by the server, so we don't need to add them to the vector that resets tiles changes made - // on the client as we KNOW that the server is matching these changes - MultiPlayerLevel *mpLevel = (MultiPlayerLevel *)minecraft->level; - mpLevel->enableResetChanges(false); - // 4J - now directly pass a pointer to the toBlow array in the packet rather than copying around - e->finalizeExplosion(true, &packet->toBlow); - mpLevel->enableResetChanges(true); - PIXEndNamedEvent(); - PIXEndNamedEvent(); - delete e; - } - else - { - //app.DebugPrintf("Received ExplodePacket with knockback only data\n"); + // Fix for #81758 - TCR 006 BAS Non-Interactive Pause: TU9: Performance: Gameplay: After detonating bunch of TNT, game enters unresponsive state for couple of seconds. + // The changes we are making here have been decided by the server, so we don't need to add them to the vector that resets tiles changes made + // on the client as we KNOW that the server is matching these changes + MultiPlayerLevel *mpLevel = (MultiPlayerLevel *)minecraft->level; + mpLevel->enableResetChanges(false); + // 4J - now directly pass a pointer to the toBlow array in the packet rather than copying around + e->finalizeExplosion(true, &packet->toBlow); + mpLevel->enableResetChanges(true); + PIXEndNamedEvent(); + PIXEndNamedEvent(); + delete e; + } } + // Per-player knockback — each connection applies to its own local player //app.DebugPrintf("Adding knockback (%f,%f,%f) for player %d\n", packet->getKnockbackX(), packet->getKnockbackY(), packet->getKnockbackZ(), m_userIndex); + if (minecraft->localplayers[m_userIndex] == NULL) + return; minecraft->localplayers[m_userIndex]->xd += packet->getKnockbackX(); minecraft->localplayers[m_userIndex]->yd += packet->getKnockbackY(); minecraft->localplayers[m_userIndex]->zd += packet->getKnockbackZ(); @@ -2893,6 +2936,8 @@ void ClientConnection::handleContainerOpen(shared_ptr packe { bool failed = false; shared_ptr player = minecraft->localplayers[m_userIndex]; + if (player == NULL) + return; switch(packet->type) { case ContainerOpenPacket::BONUS_CHEST: @@ -3199,6 +3244,7 @@ void ClientConnection::handleTileEditorOpen(shared_ptr pac void ClientConnection::handleSignUpdate(shared_ptr packet) { + if (!isPrimaryConnection()) return; app.DebugPrintf("ClientConnection::handleSignUpdate - "); if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z)) { @@ -3232,6 +3278,7 @@ void ClientConnection::handleSignUpdate(shared_ptr packet) void ClientConnection::handleTileEntityData(shared_ptr packet) { + if (!isPrimaryConnection()) return; if (minecraft->level->hasChunkAt(packet->x, packet->y, packet->z)) { shared_ptr te = minecraft->level->getTileEntity(packet->x, packet->y, packet->z); @@ -3284,6 +3331,7 @@ void ClientConnection::handleContainerClose(shared_ptr pac void ClientConnection::handleTileEvent(shared_ptr packet) { + if (!isPrimaryConnection()) return; PIXBeginNamedEvent(0,"Handle tile event\n"); minecraft->level->tileEvent(packet->x, packet->y, packet->z, packet->tile, packet->b0, packet->b1); PIXEndNamedEvent(); @@ -3291,6 +3339,7 @@ void ClientConnection::handleTileEvent(shared_ptr packet) void ClientConnection::handleTileDestruction(shared_ptr packet) { + if (!isPrimaryConnection()) return; minecraft->level->destroyTileProgress(packet->getEntityId(), packet->getX(), packet->getY(), packet->getZ(), packet->getState()); } @@ -3372,6 +3421,7 @@ void ClientConnection::handleGameEvent(shared_ptr gameEventPack void ClientConnection::handleComplexItemData(shared_ptr packet) { + if (!isPrimaryConnection()) return; if (packet->itemType == Item::map->id) { MapItem::getSavedData(packet->itemId, minecraft->level)->handleComplexItemData(packet->data); @@ -3386,6 +3436,7 @@ void ClientConnection::handleComplexItemData(shared_ptr p void ClientConnection::handleLevelEvent(shared_ptr packet) { + if (!isPrimaryConnection()) return; if (packet->type == LevelEvent::SOUND_DRAGON_DEATH) { for(unsigned int i = 0; i < XUSER_MAX_COUNT; ++i) @@ -3609,6 +3660,7 @@ void ClientConnection::handlePlayerAbilities(shared_ptr p void ClientConnection::handleSoundEvent(shared_ptr packet) { + if (!isPrimaryConnection()) return; minecraft->level->playLocalSound(packet->getX(), packet->getY(), packet->getZ(), packet->getSound(), packet->getVolume(), packet->getPitch(), false); } @@ -3921,6 +3973,7 @@ void ClientConnection::handleSetPlayerTeamPacket(shared_ptr void ClientConnection::handleParticleEvent(shared_ptr packet) { + if (!isPrimaryConnection()) return; for (int i = 0; i < packet->getCount(); i++) { double xVarience = random->nextGaussian() * packet->getXDist(); diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index a80c10f78..f13b93e7f 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -43,6 +43,7 @@ public: private: DWORD m_userIndex; // 4J Added + bool isPrimaryConnection() const; public: SavedDataStorage *savedDataStorage; ClientConnection(Minecraft *minecraft, const wstring& ip, int port); diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index aef1ef9c2..3c032bf97 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.cpp +++ b/Minecraft.Client/Common/Network/GameNetworkManager.cpp @@ -41,6 +41,11 @@ #include "..\Minecraft.World\DurangoStats.h" #endif +#ifdef _WINDOWS64 +#include "..\..\Windows64\Network\WinsockNetLayer.h" +#include "..\..\Windows64\Windows64_Xuid.h" +#endif + // Global instance CGameNetworkManager g_NetworkManager; CPlatformNetworkManager *CGameNetworkManager::s_pPlatformNetworkManager; @@ -1501,6 +1506,45 @@ void CGameNetworkManager::CreateSocket( INetworkPlayer *pNetworkPlayer, bool loc } else { +#ifdef _WINDOWS64 + // Non-host split-screen: open a dedicated TCP connection for this pad + if (localPlayer && !g_NetworkManager.IsHost() && g_NetworkManager.IsInGameplay()) + { + int padIdx = pNetworkPlayer->GetUserIndex(); + BYTE assignedSmallId = 0; + + if (!WinsockNetLayer::JoinSplitScreen(padIdx, &assignedSmallId)) + { + app.DebugPrintf("Split-screen pad %d: failed to open TCP to host\n", padIdx); + pMinecraft->connectionDisconnected(padIdx, DisconnectPacket::eDisconnect_ConnectionCreationFailed); + return; + } + + // Update the local IQNetPlayer (at pad index) with the host-assigned smallId. + // The NetworkPlayerXbox created by NotifyPlayerJoined already points to + // m_player[padIdx], so we just set the smallId for network routing. + IQNet::m_player[padIdx].m_smallId = assignedSmallId; + IQNet::m_player[padIdx].m_resolvedXuid = Win64Xuid::DeriveXuidForPad(Win64Xuid::ResolvePersistentXuid(), padIdx); + + // Network socket (not hostLocal) — data goes through TCP via GetLocalSocket + socket = new Socket(pNetworkPlayer, false, false); + pNetworkPlayer->SetSocket(socket); + + ClientConnection* connection = new ClientConnection(pMinecraft, socket, padIdx); + if (connection->createdOk) + { + connection->send(shared_ptr(new PreLoginPacket(pNetworkPlayer->GetOnlineName()))); + pMinecraft->addPendingLocalConnection(padIdx, connection); + } + else + { + pMinecraft->connectionDisconnected(padIdx, DisconnectPacket::eDisconnect_ConnectionCreationFailed); + delete connection; + } + return; + } +#endif + socket = new Socket( pNetworkPlayer, g_NetworkManager.IsHost(), g_NetworkManager.IsHost() && localPlayer ); pNetworkPlayer->SetSocket( socket ); diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index 03be8ec9b..b32cc9346 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -90,8 +90,8 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) pQNetPlayer, pQNetPlayer->GetGamertag(), pszDescription, - (int) pQNetPlayer->HasVoice(), - (int) pQNetPlayer->HasCamera() ); + pQNetPlayer->HasVoice(), + pQNetPlayer->HasCamera() ); if( m_pIQNet->IsHost() ) @@ -243,12 +243,18 @@ void CPlatformNetworkManagerStub::DoWork() if (IQNet::s_playerCount > 1) IQNet::s_playerCount--; } - // Always return smallId to the free pool so it can be reused (game may have already cleared the slot). - WinsockNetLayer::PushFreeSmallId(disconnectedSmallId); - // Clear O(1) socket lookup so GetSocketForSmallId stays fast (s_connections never shrinks). - WinsockNetLayer::ClearSocketForSmallId(disconnectedSmallId); + // NOTE: Do NOT call PushFreeSmallId here. The old PlayerConnection's + // write thread may still be alive (it dies in PlayerList::tick when + // m_smallIdsToClose is processed). If we recycle the smallId now, + // AcceptThread can reuse it for a new connection, and the old write + // thread's getPlayer() lookup will resolve to the NEW player, sending + // stale game packets to the new client's TCP socket — corrupting its + // login handshake (bad packet id crash). PushFreeSmallId and + // ClearSocketForSmallId are called from PlayerList::tick after the + // old Connection threads are dead. + // // Clear chunk visibility flags for this system so rejoin gets fresh chunk state. - SystemFlagRemoveBySmallId((int)disconnectedSmallId); + SystemFlagRemoveBySmallId(disconnectedSmallId); } } #endif @@ -261,7 +267,7 @@ bool CPlatformNetworkManagerStub::CanAcceptMoreConnections() if (server == NULL) return true; PlayerList* list = server->getPlayerList(); if (list == NULL) return true; - return (unsigned int)list->players.size() < (unsigned int)list->getMaxPlayers(); + return static_cast(list->players.size()) < static_cast(list->getMaxPlayers()); #else return true; #endif @@ -289,12 +295,40 @@ int CPlatformNetworkManagerStub::GetLocalPlayerMask(int playerIndex) bool CPlatformNetworkManagerStub::AddLocalPlayerByUserIndex( int userIndex ) { - NotifyPlayerJoined(m_pIQNet->GetLocalPlayerByUserIndex(userIndex)); - return ( m_pIQNet->AddLocalPlayerByUserIndex(userIndex) == S_OK ); + if ( m_pIQNet->AddLocalPlayerByUserIndex(userIndex) != S_OK ) + return false; + // Player is now registered in IQNet — get a pointer and notify the network layer. + // Use the static array directly: GetLocalPlayerByUserIndex checks customData which + // isn't set until addNetworkPlayer runs inside NotifyPlayerJoined. + NotifyPlayerJoined(&IQNet::m_player[userIndex]); + return true; } bool CPlatformNetworkManagerStub::RemoveLocalPlayerByUserIndex( int userIndex ) { +#ifdef _WINDOWS64 + if (userIndex > 0 && userIndex < XUSER_MAX_COUNT && !m_pIQNet->IsHost()) + { + IQNetPlayer* qp = &IQNet::m_player[userIndex]; + + // Notify the network layer before clearing the slot + if (qp->GetCustomDataValue() != 0) + { + NotifyPlayerLeaving(qp); + } + + // Close the split-screen TCP connection and reset WinsockNetLayer state + WinsockNetLayer::CloseSplitScreenConnection(userIndex); + + // Clear the IQNet slot so it can be reused on rejoin + qp->m_smallId = 0; + qp->m_isRemote = false; + qp->m_isHostPlayer = false; + qp->m_resolvedXuid = INVALID_XUID; + qp->m_gamertag[0] = 0; + qp->SetCustomDataValue(0); + } +#endif return true; } @@ -601,7 +635,7 @@ CPlatformNetworkManagerStub::PlayerFlags::PlayerFlags(INetworkPlayer *pNetworkPl this->flags = new unsigned char [ count / 8 ]; memset( this->flags, 0, count / 8 ); this->count = count; - this->m_smallId = (pNetworkPlayer && pNetworkPlayer->IsLocal()) ? 256 : (pNetworkPlayer ? (int)pNetworkPlayer->GetSmallId() : -1); + this->m_smallId = (pNetworkPlayer && pNetworkPlayer->IsLocal()) ? 256 : (pNetworkPlayer ? static_cast(pNetworkPlayer->GetSmallId()) : -1); } CPlatformNetworkManagerStub::PlayerFlags::~PlayerFlags() { @@ -778,52 +812,52 @@ void CPlatformNetworkManagerStub::SearchForGames() friendsSessions[0].push_back(info); } - std::FILE* file = std::fopen("servers.txt", "r"); + if (std::FILE* file = std::fopen("servers.db", "rb")) { + char magic[4] = {}; + if (std::fread(magic, 1, 4, file) == 4 && memcmp(magic, "MCSV", 4) == 0) + { + uint32_t version = 0, count = 0; + std::fread(&version, sizeof(uint32_t), 1, file); + std::fread(&count, sizeof(uint32_t), 1, file); - if (file) { - wstring wline; - int phase = 0; + if (version == 1) + { + for (uint32_t s = 0; s < count; s++) + { + uint16_t ipLen = 0, port = 0, nameLen = 0; + if (std::fread(&ipLen, sizeof(uint16_t), 1, file) != 1) break; + if (ipLen == 0 || ipLen > 256) break; - string ip; - wstring port; - wstring name; + char ipBuf[257] = {}; + if (std::fread(ipBuf, 1, ipLen, file) != ipLen) break; + if (std::fread(&port, sizeof(uint16_t), 1, file) != 1) break; - char buffer[512]; - while (std::fgets(buffer, sizeof(buffer), file)) { - if (phase == 0) { - ip = buffer; - if (!ip.empty() && (ip.back() == '\n' || ip.back() == '\r')) - ip.pop_back(); - phase = 1; - } - else if (phase == 1) { - wline = convStringToWstring(buffer); - port = wline; - phase = 2; - } - else if (phase == 2) { - wline = convStringToWstring(buffer); - name = wline; - phase = 0; + if (std::fread(&nameLen, sizeof(uint16_t), 1, file) != 1) break; + if (nameLen > 256) break; - //THEY GET DELETED AFTER USE LIKE 30 LINES UP!! - FriendSessionInfo* info = new FriendSessionInfo(); - wchar_t label[128]; - wcsncpy_s(label, sizeof(label)/sizeof(wchar_t), name.c_str(), _TRUNCATE); - size_t nameLen = wcslen(label); - info->displayLabel = new wchar_t[nameLen+1]; - wcscpy_s(info->displayLabel, nameLen + 1, label); - info->displayLabelLength = static_cast(nameLen); - info->displayLabelViewableStartIndex = 0; - info->data.isReadyToJoin = true; - info->data.isJoinable = true; - strncpy_s(info->data.hostIP, sizeof(info->data.hostIP), ip.c_str(), _TRUNCATE); - info->data.hostPort = stoi(port); - info->sessionId = (SessionID)(static_cast(inet_addr(ip.c_str())) | (static_cast(stoi(port)) << 32)); - friendsSessions[0].push_back(info); + char nameBuf[257] = {}; + if (nameLen > 0) + { + if (std::fread(nameBuf, 1, nameLen, file) != nameLen) break; + } + + wstring wName = convStringToWstring(nameBuf); + + FriendSessionInfo* info = new FriendSessionInfo(); + size_t nLen = wName.length(); + info->displayLabel = new wchar_t[nLen + 1]; + wcscpy_s(info->displayLabel, nLen + 1, wName.c_str()); + info->displayLabelLength = static_cast(nLen); + info->displayLabelViewableStartIndex = 0; + info->data.isReadyToJoin = true; + info->data.isJoinable = true; + strncpy_s(info->data.hostIP, sizeof(info->data.hostIP), ipBuf, _TRUNCATE); + info->data.hostPort = port; + info->sessionId = static_cast(inet_addr(ipBuf)) | static_cast(port) << 32; + friendsSessions[0].push_back(info); + } } } - std::fclose(file); } @@ -849,7 +883,7 @@ vector *CPlatformNetworkManagerStub::GetSessionList(int iPa { vector* filteredList = new vector(); for (size_t i = 0; i < friendsSessions[0].size(); i++) - filteredList->push_back(friendsSessions[0][i]); + filteredList->push_back(new FriendSessionInfo(*friendsSessions[0][i])); return filteredList; } diff --git a/Minecraft.Client/Common/Network/SessionInfo.h b/Minecraft.Client/Common/Network/SessionInfo.h index 7ae53c289..4e091c871 100644 --- a/Minecraft.Client/Common/Network/SessionInfo.h +++ b/Minecraft.Client/Common/Network/SessionInfo.h @@ -119,9 +119,36 @@ public: hasPartyMember = false; } + FriendSessionInfo(const FriendSessionInfo& other) + { + sessionId = other.sessionId; +#ifdef _XBOX + searchResult = other.searchResult; +#elif defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) + searchResult = other.searchResult; +#elif defined(_DURANGO) + searchResult = other.searchResult; +#endif + displayLabelLength = other.displayLabelLength; + displayLabelViewableStartIndex = other.displayLabelViewableStartIndex; + data = other.data; + hasPartyMember = other.hasPartyMember; + if (other.displayLabel != NULL) + { + displayLabel = new wchar_t[displayLabelLength + 1]; + wcscpy_s(displayLabel, displayLabelLength + 1, other.displayLabel); + } + else + { + displayLabel = NULL; + } + } + + FriendSessionInfo& operator=(const FriendSessionInfo&) = delete; + ~FriendSessionInfo() { if (displayLabel != nullptr) - delete displayLabel; + delete[] displayLabel; } }; diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp index 21f216274..d18bfd8fc 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp @@ -482,25 +482,14 @@ void IUIScene_AbstractContainerMenu::onMouseTick() #endif #ifdef _WINDOWS64 - if (!g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) + if (iPad == 0 && !g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) { int deltaX = g_KBMInput.GetMouseDeltaX(); int deltaY = g_KBMInput.GetMouseDeltaY(); - - extern HWND g_hWnd; - RECT rc; - GetClientRect(g_hWnd, &rc); - int winW = rc.right - rc.left; - int winH = rc.bottom - rc.top; - - if (winW > 0 && winH > 0) - { - float scaleX = static_cast(getMovieWidth()) / static_cast(winW); - float scaleY = static_cast(getMovieHeight()) / static_cast(winH); - - vPointerPos.x += static_cast(deltaX) * scaleX; - vPointerPos.y += static_cast(deltaY) * scaleY; - } + float scaleX, scaleY; + getMouseToSWFScale(scaleX, scaleY); + vPointerPos.x += static_cast(deltaX) * scaleX; + vPointerPos.y += static_cast(deltaY) * scaleY; if (deltaX != 0 || deltaY != 0) { diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h index 4877cfce8..718a2d447 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h @@ -277,4 +277,5 @@ public: virtual int getPad() = 0; virtual int getMovieWidth() = 0; virtual int getMovieHeight() = 0; + virtual void getMouseToSWFScale(float &scaleX, float &scaleY) = 0; }; diff --git a/Minecraft.Client/Common/UI/UIBitmapFont.cpp b/Minecraft.Client/Common/UI/UIBitmapFont.cpp index e9d110b71..2b1518c4c 100644 --- a/Minecraft.Client/Common/UI/UIBitmapFont.cpp +++ b/Minecraft.Client/Common/UI/UIBitmapFont.cpp @@ -250,15 +250,22 @@ rrbool UIBitmapFont::GetGlyphBitmap(S32 glyph,F32 pixel_scale,IggyBitmapCharacte // Choose a reasonable glyph scale. float glyphScale = 1.0f, truePixelScale = 1.0f / m_cFontData->getFontData()->m_fAdvPerPixel; - F32 targetPixelScale = pixel_scale; - //if(!RenderManager.IsWidescreen()) - //{ - // // Fix for different scales in 480 - // targetPixelScale = pixel_scale*2/3; - //} - while ( (0.5f + glyphScale) * truePixelScale < targetPixelScale) + while ( (0.5f + glyphScale) * truePixelScale < pixel_scale) glyphScale++; + // Debug: log each unique (font, pixel_scale) pair + { + static std::unordered_set s_loggedScaleKeys; + // Encode font pointer + quantized scale into a key to log each combo once + int scaleKey = (int)(pixel_scale * 100.0f) ^ (int)(uintptr_t)m_cFontData; + if (s_loggedScaleKeys.find(scaleKey) == s_loggedScaleKeys.end() && s_loggedScaleKeys.size() < 50) { + s_loggedScaleKeys.insert(scaleKey); + float tps = truePixelScale; + app.DebugPrintf("[FONT-DBG] GetGlyphBitmap: font=%s glyph=%d pixel_scale=%.3f truePixelScale=%.1f glyphScale=%.0f\n", + m_cFontData->getFontName().c_str(), glyph, pixel_scale, tps, glyphScale); + } + } + // 4J-JEV: Debug code to check which font sizes are being used. #if (!defined _CONTENT_PACKAGE) && (VERBOSE_FONT_OUTPUT > 0) @@ -303,9 +310,6 @@ rrbool UIBitmapFont::GetGlyphBitmap(S32 glyph,F32 pixel_scale,IggyBitmapCharacte } #endif - //app.DebugPrintf("Request glyph_%d (U+%.4X) at %f, converted to %f (%f)\n", - // glyph, GetUnicode(glyph), pixel_scale, targetPixelScale, glyphScale); - // It is not necessary to shrink the glyph width here // as its already been done in 'GetGlyphMetrics' by: // > metrics->x1 = m_kerningTable[glyph] * ratio; @@ -324,27 +328,57 @@ rrbool UIBitmapFont::GetGlyphBitmap(S32 glyph,F32 pixel_scale,IggyBitmapCharacte bitmap->top_left_y = -static_cast(m_cFontData->getFontData()->m_uiGlyphHeight) * m_cFontData->getFontData()->m_fAscent; bitmap->oversample = 0; - bitmap->point_sample = true; - // 4J-JEV: - // pixel_scale == font size chosen in flash. - // bitmap->pixel_scale_correct = (float) m_glyphHeight; // Scales the glyph to desired size. - // bitmap->pixel_scale_correct = pixel_scale; // Always the same size (not desired size). - // bitmap->pixel_scale_correct = pixel_scale * 0.5; // Doubles original size. - // bitmap->pixel_scale_correct = pixel_scale * 2; // Halves original size. - - // Actual scale, and possible range of scales. - bitmap->pixel_scale_correct = pixel_scale / glyphScale; - bitmap->pixel_scale_max = 99.0f; - bitmap->pixel_scale_min = 0.0f; - - /* 4J-JEV: Some of Sean's code. - int glyphScaleMin = 1; - int glyphScaleMax = 3; - float actualScale = pixel_scale / glyphScale; - bitmap->pixel_scale_correct = actualScale; - bitmap->pixel_scale_min = actualScale * glyphScaleMin * 0.999f; - bitmap->pixel_scale_max = actualScale * glyphScaleMax * 1.001f; */ +#ifdef _WINDOWS64 + // On Windows64 the window can be any size, producing fractional + // pixel_scale values that don't align to integer multiples of + // truePixelScale. The original console code cached glyphs with a + // broad [truePixelScale, 99] range in the "normal" branch, which + // works on consoles (fixed 1080p — font sizes are exact multiples) + // but causes cache pollution on Windows: the first glyph cached in + // that range sets pixel_scale_correct for ALL subsequent requests, + // so different font sizes get scaled by wrong ratios, producing + // mixed letter sizes on screen. + // + // Fix: always use pixel_scale_correct = truePixelScale so every + // cache entry is consistent. Two ranges: downscale (bilinear for + // smooth reduction) and upscale (point_sample for crisp pixel-art). + bitmap->pixel_scale_correct = truePixelScale; + if (pixel_scale < truePixelScale) + { + bitmap->pixel_scale_min = 0.0f; + bitmap->pixel_scale_max = truePixelScale; + bitmap->point_sample = false; + } + else + { + bitmap->pixel_scale_min = truePixelScale; + bitmap->pixel_scale_max = 99.0f; + bitmap->point_sample = true; + } +#else + if (glyphScale <= 1 && pixel_scale < truePixelScale) + { + // Small display: pixel_scale is less than the native glyph size. + // Report the bitmap at its true native scale so Iggy downscales it + // to match the layout metrics (bilinear for smooth downscaling). + bitmap->pixel_scale_correct = truePixelScale; + bitmap->pixel_scale_min = 0.0f; + bitmap->pixel_scale_max = truePixelScale * 1.001f; + bitmap->point_sample = false; + } + else + { + // Normal/upscale case: integer-multiple scaling for pixel-art look. + // Console-only — fixed resolution means pixel_scale values are exact + // integer multiples of truePixelScale, so cache sharing is safe. + float actualScale = pixel_scale / glyphScale; + bitmap->pixel_scale_correct = actualScale; + bitmap->pixel_scale_min = truePixelScale; + bitmap->pixel_scale_max = 99.0f; + bitmap->point_sample = true; + } +#endif // 4J-JEV: Nothing to do with glyph placement, // entirely to do with cropping your glyph out of an archive. diff --git a/Minecraft.Client/Common/UI/UIComponent_Chat.cpp b/Minecraft.Client/Common/UI/UIComponent_Chat.cpp index 555f36e4e..257199810 100644 --- a/Minecraft.Client/Common/UI/UIComponent_Chat.cpp +++ b/Minecraft.Client/Common/UI/UIComponent_Chat.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIComponent_Chat.h" +#include "UISplitScreenHelpers.h" #include "..\..\Minecraft.h" #include "..\..\Gui.h" @@ -120,6 +121,7 @@ void UIComponent_Chat::render(S32 width, S32 height, C4JRender::eViewportType vi S32 tileWidth = width; S32 tileHeight = height; + bool needsYTile = false; switch( viewport ) { case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: @@ -127,25 +129,30 @@ void UIComponent_Chat::render(S32 width, S32 height, C4JRender::eViewportType vi tileHeight = static_cast(ui.getScreenHeight()); break; case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: - tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); - break; case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); + needsYTile = true; 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: - tileYStart = (S32)(m_movieHeight / 2); + needsYTile = true; break; } - IggyPlayerSetDisplaySize( getMovie(), m_movieWidth, m_movieHeight ); + F32 scale; + ComputeTileScale(tileWidth, tileHeight, m_movieWidth, m_movieHeight, needsYTile, scale, tileYStart); + IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) ); + + S32 contentOffX, contentOffY; + ComputeSplitContentOffset(viewport, m_movieWidth, m_movieHeight, scale, tileWidth, tileHeight, tileYStart, contentOffX, contentOffY); + xPos += contentOffX; + yPos += contentOffY; + ui.setupRenderPosition(xPos, yPos); IggyPlayerDrawTilesStart ( getMovie() ); - + m_renderWidth = tileWidth; m_renderHeight = tileHeight; IggyPlayerDrawTile ( getMovie() , @@ -153,7 +160,7 @@ void UIComponent_Chat::render(S32 width, S32 height, C4JRender::eViewportType vi tileYStart , tileXStart + tileWidth , tileYStart + tileHeight , - 0 ); + 0 ); IggyPlayerDrawTilesEnd ( getMovie() ); } else diff --git a/Minecraft.Client/Common/UI/UIComponent_MenuBackground.cpp b/Minecraft.Client/Common/UI/UIComponent_MenuBackground.cpp index 1718e8cb8..b7c4ebbdb 100644 --- a/Minecraft.Client/Common/UI/UIComponent_MenuBackground.cpp +++ b/Minecraft.Client/Common/UI/UIComponent_MenuBackground.cpp @@ -2,6 +2,8 @@ #include "UI.h" #include "UIComponent_MenuBackground.h" +#include + UIComponent_MenuBackground::UIComponent_MenuBackground(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { m_bSplitscreen = false; @@ -68,24 +70,29 @@ void UIComponent_MenuBackground::render(S32 width, S32 height, C4JRender::eViewp break; case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); + tileYStart = static_cast(ui.getScreenHeight() / 2); break; case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); + tileYStart = static_cast(ui.getScreenHeight() / 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: - tileYStart = (S32)(m_movieHeight / 2); + tileYStart = static_cast(ui.getScreenHeight() / 2); break; } - IggyPlayerSetDisplaySize( getMovie(), m_movieWidth, m_movieHeight ); + F32 scaleW = static_cast(tileXStart + tileWidth) / static_cast(m_movieWidth); + F32 scaleH = static_cast(tileYStart + tileHeight) / static_cast(m_movieHeight); + F32 scale = (scaleW > scaleH) ? scaleW : scaleH; + scale = max(scale, 1.0f); + + IggyPlayerSetDisplaySize( getMovie(), static_cast(m_movieWidth * scale), static_cast(m_movieHeight * scale) ); IggyPlayerDrawTilesStart ( getMovie() ); - + m_renderWidth = tileWidth; m_renderHeight = tileHeight; IggyPlayerDrawTile ( getMovie() , @@ -93,11 +100,15 @@ void UIComponent_MenuBackground::render(S32 width, S32 height, C4JRender::eViewp tileYStart , tileXStart + tileWidth , tileYStart + tileHeight , - 0 ); + 0 ); IggyPlayerDrawTilesEnd ( getMovie() ); } else { - UIScene::render(width, height, viewport); + if(m_bIsReloading) return; + if(!m_hasTickedOnce || !getMovie()) return; + ui.setupRenderPosition(0, 0); + IggyPlayerSetDisplaySize( getMovie(), static_cast(ui.getScreenWidth()), static_cast(ui.getScreenHeight()) ); + IggyPlayerDraw( getMovie() ); } } \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIComponent_Panorama.cpp b/Minecraft.Client/Common/UI/UIComponent_Panorama.cpp index a5b93118a..3da8174c7 100644 --- a/Minecraft.Client/Common/UI/UIComponent_Panorama.cpp +++ b/Minecraft.Client/Common/UI/UIComponent_Panorama.cpp @@ -93,38 +93,47 @@ void UIComponent_Panorama::render(S32 width, S32 height, C4JRender::eViewportTyp } ui.setupRenderPosition(xPos, yPos); - if((viewport == C4JRender::VIEWPORT_TYPE_SPLIT_LEFT) || (viewport == C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT)) + S32 tileXStart = 0; + S32 tileYStart = 0; + S32 tileWidth = width; + S32 tileHeight = height; + + if((viewport == C4JRender::VIEWPORT_TYPE_SPLIT_LEFT) || (viewport == C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT)) { - // Need to render at full height, but only the left side of the scene - S32 tileXStart = 0; - S32 tileYStart = 0; - S32 tileWidth = width; - S32 tileHeight = static_cast(ui.getScreenHeight()); - - IggyPlayerSetDisplaySize( getMovie(), m_movieWidth, m_movieHeight ); - - IggyPlayerDrawTilesStart ( getMovie() ); - - m_renderWidth = tileWidth; - m_renderHeight = tileHeight; - IggyPlayerDrawTile ( getMovie() , - tileXStart , - tileYStart , - tileXStart + tileWidth , - tileYStart + tileHeight , - 0 ); - IggyPlayerDrawTilesEnd ( getMovie() ); + tileHeight = static_cast(ui.getScreenHeight()); } else { - // Need to render at full height, and full width. But compressed into the viewport - IggyPlayerSetDisplaySize( getMovie(), ui.getScreenWidth(), ui.getScreenHeight()/2 ); - IggyPlayerDraw( getMovie() ); + tileWidth = static_cast(ui.getScreenWidth()); + tileYStart = static_cast(ui.getScreenHeight() / 2); } + + F32 scaleW = static_cast(tileXStart + tileWidth) / static_cast(m_movieWidth); + F32 scaleH = static_cast(tileYStart + tileHeight) / static_cast(m_movieHeight); + F32 scale = (scaleW > scaleH) ? scaleW : scaleH; + if(scale < 1.0f) scale = 1.0f; + + IggyPlayerSetDisplaySize( getMovie(), static_cast(m_movieWidth * scale), static_cast(m_movieHeight * scale) ); + + IggyPlayerDrawTilesStart ( getMovie() ); + + m_renderWidth = tileWidth; + m_renderHeight = tileHeight; + IggyPlayerDrawTile ( getMovie() , + tileXStart , + tileYStart , + tileXStart + tileWidth , + tileYStart + tileHeight , + 0 ); + IggyPlayerDrawTilesEnd ( getMovie() ); } else { - UIScene::render(width, height, viewport); + if(m_bIsReloading) return; + if(!m_hasTickedOnce || !getMovie()) return; + ui.setupRenderPosition(0, 0); + IggyPlayerSetDisplaySize( getMovie(), static_cast(ui.getScreenWidth()), static_cast(ui.getScreenHeight()) ); + IggyPlayerDraw( getMovie() ); } } diff --git a/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp b/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp index 856611256..566f759d2 100644 --- a/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp +++ b/Minecraft.Client/Common/UI/UIComponent_Tooltips.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIComponent_Tooltips.h" +#include "UISplitScreenHelpers.h" UIComponent_Tooltips::UIComponent_Tooltips(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { @@ -224,6 +225,7 @@ void UIComponent_Tooltips::render(S32 width, S32 height, C4JRender::eViewportTyp S32 tileWidth = width; S32 tileHeight = height; + bool needsYTile = false; switch( viewport ) { case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: @@ -231,25 +233,30 @@ void UIComponent_Tooltips::render(S32 width, S32 height, C4JRender::eViewportTyp tileHeight = static_cast(ui.getScreenHeight()); break; case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: - tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); - break; case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); + needsYTile = true; 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: - tileYStart = (S32)(m_movieHeight / 2); + needsYTile = true; break; } - IggyPlayerSetDisplaySize( getMovie(), m_movieWidth, m_movieHeight ); + F32 scale; + ComputeTileScale(tileWidth, tileHeight, m_movieWidth, m_movieHeight, needsYTile, scale, tileYStart); + IggyPlayerSetDisplaySize( getMovie(), (S32)(m_movieWidth * scale), (S32)(m_movieHeight * scale) ); + + S32 contentOffX, contentOffY; + ComputeSplitContentOffset(viewport, m_movieWidth, m_movieHeight, scale, tileWidth, tileHeight, tileYStart, contentOffX, contentOffY); + xPos += contentOffX; + yPos += contentOffY; + ui.setupRenderPosition(xPos, yPos); IggyPlayerDrawTilesStart ( getMovie() ); - + m_renderWidth = tileWidth; m_renderHeight = tileHeight; IggyPlayerDrawTile ( getMovie() , @@ -257,7 +264,7 @@ void UIComponent_Tooltips::render(S32 width, S32 height, C4JRender::eViewportTyp tileYStart , tileXStart + tileWidth , tileYStart + tileHeight , - 0 ); + 0 ); IggyPlayerDrawTilesEnd ( getMovie() ); } else diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index 3da1b11a5..e64808fc9 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -13,6 +13,7 @@ #include "..\..\EnderDragonRenderer.h" #include "..\..\MultiPlayerLocalPlayer.h" #include "UIFontData.h" +#include "UISplitScreenHelpers.h" #ifdef _WINDOWS64 #include "..\..\Windows64\KeyboardMouseInput.h" #endif @@ -57,6 +58,8 @@ bool UIController::ms_bReloadSkinCSInitialised = false; DWORD UIController::m_dwTrialTimerLimitSecs=DYNAMIC_CONFIG_DEFAULT_TRIAL_TIME; +// GetViewportRect and Fit16x9 are now in UISplitScreenHelpers.h + #ifdef _WINDOWS64 static UIControl_Slider *FindSliderById(UIScene *pScene, int sliderId) { @@ -801,20 +804,22 @@ void UIController::tickInput() #ifndef _CONTENT_PACKAGE eUILayer_Debug, #endif - eUILayer_Error, - eUILayer_Alert, - eUILayer_Popup, - eUILayer_Fullscreen, - eUILayer_Scene, - }; - - for (int l = 0; l < _countof(mouseLayers) && !pScene; ++l) + eUILayer_Error, + eUILayer_Alert, + eUILayer_Popup, + eUILayer_Fullscreen, + eUILayer_Scene, + }; + // Only check the fullscreen group and the primary (KBM) player's group. + // Other splitscreen players use controllers — mouse must not affect them. + const int mouseGroups[] = { static_cast(eUIGroup_Fullscreen), ProfileManager.GetPrimaryPad() + 1 }; + for (int l = 0; l < _countof(mouseLayers) && !pScene; ++l) + { + for (int g = 0; g < _countof(mouseGroups) && !pScene; ++g) { - for (int grp = 0; grp < eUIGroup_COUNT && !pScene; ++grp) - { - pScene = m_groups[grp]->GetTopScene(mouseLayers[l]); - } + pScene = m_groups[mouseGroups[g]]->GetTopScene(mouseLayers[l]); } + } if (pScene && pScene->getMovie()) { int rawMouseX = g_KBMInput.GetMouseX(); @@ -843,9 +848,14 @@ void UIController::tickInput() m_lastHoverMouseX = rawMouseX; m_lastHoverMouseY = rawMouseY; - // Convert mouse to scene/movie coordinates - F32 sceneMouseX = (F32)rawMouseX; - F32 sceneMouseY = (F32)rawMouseY; + // Convert mouse window-pixel coords to Flash/SWF authoring coords. + // In split-screen the scene is rendered at a tile-origin offset + // and at a smaller display size, so we must: + // 1. Map window pixels -> UIController screen space + // 2. Subtract the viewport tile origin + // 3. Scale from display dimensions to SWF authoring dimensions + F32 sceneMouseX = static_cast(rawMouseX); + F32 sceneMouseY = static_cast(rawMouseY); { extern HWND g_hWnd; RECT rc; @@ -855,8 +865,30 @@ void UIController::tickInput() int winH = rc.bottom - rc.top; if (winW > 0 && winH > 0) { - sceneMouseX = sceneMouseX * ((F32)pScene->getRenderWidth() / (F32)winW); - sceneMouseY = sceneMouseY * ((F32)pScene->getRenderHeight() / (F32)winH); + // Step 1: window pixels -> screen space + F32 screenX = sceneMouseX * (getScreenWidth() / static_cast(winW)); + F32 screenY = sceneMouseY * (getScreenHeight() / static_cast(winH)); + + // Step 2 & 3: account for split-screen viewport + C4JRender::eViewportType vp = pScene->GetParentLayer()->getViewport(); + S32 displayW = 0, displayH = 0; + getRenderDimensions(vp, displayW, displayH); + + F32 vpOriginX, vpOriginY, vpW, vpH; + GetViewportRect(getScreenWidth(), getScreenHeight(), vp, vpOriginX, vpOriginY, vpW, vpH); + // All viewports use Fit16x9 for menu scenes + S32 fitW, fitH, fitOffsetX, fitOffsetY; + Fit16x9(vpW, vpH, fitW, fitH, fitOffsetX, fitOffsetY); + S32 originX = static_cast(vpOriginX) + fitOffsetX; + S32 originY = static_cast(vpOriginY) + fitOffsetY; + displayW = fitW; + displayH = fitH; + + if (displayW > 0 && displayH > 0) + { + sceneMouseX = (screenX - originX) * (static_cast(pScene->getRenderWidth()) / static_cast(displayW)); + sceneMouseY = (screenY - originY) * (static_cast(pScene->getRenderHeight()) / static_cast(displayH)); + } } } } @@ -908,7 +940,7 @@ void UIController::tickInput() // TexturePackList origin is where the slot area starts, // not the top-left of the whole control — use GetRealHeight. if (type == UIControl::eTexturePackList) - ch = ((UIControl_TexturePackList *)ctrl)->GetRealHeight(); + ch = static_cast(ctrl)->GetRealHeight(); if (cw <= 0 || ch <= 0) continue; @@ -919,8 +951,8 @@ void UIController::tickInput() { // ButtonList manages focus internally via Flash — // pass mouse coords so it can highlight the right item. - ((UIControl_ButtonList *)ctrl)->SetTouchFocus( - (S32)sceneMouseX, (S32)sceneMouseY, false); + static_cast(ctrl)->SetTouchFocus( + static_cast(sceneMouseX), static_cast(sceneMouseY), false); hitControlId = -1; hitArea = INT_MAX; hitCtrl = NULL; @@ -929,10 +961,10 @@ void UIController::tickInput() if (type == UIControl::eTexturePackList) { // TexturePackList expects coords relative to its origin. - UIControl_TexturePackList *pList = (UIControl_TexturePackList *)ctrl; + UIControl_TexturePackList *pList = static_cast(ctrl); pScene->SetFocusToElement(ctrl->getId()); pList->SetTouchFocus( - (S32)(sceneMouseX - cx), (S32)(sceneMouseY - cy), false); + static_cast(sceneMouseX - cx), static_cast(sceneMouseY - cy), false); m_bMouseHoverHorizontalList = true; hitControlId = -1; hitArea = INT_MAX; @@ -968,7 +1000,7 @@ void UIController::tickInput() // happens after both tickInput and scene tick, so no flicker. if (hitCtrl && hitCtrl->getControlType() == UIControl::eTextInput) { - ((UIControl_TextInput *)hitCtrl)->setCaretVisible(false); + static_cast(hitCtrl)->setCaretVisible(false); } } } @@ -1584,73 +1616,48 @@ void UIController::renderScenes() void UIController::getRenderDimensions(C4JRender::eViewportType viewport, S32 &width, S32 &height) { - switch( viewport ) + F32 originX, originY, viewW, viewH; + GetViewportRect(getScreenWidth(), getScreenHeight(), viewport, originX, originY, viewW, viewH); + + if(viewport == C4JRender::VIEWPORT_TYPE_FULLSCREEN) { - case C4JRender::VIEWPORT_TYPE_FULLSCREEN: - width = static_cast(getScreenWidth()); - height = static_cast(getScreenHeight()); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: - case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - width = static_cast(getScreenWidth() / 2); - height = static_cast(getScreenHeight() / 2); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: - case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: - width = static_cast(getScreenWidth() / 2); - height = static_cast(getScreenHeight() / 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: - width = static_cast(getScreenWidth() / 2); - height = static_cast(getScreenHeight() / 2); - break; + S32 offsetX, offsetY; + Fit16x9(viewW, viewH, width, height, offsetX, offsetY); + } + else + { + // Split-screen: use raw viewport dims — the SWF tiling code handles non-16:9 + width = static_cast(viewW); + height = static_cast(viewH); } } void UIController::setupRenderPosition(C4JRender::eViewportType viewport) { - if(m_bCustomRenderPosition || m_currentRenderViewport != viewport) + m_currentRenderViewport = viewport; + m_bCustomRenderPosition = false; + + F32 vpOriginX, vpOriginY, vpW, vpH; + GetViewportRect(getScreenWidth(), getScreenHeight(), viewport, vpOriginX, vpOriginY, vpW, vpH); + + S32 xPos, yPos; + if(viewport == C4JRender::VIEWPORT_TYPE_FULLSCREEN) { - m_currentRenderViewport = viewport; - m_bCustomRenderPosition = false; - S32 xPos = 0; - S32 yPos = 0; - switch( viewport ) - { - case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: - xPos = static_cast(getScreenWidth() / 4); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - xPos = static_cast(getScreenWidth() / 4); - yPos = static_cast(getScreenHeight() / 2); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: - yPos = static_cast(getScreenHeight() / 4); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: - xPos = static_cast(getScreenWidth() / 2); - yPos = static_cast(getScreenHeight() / 4); - break; - case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT: - break; - case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: - xPos = static_cast(getScreenWidth() / 2); - break; - case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: - yPos = static_cast(getScreenHeight() / 2); - break; - case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: - xPos = static_cast(getScreenWidth() / 2); - yPos = static_cast(getScreenHeight() / 2); - break; - } - m_tileOriginX = xPos; - m_tileOriginY = yPos; - setTileOrigin(xPos, yPos); + S32 fitW, fitH, fitOffsetX, fitOffsetY; + Fit16x9(vpW, vpH, fitW, fitH, fitOffsetX, fitOffsetY); + xPos = static_cast(vpOriginX) + fitOffsetX; + yPos = static_cast(vpOriginY) + fitOffsetY; } + else + { + // Split-screen: position at viewport origin, no 16:9 fitting + xPos = static_cast(vpOriginX); + yPos = static_cast(vpOriginY); + } + + m_tileOriginX = xPos; + m_tileOriginY = yPos; + setTileOrigin(xPos, yPos); } void UIController::setupRenderPosition(S32 xOrigin, S32 yOrigin) @@ -1858,8 +1865,11 @@ void RADLINK UIController::TextureSubstitutionDestroyCallback ( void * user_call ui.destroySubstitutionTexture(user_callback_data, handle); - Textures *t = Minecraft::GetInstance()->textures; - t->releaseTexture( id ); + Minecraft* mc = Minecraft::GetInstance(); + if (mc && mc->textures) + { + mc->textures->releaseTexture( id ); + } } void UIController::registerSubstitutionTexture(const wstring &textureName, PBYTE pbData, DWORD dwLength) diff --git a/Minecraft.Client/Common/UI/UIController.h b/Minecraft.Client/Common/UI/UIController.h index f6d0e87d6..29c278129 100644 --- a/Minecraft.Client/Common/UI/UIController.h +++ b/Minecraft.Client/Common/UI/UIController.h @@ -257,6 +257,7 @@ public: // RENDERING float getScreenWidth() { return m_fScreenWidth; } float getScreenHeight() { return m_fScreenHeight; } + void updateScreenSize(S32 w, S32 h) { m_fScreenWidth = (float)w; m_fScreenHeight = (float)h; app.DebugPrintf("[UI-INIT] updateScreenSize: %d x %d\n", w, h); } virtual void render() = 0; void getRenderDimensions(C4JRender::eViewportType viewport, S32 &width, S32 &height); diff --git a/Minecraft.Client/Common/UI/UIScene.cpp b/Minecraft.Client/Common/UI/UIScene.cpp index a714d32ef..6918bc5bd 100644 --- a/Minecraft.Client/Common/UI/UIScene.cpp +++ b/Minecraft.Client/Common/UI/UIScene.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIScene.h" +#include "UISplitScreenHelpers.h" #include "..\..\Lighting.h" #include "..\..\LocalPlayer.h" @@ -285,26 +286,8 @@ void UIScene::loadMovie() moviePath.append(L"Vita.swf"); m_loadedResolution = eSceneResolution_Vita; #elif defined _WINDOWS64 - if(ui.getScreenHeight() == 720) - { - moviePath.append(L"720.swf"); - m_loadedResolution = eSceneResolution_720; - } - else if(ui.getScreenHeight() == 480) - { - moviePath.append(L"480.swf"); - m_loadedResolution = eSceneResolution_480; - } - else if(ui.getScreenHeight() < 720) - { - moviePath.append(L"Vita.swf"); - m_loadedResolution = eSceneResolution_Vita; - } - else - { - moviePath.append(L"1080.swf"); - m_loadedResolution = eSceneResolution_1080; - } + moviePath.append(L"1080.swf"); + m_loadedResolution = eSceneResolution_1080; #else moviePath.append(L"1080.swf"); m_loadedResolution = eSceneResolution_1080; @@ -332,8 +315,6 @@ void UIScene::loadMovie() int64_t beforeLoad = ui.iggyAllocCount; swf = IggyPlayerCreateFromMemory ( baFile.data , baFile.length, nullptr); int64_t afterLoad = ui.iggyAllocCount; - IggyPlayerInitializeAndTickRS ( swf ); - int64_t afterTick = ui.iggyAllocCount; if(!swf) { @@ -343,17 +324,44 @@ void UIScene::loadMovie() #endif app.FatalLoadError(); } - app.DebugPrintf( app.USER_SR, "Loaded iggy movie %ls\n", moviePath.c_str() ); + + // Read movie dimensions from the SWF header (available immediately after + // CreateFromMemory, no init tick needed). IggyProperties *properties = IggyPlayerProperties ( swf ); m_movieHeight = properties->movie_height_in_pixels; m_movieWidth = properties->movie_width_in_pixels; - m_renderWidth = m_movieWidth; m_renderHeight = m_movieHeight; - S32 width, height; - m_parentLayer->getRenderDimensions(width, height); - IggyPlayerSetDisplaySize( swf, width, height ); + // Set display size BEFORE the init tick to match what render() will use. + // InitializeAndTickRS runs ActionScript that creates text fields. If the + // display size here differs from what render() passes to SetDisplaySize, + // Iggy can cache glyph rasterizations at one scale during init and then + // reuse them at a different scale during draw, producing mixed glyph sizes. +#ifdef _WINDOWS64 + { + S32 fitW, fitH, fitOffX, fitOffY; + Fit16x9(ui.getScreenWidth(), ui.getScreenHeight(), fitW, fitH, fitOffX, fitOffY); + IggyPlayerSetDisplaySize( swf, fitW, fitH ); + } +#else + IggyPlayerSetDisplaySize( swf, m_movieWidth, m_movieHeight ); +#endif + + IggyPlayerInitializeAndTickRS ( swf ); + int64_t afterTick = ui.iggyAllocCount; + +#ifdef _WINDOWS64 + // Flush Iggy's internal font caches so all glyphs get rasterized fresh + // at the current display scale on the first Draw. Without this, stale + // cache entries from a previous scene (loaded at a different display size) + // cause mixed glyph sizes. ResizeD3D already calls this, which is why + // fonts look correct after a resize but break when a scene reloads + // without one. + IggyFlushInstalledFonts(); +#endif + + app.DebugPrintf( app.USER_SR, "Loaded iggy movie %ls\n", moviePath.c_str() ); IggyPlayerSetUserdata(swf,this); @@ -561,7 +569,7 @@ bool UIScene::handleMouseClick(F32 x, F32 y) { if (bestCtrl->getControlType() == UIControl::eCheckBox) { - UIControl_CheckBox *cb = (UIControl_CheckBox *)bestCtrl; + UIControl_CheckBox *cb = static_cast(bestCtrl); bool newState = !cb->IsChecked(); cb->setChecked(newState); handleCheckboxToggled((F64)bestId, newState); @@ -686,9 +694,23 @@ void UIScene::render(S32 width, S32 height, C4JRender::eViewportType viewport) { if(m_bIsReloading) return; if(!m_hasTickedOnce || !swf) return; - ui.setupRenderPosition(viewport); - IggyPlayerSetDisplaySize( swf, width, height ); - IggyPlayerDraw( swf ); + + if(viewport != C4JRender::VIEWPORT_TYPE_FULLSCREEN) + { + 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); + ui.setupRenderPosition(static_cast(originX) + offsetX, static_cast(originY) + offsetY); + IggyPlayerSetDisplaySize( swf, fitW, fitH ); + IggyPlayerDraw( swf ); + } + else + { + ui.setupRenderPosition(viewport); + IggyPlayerSetDisplaySize( swf, width, height ); + IggyPlayerDraw( swf ); + } } void UIScene::setOpacity(float percent) @@ -1332,11 +1354,16 @@ bool UIScene::hasRegisteredSubstitutionTexture(const wstring &textureName) void UIScene::_handleFocusChange(F64 controlId, F64 childId) { - m_iFocusControl = static_cast(controlId); - m_iFocusChild = static_cast(childId); + int newControl = static_cast(controlId); + int newChild = static_cast(childId); + if (newControl != m_iFocusControl || newChild != m_iFocusChild) + { + m_iFocusControl = newControl; + m_iFocusChild = newChild; - handleFocusChange(controlId, childId); - ui.PlayUISFX(eSFX_Focus); + handleFocusChange(controlId, childId); + ui.PlayUISFX(eSFX_Focus); + } } void UIScene::_handleInitFocus(F64 controlId, F64 childId) diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp index b8193bac0..0c3b6096d 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIScene_AbstractContainerMenu.h" +#include "UISplitScreenHelpers.h" #include "..\..\..\Minecraft.World\net.minecraft.world.inventory.h" #include "..\..\..\Minecraft.World\net.minecraft.world.item.h" @@ -187,14 +188,19 @@ void UIScene_AbstractContainerMenu::PlatformInitialize(int iPad, int startIndex) IggyEvent mouseEvent; S32 width, height; m_parentLayer->getRenderDimensions(width, height); + + C4JRender::eViewportType vp = m_parentLayer->getViewport(); + if(vp != C4JRender::VIEWPORT_TYPE_FULLSCREEN) + Fit16x9(width, height); + S32 x = m_pointerPos.x*(static_cast(width)/m_movieWidth); - S32 y = m_pointerPos.y*(static_cast(height)/m_movieHeight); + S32 y = m_pointerPos.y*(static_cast(height)/m_movieHeight); IggyMakeEventMouseMove( &mouseEvent, x, y); IggyEventResult result; IggyPlayerDispatchEventRS ( getMovie() , &mouseEvent , &result ); -#ifdef USE_POINTER_ACCEL +#ifdef USE_POINTER_ACCEL m_fPointerVelX = 0.0f; m_fPointerVelY = 0.0f; m_fPointerAccelX = 0.0f; @@ -212,8 +218,12 @@ void UIScene_AbstractContainerMenu::tick() S32 width, height; m_parentLayer->getRenderDimensions(width, height); - S32 x = static_cast(m_pointerPos.x * ((float)width / m_movieWidth)); - S32 y = static_cast(m_pointerPos.y * ((float)height / m_movieHeight)); + C4JRender::eViewportType vp = m_parentLayer->getViewport(); + if(vp != C4JRender::VIEWPORT_TYPE_FULLSCREEN) + Fit16x9(width, height); + + S32 x = static_cast(m_pointerPos.x * (static_cast(width) / m_movieWidth)); + S32 y = static_cast(m_pointerPos.y * (static_cast(height) / m_movieHeight)); IggyMakeEventMouseMove( &mouseEvent, x, y); @@ -251,6 +261,27 @@ void UIScene_AbstractContainerMenu::render(S32 width, S32 height, C4JRender::eVi m_needsCacheRendered = false; } +void UIScene_AbstractContainerMenu::getMouseToSWFScale(float &scaleX, float &scaleY) +{ + extern HWND g_hWnd; + RECT rc; + GetClientRect(g_hWnd, &rc); + int winW = rc.right - rc.left; + int winH = rc.bottom - rc.top; + if(winW <= 0 || winH <= 0) { scaleX = 1.0f; scaleY = 1.0f; return; } + + S32 renderW, renderH; + C4JRender::eViewportType vp = GetParentLayer()->getViewport(); + ui.getRenderDimensions(vp, renderW, renderH); + if(vp != C4JRender::VIEWPORT_TYPE_FULLSCREEN) + Fit16x9(renderW, renderH); + + float screenW = (float)ui.getScreenWidth(); + float screenH = (float)ui.getScreenHeight(); + scaleX = static_cast(m_movieWidth) * screenW / (static_cast(renderW) * static_cast(winW)); + scaleY = static_cast(m_movieHeight) * screenH / (static_cast(renderH) * static_cast(winH)); +} + void UIScene_AbstractContainerMenu::customDraw(IggyCustomDrawCallbackRegion *region) { Minecraft *pMinecraft = Minecraft::GetInstance(); diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h index c6ef29bbd..f28a5830a 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h @@ -38,6 +38,7 @@ public: int getPad() { return m_iPad; } int getMovieWidth() { return m_movieWidth; } int getMovieHeight() { return m_movieHeight; } + void getMouseToSWFScale(float &scaleX, float &scaleY); bool getIgnoreInput() { return m_bIgnoreInput; } void setIgnoreInput(bool bVal) { m_bIgnoreInput=bVal; } diff --git a/Minecraft.Client/Common/UI/UIScene_HUD.cpp b/Minecraft.Client/Common/UI/UIScene_HUD.cpp index 2773b874b..61cec0f09 100644 --- a/Minecraft.Client/Common/UI/UIScene_HUD.cpp +++ b/Minecraft.Client/Common/UI/UIScene_HUD.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIScene_HUD.h" +#include "UISplitScreenHelpers.h" #include "BossMobGuiInfo.h" #include "..\..\Minecraft.h" #include "..\..\MultiplayerLocalPlayer.h" @@ -266,8 +267,6 @@ void UIScene_HUD::handleReload() SetDisplayName(ProfileManager.GetDisplayName(m_iPad)); - repositionHud(); - SetTooltipsEnabled(((ui.GetMenuDisplayed(ProfileManager.GetPrimaryPad())) || (app.GetGameSettings(ProfileManager.GetPrimaryPad(),eGameSetting_Tooltips) != 0))); } @@ -697,30 +696,32 @@ void UIScene_HUD::render(S32 width, S32 height, C4JRender::eViewportType viewpor S32 tileWidth = width; S32 tileHeight = height; + bool needsYTile = false; switch( viewport ) { case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: tileHeight = static_cast(ui.getScreenHeight()); break; - case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: + case C4JRender::VIEWPORT_TYPE_SPLIT_TOP:case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - tileWidth = static_cast(ui.getScreenWidth()); - tileYStart = (S32)(m_movieHeight / 2); + needsYTile = true; 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: - tileYStart = (S32)(m_movieHeight / 2); + needsYTile = true; break; } - IggyPlayerSetDisplaySize( getMovie(), m_movieWidth, m_movieHeight ); - + F32 scale; + ComputeTileScale(tileWidth, tileHeight, m_movieWidth, m_movieHeight, needsYTile, scale, tileYStart); + + IggyPlayerSetDisplaySize( getMovie(), static_cast(m_movieWidth * scale), static_cast(m_movieHeight * scale) ); + + repositionHud(tileWidth, tileHeight, scale); + m_renderWidth = tileWidth; m_renderHeight = tileHeight; @@ -730,7 +731,7 @@ void UIScene_HUD::render(S32 width, S32 height, C4JRender::eViewportType viewpor tileYStart , tileXStart + tileWidth , tileYStart + tileHeight , - 0 ); + 0 ); IggyPlayerDrawTilesEnd ( getMovie() ); } else @@ -790,34 +791,24 @@ void UIScene_HUD::handleTimerComplete(int id) //setVisible(anyVisible); } -void UIScene_HUD::repositionHud() +void UIScene_HUD::repositionHud(S32 tileWidth, S32 tileHeight, F32 scale) { if(!m_bSplitscreen) return; - S32 width = 0; - S32 height = 0; - m_parentLayer->getRenderDimensions( width, height ); + // Pass the visible tile area in SWF coordinates so ActionScript + // positions elements (crosshair, hotbar, etc.) centered in the + // actually visible region, not the raw viewport. + S32 visibleW = static_cast(tileWidth / scale); + S32 visibleH = static_cast(tileHeight / scale); - switch( m_parentLayer->getViewport() ) - { - case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: - case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: - height = static_cast(ui.getScreenHeight()); - break; - case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: - case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: - width = static_cast(ui.getScreenWidth()); - break; - } - - app.DebugPrintf(app.USER_SR, "Reposition HUD with dims %d, %d\n", width, height ); + app.DebugPrintf(app.USER_SR, "Reposition HUD: tile %dx%d, scale %.3f, visible SWF %dx%d\n", tileWidth, tileHeight, scale, visibleW, visibleH ); IggyDataValue result; IggyDataValue value[2]; value[0].type = IGGY_DATATYPE_number; - value[0].number = width; + value[0].number = visibleW; value[1].type = IGGY_DATATYPE_number; - value[1].number = height; + value[1].number = visibleH; IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcRepositionHud , 2 , value ); } diff --git a/Minecraft.Client/Common/UI/UIScene_HUD.h b/Minecraft.Client/Common/UI/UIScene_HUD.h index 9d58ba4ba..569b52349 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(); + void repositionHud(S32 tileWidth, S32 tileHeight, F32 scale); }; diff --git a/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp index 5ea5dd324..417c1700c 100644 --- a/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp @@ -21,12 +21,19 @@ UIScene_JoinMenu::UIScene_JoinMenu(int iPad, void *_initData, UILayer *parentLay m_friendInfoUpdatedOK = false; m_friendInfoUpdatedERROR = false; m_friendInfoRequestIssued = false; +#ifdef _WINDOWS64 + m_serverIndex = initData->serverIndex; + m_editServerPhase = eEditServer_Idle; + m_editServerButtonIndex = -1; + m_deleteServerButtonIndex = -1; +#endif } void UIScene_JoinMenu::updateTooltips() { int iA = -1; int iY = -1; + int iX = -1; if (getControlFocus() == eControl_GamePlayers) { #ifdef _DURANGO @@ -38,7 +45,15 @@ void UIScene_JoinMenu::updateTooltips() iA = IDS_TOOLTIPS_SELECT; } - ui.SetTooltips( DEFAULT_XUI_MENU_USER, iA, IDS_TOOLTIPS_BACK, -1, iY ); +#ifdef _WINDOWS64 + if (m_serverIndex >= 0) + { + iX = IDS_TOOLTIPS_DELETE; + iY = IDS_TITLE_RENAME; + } +#endif + + ui.SetTooltips( DEFAULT_XUI_MENU_USER, iA, IDS_TOOLTIPS_BACK, iX, iY ); } @@ -107,6 +122,16 @@ void UIScene_JoinMenu::tick() } #endif +#ifdef _WINDOWS64 + if (m_serverIndex >= 0) + { + m_editServerButtonIndex = m_buttonListPlayers.getItemCount(); + m_buttonListPlayers.addItem(L"Edit Server"); + m_deleteServerButtonIndex = m_buttonListPlayers.getItemCount(); + m_buttonListPlayers.addItem(L"Delete Server"); + } +#endif + m_labelLabels[eLabel_Difficulty].init(app.GetString(IDS_LABEL_DIFFICULTY)); m_labelLabels[eLabel_GameType].init(app.GetString(IDS_LABEL_GAME_TYPE)); m_labelLabels[eLabel_GamertagsOn].init(app.GetString(IDS_LABEL_GAMERTAGS)); @@ -278,12 +303,38 @@ void UIScene_JoinMenu::handleInput(int iPad, int key, bool repeat, bool pressed, if( uid != INVALID_XUID ) ProfileManager.ShowProfileCard(ProfileManager.GetLockedProfile(),uid); } break; +#endif +#ifdef _WINDOWS64 + case ACTION_MENU_X: + if(pressed && m_serverIndex >= 0) + { + BeginDeleteServer(); + handled = true; + } + break; + case ACTION_MENU_Y: + if(pressed && m_serverIndex >= 0) + { + BeginEditServer(); + handled = true; + } + break; #endif case ACTION_MENU_OK: if (getControlFocus() != eControl_GamePlayers) { sendInputToMovie(key, repeat, pressed, released); } +#ifdef _WINDOWS64 + else if (pressed && m_serverIndex >= 0) + { + int sel = m_buttonListPlayers.getCurrentSelection(); + if (sel == m_editServerButtonIndex) + BeginEditServer(); + else if (sel == m_deleteServerButtonIndex) + BeginDeleteServer(); + } +#endif handled = true; break; #ifdef __ORBIS__ @@ -318,6 +369,16 @@ void UIScene_JoinMenu::handlePress(F64 controlId, F64 childId) } break; case eControl_GamePlayers: +#ifdef _WINDOWS64 + if (m_serverIndex >= 0) + { + int sel = (int)childId; + if (sel == m_editServerButtonIndex) + BeginEditServer(); + else if (sel == m_deleteServerButtonIndex) + BeginDeleteServer(); + } +#endif break; }; } @@ -635,4 +696,279 @@ void UIScene_JoinMenu::handleTimerComplete(int id) } break; }; -} \ No newline at end of file +} + +#ifdef _WINDOWS64 +void UIScene_JoinMenu::BeginDeleteServer() +{ + m_bIgnoreInput = true; + UINT uiIDA[2]; + uiIDA[0] = IDS_CONFIRM_CANCEL; + uiIDA[1] = IDS_CONFIRM_OK; + ui.RequestAlertMessage(IDS_TOOLTIPS_DELETE, IDS_TEXT_DELETE_SAVE, uiIDA, 2, m_iPad, &UIScene_JoinMenu::DeleteServerDialogReturned, this); +} + +int UIScene_JoinMenu::DeleteServerDialogReturned(void *pParam, int iPad, C4JStorage::EMessageResult result) +{ + UIScene_JoinMenu* pClass = (UIScene_JoinMenu*)pParam; + + if (result == C4JStorage::EMessage_ResultDecline) + { + pClass->RemoveServerFromFile(); + g_NetworkManager.ForceFriendsSessionRefresh(); + pClass->navigateBack(); + } + else + { + pClass->m_bIgnoreInput = false; + } + + return 0; +} + +void UIScene_JoinMenu::BeginEditServer() +{ + m_bIgnoreInput = true; + m_editServerPhase = eEditServer_IP; + m_editServerIP.clear(); + m_editServerPort.clear(); + + wchar_t wDefaultIP[64] = {}; + mbstowcs(wDefaultIP, m_selectedSession->data.hostIP, 63); + + UIKeyboardInitData kbData; + kbData.title = L"Server Address"; + kbData.defaultText = wDefaultIP; + kbData.maxChars = 128; + kbData.callback = &UIScene_JoinMenu::EditServerKeyboardCallback; + kbData.lpParam = this; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(m_iPad, eUIScene_Keyboard, &kbData); +} + +int UIScene_JoinMenu::EditServerKeyboardCallback(LPVOID lpParam, bool bRes) +{ + UIScene_JoinMenu *pClass = (UIScene_JoinMenu *)lpParam; + + if (!bRes) + { + pClass->m_editServerPhase = eEditServer_Idle; + pClass->m_bIgnoreInput = false; + return 0; + } + + uint16_t ui16Text[256]; + ZeroMemory(ui16Text, sizeof(ui16Text)); + Win64_GetKeyboardText(ui16Text, 256); + + wchar_t wBuf[256] = {}; + for (int k = 0; k < 255 && ui16Text[k]; k++) + wBuf[k] = (wchar_t)ui16Text[k]; + + if (wBuf[0] == 0) + { + pClass->m_editServerPhase = eEditServer_Idle; + pClass->m_bIgnoreInput = false; + return 0; + } + + switch (pClass->m_editServerPhase) + { + case eEditServer_IP: + { + pClass->m_editServerIP = wBuf; + pClass->m_editServerPhase = eEditServer_Port; + + wchar_t wDefaultPort[16] = {}; + swprintf(wDefaultPort, 16, L"%d", pClass->m_selectedSession->data.hostPort); + + UIKeyboardInitData kbData; + kbData.title = L"Server Port"; + kbData.defaultText = wDefaultPort; + kbData.maxChars = 6; + kbData.callback = &UIScene_JoinMenu::EditServerKeyboardCallback; + kbData.lpParam = pClass; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(pClass->m_iPad, eUIScene_Keyboard, &kbData); + break; + } + case eEditServer_Port: + { + pClass->m_editServerPort = wBuf; + pClass->m_editServerPhase = eEditServer_Name; + + wchar_t wDefaultName[64] = {}; + if (pClass->m_selectedSession->displayLabel) + wcsncpy(wDefaultName, pClass->m_selectedSession->displayLabel, 63); + + UIKeyboardInitData kbData; + kbData.title = L"Server Name"; + kbData.defaultText = wDefaultName; + kbData.maxChars = 64; + kbData.callback = &UIScene_JoinMenu::EditServerKeyboardCallback; + kbData.lpParam = pClass; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(pClass->m_iPad, eUIScene_Keyboard, &kbData); + break; + } + case eEditServer_Name: + { + wstring newName = wBuf; + pClass->UpdateServerInFile(pClass->m_editServerIP, pClass->m_editServerPort, newName); + pClass->m_editServerPhase = eEditServer_Idle; + pClass->m_bIgnoreInput = false; + + g_NetworkManager.ForceFriendsSessionRefresh(); + pClass->navigateBack(); + break; + } + default: + pClass->m_editServerPhase = eEditServer_Idle; + pClass->m_bIgnoreInput = false; + break; + } + + return 0; +} + +void UIScene_JoinMenu::UpdateServerInFile(const wstring& newIP, const wstring& newPort, const wstring& newName) +{ + char narrowNewIP[256] = {}; + char narrowNewPort[16] = {}; + char narrowNewName[256] = {}; + wcstombs(narrowNewIP, newIP.c_str(), sizeof(narrowNewIP) - 1); + wcstombs(narrowNewPort, newPort.c_str(), sizeof(narrowNewPort) - 1); + wcstombs(narrowNewName, newName.c_str(), sizeof(narrowNewName) - 1); + + uint16_t newPortNum = (uint16_t)atoi(narrowNewPort); + + struct ServerEntry { std::string ip; uint16_t port; std::string name; }; + std::vector entries; + + FILE* file = fopen("servers.db", "rb"); + if (file) + { + char magic[4] = {}; + if (fread(magic, 1, 4, file) == 4 && memcmp(magic, "MCSV", 4) == 0) + { + uint32_t version = 0, count = 0; + fread(&version, sizeof(uint32_t), 1, file); + fread(&count, sizeof(uint32_t), 1, file); + if (version == 1) + { + for (uint32_t s = 0; s < count; s++) + { + uint16_t ipLen = 0, p = 0, nameLen = 0; + if (fread(&ipLen, sizeof(uint16_t), 1, file) != 1) break; + if (ipLen == 0 || ipLen > 256) break; + char ipBuf[257] = {}; + if (fread(ipBuf, 1, ipLen, file) != ipLen) break; + if (fread(&p, sizeof(uint16_t), 1, file) != 1) break; + if (fread(&nameLen, sizeof(uint16_t), 1, file) != 1) break; + if (nameLen > 256) break; + char nameBuf[257] = {}; + if (nameLen > 0 && fread(nameBuf, 1, nameLen, file) != nameLen) break; + entries.push_back({std::string(ipBuf), p, std::string(nameBuf)}); + } + } + } + fclose(file); + } + + // Find and update the matching entry by original IP and port + int idx = m_serverIndex; + if (idx >= 0 && idx < (int)entries.size()) + { + entries[idx].ip = std::string(narrowNewIP); + entries[idx].port = newPortNum; + entries[idx].name = std::string(narrowNewName); + } + + file = fopen("servers.db", "wb"); + if (file) + { + fwrite("MCSV", 1, 4, file); + uint32_t version = 1; + uint32_t count = (uint32_t)entries.size(); + fwrite(&version, sizeof(uint32_t), 1, file); + fwrite(&count, sizeof(uint32_t), 1, file); + + for (size_t i = 0; i < entries.size(); i++) + { + uint16_t ipLen = (uint16_t)entries[i].ip.length(); + fwrite(&ipLen, sizeof(uint16_t), 1, file); + fwrite(entries[i].ip.c_str(), 1, ipLen, file); + fwrite(&entries[i].port, sizeof(uint16_t), 1, file); + uint16_t nameLen = (uint16_t)entries[i].name.length(); + fwrite(&nameLen, sizeof(uint16_t), 1, file); + fwrite(entries[i].name.c_str(), 1, nameLen, file); + } + fclose(file); + } +} + +void UIScene_JoinMenu::RemoveServerFromFile() +{ + struct ServerEntry { std::string ip; uint16_t port; std::string name; }; + std::vector entries; + + FILE* file = fopen("servers.db", "rb"); + if (file) + { + char magic[4] = {}; + if (fread(magic, 1, 4, file) == 4 && memcmp(magic, "MCSV", 4) == 0) + { + uint32_t version = 0, count = 0; + fread(&version, sizeof(uint32_t), 1, file); + fread(&count, sizeof(uint32_t), 1, file); + if (version == 1) + { + for (uint32_t s = 0; s < count; s++) + { + uint16_t ipLen = 0, p = 0, nameLen = 0; + if (fread(&ipLen, sizeof(uint16_t), 1, file) != 1) break; + if (ipLen == 0 || ipLen > 256) break; + char ipBuf[257] = {}; + if (fread(ipBuf, 1, ipLen, file) != ipLen) break; + if (fread(&p, sizeof(uint16_t), 1, file) != 1) break; + if (fread(&nameLen, sizeof(uint16_t), 1, file) != 1) break; + if (nameLen > 256) break; + char nameBuf[257] = {}; + if (nameLen > 0 && fread(nameBuf, 1, nameLen, file) != nameLen) break; + entries.push_back({std::string(ipBuf), p, std::string(nameBuf)}); + } + } + } + fclose(file); + } + + // Remove the entry at m_serverIndex + int idx = m_serverIndex; + if (idx >= 0 && idx < (int)entries.size()) + { + entries.erase(entries.begin() + idx); + } + + file = fopen("servers.db", "wb"); + if (file) + { + fwrite("MCSV", 1, 4, file); + uint32_t version = 1; + uint32_t count = (uint32_t)entries.size(); + fwrite(&version, sizeof(uint32_t), 1, file); + fwrite(&count, sizeof(uint32_t), 1, file); + + for (size_t i = 0; i < entries.size(); i++) + { + uint16_t ipLen = (uint16_t)entries[i].ip.length(); + fwrite(&ipLen, sizeof(uint16_t), 1, file); + fwrite(entries[i].ip.c_str(), 1, ipLen, file); + fwrite(&entries[i].port, sizeof(uint16_t), 1, file); + uint16_t nameLen = (uint16_t)entries[i].name.length(); + fwrite(&nameLen, sizeof(uint16_t), 1, file); + fwrite(entries[i].name.c_str(), 1, nameLen, file); + } + fclose(file); + } +} +#endif // _WINDOWS64 \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIScene_JoinMenu.h b/Minecraft.Client/Common/UI/UIScene_JoinMenu.h index 817360efc..566697cd3 100644 --- a/Minecraft.Client/Common/UI/UIScene_JoinMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_JoinMenu.h @@ -62,6 +62,16 @@ private: bool m_friendInfoUpdatedOK; bool m_friendInfoUpdatedERROR; +#ifdef _WINDOWS64 + int m_serverIndex; // Index in servers.db, -1 if not a saved server + enum eEditServerPhase { eEditServer_Idle, eEditServer_IP, eEditServer_Port, eEditServer_Name }; + eEditServerPhase m_editServerPhase; + wstring m_editServerIP; + wstring m_editServerPort; + int m_editServerButtonIndex; + int m_deleteServerButtonIndex; +#endif + public: UIScene_JoinMenu(int iPad, void *initData, UILayer *parentLayer); void tick(); @@ -95,4 +105,13 @@ protected: static int StartGame_SignInReturned(void *pParam, bool, int); static void JoinGame(UIScene_JoinMenu* pClass); + +#ifdef _WINDOWS64 + void BeginEditServer(); + void BeginDeleteServer(); + static int EditServerKeyboardCallback(LPVOID lpParam, bool bRes); + static int DeleteServerDialogReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + void UpdateServerInFile(const wstring& newIP, const wstring& newPort, const wstring& newName); + void RemoveServerFromFile(); +#endif }; diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp index 352f2b7b8..2f2f91326 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp @@ -38,6 +38,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye } m_win64TextBuffer = defaultText; + m_iCursorPos = (int)m_win64TextBuffer.length(); m_EnterTextLabel.init(titleText); m_KeyboardTextInput.init(defaultText, -1); @@ -111,6 +112,9 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye if (IggyValuePathMakeNameRef(&keyPath, root, s_keyNames[i])) IggyValueSetBooleanRS(&keyPath, nameVisible, nullptr, false); } + + m_KeyboardTextInput.setCaretVisible(true); + m_KeyboardTextInput.setCaretIndex(m_iCursorPos); } #endif @@ -165,9 +169,13 @@ void UIScene_Keyboard::tick() // Sync our buffer from Flash so we pick up changes made via controller/on-screen buttons. // Without this, switching between controller and keyboard would use stale text. - const wchar_t* flashText = m_KeyboardTextInput.getLabel(); - if (flashText) - m_win64TextBuffer = flashText; + // In PC mode we own the buffer — skip sync to preserve cursor position. + if (!m_bPCMode) + { + const wchar_t* flashText = m_KeyboardTextInput.getLabel(); + if (flashText) + m_win64TextBuffer = flashText; + } // Accumulate physical keyboard chars into our own buffer, then push to Flash via setLabel. // This bypasses Iggy's focus system (char events only route to the focused element). @@ -178,7 +186,16 @@ void UIScene_Keyboard::tick() { if (ch == 0x08) // backspace { - if (!m_win64TextBuffer.empty()) + if (m_bPCMode) + { + if (m_iCursorPos > 0) + { + m_win64TextBuffer.erase(m_iCursorPos - 1, 1); + m_iCursorPos--; + changed = true; + } + } + else if (!m_win64TextBuffer.empty()) { m_win64TextBuffer.pop_back(); changed = true; @@ -194,13 +211,45 @@ void UIScene_Keyboard::tick() } else if (static_cast(m_win64TextBuffer.length()) < m_win64MaxChars) { - m_win64TextBuffer += ch; + if (m_bPCMode) + { + m_win64TextBuffer.insert(m_iCursorPos, 1, ch); + m_iCursorPos++; + } + else + { + m_win64TextBuffer += ch; + } + changed = true; + } + } + + if (m_bPCMode) + { + // Arrow keys, Home, End, Delete for cursor movement + if (g_KBMInput.IsKeyPressed(VK_LEFT) && m_iCursorPos > 0) + m_iCursorPos--; + if (g_KBMInput.IsKeyPressed(VK_RIGHT) && m_iCursorPos < (int)m_win64TextBuffer.length()) + m_iCursorPos++; + if (g_KBMInput.IsKeyPressed(VK_HOME)) + m_iCursorPos = 0; + if (g_KBMInput.IsKeyPressed(VK_END)) + m_iCursorPos = (int)m_win64TextBuffer.length(); + if (g_KBMInput.IsKeyPressed(VK_DELETE) && m_iCursorPos < (int)m_win64TextBuffer.length()) + { + m_win64TextBuffer.erase(m_iCursorPos, 1); changed = true; } } if (changed) m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/); + + if (m_bPCMode) + { + m_KeyboardTextInput.setCaretVisible(true); + m_KeyboardTextInput.setCaretIndex(m_iCursorPos); + } } #endif @@ -229,27 +278,31 @@ void UIScene_Keyboard::handleInput(int iPad, int key, bool repeat, bool pressed, handled = true; break; case ACTION_MENU_X: // X - out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcBackspaceButtonPressed, 0 , nullptr ); - handled = true; - break; case ACTION_MENU_PAGEUP: // LT - out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSymbolButtonPressed, 0 , nullptr ); - handled = true; - break; case ACTION_MENU_Y: // Y - out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSpaceButtonPressed, 0 , nullptr ); - handled = true; - break; case ACTION_MENU_STICK_PRESS: // LS - out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcCapsButtonPressed, 0 , nullptr ); - handled = true; - break; case ACTION_MENU_LEFT_SCROLL: // LB - out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcCursorLeftButtonPressed, 0 , nullptr ); - handled = true; - break; case ACTION_MENU_RIGHT_SCROLL: // RB - out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcCursorRightButtonPressed, 0 , nullptr ); +#ifdef _WINDOWS64 + + if (m_bPCMode) + { + handled = true; + break; + } +#endif + if (key == ACTION_MENU_X) + out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcBackspaceButtonPressed, 0 , nullptr); + else if (key == ACTION_MENU_PAGEUP) + out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSymbolButtonPressed, 0 , nullptr); + else if (key == ACTION_MENU_Y) + out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSpaceButtonPressed, 0 , nullptr); + else if (key == ACTION_MENU_STICK_PRESS) + out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcCapsButtonPressed, 0 , nullptr); + else if (key == ACTION_MENU_LEFT_SCROLL) + out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcCursorLeftButtonPressed, 0 , nullptr); + else if (key == ACTION_MENU_RIGHT_SCROLL) + out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcCursorRightButtonPressed, 0 , nullptr); handled = true; break; case ACTION_MENU_PAUSEMENU: // Start @@ -269,11 +322,23 @@ void UIScene_Keyboard::handleInput(int iPad, int key, bool repeat, bool pressed, switch(key) { case ACTION_MENU_OK: +#ifdef _WINDOWS64 + if (m_bPCMode) + { + // pressing enter sometimes causes a "y" to be entered. + handled = true; + break; + } +#endif + // fall through for controller mode case ACTION_MENU_LEFT: case ACTION_MENU_RIGHT: case ACTION_MENU_UP: case ACTION_MENU_DOWN: - sendInputToMovie(key, repeat, pressed, released); +#ifdef _WINDOWS64 + if (!m_bPCMode) +#endif + sendInputToMovie(key, repeat, pressed, released); handled = true; break; } diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.h b/Minecraft.Client/Common/UI/UIScene_Keyboard.h index 054322f20..146934c1f 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.h +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.h @@ -13,6 +13,7 @@ private: wstring m_win64TextBuffer; int m_win64MaxChars; bool m_bPCMode; // Hides on-screen keyboard buttons; physical keyboard only + int m_iCursorPos; #endif protected: diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index c48293501..d73148f5d 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -402,8 +402,13 @@ UIScene_LoadOrJoinMenu::~UIScene_LoadOrJoinMenu() g_NetworkManager.SetSessionsUpdatedCallback( nullptr, nullptr ); app.SetLiveLinkRequired( false ); - delete m_currentSessions; - m_currentSessions = nullptr; + if (m_currentSessions) + { + for (const auto& it : *m_currentSessions) + delete it; + delete m_currentSessions; + m_currentSessions = nullptr; + } #if TO_BE_IMPLEMENTED // Reset the background downloading, in case we changed it by attempting to download a texture pack @@ -520,6 +525,9 @@ void UIScene_LoadOrJoinMenu::Initialise() { m_iSaveListIndex = 0; m_iGameListIndex = 0; +#ifdef _WINDOWS64 + m_addServerPhase = eAddServer_Idle; +#endif m_iDefaultButtonsC = 0; m_iMashUpButtonsC=0; @@ -1051,8 +1059,7 @@ void UIScene_LoadOrJoinMenu::GetSaveInfo() m_pSaveDetails=StorageManager.ReturnSavesInfo(); if(m_pSaveDetails==nullptr) { - char savename[] = "save"; - C4JStorage::ESaveGameState eSGIStatus = StorageManager.GetSavesInfo(m_iPad, nullptr, this, savename); + C4JStorage::ESaveGameState eSGIStatus= StorageManager.GetSavesInfo(m_iPad, nullptr,this,"save"); } #if TO_BE_IMPLEMENTED @@ -1471,6 +1478,10 @@ void UIScene_LoadOrJoinMenu::handleFocusChange(F64 controlId, F64 childId) { case eControl_GamesList: m_iGameListIndex = childId; +#ifdef _WINDOWS64 + // Offset past the "Add Server" button so m_iGameListIndex is a session index + m_iGameListIndex -= 1; +#endif m_buttonListGames.updateChildFocus( static_cast(childId) ); break; case eControl_SavesList: @@ -1598,6 +1609,14 @@ void UIScene_LoadOrJoinMenu::handlePress(F64 controlId, F64 childId) break; case eControl_GamesList: { +#ifdef _WINDOWS64 + if (static_cast(childId) == ADD_SERVER_BUTTON_INDEX) + { + ui.PlayUISFX(eSFX_Press); + BeginAddServer(); + break; + } +#endif m_bIgnoreInput=true; m_eAction = eAction_JoinGame; @@ -1607,6 +1626,10 @@ void UIScene_LoadOrJoinMenu::handlePress(F64 controlId, F64 childId) { int nIndex = static_cast(childId); +#ifdef _WINDOWS64 + // Offset by 1 because the "Add Server" button is at index 0 + nIndex -= 1; +#endif m_iGameListIndex = nIndex; CheckAndJoinGame(nIndex); } @@ -1744,12 +1767,34 @@ void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) #endif #endif - //CScene_MultiGameInfo::JoinMenuInitData *initData = new CScene_MultiGameInfo::JoinMenuInitData(); m_initData->iPad = 0;; m_initData->selectedSession = m_currentSessions->at( gameIndex ); +#ifdef _WINDOWS64 + { + + int serverDbCount = 0; + FILE* dbFile = fopen("servers.db", "rb"); + if (dbFile) + { + char magic[4] = {}; + if (fread(magic, 1, 4, dbFile) == 4 && memcmp(magic, "MCSV", 4) == 0) + { + uint32_t version = 0, count = 0; + fread(&version, sizeof(uint32_t), 1, dbFile); + fread(&count, sizeof(uint32_t), 1, dbFile); + if (version == 1) + serverDbCount = static_cast(count); + } + fclose(dbFile); + } + int lanCount = static_cast(m_currentSessions->size()) - serverDbCount; + if (gameIndex >= lanCount && lanCount >= 0) + m_initData->serverIndex = gameIndex - lanCount; + else + m_initData->serverIndex = -1; + } +#endif - // check that we have the texture pack available - // If it's not the default texture pack if(m_initData->selectedSession->data.texturePackParentId!=0) { int texturePacksCount = Minecraft::GetInstance()->skins->getTexturePackCount(); @@ -1767,8 +1812,7 @@ void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) if(bHasTexturePackInstalled==false) { - // upsell the texture pack - // tell sentient about the upsell of the full version of the skin pack + #ifdef _XBOX ULONGLONG ullOfferID_Full; app.GetDLCFullOfferIDForPackID(m_initData->selectedSession->data.texturePackParentId,&ullOfferID_Full); @@ -1781,8 +1825,6 @@ void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) //uiIDA[1]=IDS_TEXTURE_PACK_TRIALVERSION; uiIDA[1]=IDS_CONFIRM_CANCEL; - - // Give the player a warning about the texture pack missing ui.RequestAlertMessage(IDS_DLC_TEXTUREPACK_NOT_PRESENT_TITLE, IDS_DLC_TEXTUREPACK_NOT_PRESENT, uiIDA, 2, m_iPad,&UIScene_LoadOrJoinMenu::TexturePackDialogReturned,this); return; @@ -1800,7 +1842,6 @@ void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) m_controlJoinTimer.setVisible( false ); #ifdef _XBOX - // Reset the background downloading, in case we changed it by attempting to download a texture pack XBackgroundDownloadSetMode(XBACKGROUND_DOWNLOAD_MODE_AUTO); #endif @@ -1896,7 +1937,13 @@ void UIScene_LoadOrJoinMenu::UpdateGamesList() if(DoesGamesListHaveFocus() && m_buttonListGames.getItemCount() > 0) { unsigned int nIndex = m_buttonListGames.getCurrentSelection(); +#ifdef _WINDOWS64 + // Offset past the "Add Server" button + if (nIndex > 0) + pSelectedSession = m_currentSessions->at( nIndex - 1 ); +#else pSelectedSession = m_currentSessions->at( nIndex ); +#endif } SessionID selectedSessionId; @@ -1912,8 +1959,37 @@ void UIScene_LoadOrJoinMenu::UpdateGamesList() int iY = -1; int iX=-1; - delete m_currentSessions; - m_currentSessions = g_NetworkManager.GetSessionList( m_iPad, 1, m_bShowingPartyGamesOnly ); + vector* newSessions = g_NetworkManager.GetSessionList( m_iPad, 1, m_bShowingPartyGamesOnly ); + + if (m_currentSessions != NULL && m_currentSessions->size() == newSessions->size()) + { + bool same = true; + for (size_t i = 0; i < newSessions->size(); i++) + { + if (memcmp(&(*m_currentSessions)[i]->sessionId, &(*newSessions)[i]->sessionId, sizeof(SessionID)) != 0 || + wcscmp((*m_currentSessions)[i]->displayLabel ? (*m_currentSessions)[i]->displayLabel : L"", + (*newSessions)[i]->displayLabel ? (*newSessions)[i]->displayLabel : L"") != 0) + { + same = false; + break; + } + } + if (same) + { + for (auto& it : *newSessions) + delete it; + delete newSessions; + return; + } + } + + if (m_currentSessions) + { + for (auto& it : *m_currentSessions) + delete it; + delete m_currentSessions; + } + m_currentSessions = newSessions; // Update the xui list displayed unsigned int xuiListSize = m_buttonListGames.getItemCount(); @@ -1949,6 +2025,11 @@ void UIScene_LoadOrJoinMenu::UpdateGamesList() // clear out the games list and re-fill m_buttonListGames.clearList(); +#ifdef _WINDOWS64 + // Always add the "Add Server" button as the first entry in the games list + m_buttonListGames.addItem(wstring(L"Add Server")); +#endif + if( filteredListSize > 0 ) { // Reset the focus to the selected session if it still exists @@ -2014,7 +2095,12 @@ void UIScene_LoadOrJoinMenu::UpdateGamesList() if(memcmp( &selectedSessionId, &sessionInfo->sessionId, sizeof(SessionID) ) == 0) { +#ifdef _WINDOWS64 + // Offset past the "Add Server" button + m_buttonListGames.setCurrentSelection(sessionIndex + 1); +#else m_buttonListGames.setCurrentSelection(sessionIndex); +#endif break; } ++sessionIndex; @@ -4051,3 +4137,168 @@ int UIScene_LoadOrJoinMenu::CopySaveErrorDialogFinishedCallback(void *pParam,int } #endif // _XBOX_ONE +#ifdef _WINDOWS64 +// adding servers bellow + +void UIScene_LoadOrJoinMenu::BeginAddServer() +{ + m_addServerPhase = eAddServer_IP; + m_addServerIP.clear(); + m_addServerPort.clear(); + + UIKeyboardInitData kbData; + kbData.title = L"Server Address"; + kbData.defaultText = L""; + kbData.maxChars = 128; + kbData.callback = &UIScene_LoadOrJoinMenu::AddServerKeyboardCallback; + kbData.lpParam = this; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(m_iPad, eUIScene_Keyboard, &kbData); +} + +int UIScene_LoadOrJoinMenu::AddServerKeyboardCallback(LPVOID lpParam, bool bRes) +{ + UIScene_LoadOrJoinMenu *pClass = static_cast(lpParam); + + if (!bRes) + { + pClass->m_addServerPhase = eAddServer_Idle; + pClass->m_bIgnoreInput = false; + return 0; + } + + uint16_t ui16Text[256]; + ZeroMemory(ui16Text, sizeof(ui16Text)); + Win64_GetKeyboardText(ui16Text, 256); + + wchar_t wBuf[256] = {}; + for (int k = 0; k < 255 && ui16Text[k]; k++) + wBuf[k] = static_cast(ui16Text[k]); + + if (wBuf[0] == 0) + { + pClass->m_addServerPhase = eAddServer_Idle; + pClass->m_bIgnoreInput = false; + return 0; + } + + switch (pClass->m_addServerPhase) + { + case eAddServer_IP: + { + pClass->m_addServerIP = wBuf; + pClass->m_addServerPhase = eAddServer_Port; + + UIKeyboardInitData kbData; + kbData.title = L"Server Port"; + kbData.defaultText = L"25565"; + kbData.maxChars = 6; + kbData.callback = &UIScene_LoadOrJoinMenu::AddServerKeyboardCallback; + kbData.lpParam = pClass; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(pClass->m_iPad, eUIScene_Keyboard, &kbData); + break; + } + case eAddServer_Port: + { + pClass->m_addServerPort = wBuf; + pClass->m_addServerPhase = eAddServer_Name; + + UIKeyboardInitData kbData; + kbData.title = L"Server Name"; + kbData.defaultText = L"Minecraft Server"; + kbData.maxChars = 64; + kbData.callback = &UIScene_LoadOrJoinMenu::AddServerKeyboardCallback; + kbData.lpParam = pClass; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(pClass->m_iPad, eUIScene_Keyboard, &kbData); + break; + } + case eAddServer_Name: + { + wstring name = wBuf; + pClass->AppendServerToFile(pClass->m_addServerIP, pClass->m_addServerPort, name); + pClass->m_addServerPhase = eAddServer_Idle; + pClass->m_bIgnoreInput = false; + + g_NetworkManager.ForceFriendsSessionRefresh(); + break; + } + default: + pClass->m_addServerPhase = eAddServer_Idle; + pClass->m_bIgnoreInput = false; + break; + } + + return 0; +} + +void UIScene_LoadOrJoinMenu::AppendServerToFile(const wstring& ip, const wstring& port, const wstring& name) +{ + char narrowIP[256] = {}; + char narrowPort[16] = {}; + char narrowName[256] = {}; + wcstombs(narrowIP, ip.c_str(), sizeof(narrowIP) - 1); + wcstombs(narrowPort, port.c_str(), sizeof(narrowPort) - 1); + wcstombs(narrowName, name.c_str(), sizeof(narrowName) - 1); + + uint16_t portNum = static_cast(atoi(narrowPort)); + + struct ServerEntry { std::string ip; uint16_t port; std::string name; }; + std::vector entries; + + FILE* file = fopen("servers.db", "rb"); + if (file) + { + char magic[4] = {}; + if (fread(magic, 1, 4, file) == 4 && memcmp(magic, "MCSV", 4) == 0) + { + uint32_t version = 0, count = 0; + fread(&version, sizeof(uint32_t), 1, file); + fread(&count, sizeof(uint32_t), 1, file); + if (version == 1) + { + for (uint32_t s = 0; s < count; s++) + { + uint16_t ipLen = 0, p = 0, nameLen = 0; + if (fread(&ipLen, sizeof(uint16_t), 1, file) != 1) break; + if (ipLen == 0 || ipLen > 256) break; + char ipBuf[257] = {}; + if (fread(ipBuf, 1, ipLen, file) != ipLen) break; + if (fread(&p, sizeof(uint16_t), 1, file) != 1) break; + if (fread(&nameLen, sizeof(uint16_t), 1, file) != 1) break; + if (nameLen > 256) break; + char nameBuf[257] = {}; + if (nameLen > 0 && fread(nameBuf, 1, nameLen, file) != nameLen) break; + entries.push_back({std::string(ipBuf), p, std::string(nameBuf)}); + } + } + } + fclose(file); + } + + entries.push_back({std::string(narrowIP), portNum, std::string(narrowName)}); + + file = fopen("servers.db", "wb"); + if (file) + { + fwrite("MCSV", 1, 4, file); + uint32_t version = 1; + uint32_t count = static_cast(entries.size()); + fwrite(&version, sizeof(uint32_t), 1, file); + fwrite(&count, sizeof(uint32_t), 1, file); + + for (size_t i = 0; i < entries.size(); i++) + { + uint16_t ipLen = static_cast(entries[i].ip.length()); + fwrite(&ipLen, sizeof(uint16_t), 1, file); + fwrite(entries[i].ip.c_str(), 1, ipLen, file); + fwrite(&entries[i].port, sizeof(uint16_t), 1, file); + uint16_t nameLen = static_cast(entries[i].name.length()); + fwrite(&nameLen, sizeof(uint16_t), 1, file); + fwrite(entries[i].name.c_str(), 1, nameLen, file); + } + fclose(file); + } +} +#endif // _WINDOWS64 \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h index 3599aa37c..9f5fe17f3 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h @@ -175,6 +175,18 @@ public: private: void CheckAndJoinGame(int gameIndex); + +#ifdef _WINDOWS64 + static const int ADD_SERVER_BUTTON_INDEX = 0; + enum eAddServerPhase { eAddServer_Idle, eAddServer_IP, eAddServer_Port, eAddServer_Name }; + eAddServerPhase m_addServerPhase; + wstring m_addServerIP; + wstring m_addServerPort; + void BeginAddServer(); + void AppendServerToFile(const wstring& ip, const wstring& port, const wstring& name); + static int AddServerKeyboardCallback(LPVOID lpParam, bool bRes); +#endif + #if defined(__PS3__) || defined(__PSVITA__) || defined(__ORBIS__) static int MustSignInReturnedPSN(void *pParam,int iPad,C4JStorage::EMessageResult result); static int PSN_SignInReturned(void *pParam,bool bContinue, int iPad); diff --git a/Minecraft.Client/Common/UI/UISplitScreenHelpers.h b/Minecraft.Client/Common/UI/UISplitScreenHelpers.h new file mode 100644 index 000000000..e451b3f2d --- /dev/null +++ b/Minecraft.Client/Common/UI/UISplitScreenHelpers.h @@ -0,0 +1,114 @@ +#pragma once + +// Shared split-screen UI helpers to avoid duplicating viewport math +// across HUD, Chat, Tooltips, and container menus. + +// Compute the raw viewport rectangle for a given viewport type. +inline void GetViewportRect(F32 screenW, F32 screenH, C4JRender::eViewportType viewport, + F32 &originX, F32 &originY, F32 &viewW, F32 &viewH) +{ + originX = originY = 0; + viewW = screenW; + viewH = screenH; + switch(viewport) + { + case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: + viewH = screenH * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: + originY = screenH * 0.5f; viewH = screenH * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: + viewW = screenW * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: + originX = screenW * 0.5f; viewW = screenW * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT: + viewW = screenW * 0.5f; viewH = screenH * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: + originX = screenW * 0.5f; viewW = screenW * 0.5f; viewH = screenH * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: + originY = screenH * 0.5f; viewW = screenW * 0.5f; viewH = screenH * 0.5f; break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: + originX = screenW * 0.5f; originY = screenH * 0.5f; + viewW = screenW * 0.5f; viewH = screenH * 0.5f; break; + default: break; + } +} + +// Fit a 16:9 rectangle inside the given dimensions. +inline void Fit16x9(F32 viewW, F32 viewH, S32 &fitW, S32 &fitH, S32 &offsetX, S32 &offsetY) +{ + const F32 kAspect = 16.0f / 9.0f; + if(viewW / viewH > kAspect) + { + fitH = (S32)viewH; + fitW = (S32)(viewH * kAspect); + } + else + { + fitW = (S32)viewW; + fitH = (S32)(viewW / kAspect); + } + offsetX = (S32)((viewW - fitW) * 0.5f); + offsetY = (S32)((viewH - fitH) * 0.5f); +} + +// Convenience: just fit 16:9 dimensions, ignore offsets. +inline void Fit16x9(S32 &width, S32 &height) +{ + S32 offX, offY; + Fit16x9((F32)width, (F32)height, width, height, offX, offY); +} + +// Compute the uniform scale and tileYStart for split-screen tile rendering. +// Used by HUD, Chat, and Tooltips to scale the SWF movie to cover the viewport tile. +inline void ComputeTileScale(S32 tileWidth, S32 tileHeight, S32 movieWidth, S32 movieHeight, + bool needsYTile, F32 &outScale, S32 &outTileYStart) +{ + F32 scaleW = (F32)tileWidth / (F32)movieWidth; + F32 scaleH = (F32)tileHeight / (F32)movieHeight; + F32 scale = (scaleW > scaleH) ? scaleW : scaleH; + if(scale < 1.0f) scale = 1.0f; + + outTileYStart = 0; + if(needsYTile) + { + S32 dispH = (S32)(movieHeight * scale); + outTileYStart = dispH - tileHeight; + if(outTileYStart < 0) outTileYStart = 0; + scaleH = (F32)(outTileYStart + tileHeight) / (F32)movieHeight; + scale = (scaleW > scaleH) ? scaleW : scaleH; + if(scale < 1.0f) scale = 1.0f; + } + + outScale = scale; +} + +// Compute the render offset to center split-screen SWF content in the viewport. +// Used by Chat and Tooltips (HUD uses repositionHud instead). +inline void ComputeSplitContentOffset(C4JRender::eViewportType viewport, S32 movieWidth, S32 movieHeight, + F32 scale, S32 tileWidth, S32 tileHeight, S32 tileYStart, + S32 &outXOffset, S32 &outYOffset) +{ + S32 contentCenterX, contentCenterY; + if(viewport == C4JRender::VIEWPORT_TYPE_SPLIT_LEFT || viewport == C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT) + { + contentCenterX = (S32)(movieWidth * scale / 4); + contentCenterY = (S32)(movieHeight * scale / 2); + } + else if(viewport == C4JRender::VIEWPORT_TYPE_SPLIT_TOP || viewport == C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM) + { + contentCenterX = (S32)(movieWidth * scale / 2); + contentCenterY = (S32)(movieHeight * scale * 3 / 4); + } + else + { + contentCenterX = (S32)(movieWidth * scale / 4); + contentCenterY = (S32)(movieHeight * scale * 3 / 4); + } + + outXOffset = 0; + outYOffset = 0; + if(viewport == C4JRender::VIEWPORT_TYPE_SPLIT_LEFT || viewport == C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT || viewport == C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT) + outXOffset = -(tileWidth / 2 - contentCenterX); + if(viewport == C4JRender::VIEWPORT_TYPE_SPLIT_TOP || viewport == C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_LEFT || viewport == C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT) + outYOffset = -(tileHeight / 2 - (contentCenterY - tileYStart)); +} diff --git a/Minecraft.Client/Common/UI/UIStructs.h b/Minecraft.Client/Common/UI/UIStructs.h index e5f5a8333..99c3d7bda 100644 --- a/Minecraft.Client/Common/UI/UIStructs.h +++ b/Minecraft.Client/Common/UI/UIStructs.h @@ -280,6 +280,9 @@ typedef struct _JoinMenuInitData { FriendSessionInfo *selectedSession; int iPad; +#ifdef _WINDOWS64 + int serverIndex; // Index of the server in servers.db, -1 if not a saved server +#endif } JoinMenuInitData; // Native keyboard (Windows64 replacement for InputManager.RequestKeyboard WinAPI dialog) diff --git a/Minecraft.Client/EntityRenderDispatcher.cpp b/Minecraft.Client/EntityRenderDispatcher.cpp index b54967bde..01972e568 100644 --- a/Minecraft.Client/EntityRenderDispatcher.cpp +++ b/Minecraft.Client/EntityRenderDispatcher.cpp @@ -234,6 +234,7 @@ void EntityRenderDispatcher::prepare(Level *level, Textures *textures, Font *fon if (pl->ThirdPersonView() == 2) { playerRotY += 180; + playerRotX = -playerRotX; } xPlayer = player->xOld + (player->x - player->xOld) * a; diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index 5d676b290..0147896ca 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -194,7 +194,16 @@ void IQNetPlayer::SendData(IQNetPlayer * player, const void* pvData, DWORD dwDat { if (WinsockNetLayer::IsActive()) { - WinsockNetLayer::SendToSmallId(player->m_smallId, pvData, dwDataSize); + if (!WinsockNetLayer::IsHosting() && !m_isRemote) + { + SOCKET sock = WinsockNetLayer::GetLocalSocket(m_smallId); + if (sock != INVALID_SOCKET) + WinsockNetLayer::SendOnSocket(sock, pvData, dwDataSize); + } + else + { + WinsockNetLayer::SendToSmallId(player->m_smallId, pvData, dwDataSize); + } } } bool IQNetPlayer::IsSameSystem(IQNetPlayer * player) { return (this == player) || (!m_isRemote && !player->m_isRemote); } @@ -245,7 +254,20 @@ void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost static bool Win64_IsActivePlayer(IQNetPlayer* p, DWORD index); -HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex) { return S_OK; } +HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex) { + if (dwUserIndex >= MINECRAFT_NET_MAX_PLAYERS) return E_FAIL; + m_player[dwUserIndex].m_isRemote = false; + m_player[dwUserIndex].m_isHostPlayer = false; + // Give the joining player a distinct gamertag + extern wchar_t g_Win64UsernameW[17]; + if (dwUserIndex == 0) + wcscpy_s(m_player[0].m_gamertag, 32, g_Win64UsernameW); + else + swprintf_s(m_player[dwUserIndex].m_gamertag, 32, L"%s(%d)", g_Win64UsernameW, dwUserIndex + 1); + if (dwUserIndex >= s_playerCount) + s_playerCount = dwUserIndex + 1; + return S_OK; +} IQNetPlayer* IQNet::GetHostPlayer() { return &m_player[0]; } IQNetPlayer* IQNet::GetLocalPlayerByUserIndex(DWORD dwUserIndex) { @@ -257,13 +279,31 @@ IQNetPlayer* IQNet::GetLocalPlayerByUserIndex(DWORD dwUserIndex) return &m_player[dwUserIndex]; return nullptr; } - if (dwUserIndex != 0) - return nullptr; - for (DWORD i = 0; i < s_playerCount; i++) + if (dwUserIndex == 0) { - if (!m_player[i].m_isRemote && Win64_IsActivePlayer(&m_player[i], i)) - return &m_player[i]; + // Primary pad: use direct index when networking is active (smallId may not be 0) + if (WinsockNetLayer::IsActive()) + { + DWORD idx = WinsockNetLayer::GetLocalSmallId(); + if (idx < MINECRAFT_NET_MAX_PLAYERS && + !m_player[idx].m_isRemote && + Win64_IsActivePlayer(&m_player[idx], idx)) + return &m_player[idx]; + return nullptr; + } + // Offline: scan for first local player + for (DWORD i = 0; i < s_playerCount; i++) + { + if (!m_player[i].m_isRemote && Win64_IsActivePlayer(&m_player[i], i)) + return &m_player[i]; + } + return nullptr; } + // Split-screen pads 1-3: the player is at m_player[dwUserIndex] with isRemote=false + if (dwUserIndex < MINECRAFT_NET_MAX_PLAYERS && + !m_player[dwUserIndex].m_isRemote && + Win64_IsActivePlayer(&m_player[dwUserIndex], dwUserIndex)) + return &m_player[dwUserIndex]; return nullptr; } static bool Win64_IsActivePlayer(IQNetPlayer * p, DWORD index) @@ -584,7 +624,7 @@ void C_4JProfile::SetTrialTextStringTable(CXuiStringTable * pStringTable, int void C_4JProfile::SetTrialAwardText(eAwardType AwardType, int iTitle, int iText) {} int C_4JProfile::GetLockedProfile() { return 0; } void C_4JProfile::SetLockedProfile(int iProf) {} -bool C_4JProfile::IsSignedIn(int iQuadrant) { return (iQuadrant == 0); } +bool C_4JProfile::IsSignedIn(int iQuadrant) { return (iQuadrant == 0) || InputManager.IsPadConnected(iQuadrant); } bool C_4JProfile::IsSignedInLive(int iProf) { return true; } bool C_4JProfile::IsGuest(int iQuadrant) { return false; } UINT C_4JProfile::RequestSignInUI(bool bFromInvite, bool bLocalGame, bool bNoGuestsAllowed, bool bMultiplayerSignIn, bool bAddUser, int(*Func)(LPVOID, const bool, const int iPad), LPVOID lpParam, int iQuadrant) { return 0; } @@ -595,18 +635,10 @@ bool C_4JProfile::QuerySigninStatus(void) { return true; } void C_4JProfile::GetXUID(int iPad, PlayerUID * pXuid, bool bOnlineXuid) { #ifdef _WINDOWS64 - if (iPad != 0) - { - *pXuid = INVALID_XUID; - return; - } - // LoginPacket reads this value as client identity: - // - host keeps legacy host XUID for world compatibility - // - non-host uses persistent uid.dat-backed XUID - if (IQNet::s_isHosting) - *pXuid = Win64Xuid::GetLegacyEmbeddedHostXuid(); - else - *pXuid = Win64Xuid::ResolvePersistentXuid(); + // Each pad gets a unique XUID derived from the persistent uid.dat value. + // Pad 0 uses the base XUID directly. Pads 1-3 get a deterministic hash + // of (base + pad) to produce fully independent IDs with no overlap risk. + *pXuid = Win64Xuid::DeriveXuidForPad(Win64Xuid::ResolvePersistentXuid(), iPad); #else * pXuid = 0xe000d45248242f2e + iPad; #endif @@ -636,8 +668,24 @@ void C_4JProfile::SetPrimaryPad(int iPad) {} char fakeGamerTag[32] = "PlayerName"; void SetFakeGamertag(char* name) { strcpy_s(fakeGamerTag, name); } #else -char* C_4JProfile::GetGamertag(int iPad) { extern char g_Win64Username[17]; return g_Win64Username; } -wstring C_4JProfile::GetDisplayName(int iPad) { extern wchar_t g_Win64UsernameW[17]; return g_Win64UsernameW; } +char* C_4JProfile::GetGamertag(int iPad) { + extern char g_Win64Username[17]; + if (iPad > 0 && iPad < XUSER_MAX_COUNT && IQNet::m_player[iPad].m_gamertag[0] != 0 && + !IQNet::m_player[iPad].m_isRemote) + { + static char s_padGamertag[XUSER_MAX_COUNT][17]; + WideCharToMultiByte(CP_ACP, 0, IQNet::m_player[iPad].m_gamertag, -1, s_padGamertag[iPad], 17, nullptr, nullptr); + return s_padGamertag[iPad]; + } + return g_Win64Username; +} +wstring C_4JProfile::GetDisplayName(int iPad) { + extern wchar_t g_Win64UsernameW[17]; + if (iPad > 0 && iPad < XUSER_MAX_COUNT && IQNet::m_player[iPad].m_gamertag[0] != 0 && + !IQNet::m_player[iPad].m_isRemote) + return IQNet::m_player[iPad].m_gamertag; + return g_Win64UsernameW; +} #endif bool C_4JProfile::IsFullVersion() { return s_bProfileIsFullVersion; } void C_4JProfile::SetSignInChangeCallback(void (*Func)(LPVOID, bool, unsigned int), LPVOID lpParam) {} diff --git a/Minecraft.Client/Font.cpp b/Minecraft.Client/Font.cpp index b6d96e8e7..db2a18c04 100644 --- a/Minecraft.Client/Font.cpp +++ b/Minecraft.Client/Font.cpp @@ -149,7 +149,7 @@ void Font::renderStyleLine(float x0, float y0, float x1, float y1) void Font::addCharacterQuad(wchar_t c) { float xOff = c % m_cols * m_charWidth; - float yOff = c / m_cols * m_charWidth; + float yOff = c / m_cols * m_charHeight; // was m_charWidth — wrong when glyphs aren't square float width = charWidths[c] - .01f; float height = m_charHeight - .01f; float fontWidth = m_cols * m_charWidth; @@ -187,7 +187,7 @@ void Font::addCharacterQuad(wchar_t c) void Font::renderCharacter(wchar_t c) { float xOff = c % m_cols * m_charWidth; - float yOff = c / m_cols * m_charWidth; + float yOff = c / m_cols * m_charHeight; // was m_charWidth — wrong when glyphs aren't square float width = charWidths[c] - .01f; float height = m_charHeight - .01f; diff --git a/Minecraft.Client/GameRenderer.cpp b/Minecraft.Client/GameRenderer.cpp index 9fe3e26ba..169f1280b 100644 --- a/Minecraft.Client/GameRenderer.cpp +++ b/Minecraft.Client/GameRenderer.cpp @@ -594,9 +594,10 @@ void GameRenderer::unZoomRegion() // 4J added as we have more complex adjustments to make for fov & aspect on account of viewports void GameRenderer::getFovAndAspect(float& fov, float& aspect, float a, bool applyEffects) { - // 4J - split out aspect ratio and fov here so we can adjust for viewports - we might need to revisit these as - // they are maybe be too generous for performance. - aspect = mc->width / static_cast(mc->height); + // Use the real window dimensions so the perspective updates on resize. + extern int g_rScreenWidth; + extern int g_rScreenHeight; + aspect = g_rScreenWidth / static_cast(g_rScreenHeight); fov = getFov(a, applyEffects); if( ( mc->player->m_iScreenSection == C4JRender::VIEWPORT_TYPE_SPLIT_TOP ) || @@ -946,9 +947,9 @@ float GameRenderer::ComputeGammaFromSlider(float slider0to100) slider = min(slider, 100.0f); if (slider > 50.0f) - return 1.0f + (slider - 50.0f) / 50.0f * 0.5f; // 1.0 -> 1.5 + return 1.0f + (slider - 50.0f) / 50.0f * 1.2f; // 1.0 -> 1.5 else - return 1.0f - (50.0f - slider) / 50.0f * 0.5f; // 1.0 -> 0.5 + return 1.0f - (50.0f - slider) / 50.0f * 0.4f; // 1.0 -> 0.5 } void GameRenderer::CachePlayerGammas() @@ -969,6 +970,10 @@ void GameRenderer::CachePlayerGammas() bool GameRenderer::ComputeViewportForPlayer(int j, D3D11_VIEWPORT &outViewport) const { + // Use the actual backbuffer dimensions so viewports adapt to window resize. + extern int g_rScreenWidth; + extern int g_rScreenHeight; + int active = 0; int indexMap[NUM_LIGHT_TEXTURES] = {-1, -1, -1, -1}; for (int i = 0; i < XUSER_MAX_COUNT && i < NUM_LIGHT_TEXTURES; ++i) @@ -981,8 +986,8 @@ bool GameRenderer::ComputeViewportForPlayer(int j, D3D11_VIEWPORT &outViewport) { outViewport.TopLeftX = 0.0f; outViewport.TopLeftY = 0.0f; - outViewport.Width = static_cast(mc->width); - outViewport.Height = static_cast(mc->height); + outViewport.Width = static_cast(g_rScreenWidth); + outViewport.Height = static_cast(g_rScreenHeight); outViewport.MinDepth = 0.0f; outViewport.MaxDepth = 1.0f; return true; @@ -998,8 +1003,8 @@ bool GameRenderer::ComputeViewportForPlayer(int j, D3D11_VIEWPORT &outViewport) if (k < 0) return false; - const float width = static_cast(mc->width); - const float height = static_cast(mc->height); + const float width = static_cast(g_rScreenWidth); + const float height = static_cast(g_rScreenHeight); if (active == 2) { @@ -1176,16 +1181,16 @@ void GameRenderer::render(float a, bool bFirst) if (mc->noRender) return; - GameRenderer::anaglyph3d = mc->options->anaglyph3d; + anaglyph3d = mc->options->anaglyph3d; - glViewport(0, 0, mc->width, mc->height); // 4J - added - ScreenSizeCalculator ssc(mc->options, mc->width, mc->height); - int screenWidth = ssc.getWidth(); - int screenHeight = ssc.getHeight(); - int xMouse = Mouse::getX() * screenWidth / mc->width; - int yMouse = screenHeight - Mouse::getY() * screenHeight / mc->height - 1; + glViewport(0, 0, mc->width, mc->height); // 4J - added (no-op on Win64, viewport set by StateSetViewport) + ScreenSizeCalculator ssc(mc->options, mc->width, mc->height); + const int screenWidth = ssc.getWidth(); + const int screenHeight = ssc.getHeight(); + const int xMouse = Mouse::getX() * screenWidth / mc->width; + const int yMouse = screenHeight - Mouse::getY() * screenHeight / mc->height - 1; - int maxFps = getFpsCap(mc->options->framerateLimit); + const int maxFps = getFpsCap(mc->options->framerateLimit); if (mc->level != nullptr) { diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index 8ca65dfed..d3bcda9bc 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -902,14 +902,14 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) int zBlockPos = Mth::floor(minecraft->player->z); // Chunk player is in - int xChunkPos = minecraft->player->xChunk; - int yChunkPos = minecraft->player->yChunk; - int zChunkPos = minecraft->player->zChunk; + int xChunkPos = xBlockPos >> 4; + int yChunkPos = yBlockPos >> 4; + int zChunkPos = zBlockPos >> 4; // Players offset within the chunk - int xChunkOffset = xBlockPos - xChunkPos * 16; - int yChunkOffset = yBlockPos - yChunkPos * 16; - int zChunkOffset = zBlockPos - zChunkPos * 16; + 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) @@ -958,18 +958,20 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) 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)"); - 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))); - 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")"); - 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"Difficulty: " + std::to_wstring(minecraft->level->difficulty) + L" (Day " + std::to_wstring(minecraft->level->getGameTime() / Level::TICKS_PER_DAY) + L")"); + } } diff --git a/Minecraft.Client/HorseRenderer.cpp b/Minecraft.Client/HorseRenderer.cpp index e01542d5a..5bbf76616 100644 --- a/Minecraft.Client/HorseRenderer.cpp +++ b/Minecraft.Client/HorseRenderer.cpp @@ -55,8 +55,15 @@ void HorseRenderer::renderModel(shared_ptr mob, float wp, float ws void HorseRenderer::bindTexture(ResourceLocation *location) { - // Set up (potentially) multiple texture layers for the horse - entityRenderDispatcher->textures->bindTextureLayers(location); + if (location->getTextureCount() > 1) + { + // Set up multiple texture layers for the horse + entityRenderDispatcher->textures->bindTextureLayers(location); + } + else + { + EntityRenderer::bindTexture(location); + } } ResourceLocation *HorseRenderer::getTextureLocation(shared_ptr entity) diff --git a/Minecraft.Client/ItemInHandRenderer.cpp b/Minecraft.Client/ItemInHandRenderer.cpp index 152a56688..66c922b42 100644 --- a/Minecraft.Client/ItemInHandRenderer.cpp +++ b/Minecraft.Client/ItemInHandRenderer.cpp @@ -935,6 +935,14 @@ void ItemInHandRenderer::tick() } +void ItemInHandRenderer::reset() +{ + selectedItem = nullptr; + lastSlot = -1; + height = 0.0f; + oHeight = 0.0f; +} + void ItemInHandRenderer::itemPlaced() { height = 0; diff --git a/Minecraft.Client/ItemInHandRenderer.h b/Minecraft.Client/ItemInHandRenderer.h index b5d840a26..0c0ac307b 100644 --- a/Minecraft.Client/ItemInHandRenderer.h +++ b/Minecraft.Client/ItemInHandRenderer.h @@ -41,6 +41,7 @@ private: int lastSlot; public: void tick(); + void reset(); void itemPlaced(); void itemUsed(); }; diff --git a/Minecraft.Client/LevelRenderer.cpp b/Minecraft.Client/LevelRenderer.cpp index 82fc3e9bc..cf2937f4f 100644 --- a/Minecraft.Client/LevelRenderer.cpp +++ b/Minecraft.Client/LevelRenderer.cpp @@ -554,7 +554,8 @@ void LevelRenderer::renderEntities(Vec3 *cam, Culler *culler, float a) for (auto& entity : entities) { - bool shouldRender = (entity->shouldRender(cam) && (entity->noCulling || culler->isVisible(entity->bb))); + bool isPlayerVehicle = (entity == mc->cameraTargetPlayer->riding); + bool shouldRender = (entity->shouldRender(cam) && (entity->noCulling || isPlayerVehicle || culler->isVisible(entity->bb))); // Render the mob if the mob's leash holder is within the culler if ( !shouldRender && entity->instanceof(eTYPE_MOB) ) diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index 4b9fd74ce..aa8fa1fa3 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "Minecraft.h" +#include "Common/UI/UIScene.h" #include "GameMode.h" #include "Timer.h" #include "ProgressRenderer.h" @@ -9,6 +10,7 @@ #include "User.h" #include "Textures.h" #include "GameRenderer.h" +#include "ItemInHandRenderer.h" #include "HumanoidModel.h" #include "Options.h" #include "TexturePackRepository.h" @@ -217,7 +219,8 @@ Minecraft::Minecraft(Component *mouseComponent, Canvas *parent, MinecraftApplet { m_pendingLocalConnections[i] = nullptr; m_connectionFailed[i] = false; - localgameModes[i]=nullptr; + localgameModes[i]= nullptr; + localitemInHandRenderers[i] = nullptr; } animateTickLevel = nullptr; // 4J added @@ -1481,9 +1484,22 @@ void Minecraft::run_middle() if(g_KBMInput.IsMouseButtonPressed(KeyboardMouseInput::MOUSE_RIGHT)) localplayers[i]->ullButtonsPressed|=1LL<isDirectEditBlocking(); + if(g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_INVENTORY)) { - if(ui.IsSceneInStack(i, eUIScene_InventoryMenu)) + if(isClosableByEitherKey && !isEditing) { ui.CloseUIScenes(i); } @@ -1498,7 +1514,7 @@ void Minecraft::run_middle() if(g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_CRAFTING) || g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_CRAFTING_ALT)) { - if(ui.IsSceneInStack(i, eUIScene_Crafting2x2Menu) || ui.IsSceneInStack(i, eUIScene_Crafting3x3Menu) || ui.IsSceneInStack(i, eUIScene_CreativeMenu)) + if((ui.IsSceneInStack(i, eUIScene_Crafting2x2Menu) || ui.IsSceneInStack(i, eUIScene_Crafting3x3Menu) || ui.IsSceneInStack(i, eUIScene_CreativeMenu) || isClosableByEitherKey) && !isEditing) { ui.CloseUIScenes(i); } @@ -2359,16 +2375,21 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) } #ifdef _WINDOWS64 - if ((screen != nullptr || ui.GetMenuDisplayed(iPad)) && g_KBMInput.IsMouseGrabbed()) + // Mouse grab/release only for the primary (KBM) player — splitscreen + // players use controllers and must never fight over the cursor state. + if (iPad == ProfileManager.GetPrimaryPad()) { - g_KBMInput.SetMouseGrabbed(false); + if ((screen != nullptr || ui.GetMenuDisplayed(iPad)) && g_KBMInput.IsMouseGrabbed()) + { + g_KBMInput.SetMouseGrabbed(false); + } } #endif if (screen == nullptr && !ui.GetMenuDisplayed(iPad) ) { #ifdef _WINDOWS64 - if (!g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsWindowFocused()) + if (iPad == ProfileManager.GetPrimaryPad() && !g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsWindowFocused()) { g_KBMInput.SetMouseGrabbed(true); } @@ -4228,6 +4249,17 @@ void Minecraft::setLevel(MultiPlayerLevel *level, int message /*=-1*/, shared_pt // 4J - stop update thread from processing this level, which blocks until it is safe to move on - will be re-enabled if we set the level to be non-nullptr gameRenderer->DisableUpdateThread(); + if (level == nullptr || player == nullptr) + { + for (int i = 0; i < XUSER_MAX_COUNT; ++i) + { + if (localitemInHandRenderers[i] != nullptr) + { + localitemInHandRenderers[i]->reset(); + } + } + } + for(unsigned int i = 0; i < levels.length; ++i) { // 4J We only need to save out in multiplayer is we are setting the level to nullptr @@ -4623,8 +4655,8 @@ void Minecraft::start(const wstring& name, const wstring& sid) void Minecraft::startAndConnectTo(const wstring& name, const wstring& sid, const wstring& url) { - bool fullScreen = false; - wstring userName = name; + const bool fullScreen = false; + const wstring userName = name; /* 4J - removed window handling things here final Frame frame = new Frame("Minecraft"); @@ -4643,10 +4675,15 @@ void Minecraft::startAndConnectTo(const wstring& name, const wstring& sid, const */ Minecraft *minecraft; - // 4J - was new Minecraft(frame, canvas, nullptr, 854, 480, fullScreen); - - minecraft = new Minecraft(nullptr, nullptr, nullptr, 1280, 720, fullScreen); + // 4J - was new Minecraft(frame, canvas, NULL, 854, 480, fullScreen); + // Logical width is proportional to the real screen aspect ratio so that + // the ortho projection and HUD layout match the viewport without stretching. + extern int g_iScreenWidth; + extern int g_iScreenHeight; + constexpr int logicalH = 720; + const int logicalW = logicalH * g_iScreenWidth / g_iScreenHeight; + minecraft = new Minecraft(nullptr, nullptr, nullptr, logicalW, logicalH, fullScreen); /* - 4J - removed { @Override diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index de9d26b93..bdcc9f813 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -2169,12 +2169,16 @@ void MinecraftServer::tick() } Entity::tickExtraWandering(); // 4J added - PIXBeginNamedEvent(0,"Connection tick"); - connection->tick(); - PIXEndNamedEvent(); + // Process player disconnect/kick queue BEFORE ticking connections. + // PendingConnection::handleLogin rejects duplicate XUIDs, so the old + // player must be removed from PlayerList before a reconnecting client's + // LoginPacket is processed. PIXBeginNamedEvent(0,"Players tick"); players->tick(); PIXEndNamedEvent(); + PIXBeginNamedEvent(0,"Connection tick"); + connection->tick(); + PIXEndNamedEvent(); // 4J - removed #if 0 diff --git a/Minecraft.Client/PS3/Media/splashes.txt b/Minecraft.Client/PS3/Media/splashes.txt index 7f7835c6c..df4a4be19 100644 --- a/Minecraft.Client/PS3/Media/splashes.txt +++ b/Minecraft.Client/PS3/Media/splashes.txt @@ -43,7 +43,7 @@ Dungeon! Exclusive! The bee's knees! Down with O.P.P.! -Closed source! +Closed source xD! Classy! Wow! Not on steam! diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 36e37520f..0a0f3c44b 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -190,9 +190,34 @@ void PendingConnection::handleLogin(shared_ptr packet) } else if (duplicateXuid) { - // if same XUID already in use by another player so disconnect this one. - app.DebugPrintf("Rejecting duplicate xuid for name: %ls\n", name.c_str()); - disconnect(DisconnectPacket::eDisconnect_Banned); + // The old player is still in PlayerList (disconnect hasn't been + // processed yet). Force-close the stale connection so the + // reconnecting client isn't rejected. + app.DebugPrintf("RECONNECT: Duplicate xuid for name: %ls, forcing old connection closed\n", name.c_str()); + shared_ptr stalePlayer = server->getPlayers()->getPlayer(loginXuid); + if (stalePlayer == nullptr && packet->m_onlineXuid != INVALID_XUID) + stalePlayer = server->getPlayers()->getPlayer(packet->m_onlineXuid); + + if (stalePlayer != nullptr && stalePlayer->connection != nullptr) + { + BYTE oldSmallId = 0; + if (stalePlayer->connection->connection != nullptr && stalePlayer->connection->connection->getSocket() != nullptr) + oldSmallId = stalePlayer->connection->connection->getSocket()->getSmallId(); + app.DebugPrintf("RECONNECT: Force-disconnecting old player smallId=%d\n", oldSmallId); + stalePlayer->connection->disconnect(DisconnectPacket::eDisconnect_Closed); + + // Queue the old SmallId for recycling so it's not permanently leaked. + // PlayerList::tick() will call PushFreeSmallId/ClearSocketForSmallId. + if (oldSmallId != 0) + server->getPlayers()->queueSmallIdForRecycle(oldSmallId); + + app.DebugPrintf("RECONNECT: Old player force-disconnect complete\n"); + } + + // Accept the login now that the old entry is removed. + app.DebugPrintf("RECONNECT: Calling handleAcceptedLogin for new connection\n"); + handleAcceptedLogin(packet); + app.DebugPrintf("RECONNECT: handleAcceptedLogin complete\n"); } #ifdef _WINDOWS64 else if (g_bRejectDuplicateNames) diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index ee0ba1564..645e3fb54 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -19,6 +19,9 @@ #include "..\Minecraft.World\net.minecraft.network.packet.h" #include "..\Minecraft.World\net.minecraft.network.h" #include "Windows64\Windows64_Xuid.h" +#ifdef _WINDOWS64 +#include "Windows64\Network\WinsockNetLayer.h" +#endif #include "..\Minecraft.World\Pos.h" #include "..\Minecraft.World\ProgressListener.h" #include "..\Minecraft.World\HellRandomLevelSource.h" @@ -245,10 +248,25 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr addPlayerToReceiving( player ); int maxPlayersForPacket = getMaxPlayers() > 255 ? 255 : getMaxPlayers(); - playerConnection->send(std::make_shared(L"", player->entityId, level->getLevelData()->getGenerator(), level->getSeed(), player->gameMode->getGameModeForPlayer()->getId(), - static_cast(level->dimension->id), static_cast(level->getMaxBuildHeight()), static_cast(maxPlayersForPacket), - level->difficulty, TelemetryManager->GetMultiplayerInstanceID(), static_cast(playerIndex), level->useNewSeaLevel(), player->getAllPlayerGamePrivileges(), - level->getLevelData()->getXZSize(), level->getLevelData()->getHellScale())); + + BYTE newSmallId = 0; + Socket *sock = connection->getSocket(); + INetworkPlayer *np = sock ? sock->getPlayer() : nullptr; + if (np) newSmallId = np->GetSmallId(); + app.DebugPrintf("RECONNECT: placeNewPlayer smallId=%d entityId=%d dim=%d\n", + newSmallId, player->entityId, level->dimension->id); + + playerConnection->send(std::make_shared(L"", player->entityId, level->getLevelData()->getGenerator(), + level->getSeed(), + player->gameMode->getGameModeForPlayer()->getId(), + static_cast(level->dimension->id), static_cast(level->getMaxBuildHeight()), + static_cast(maxPlayersForPacket), + level->difficulty, + TelemetryManager->GetMultiplayerInstanceID(), + static_cast(playerIndex), level->useNewSeaLevel(), + player->getAllPlayerGamePrivileges(), + level->getLevelData()->getXZSize(), + level->getLevelData()->getHellScale())); playerConnection->send(std::make_shared(spawnPos->x, spawnPos->y, spawnPos->z)); playerConnection->send(std::make_shared(&player->abilities)); playerConnection->send(std::make_shared(player->inventory->selected)); @@ -930,8 +948,8 @@ void PlayerList::repositionAcrossDimension(shared_ptr entity, int lastDi if (lastDimension != 1) { - xt = static_cast(Mth::clamp((int)xt, -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128)); - zt = static_cast(Mth::clamp((int)zt, -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128)); + xt = static_cast(Mth::clamp(static_cast(xt), -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128)); + zt = static_cast(Mth::clamp(static_cast(zt), -Level::MAX_LEVEL_SIZE + 128, Level::MAX_LEVEL_SIZE - 128)); if (entity->isAlive()) { newLevel->addEntity(entity); @@ -987,6 +1005,14 @@ void PlayerList::tick() { player->connection->disconnect( DisconnectPacket::eDisconnect_Closed ); } + +#ifdef _WINDOWS64 + // The old Connection's read/write threads are now dead (disconnect waits + // for them). Safe to recycle the smallId — no stale write thread can + // resolve getPlayer() to a new connection that reuses this slot. + WinsockNetLayer::PushFreeSmallId(smallId); + WinsockNetLayer::ClearSocketForSmallId(smallId); +#endif } LeaveCriticalSection(&m_closePlayersCS); @@ -1626,6 +1652,13 @@ void PlayerList::closePlayerConnectionBySmallId(BYTE networkSmallId) LeaveCriticalSection(&m_closePlayersCS); } +void PlayerList::queueSmallIdForRecycle(BYTE smallId) +{ + EnterCriticalSection(&m_closePlayersCS); + m_smallIdsToClose.push_back(smallId); + LeaveCriticalSection(&m_closePlayersCS); +} + bool PlayerList::isXuidBanned(PlayerUID xuid) { if( xuid == INVALID_XUID ) return false; diff --git a/Minecraft.Client/PlayerList.h b/Minecraft.Client/PlayerList.h index 03ed2398b..a4ae9c5d7 100644 --- a/Minecraft.Client/PlayerList.h +++ b/Minecraft.Client/PlayerList.h @@ -133,6 +133,7 @@ public: // 4J Added void kickPlayerByShortId(BYTE networkSmallId); void closePlayerConnectionBySmallId(BYTE networkSmallId); + void queueSmallIdForRecycle(BYTE smallId); bool isXuidBanned(PlayerUID xuid); // AP added for Vita so the range can be increased once the level starts void setViewDistance(int newViewDistance); diff --git a/Minecraft.Client/ServerConnection.cpp b/Minecraft.Client/ServerConnection.cpp index cca4c619f..0f96e0325 100644 --- a/Minecraft.Client/ServerConnection.cpp +++ b/Minecraft.Client/ServerConnection.cpp @@ -46,19 +46,30 @@ void ServerConnection::handleConnection(shared_ptr uc) void ServerConnection::stop() { + std::vector > pendingSnapshot; EnterCriticalSection(&pending_cs); - for (unsigned int i = 0; i < pending.size(); i++) - { - shared_ptr uc = pending[i]; - uc->connection->close(DisconnectPacket::eDisconnect_Closed); - } + pendingSnapshot = pending; LeaveCriticalSection(&pending_cs); - for (unsigned int i = 0; i < players.size(); i++) + for (unsigned int i = 0; i < pendingSnapshot.size(); i++) { - shared_ptr player = players[i]; - player->connection->close(DisconnectPacket::eDisconnect_Closed); - } + shared_ptr uc = pendingSnapshot[i]; + if (uc != NULL && !uc->done) + { + uc->disconnect(DisconnectPacket::eDisconnect_Closed); + } + } + + // Snapshot to avoid iterator invalidation if disconnect modifies the vector. + std::vector > playerSnapshot = players; + for (unsigned int i = 0; i < playerSnapshot.size(); i++) + { + shared_ptr player = playerSnapshot[i]; + if (player != NULL && !player->done) + { + player->disconnect(DisconnectPacket::eDisconnect_Quitting); + } + } } void ServerConnection::tick() @@ -107,7 +118,10 @@ void ServerConnection::tick() players.erase(players.begin()+i); i--; } - player->connection->flush(); + else + { + player->connection->flush(); + } } } diff --git a/Minecraft.Client/ServerLevel.cpp b/Minecraft.Client/ServerLevel.cpp index f158a1d97..d322d9b6d 100644 --- a/Minecraft.Client/ServerLevel.cpp +++ b/Minecraft.Client/ServerLevel.cpp @@ -256,7 +256,6 @@ void ServerLevel::tick() if (time % (saveInterval) == (dimension->id * dimension->id * (saveInterval/2))) #endif { - //app.DebugPrintf("Incremental save\n"); PIXBeginNamedEvent(0,"Incremental save"); save(false, nullptr); PIXEndNamedEvent(); diff --git a/Minecraft.Client/ServerPlayer.cpp b/Minecraft.Client/ServerPlayer.cpp index 10486de1d..c7ff4fda2 100644 --- a/Minecraft.Client/ServerPlayer.cpp +++ b/Minecraft.Client/ServerPlayer.cpp @@ -426,7 +426,6 @@ void ServerPlayer::doChunkSendingTick(bool dontDelayChunks) // unloaded on the client and so just gradually build up more and more of the finite set of chunks as the player moves if( !g_NetworkManager.SystemFlagGet(connection->getNetworkPlayer(),flagIndex) ) { - // app.DebugPrintf("Creating BRUP for %d %d\n",nearest.x, nearest.z); PIXBeginNamedEvent(0,"Creation BRUP for sending\n"); int64_t before = System::currentTimeMillis(); const auto packet = std::make_shared(nearest.x * 16, 0, nearest.z * 16, 16, Level::maxBuildHeight, 16, level); diff --git a/Minecraft.Client/StringTable.cpp b/Minecraft.Client/StringTable.cpp index 234a8943b..bdee2ca91 100644 --- a/Minecraft.Client/StringTable.cpp +++ b/Minecraft.Client/StringTable.cpp @@ -118,9 +118,7 @@ void StringTable::ProcessStringTableData(void) else { app.DebugPrintf("Failed to get language\n"); -#ifdef _DEBUG - __debugbreak(); -#endif + isStatic = false; } diff --git a/Minecraft.Client/Textures.cpp b/Minecraft.Client/Textures.cpp index f433dc5e0..ba7c02821 100644 --- a/Minecraft.Client/Textures.cpp +++ b/Minecraft.Client/Textures.cpp @@ -997,8 +997,15 @@ void Textures::replaceTextureDirect(shortArray rawPixels, int w, int h, int id) void Textures::releaseTexture(int id) { + if (id <= 0) return; loadedImages.erase(id); - glDeleteTextures(id); + // TextureFree() has no bounds check and crashes on stale IDs (e.g. after + // RenderManager.Initialise() which memsets the texture table to zero). + // TextureGetTexture() IS safe — returns NULL for invalid/unallocated IDs. + if (RenderManager.TextureGetTexture(id) != NULL) + { + glDeleteTextures(id); + } } int Textures::loadHttpTexture(const wstring& url, const wstring& backup) diff --git a/Minecraft.Client/Windows64/Iggy/gdraw/gdraw_d3d1x_shared.inl b/Minecraft.Client/Windows64/Iggy/gdraw/gdraw_d3d1x_shared.inl index 7719ca71b..0722308ac 100644 --- a/Minecraft.Client/Windows64/Iggy/gdraw/gdraw_d3d1x_shared.inl +++ b/Minecraft.Client/Windows64/Iggy/gdraw/gdraw_d3d1x_shared.inl @@ -996,6 +996,13 @@ void gdraw_D3D1X_(SetTileOrigin)(ID3D1X(RenderTargetView) *main_rt, ID3D1X(Depth static void RADLINK gdraw_SetViewSizeAndWorldScale(S32 w, S32 h, F32 scalex, F32 scaley) { + static S32 s_lastW = 0, s_lastH = 0; + static F32 s_lastSx = 0, s_lastSy = 0; + if (w != s_lastW || h != s_lastH || scalex != s_lastSx || scaley != s_lastSy) { + app.DebugPrintf("[GDRAW] SetViewSize: fw=%d fh=%d scale=%.6f,%.6f frametex=%dx%d vx=%d vy=%d\n", + w, h, scalex, scaley, gdraw->frametex_width, gdraw->frametex_height, gdraw->vx, gdraw->vy); + s_lastW = w; s_lastH = h; s_lastSx = scalex; s_lastSy = scaley; + } memset(gdraw->frame, 0, sizeof(gdraw->frame)); gdraw->cur = gdraw->frame; gdraw->fw = w; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 276389539..e82118cd0 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -26,7 +26,7 @@ bool WinsockNetLayer::s_initialized = false; BYTE WinsockNetLayer::s_localSmallId = 0; BYTE WinsockNetLayer::s_hostSmallId = 0; -unsigned int WinsockNetLayer::s_nextSmallId = 1; +unsigned int WinsockNetLayer::s_nextSmallId = XUSER_MAX_COUNT; CRITICAL_SECTION WinsockNetLayer::s_sendLock; CRITICAL_SECTION WinsockNetLayer::s_connectionsLock; @@ -54,6 +54,10 @@ std::vector WinsockNetLayer::s_freeSmallIds; SOCKET WinsockNetLayer::s_smallIdToSocket[256]; CRITICAL_SECTION WinsockNetLayer::s_smallIdToSocketLock; +SOCKET WinsockNetLayer::s_splitScreenSocket[XUSER_MAX_COUNT] = { INVALID_SOCKET, INVALID_SOCKET, INVALID_SOCKET, INVALID_SOCKET }; +BYTE WinsockNetLayer::s_splitScreenSmallId[XUSER_MAX_COUNT] = { 0xFF, 0xFF, 0xFF, 0xFF }; +HANDLE WinsockNetLayer::s_splitScreenRecvThread[XUSER_MAX_COUNT] = {nullptr, nullptr, nullptr, nullptr}; + bool g_Win64MultiplayerHost = false; bool g_Win64MultiplayerJoin = false; int g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; @@ -111,6 +115,15 @@ void WinsockNetLayer::Shutdown() s_hostConnectionSocket = INVALID_SOCKET; } + // Stop accept loop first so no new RecvThread can be created while shutting down. + if (s_acceptThread != nullptr) + { + WaitForSingleObject(s_acceptThread, 2000); + CloseHandle(s_acceptThread); + s_acceptThread = nullptr; + } + + std::vector recvThreads; EnterCriticalSection(&s_connectionsLock); for (size_t i = 0; i < s_connections.size(); i++) { @@ -118,18 +131,27 @@ void WinsockNetLayer::Shutdown() if (s_connections[i].tcpSocket != INVALID_SOCKET) { closesocket(s_connections[i].tcpSocket); + s_connections[i].tcpSocket = INVALID_SOCKET; + } + if (s_connections[i].recvThread != nullptr) + { + recvThreads.push_back(s_connections[i].recvThread); + s_connections[i].recvThread = nullptr; } } - s_connections.clear(); LeaveCriticalSection(&s_connectionsLock); - if (s_acceptThread != nullptr) + // Wait for all host-side receive threads to exit before destroying state. + for (size_t i = 0; i < recvThreads.size(); i++) { - WaitForSingleObject(s_acceptThread, 2000); - CloseHandle(s_acceptThread); - s_acceptThread = nullptr; + WaitForSingleObject(recvThreads[i], 2000); + CloseHandle(recvThreads[i]); } + EnterCriticalSection(&s_connectionsLock); + s_connections.clear(); + LeaveCriticalSection(&s_connectionsLock); + if (s_clientRecvThread != nullptr) { WaitForSingleObject(s_clientRecvThread, 2000); @@ -137,16 +159,38 @@ void WinsockNetLayer::Shutdown() s_clientRecvThread = nullptr; } + for (int i = 0; i < XUSER_MAX_COUNT; i++) + { + if (s_splitScreenSocket[i] != INVALID_SOCKET) + { + closesocket(s_splitScreenSocket[i]); + s_splitScreenSocket[i] = INVALID_SOCKET; + } + if (s_splitScreenRecvThread[i] != nullptr) + { + WaitForSingleObject(s_splitScreenRecvThread[i], 2000); + CloseHandle(s_splitScreenRecvThread[i]); + s_splitScreenRecvThread[i] = nullptr; + } + s_splitScreenSmallId[i] = 0xFF; + } + if (s_initialized) { + EnterCriticalSection(&s_disconnectLock); + s_disconnectedSmallIds.clear(); + LeaveCriticalSection(&s_disconnectLock); + + EnterCriticalSection(&s_freeSmallIdLock); + s_freeSmallIds.clear(); + LeaveCriticalSection(&s_freeSmallIdLock); + DeleteCriticalSection(&s_sendLock); DeleteCriticalSection(&s_connectionsLock); DeleteCriticalSection(&s_advertiseLock); DeleteCriticalSection(&s_discoveryLock); DeleteCriticalSection(&s_disconnectLock); - s_disconnectedSmallIds.clear(); DeleteCriticalSection(&s_freeSmallIdLock); - s_freeSmallIds.clear(); DeleteCriticalSection(&s_smallIdToSocketLock); WSACleanup(); s_initialized = false; @@ -160,7 +204,7 @@ bool WinsockNetLayer::HostGame(int port, const char* bindIp) s_isHost = true; s_localSmallId = 0; s_hostSmallId = 0; - s_nextSmallId = 1; + s_nextSmallId = XUSER_MAX_COUNT; s_hostGamePort = port; EnterCriticalSection(&s_freeSmallIdLock); @@ -249,6 +293,17 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port) s_hostConnectionSocket = INVALID_SOCKET; } + // Wait for old client recv thread to fully exit before starting a new connection. + // Without this, the old thread can read from the new socket (s_hostConnectionSocket + // is a global) and steal bytes from the new connection's TCP stream, causing + // packet stream misalignment on reconnect. + if (s_clientRecvThread != nullptr) + { + WaitForSingleObject(s_clientRecvThread, 5000); + CloseHandle(s_clientRecvThread); + s_clientRecvThread = nullptr; + } + struct addrinfo hints = {}; struct addrinfo* result = nullptr; @@ -351,6 +406,13 @@ bool WinsockNetLayer::SendOnSocket(SOCKET sock, const void* data, int dataSize) { if (sock == INVALID_SOCKET || dataSize <= 0) return false; + // TODO: s_sendLock is a single global lock for ALL sockets. If one client's + // send() blocks (TCP window full, slow WiFi), every other write thread stalls + // waiting for this lock — no data flows to any player until the slow send + // completes. This scales badly with player count (8+ players = noticeable). + // Fix: replace with per-socket locks indexed by smallId (s_perSocketSendLock[256]). + // The lock only needs to prevent interleaving of header+payload on the SAME socket; + // sends to different sockets are independent and should never block each other. EnterCriticalSection(&s_sendLock); BYTE header[4]; @@ -450,19 +512,28 @@ void WinsockNetLayer::HandleDataReceived(BYTE fromSmallId, BYTE toSmallId, unsig INetworkPlayer* pPlayerFrom = g_NetworkManager.GetPlayerBySmallId(fromSmallId); INetworkPlayer* pPlayerTo = g_NetworkManager.GetPlayerBySmallId(toSmallId); - if (pPlayerFrom == nullptr || pPlayerTo == nullptr) return; + if (pPlayerFrom == nullptr || pPlayerTo == nullptr) + { + app.DebugPrintf("NET RECV: DROPPED %u bytes from=%d to=%d (player NULL: from=%p to=%p)\n", + dataSize, fromSmallId, toSmallId, pPlayerFrom, pPlayerTo); + return; + } if (s_isHost) { ::Socket* pSocket = pPlayerFrom->GetSocket(); if (pSocket != nullptr) pSocket->pushDataToQueue(data, dataSize, false); + else + app.DebugPrintf("NET RECV: DROPPED %u bytes, host pSocket NULL for from=%d\n", dataSize, fromSmallId); } else { ::Socket* pSocket = pPlayerTo->GetSocket(); if (pSocket != nullptr) pSocket->pushDataToQueue(data, dataSize, true); + else + app.DebugPrintf("NET RECV: DROPPED %u bytes, client pSocket NULL for to=%d\n", dataSize, toSmallId); } } @@ -490,7 +561,7 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) } extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager; - if (g_pPlatformNetworkManager != NULL && !g_pPlatformNetworkManager->CanAcceptMoreConnections()) + if (g_pPlatformNetworkManager != nullptr && !g_pPlatformNetworkManager->CanAcceptMoreConnections()) { app.DebugPrintf("Win64 LAN: Rejecting connection, server at max players\n"); SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_ServerFull); @@ -525,6 +596,7 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) { app.DebugPrintf("Failed to send small ID to client\n"); closesocket(clientSocket); + PushFreeSmallId(assignedSmallId); continue; } @@ -662,7 +734,16 @@ bool WinsockNetLayer::PopDisconnectedSmallId(BYTE* outSmallId) void WinsockNetLayer::PushFreeSmallId(BYTE smallId) { EnterCriticalSection(&s_freeSmallIdLock); - s_freeSmallIds.push_back(smallId); + // Guard against double-recycle: the reconnect path (queueSmallIdForRecycle) and + // the DoWork disconnect path can both push the same smallId. If we allow duplicates, + // AcceptThread will hand out the same smallId to two different connections. + bool alreadyFree = false; + for (size_t i = 0; i < s_freeSmallIds.size(); i++) + { + if (s_freeSmallIds[i] == smallId) { alreadyFree = true; break; } + } + if (!alreadyFree) + s_freeSmallIds.push_back(smallId); LeaveCriticalSection(&s_freeSmallIdLock); } @@ -682,6 +763,171 @@ void WinsockNetLayer::CloseConnectionBySmallId(BYTE smallId) LeaveCriticalSection(&s_connectionsLock); } +BYTE WinsockNetLayer::GetSplitScreenSmallId(int padIndex) +{ + if (padIndex <= 0 || padIndex >= XUSER_MAX_COUNT) return 0xFF; + return s_splitScreenSmallId[padIndex]; +} + +SOCKET WinsockNetLayer::GetLocalSocket(BYTE senderSmallId) +{ + if (senderSmallId == s_localSmallId) + return s_hostConnectionSocket; + for (int i = 1; i < XUSER_MAX_COUNT; i++) + { + if (s_splitScreenSmallId[i] == senderSmallId && s_splitScreenSocket[i] != INVALID_SOCKET) + return s_splitScreenSocket[i]; + } + return INVALID_SOCKET; +} + +bool WinsockNetLayer::JoinSplitScreen(int padIndex, BYTE* outSmallId) +{ + if (!s_active || s_isHost || padIndex <= 0 || padIndex >= XUSER_MAX_COUNT) + return false; + + if (s_splitScreenSocket[padIndex] != INVALID_SOCKET) + { + return false; + } + + 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", g_Win64MultiplayerPort); + if (getaddrinfo(g_Win64MultiplayerIP, portStr, &hints, &result) != 0 || result == nullptr) + { + app.DebugPrintf("Win64 LAN: Split-screen getaddrinfo failed for %s:%d\n", g_Win64MultiplayerIP, g_Win64MultiplayerPort); + return false; + } + + SOCKET sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (sock == INVALID_SOCKET) + { + freeaddrinfo(result); + return false; + } + + int noDelay = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char*)&noDelay, sizeof(noDelay)); + + if (connect(sock, result->ai_addr, (int)result->ai_addrlen) == SOCKET_ERROR) + { + app.DebugPrintf("Win64 LAN: Split-screen connect() failed: %d\n", WSAGetLastError()); + closesocket(sock); + freeaddrinfo(result); + return false; + } + freeaddrinfo(result); + + BYTE assignBuf[1]; + if (!RecvExact(sock, assignBuf, 1)) + { + app.DebugPrintf("Win64 LAN: Split-screen failed to receive smallId\n"); + closesocket(sock); + return false; + } + + if (assignBuf[0] == WIN64_SMALLID_REJECT) + { + BYTE rejectBuf[5]; + RecvExact(sock, rejectBuf, 5); + app.DebugPrintf("Win64 LAN: Split-screen connection rejected\n"); + closesocket(sock); + return false; + } + + BYTE assignedSmallId = assignBuf[0]; + s_splitScreenSocket[padIndex] = sock; + s_splitScreenSmallId[padIndex] = assignedSmallId; + *outSmallId = assignedSmallId; + + app.DebugPrintf("Win64 LAN: Split-screen pad %d connected, assigned smallId=%d\n", padIndex, assignedSmallId); + + int* threadParam = new int; + *threadParam = padIndex; + s_splitScreenRecvThread[padIndex] = CreateThread(nullptr, 0, SplitScreenRecvThreadProc, threadParam, 0, nullptr); + if (s_splitScreenRecvThread[padIndex] == nullptr) + { + delete threadParam; + closesocket(sock); + s_splitScreenSocket[padIndex] = INVALID_SOCKET; + s_splitScreenSmallId[padIndex] = 0xFF; + app.DebugPrintf("Win64 LAN: CreateThread failed for split-screen pad %d\n", padIndex); + return false; + } + + return true; +} + +void WinsockNetLayer::CloseSplitScreenConnection(int padIndex) +{ + if (padIndex <= 0 || padIndex >= XUSER_MAX_COUNT) return; + + if (s_splitScreenSocket[padIndex] != INVALID_SOCKET) + { + closesocket(s_splitScreenSocket[padIndex]); + s_splitScreenSocket[padIndex] = INVALID_SOCKET; + } + s_splitScreenSmallId[padIndex] = 0xFF; + if (s_splitScreenRecvThread[padIndex] != nullptr) + { + WaitForSingleObject(s_splitScreenRecvThread[padIndex], 2000); + CloseHandle(s_splitScreenRecvThread[padIndex]); + s_splitScreenRecvThread[padIndex] = nullptr; + } +} + +DWORD WINAPI WinsockNetLayer::SplitScreenRecvThreadProc(LPVOID param) +{ + int padIndex = *(int*)param; + delete (int*)param; + + SOCKET sock = s_splitScreenSocket[padIndex]; + BYTE localSmallId = s_splitScreenSmallId[padIndex]; + std::vector recvBuf; + recvBuf.resize(WIN64_NET_RECV_BUFFER_SIZE); + + while (s_active && s_splitScreenSocket[padIndex] != INVALID_SOCKET) + { + BYTE header[4]; + if (!RecvExact(sock, header, 4)) + { + app.DebugPrintf("Win64 LAN: Split-screen pad %d disconnected from host\n", padIndex); + break; + } + + int packetSize = ((uint32_t)header[0] << 24) | ((uint32_t)header[1] << 16) | + ((uint32_t)header[2] << 8) | ((uint32_t)header[3]); + if (packetSize <= 0 || packetSize > WIN64_NET_MAX_PACKET_SIZE) + { + app.DebugPrintf("Win64 LAN: Split-screen pad %d invalid packet size %d\n", padIndex, packetSize); + break; + } + + if ((int)recvBuf.size() < packetSize) + recvBuf.resize(packetSize); + + if (!RecvExact(sock, &recvBuf[0], packetSize)) + { + app.DebugPrintf("Win64 LAN: Split-screen pad %d disconnected from host (body)\n", padIndex); + break; + } + + HandleDataReceived(s_hostSmallId, localSmallId, &recvBuf[0], packetSize); + } + + EnterCriticalSection(&s_disconnectLock); + s_disconnectedSmallIds.push_back(localSmallId); + LeaveCriticalSection(&s_disconnectLock); + + return 0; +} + DWORD WINAPI WinsockNetLayer::ClientRecvThreadProc(LPVOID param) { std::vector recvBuf; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index 59dda29ac..c5d689752 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -72,6 +72,12 @@ public: static bool SendToSmallId(BYTE targetSmallId, const void* data, int dataSize); static bool SendOnSocket(SOCKET sock, const void* data, int dataSize); + // Non-host split-screen: additional TCP connections to host, one per pad + static bool JoinSplitScreen(int padIndex, BYTE* outSmallId); + static void CloseSplitScreenConnection(int padIndex); + static SOCKET GetLocalSocket(BYTE senderSmallId); + static BYTE GetSplitScreenSmallId(int padIndex); + static bool IsHosting() { return s_isHost; } static bool IsConnected() { return s_connected; } static bool IsActive() { return s_active; } @@ -103,6 +109,7 @@ private: static DWORD WINAPI AcceptThreadProc(LPVOID param); static DWORD WINAPI RecvThreadProc(LPVOID param); static DWORD WINAPI ClientRecvThreadProc(LPVOID param); + static DWORD WINAPI SplitScreenRecvThreadProc(LPVOID param); static DWORD WINAPI AdvertiseThreadProc(LPVOID param); static DWORD WINAPI DiscoveryThreadProc(LPVOID param); @@ -147,6 +154,11 @@ private: static SOCKET s_smallIdToSocket[256]; static CRITICAL_SECTION s_smallIdToSocketLock; + // Per-pad split-screen TCP connections (client-side, non-host only) + static SOCKET s_splitScreenSocket[XUSER_MAX_COUNT]; + static BYTE s_splitScreenSmallId[XUSER_MAX_COUNT]; + static HANDLE s_splitScreenRecvThread[XUSER_MAX_COUNT]; + public: static void ClearSocketForSmallId(BYTE smallId); }; diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 69c1a12bb..70aeb22bf 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -49,6 +49,11 @@ #include "Windows64_Xuid.h" #include "Common/UI/UI.h" +// Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib. +// C4JRender (RenderManager) is a stateless wrapper — all D3D state lives in InternalRenderManager. +class Renderer; +extern Renderer InternalRenderManager; + #include "Xbox/resource.h" #ifdef _MSC_VER @@ -91,10 +96,20 @@ DWORD dwProfileSettingsA[NUM_PROFILE_VALUES]= BOOL g_bWidescreen = TRUE; +// Screen resolution — auto-detected from the monitor at startup. +// The 3D world renders at native resolution; Flash UI is 16:9-fitted and centered +// within each viewport (pillarboxed on ultrawide, letterboxed on tall displays). +// ApplyScreenMode() can still override these for debug/test resolutions via launch args. int g_iScreenWidth = 1920; int g_iScreenHeight = 1080; +// Real window dimensions — updated on every WM_SIZE so the 3D perspective +// always matches the current window, even after a resize. +int g_rScreenWidth = 1920; +int g_rScreenHeight = 1080; + float g_iAspectRatio = static_cast(g_iScreenWidth) / g_iScreenHeight; +static bool g_bResizeReady = false; char g_Win64Username[17] = { 0 }; wchar_t g_Win64UsernameW[17] = { 0 }; @@ -103,14 +118,6 @@ wchar_t g_Win64UsernameW[17] = { 0 }; static bool g_isFullscreen = false; static WINDOWPLACEMENT g_wpPrev = { sizeof(g_wpPrev) }; -//-------------------------------------------------------------------------------------- -// Update the Aspect Ratio to support Any Aspect Ratio -//-------------------------------------------------------------------------------------- -void UpdateAspectRatio(int width, int height) -{ - g_iAspectRatio = static_cast(width) / height; -} - struct Win64LaunchOptions { int screenMode; @@ -531,6 +538,11 @@ ID3D11Texture2D* g_pDepthStencilBuffer = nullptr; // WM_SIZE - handle resizing logic to support Any Aspect Ratio // // +static bool ResizeD3D(int newW, int newH); // forward declaration +static bool g_bInSizeMove = false; // true while the user is dragging the window border +static int g_pendingResizeW = 0; // deferred resize dimensions +static int g_pendingResizeH = 0; + LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmId, wmEvent; @@ -668,9 +680,40 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } } break; + case WM_ENTERSIZEMOVE: + g_bInSizeMove = true; + break; + + case WM_EXITSIZEMOVE: + g_bInSizeMove = false; + if (g_pendingResizeW > 0 && g_pendingResizeH > 0) + { + // g_rScreenWidth/Height updated inside ResizeD3D to backbuffer dims + ResizeD3D(g_pendingResizeW, g_pendingResizeH); + g_pendingResizeW = 0; + g_pendingResizeH = 0; + } + break; + case WM_SIZE: { - UpdateAspectRatio(LOWORD(lParam), HIWORD(lParam)); + int newW = LOWORD(lParam); + int newH = HIWORD(lParam); + if (newW > 0 && newH > 0) + { + if (g_bInSizeMove) + { + // Just store the latest size, resize when dragging ends + g_pendingResizeW = newW; + g_pendingResizeH = newH; + } + else + { + // Immediate resize (maximize, programmatic resize, etc.) + // g_rScreenWidth/Height updated inside ResizeD3D to backbuffer dims + ResizeD3D(newW, newH); + } + } } break; default: @@ -719,7 +762,7 @@ BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { g_hInst = hInstance; // Store instance handle in our global variable - RECT wr = {0, 0, g_iScreenWidth, g_iScreenHeight}; // set the size, but not the position + RECT wr = {0, 0, g_rScreenWidth, g_rScreenHeight}; // set the size, but not the position AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE); // adjust the size g_hWnd = CreateWindow( "MinecraftClass", @@ -805,8 +848,8 @@ HRESULT InitDevice() UINT width = rc.right - rc.left; UINT height = rc.bottom - rc.top; //app.DebugPrintf("width: %d, height: %d\n", width, height); - width = g_iScreenWidth; - height = g_iScreenHeight; + width = g_rScreenWidth; + height = g_rScreenHeight; //app.DebugPrintf("width: %d, height: %d\n", width, height); UINT createDeviceFlags = 0; @@ -838,7 +881,7 @@ HRESULT InitDevice() sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; - sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT; sd.OutputWindow = g_hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; @@ -922,6 +965,281 @@ void Render() g_pSwapChain->Present( 0, 0 ); } +//-------------------------------------------------------------------------------------- +// Rebuild D3D11 resources after a window resize +//-------------------------------------------------------------------------------------- +static bool ResizeD3D(int newW, int newH) +{ + if (newW <= 0 || newH <= 0) return false; + if (!g_pSwapChain) return false; + if (!g_bResizeReady) return false; + + int bbW = newW; + int bbH = newH; + + // InternalRenderManager member offsets (from decompiled Renderer.h): + // 0x10 m_pDevice (ID3D11Device*) + // 0x18 m_pDeviceContext (ID3D11DeviceContext*) + // 0x20 m_pSwapChain (IDXGISwapChain*) + // 0x28 renderTargetView (ID3D11RenderTargetView*) — backbuffer RTV + // 0x50 renderTargetShaderResourceView (ID3D11ShaderResourceView*) + // 0x98 depthStencilView (ID3D11DepthStencilView*) + // 0x5138 backBufferWidth (DWORD) — used by StartFrame() for viewport + // 0x513C backBufferHeight (DWORD) — used by StartFrame() for viewport + // + // Strategy: destroy old swap chain, create new one, patch Renderer's internal + // pointers directly. This avoids both ResizeBuffers (outstanding ref issues) + // and Initialise() (which wipes the texture table via memset). + // The Renderer's old RTV/SRV/DSV are intentionally NOT released — they become + // orphaned with the old swap chain. Tiny leak, but avoids fighting unknown refs. + char* pRM = (char*)&InternalRenderManager; + ID3D11RenderTargetView** ppRM_RTV = (ID3D11RenderTargetView**)(pRM + 0x28); + ID3D11ShaderResourceView** ppRM_SRV = (ID3D11ShaderResourceView**)(pRM + 0x50); + ID3D11DepthStencilView** ppRM_DSV = (ID3D11DepthStencilView**)(pRM + 0x98); + IDXGISwapChain** ppRM_SC = (IDXGISwapChain**)(pRM + 0x20); + DWORD* pRM_BBWidth = (DWORD*)(pRM + 0x5138); + DWORD* pRM_BBHeight = (DWORD*)(pRM + 0x513C); + + // Verify offsets by checking device and swap chain pointers + ID3D11Device** ppRM_Device = (ID3D11Device**)(pRM + 0x10); + if (*ppRM_Device != g_pd3dDevice || *ppRM_SC != g_pSwapChain) + { + app.DebugPrintf("[RESIZE] ERROR: RenderManager offset verification failed! " + "device=%p (expected %p) swapchain=%p (expected %p)\n", + *ppRM_Device, g_pd3dDevice, *ppRM_SC, g_pSwapChain); + return false; + } + + // Cross-check backbuffer dimension offsets against swap chain desc + DXGI_SWAP_CHAIN_DESC oldScDesc; + g_pSwapChain->GetDesc(&oldScDesc); + bool bbDimsValid = (*pRM_BBWidth == oldScDesc.BufferDesc.Width && + *pRM_BBHeight == oldScDesc.BufferDesc.Height); + if (!bbDimsValid) + { + app.DebugPrintf("[RESIZE] WARNING: backBuffer dim offsets wrong: " + "stored=%ux%u, swapchain=%ux%u\n", + *pRM_BBWidth, *pRM_BBHeight, oldScDesc.BufferDesc.Width, oldScDesc.BufferDesc.Height); + } + + RenderManager.Suspend(); + while (!RenderManager.Suspended()) { Sleep(1); } + + PostProcesser::GetInstance().Cleanup(); + + g_pImmediateContext->ClearState(); + g_pImmediateContext->Flush(); + + // Release OUR views and depth buffer + if (g_pRenderTargetView) { g_pRenderTargetView->Release(); g_pRenderTargetView = NULL; } + if (g_pDepthStencilView) { g_pDepthStencilView->Release(); g_pDepthStencilView = NULL; } + if (g_pDepthStencilBuffer) { g_pDepthStencilBuffer->Release(); g_pDepthStencilBuffer = NULL; } + + gdraw_D3D11_PreReset(); + + // Get IDXGIFactory from the existing device BEFORE destroying the old swap chain. + // If anything fails before we have a new swap chain, we abort without destroying + // the old one — leaving the Renderer in a valid (old-size) state. + IDXGISwapChain* pOldSwapChain = g_pSwapChain; + bool success = false; + HRESULT hr; + + IDXGIDevice* dxgiDevice = NULL; + IDXGIAdapter* dxgiAdapter = NULL; + IDXGIFactory* dxgiFactory = NULL; + hr = g_pd3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice); + if (FAILED(hr)) goto postReset; + hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&dxgiAdapter); + if (FAILED(hr)) { dxgiDevice->Release(); goto postReset; } + hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory), (void**)&dxgiFactory); + dxgiAdapter->Release(); + dxgiDevice->Release(); + if (FAILED(hr)) goto postReset; + + // Create new swap chain at backbuffer size + { + DXGI_SWAP_CHAIN_DESC sd = {}; + sd.BufferCount = 1; + sd.BufferDesc.Width = bbW; + sd.BufferDesc.Height = bbH; + sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sd.BufferDesc.RefreshRate.Numerator = 60; + sd.BufferDesc.RefreshRate.Denominator = 1; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT; + sd.OutputWindow = g_hWnd; + sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; + sd.Windowed = TRUE; + + IDXGISwapChain* pNewSwapChain = NULL; + hr = dxgiFactory->CreateSwapChain(g_pd3dDevice, &sd, &pNewSwapChain); + dxgiFactory->Release(); + if (FAILED(hr) || pNewSwapChain == NULL) + { + app.DebugPrintf("[RESIZE] CreateSwapChain FAILED hr=0x%08X — keeping old swap chain\n", (unsigned)hr); + goto postReset; + } + + // New swap chain created successfully — NOW destroy the old one. + // The Renderer's internal RTV/SRV still reference the old backbuffer — + // those COM objects become orphaned (tiny leak, harmless). We DON'T + // release them because unknown code may also hold refs. + pOldSwapChain->Release(); + g_pSwapChain = pNewSwapChain; + } + + // Patch Renderer's swap chain pointer + *ppRM_SC = g_pSwapChain; + + // Create render target views from new backbuffer + { + ID3D11Texture2D* pBackBuffer = NULL; + hr = g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer); + if (FAILED(hr)) goto postReset; + + // Our RTV + hr = g_pd3dDevice->CreateRenderTargetView(pBackBuffer, NULL, &g_pRenderTargetView); + if (FAILED(hr)) { pBackBuffer->Release(); goto postReset; } + + // Renderer's internal RTV (offset 0x28) + hr = g_pd3dDevice->CreateRenderTargetView(pBackBuffer, NULL, ppRM_RTV); + if (FAILED(hr)) { pBackBuffer->Release(); goto postReset; } + + // Renderer's SRV: separate texture matching backbuffer dims (used by CaptureThumbnail) + D3D11_TEXTURE2D_DESC backDesc = {}; + pBackBuffer->GetDesc(&backDesc); + pBackBuffer->Release(); + + D3D11_TEXTURE2D_DESC srvDesc = backDesc; + srvDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + ID3D11Texture2D* srvTexture = NULL; + hr = g_pd3dDevice->CreateTexture2D(&srvDesc, NULL, &srvTexture); + if (SUCCEEDED(hr)) + { + hr = g_pd3dDevice->CreateShaderResourceView(srvTexture, NULL, ppRM_SRV); + srvTexture->Release(); + } + if (FAILED(hr)) goto postReset; + } + + // Recreate depth stencil at backbuffer size + { + D3D11_TEXTURE2D_DESC descDepth = {}; + descDepth.Width = bbW; + descDepth.Height = bbH; + descDepth.MipLevels = 1; + descDepth.ArraySize = 1; + descDepth.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + descDepth.SampleDesc.Count = 1; + descDepth.SampleDesc.Quality = 0; + descDepth.Usage = D3D11_USAGE_DEFAULT; + descDepth.BindFlags = D3D11_BIND_DEPTH_STENCIL; + hr = g_pd3dDevice->CreateTexture2D(&descDepth, NULL, &g_pDepthStencilBuffer); + if (FAILED(hr)) goto postReset; + + D3D11_DEPTH_STENCIL_VIEW_DESC descDSView = {}; + descDSView.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + descDSView.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D; + hr = g_pd3dDevice->CreateDepthStencilView(g_pDepthStencilBuffer, &descDSView, &g_pDepthStencilView); + if (FAILED(hr)) goto postReset; + } + + // Patch Renderer's DSV (AddRef because both we and the Renderer reference it) + g_pDepthStencilView->AddRef(); + *ppRM_DSV = g_pDepthStencilView; + + // Update Renderer's cached backbuffer dimensions (StartFrame uses these for viewport) + if (bbDimsValid) + { + *pRM_BBWidth = (DWORD)bbW; + *pRM_BBHeight = (DWORD)bbH; + } + + // Rebind render targets and viewport + g_pImmediateContext->OMSetRenderTargets(1, &g_pRenderTargetView, g_pDepthStencilView); + { + D3D11_VIEWPORT vp = {}; + vp.Width = (FLOAT)bbW; + vp.Height = (FLOAT)bbH; + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + g_pImmediateContext->RSSetViewports(1, &vp); + } + + ui.updateRenderTargets(g_pRenderTargetView, g_pDepthStencilView); + ui.updateScreenSize(bbW, bbH); + + // Track actual backbuffer dimensions for the rest of the engine + g_rScreenWidth = bbW; + g_rScreenHeight = bbH; + + success = true; + +postReset: + if (!success && g_pSwapChain != NULL) + { + // Failure recovery: recreate our views from whatever swap chain survived + // so ui.m_pRenderTargetView / m_pDepthStencilView don't dangle. + DXGI_SWAP_CHAIN_DESC recoveryDesc; + g_pSwapChain->GetDesc(&recoveryDesc); + int recW = (int)recoveryDesc.BufferDesc.Width; + int recH = (int)recoveryDesc.BufferDesc.Height; + + if (g_pRenderTargetView == NULL) + { + ID3D11Texture2D* pBB = NULL; + if (SUCCEEDED(g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBB))) + { + g_pd3dDevice->CreateRenderTargetView(pBB, NULL, &g_pRenderTargetView); + pBB->Release(); + } + } + if (g_pDepthStencilView == NULL) + { + D3D11_TEXTURE2D_DESC dd = {}; + dd.Width = recW; dd.Height = recH; dd.MipLevels = 1; dd.ArraySize = 1; + dd.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + dd.SampleDesc.Count = 1; dd.Usage = D3D11_USAGE_DEFAULT; + dd.BindFlags = D3D11_BIND_DEPTH_STENCIL; + if (g_pDepthStencilBuffer == NULL) + g_pd3dDevice->CreateTexture2D(&dd, NULL, &g_pDepthStencilBuffer); + if (g_pDepthStencilBuffer != NULL) + { + D3D11_DEPTH_STENCIL_VIEW_DESC dsvd = {}; + dsvd.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + dsvd.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D; + g_pd3dDevice->CreateDepthStencilView(g_pDepthStencilBuffer, &dsvd, &g_pDepthStencilView); + } + } + if (g_pRenderTargetView != NULL) + g_pImmediateContext->OMSetRenderTargets(1, &g_pRenderTargetView, g_pDepthStencilView); + + ui.updateRenderTargets(g_pRenderTargetView, g_pDepthStencilView); + + // If the surviving swap chain is the OLD one, dims are unchanged. + // If it's the NEW one (partial failure after swap), update to new dims. + if (g_pSwapChain != pOldSwapChain) + { + g_rScreenWidth = recW; + g_rScreenHeight = recH; + ui.updateScreenSize(recW, recH); + } + + app.DebugPrintf("[RESIZE] FAILED but recovered views at %dx%d\n", g_rScreenWidth, g_rScreenHeight); + } + + gdraw_D3D11_PostReset(); + gdraw_D3D11_SetRendertargetSize(g_rScreenWidth, g_rScreenHeight); + if (success) + IggyFlushInstalledFonts(); + RenderManager.Resume(); + + if (success) + PostProcesser::GetInstance().Init(); + + return success; +} + //-------------------------------------------------------------------------------------- // Toggle borderless fullscreen //-------------------------------------------------------------------------------------- @@ -963,6 +1281,8 @@ void CleanupDevice() { if( g_pImmediateContext ) g_pImmediateContext->ClearState(); + if( g_pDepthStencilView ) g_pDepthStencilView->Release(); + if( g_pDepthStencilBuffer ) g_pDepthStencilBuffer->Release(); if( g_pRenderTargetView ) g_pRenderTargetView->Release(); if( g_pSwapChain ) g_pSwapChain->Release(); if( g_pImmediateContext ) g_pImmediateContext->Release(); @@ -976,7 +1296,7 @@ static Minecraft* InitialiseMinecraftRuntime() RenderManager.Initialise(g_pd3dDevice, g_pSwapChain); app.loadStringTable(); - ui.init(g_pd3dDevice, g_pImmediateContext, g_pRenderTargetView, g_pDepthStencilView, g_iScreenWidth, g_iScreenHeight); + ui.init(g_pd3dDevice, g_pImmediateContext, g_pRenderTargetView, g_pDepthStencilView, g_rScreenWidth, g_rScreenHeight); InputManager.Initialise(1, 3, MINECRAFT_ACTION_MAX, ACTION_MAX_MENU); g_KBMInput.Init(); @@ -1203,8 +1523,12 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // Declare DPI awareness so GetSystemMetrics returns physical pixels SetProcessDPIAware(); - g_iScreenWidth = GetSystemMetrics(SM_CXSCREEN); - g_iScreenHeight = GetSystemMetrics(SM_CYSCREEN); + // Use the native monitor resolution for the window and swap chain, + // but keep g_iScreenWidth/Height at 1920x1080 for logical resolution + // (SWF selection, ortho projection, game logic). The real window + // dimensions are tracked by g_rScreenWidth/g_rScreenHeight. + g_rScreenWidth = GetSystemMetrics(SM_CXSCREEN); + g_rScreenHeight = GetSystemMetrics(SM_CYSCREEN); // Load username from username.txt char exePath[MAX_PATH] = {}; @@ -1257,6 +1581,72 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, MultiByteToWideChar(CP_ACP, 0, g_Win64Username, -1, g_Win64UsernameW, 17); + // convert servers.txt to servers.db + if (GetFileAttributesA("servers.txt") != INVALID_FILE_ATTRIBUTES && + GetFileAttributesA("servers.db") == INVALID_FILE_ATTRIBUTES) + { + FILE* txtFile = nullptr; + if (fopen_s(&txtFile, "servers.txt", "r") == 0 && txtFile) + { + struct MigEntry { std::string ip; uint16_t port; std::string name; }; + std::vector migEntries; + char line[512]; + + while (fgets(line, sizeof(line), txtFile)) + { + int l = (int)strlen(line); + while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r' || line[l - 1] == ' ')) + line[--l] = '\0'; + if (l == 0) continue; + + std::string srvIP = line; + + if (!fgets(line, sizeof(line), txtFile)) break; + l = (int)strlen(line); + while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r' || line[l - 1] == ' ')) + line[--l] = '\0'; + uint16_t srvPort = (uint16_t)atoi(line); + + std::string srvName; + if (fgets(line, sizeof(line), txtFile)) + { + l = (int)strlen(line); + while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r' || line[l - 1] == ' ')) + line[--l] = '\0'; + srvName = line; + } + + if (!srvIP.empty() && srvPort > 0) + migEntries.push_back({srvIP, srvPort, srvName}); + } + fclose(txtFile); + + if (!migEntries.empty()) + { + FILE* dbFile = nullptr; + if (fopen_s(&dbFile, "servers.db", "wb") == 0 && dbFile) + { + fwrite("MCSV", 1, 4, dbFile); + uint32_t ver = 1; + uint32_t cnt = (uint32_t)migEntries.size(); + fwrite(&ver, sizeof(uint32_t), 1, dbFile); + fwrite(&cnt, sizeof(uint32_t), 1, dbFile); + for (size_t i = 0; i < migEntries.size(); i++) + { + uint16_t ipLen = (uint16_t)migEntries[i].ip.length(); + fwrite(&ipLen, sizeof(uint16_t), 1, dbFile); + fwrite(migEntries[i].ip.c_str(), 1, ipLen, dbFile); + fwrite(&migEntries[i].port, sizeof(uint16_t), 1, dbFile); + uint16_t nameLen = (uint16_t)migEntries[i].name.length(); + fwrite(&nameLen, sizeof(uint16_t), 1, dbFile); + fwrite(migEntries[i].name.c_str(), 1, nameLen, dbFile); + } + fclose(dbFile); + } + } + } + } + // Initialize global strings MyRegisterClass(hInstance); @@ -1345,6 +1735,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, CleanupDevice(); return 1; } + g_bResizeReady = true; //app.TemporaryCreateGameStart(); @@ -1384,6 +1775,14 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } if (msg.message == WM_QUIT) break; + // When the window is minimized (e.g. "Show Desktop"), skip rendering entirely + // to avoid pegging the GPU at 100% presenting to a non-visible swap chain. + if (IsIconic(g_hWnd)) + { + Sleep(100); + continue; + } + RenderManager.StartFrame(); #if 0 if(pMinecraft->soundEngine->isStreamingWavebankReady() && diff --git a/Minecraft.Client/Windows64/Windows64_UIController.cpp b/Minecraft.Client/Windows64/Windows64_UIController.cpp index e251793ba..3929aa049 100644 --- a/Minecraft.Client/Windows64/Windows64_UIController.cpp +++ b/Minecraft.Client/Windows64/Windows64_UIController.cpp @@ -143,6 +143,12 @@ void ConsoleUIController::endCustomDraw(IggyCustomDrawCallbackRegion *region) PIXEndNamedEvent(); } +void ConsoleUIController::updateRenderTargets(ID3D11RenderTargetView* rtv, ID3D11DepthStencilView* dsv) +{ + m_pRenderTargetView = rtv; + m_pDepthStencilView = dsv; +} + void ConsoleUIController::setTileOrigin(S32 xPos, S32 yPos) { gdraw_D3D11_SetTileOrigin( m_pRenderTargetView, diff --git a/Minecraft.Client/Windows64/Windows64_UIController.h b/Minecraft.Client/Windows64/Windows64_UIController.h index 066d279fb..398e97126 100644 --- a/Minecraft.Client/Windows64/Windows64_UIController.h +++ b/Minecraft.Client/Windows64/Windows64_UIController.h @@ -16,10 +16,12 @@ public: virtual CustomDrawData *calculateCustomDraw(IggyCustomDrawCallbackRegion *region); virtual void endCustomDraw(IggyCustomDrawCallbackRegion *region); + void updateRenderTargets(ID3D11RenderTargetView* rtv, ID3D11DepthStencilView* dsv); + protected: virtual void setTileOrigin(S32 xPos, S32 yPos); -public: +public: GDrawTexture *getSubstitutionTexture(int textureId); void destroySubstitutionTexture(void *destroyCallBackData, GDrawTexture *handle); diff --git a/Minecraft.Client/Windows64/Windows64_Xuid.h b/Minecraft.Client/Windows64/Windows64_Xuid.h index aa88f296f..f5fd62b90 100644 --- a/Minecraft.Client/Windows64/Windows64_Xuid.h +++ b/Minecraft.Client/Windows64/Windows64_Xuid.h @@ -186,6 +186,29 @@ namespace Win64Xuid return xuid; } + inline PlayerUID DeriveXuidForPad(PlayerUID baseXuid, int iPad) + { + if (iPad == 0) + return baseXuid; + + // Deterministic per-pad XUID: hash the base XUID with the pad number. + // Produces a fully unique 64-bit value with no risk of overlap. + // Suggested by rtm516 to avoid adjacent-integer collisions from the old "+ iPad" approach. + uint64_t raw = Mix64((uint64_t)baseXuid + (uint64_t)iPad); + raw |= 0x8000000000000000ULL; // keep high bit set like all our XUIDs + + PlayerUID xuid = (PlayerUID)raw; + if (!IsPersistedUidValid(xuid)) + { + raw ^= 0x0100000000000001ULL; + xuid = (PlayerUID)raw; + } + if (!IsPersistedUidValid(xuid)) + xuid = (PlayerUID)(0xD15EA5E000000001ULL + iPad); + + return xuid; + } + inline PlayerUID ResolvePersistentXuid() { // Process-local cache: uid.dat is immutable during runtime and this path is hot. diff --git a/Minecraft.Client/glWrapper.cpp b/Minecraft.Client/glWrapper.cpp index 9c3c8559d..d13e1a691 100644 --- a/Minecraft.Client/glWrapper.cpp +++ b/Minecraft.Client/glWrapper.cpp @@ -48,11 +48,12 @@ void glLoadIdentity() RenderManager.MatrixSetIdentity(); } -// AAR - Use calculated aspect ratio to support dynamic resizing -extern float g_iAspectRatio; +// AAR - Use the aspect ratio passed by the caller. For single-player this +// equals g_iAspectRatio (screen width / height), but for split-screen +// getFovAndAspect adjusts it to match the viewport dimensions. void gluPerspective(float fovy, float aspect, float zNear, float zFar) { - RenderManager.MatrixPerspective(fovy, g_iAspectRatio, zNear, zFar); + RenderManager.MatrixPerspective(fovy, aspect, zNear, zFar); } void glOrtho(float left,float right,float bottom,float top,float zNear,float zFar) diff --git a/Minecraft.World/Arrow.cpp b/Minecraft.World/Arrow.cpp index c433715a5..ae30c057c 100644 --- a/Minecraft.World/Arrow.cpp +++ b/Minecraft.World/Arrow.cpp @@ -303,16 +303,20 @@ void Arrow::tick() damageSource = DamageSource::arrow(dynamic_pointer_cast(shared_from_this()), owner); } + if(!res->entity->isInvulnerable()) + { + if (isOnFire() && res->entity->GetType() != eTYPE_ENDERMAN) + { + res->entity->setOnFire(5); + } + } + if(res->entity->hurt(damageSource, dmg)) { // Firx for #67839 - Customer Encountered: Bows enchanted with "Flame" still set things on fire if pvp/attack animals is turned off // 4J Stu - We should not set the entity on fire unless we can cause some damage (this doesn't necessarily mean that the arrow hit lowered their health) // set targets on fire first because we want cooked // pork/chicken/steak - if (isOnFire() && res->entity->GetType() != eTYPE_ENDERMAN) - { - res->entity->setOnFire(5); - } if (res->entity->instanceof(eTYPE_LIVINGENTITY)) { diff --git a/Minecraft.World/BlockRegionUpdatePacket.cpp b/Minecraft.World/BlockRegionUpdatePacket.cpp index bec943d8d..730ce3edc 100644 --- a/Minecraft.World/BlockRegionUpdatePacket.cpp +++ b/Minecraft.World/BlockRegionUpdatePacket.cpp @@ -48,7 +48,7 @@ BlockRegionUpdatePacket::BlockRegionUpdatePacket(int x, int y, int z, int xs, in // TODO - we should be using compressed data directly here rather than decompressing first and then recompressing... byteArray rawBuffer; - if( xs == 16 && ys == Level::maxBuildHeight && zs == 16 && ( ( x & 15 ) == 0 ) && ( y == 0 ) && ( ( z & 15 ) == 0 ) ) + if( xs == 16 && ys == Level::maxBuildHeight && zs == 16 && ( ( x & 15 ) == 0 ) && ( y == 0 ) && ( ( z & 15 ) == 0 ) ) { bIsFullChunk = true; @@ -66,6 +66,8 @@ BlockRegionUpdatePacket::BlockRegionUpdatePacket(int x, int y, int z, int xs, in { size = 0; buffer = byteArray(); + app.DebugPrintf("[BRUP-SERVER] *** EMPTY BUFFER for chunk(%d,%d) ys=%d bIsFullChunk=%d\n", + x>>4, z>>4, this->ys, bIsFullChunk ? 1 : 0); } else { @@ -96,7 +98,7 @@ void BlockRegionUpdatePacket::read(DataInputStream *dis) //throws IOException zs = dis->read() + 1; bIsFullChunk = (chunkFlags & BLOCK_REGION_UPDATE_FULLCHUNK) ? true : false; - if(chunkFlags & BLOCK_REGION_UPDATE_ZEROHEIGHT) + if(chunkFlags & BLOCK_REGION_UPDATE_ZEROHEIGHT) ys = 0; size = dis->readInt(); @@ -106,6 +108,10 @@ void BlockRegionUpdatePacket::read(DataInputStream *dis) //throws IOException if(size == 0) { buffer = byteArray(); + if(bIsFullChunk) + { + app.DebugPrintf("[BRUP-READ] *** ZERO-SIZE full chunk packet at (%d,%d)!\n", x>>4, z>>4); + } } else { @@ -131,6 +137,11 @@ void BlockRegionUpdatePacket::read(DataInputStream *dis) //throws IOException delete [] compressedBuffer.data; + if(buffer.length != outputSize) + { + app.DebugPrintf("*** BlockRegionUpdatePacket DECOMPRESS MISMATCH: expected=%d got=%d xs=%d ys=%d zs=%d fullChunk=%d compressedSize=%d levelIdx=%d\n", + buffer.length, outputSize, xs, ys, zs, bIsFullChunk ? 1 : 0, size, levelIdx); + } assert(buffer.length == outputSize); } } diff --git a/Minecraft.World/CompressedTileStorage.cpp b/Minecraft.World/CompressedTileStorage.cpp index 8e4fd0904..1c387eeb3 100644 --- a/Minecraft.World/CompressedTileStorage.cpp +++ b/Minecraft.World/CompressedTileStorage.cpp @@ -146,7 +146,9 @@ CompressedTileStorage::CompressedTileStorage(bool isEmpty) bool CompressedTileStorage::isRenderChunkEmpty(int y) // y == 0, 16, 32... 112 (representing a 16 byte range) { int block; - unsigned short *blockIndices = (unsigned short *)indicesAndData; + unsigned char *localIndicesAndData = indicesAndData; + if(!localIndicesAndData) return true; + unsigned short *blockIndices = (unsigned short *)localIndicesAndData; for( int x = 0; x < 16; x += 4 ) for( int z = 0; z < 16; z += 4 ) @@ -533,8 +535,12 @@ void CompressedTileStorage::getData(byteArray retArray, unsigned int retOffset) // Gets all tile values into an array of length 32768. void CompressedTileStorage::getData(byteArray retArray, unsigned int retOffset) { - unsigned short *blockIndices = (unsigned short *)indicesAndData; - unsigned char *data = indicesAndData + 1024; + // Snapshot pointer to avoid race with compress() swapping indicesAndData + unsigned char *localIndicesAndData = indicesAndData; + if(!localIndicesAndData) return; + + unsigned short *blockIndices = (unsigned short *)localIndicesAndData; + unsigned char *data = localIndicesAndData + 1024; for( int i = 0; i < 512; i++ ) { @@ -588,10 +594,13 @@ void CompressedTileStorage::getData(byteArray retArray, unsigned int retOffset) // Get an individual tile value int CompressedTileStorage::get(int x, int y, int z) { - if(!indicesAndData) return 0; + // Take a local snapshot of indicesAndData to avoid a race with compress() which swaps the pointer. + // Both blockIndices and data must reference the same buffer, otherwise index offsets won't match. + unsigned char *localIndicesAndData = indicesAndData; + if(!localIndicesAndData) return 0; - unsigned short *blockIndices = (unsigned short *)indicesAndData; - unsigned char *data = indicesAndData + 1024; + unsigned short *blockIndices = (unsigned short *)localIndicesAndData; + unsigned char *data = localIndicesAndData + 1024; int block, tile; getBlockAndTile( &block, &tile, x, y, z ); @@ -1223,7 +1232,10 @@ int CompressedTileStorage::getAllocatedSize(int *count0, int *count1, int *count *count4 = 0; *count8 = 0; - unsigned short *blockIndices = (unsigned short *)indicesAndData; + // Volatile read: compress() can swap indicesAndData concurrently outside cs_write + unsigned char *localIndicesAndData = *(unsigned char *volatile *)&indicesAndData; + if(!localIndicesAndData) return 0; + unsigned short *blockIndices = (unsigned short *)localIndicesAndData; for(int i = 0; i < 512; i++ ) { unsigned short idxType = blockIndices[i] & INDEX_TYPE_MASK; @@ -1256,7 +1268,9 @@ int CompressedTileStorage::getAllocatedSize(int *count0, int *count1, int *count int CompressedTileStorage::getHighestNonEmptyY() { - unsigned short *blockIndices = (unsigned short *)indicesAndData; + unsigned char *localIndicesAndData = indicesAndData; + if(!localIndicesAndData) return -1; + unsigned short *blockIndices = (unsigned short *)localIndicesAndData; unsigned int highestYBlock = 0; bool found = false; @@ -1297,19 +1311,26 @@ int CompressedTileStorage::getHighestNonEmptyY() // Multiply by the number of vertical tiles in a block, and then add that again to be at the top of the block highestNonEmptyY = (highestYBlock * 4) + 4; } + else + { + app.DebugPrintf("[CTS-WARN] getHighestNonEmptyY() returned -1! allocatedSize=%d indicesAndData=%p\n", + allocatedSize, indicesAndData); + } return highestNonEmptyY; } void CompressedTileStorage::write(DataOutputStream *dos) { dos->writeInt(allocatedSize); - if(indicesAndData) + // Volatile read: compress() can swap indicesAndData concurrently outside cs_write + unsigned char *localIndicesAndData = *(unsigned char *volatile *)&indicesAndData; + if(localIndicesAndData) { if(LOCALSYTEM_ENDIAN == BIGENDIAN) { // The first 1024 bytes are an array of shorts, so we need to reverse the endianness byteArray indicesCopy(1024); - memcpy(indicesCopy.data, indicesAndData, 1024); + memcpy(indicesCopy.data, localIndicesAndData, 1024); reverseIndices(indicesCopy.data); dos->write(indicesCopy); delete [] indicesCopy.data; @@ -1317,13 +1338,13 @@ void CompressedTileStorage::write(DataOutputStream *dos) // Write the rest of the data if(allocatedSize > 1024) { - byteArray dataWrapper(indicesAndData + 1024, allocatedSize - 1024); + byteArray dataWrapper(localIndicesAndData + 1024, allocatedSize - 1024); dos->write(dataWrapper); } } else { - byteArray wrapper(indicesAndData, allocatedSize); + byteArray wrapper(localIndicesAndData, allocatedSize); dos->write(wrapper); } } diff --git a/Minecraft.World/Connection.cpp b/Minecraft.World/Connection.cpp index 4015d8f68..7f10dc4a7 100644 --- a/Minecraft.World/Connection.cpp +++ b/Minecraft.World/Connection.cpp @@ -250,6 +250,14 @@ bool Connection::writeTick() // Otherwise just buffer the packet with other outgoing packets as the java game did if(packet->shouldDelay) { + // Flush any buffered data BEFORE writing directly to the socket. + // bufferedDos and sos->writeWithFlags both write to the same underlying + // socket stream. If bufferedDos has unflushed bytes (from packets written + // via the outgoing queue above), writing directly to sos here would send + // the delayed packet's bytes BEFORE the buffered bytes, desynchronizing + // the TCP stream on the receiving end. + bufferedDos->flush(); + Packet::writePacket(packet, byteArrayDos); // 4J Stu - Changed this so that rather than writing to the network stream through a buffered stream we want to: diff --git a/Minecraft.World/Level.cpp b/Minecraft.World/Level.cpp index 94e2ecfba..161f4e560 100644 --- a/Minecraft.World/Level.cpp +++ b/Minecraft.World/Level.cpp @@ -4239,6 +4239,10 @@ void Level::setBlocksAndData(int x, int y, int z, int xs, int ys, int zs, byteAr if (z0 < 0) z0 = 0; if (z1 > 16) z1 = 16; LevelChunk *lc = getChunk(xc, zc); + if(lc->isEmpty()) + { + app.DebugPrintf("[SETBLOCKS-BUG] getChunk(%d,%d) returned EmptyLevelChunk! Data will be LOST\n", xc, zc); + } // 4J Stu - Unshare before we make any changes incase the server is already another step ahead of us // Fix for #7904 - Gameplay: Players can dupe torches by throwing them repeatedly into water. // This is quite expensive so only actually do it if we are hosting, online, and the update will actually diff --git a/Minecraft.World/MinecartTNT.cpp b/Minecraft.World/MinecartTNT.cpp index 57462e03d..6e72d66b7 100644 --- a/Minecraft.World/MinecartTNT.cpp +++ b/Minecraft.World/MinecartTNT.cpp @@ -80,9 +80,12 @@ void MinecartTNT::explode(double speedSqr) if (!level->isClientSide) { double speed = sqrt(speedSqr); - if (speed > 5) speed = 5; - level->explode(shared_from_this(), x, y, z, static_cast(4 + random->nextDouble() * 1.5f * speed), true); - remove(); + if (speed > 5.0) speed = 5.0; + if (app.GetGameHostOption(eGameHostOption_TNT)) + { + level->explode(shared_from_this(), x, y, z, static_cast(4 + random->nextDouble() * 1.5f * speed), true); + remove(); + } } } diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 4125f1032..129024b77 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -327,9 +327,30 @@ shared_ptr Packet::readPacket(DataInputStream *dis, bool isServer) // th id = dis->read(); if (id == -1) return nullptr; + // Track last few good packets for diagnosing TCP desync + static thread_local int s_lastIds[8] = {}; + static thread_local int s_lastIdPos = 0; + static thread_local int s_packetCount = 0; + if ((isServer && serverReceivedPackets.find(id) == serverReceivedPackets.end()) || (!isServer && clientReceivedPackets.find(id) == clientReceivedPackets.end())) { - //app.DebugPrintf("Bad packet id %d\n", id); + app.DebugPrintf("*** BAD PACKET ID %d (0x%02X) isServer=%d totalPacketsRead=%d\n", id, id, isServer ? 1 : 0, s_packetCount); + app.DebugPrintf("*** Last %d good packet IDs (oldest first): ", 8); + for (int dbg = 0; dbg < 8; dbg++) + { + int idx = (s_lastIdPos + dbg) % 8; + app.DebugPrintf("%d ", s_lastIds[idx]); + } + app.DebugPrintf("\n"); + // Dump the next 32 bytes from the stream to see what follows + app.DebugPrintf("*** Next bytes in stream: "); + for (int dbg = 0; dbg < 32; dbg++) + { + int b = dis->read(); + if (b == -1) { app.DebugPrintf("[EOS] "); break; } + app.DebugPrintf("%02X ", b); + } + app.DebugPrintf("\n"); __debugbreak(); assert(false); // throw new IOException(wstring(L"Bad packet id ") + std::to_wstring(id)); @@ -338,7 +359,10 @@ shared_ptr Packet::readPacket(DataInputStream *dis, bool isServer) // th packet = getPacket(id); if (packet == nullptr) assert(false);//throw new IOException(wstring(L"Bad packet id ") + std::to_wstring(id)); - //app.DebugPrintf("%s reading packet %d\n", isServer ? "Server" : "Client", packet->getId()); + s_lastIds[s_lastIdPos] = id; + s_lastIdPos = (s_lastIdPos + 1) % 8; + s_packetCount++; + packet->read(dis); // } // catch (EOFException e) @@ -372,7 +396,7 @@ shared_ptr Packet::readPacket(DataInputStream *dis, bool isServer) // th void Packet::writePacket(shared_ptr packet, DataOutputStream *dos) // throws IOException TODO 4J JEV, should this declare a throws? { - //app.DebugPrintf("Writing packet %d\n", packet->getId()); + //app.DebugPrintf("NET WRITE: packet id=%d (0x%02X) estSize=%d\n", packet->getId(), packet->getId(), packet->getEstimatedSize()); dos->write(packet->getId()); packet->write(dos); } diff --git a/Minecraft.World/Socket.cpp b/Minecraft.World/Socket.cpp index 290b39dca..2d5b257d0 100644 --- a/Minecraft.World/Socket.cpp +++ b/Minecraft.World/Socket.cpp @@ -137,6 +137,9 @@ void Socket::pushDataToQueue(const BYTE * pbData, DWORD dwDataSize, bool fromHos return; } + //app.DebugPrintf("SOCKET PUSH: %u bytes to queue[%d] firstByte=0x%02X smallId=%d\n", + // dwDataSize, queueIdx, dwDataSize > 0 ? pbData[0] : 0, networkPlayerSmallId); + EnterCriticalSection(&m_queueLockNetwork[queueIdx]); for( unsigned int i = 0; i < dwDataSize; i++ ) { @@ -311,7 +314,8 @@ void Socket::SocketInputStreamLocal::close() { m_streamOpen = false; EnterCriticalSection(&s_hostQueueLock[m_queueIdx]); - s_hostQueue[m_queueIdx].empty(); + std::queue empty; + std::swap(s_hostQueue[m_queueIdx], empty); LeaveCriticalSection(&s_hostQueueLock[m_queueIdx]); } @@ -359,7 +363,8 @@ void Socket::SocketOutputStreamLocal::close() { m_streamOpen = false; EnterCriticalSection(&s_hostQueueLock[m_queueIdx]); - s_hostQueue[m_queueIdx].empty(); + std::queue empty; + std::swap(s_hostQueue[m_queueIdx], empty); LeaveCriticalSection(&s_hostQueueLock[m_queueIdx]); } diff --git a/Minecraft.World/compression.cpp b/Minecraft.World/compression.cpp index 2a8f8e0b8..8c2e51c08 100644 --- a/Minecraft.World/compression.cpp +++ b/Minecraft.World/compression.cpp @@ -42,7 +42,7 @@ void Compression::UseDefaultThreadStorage() void Compression::ReleaseThreadStorage() { - ThreadStorage *tls = static_cast(TlsGetValue(tlsIdx)); + const ThreadStorage *tls = static_cast(TlsGetValue(tlsIdx)); if( tls == tlsDefault ) return; delete tls; @@ -50,7 +50,7 @@ void Compression::ReleaseThreadStorage() Compression *Compression::getCompression() { - ThreadStorage *tls = static_cast(TlsGetValue(tlsIdx)); + const ThreadStorage *tls = static_cast(TlsGetValue(tlsIdx)); return tls->compression; } @@ -59,9 +59,27 @@ HRESULT Compression::CompressLZXRLE(void *pDestination, unsigned int *pDestSize, EnterCriticalSection(&rleCompressLock); //static unsigned char rleBuf[1024*100]; - unsigned char *pucIn = static_cast(pSource); - unsigned char *pucEnd = pucIn + SrcSize; - unsigned char *pucOut = (unsigned char *)rleCompressBuf; + // RLE can expand data (each 0xFF byte becomes 2 bytes), so worst case is 2*SrcSize. + // Use the static buffer when it fits, dynamically allocate otherwise. + static const unsigned int staticCompressSize = 1024*100; + unsigned int rleBufSize = SrcSize * 2; + unsigned char *dynamicRleBuf = nullptr; + unsigned char *rleBuf; + + if(rleBufSize <= staticCompressSize) + { + rleBuf = rleCompressBuf; + rleBufSize = staticCompressSize; + } + else + { + dynamicRleBuf = new unsigned char[rleBufSize]; + rleBuf = dynamicRleBuf; + } + + const unsigned char *pucIn = static_cast(pSource); + const unsigned char *pucEnd = pucIn + SrcSize; + unsigned char *pucOut = rleBuf; // Compress with RLE first: // 0 - 254 - encodes a single byte @@ -70,7 +88,7 @@ HRESULT Compression::CompressLZXRLE(void *pDestination, unsigned int *pDestSize, PIXBeginNamedEvent(0,"RLE compression"); do { - unsigned char thisOne = *pucIn++; + const unsigned char thisOne = *pucIn++; unsigned int count = 1; while( ( pucIn != pucEnd ) && ( *pucIn == thisOne ) && ( count < 256 ) ) @@ -101,12 +119,15 @@ HRESULT Compression::CompressLZXRLE(void *pDestination, unsigned int *pDestSize, *pucOut++ = thisOne; } } while (pucIn != pucEnd); - unsigned int rleSize = static_cast(pucOut - rleCompressBuf); + const unsigned int rleSize = static_cast(pucOut - rleBuf); PIXEndNamedEvent(); PIXBeginNamedEvent(0,"Secondary compression"); - Compress(pDestination, pDestSize, rleCompressBuf, rleSize); + Compress(pDestination, pDestSize, rleBuf, rleSize); PIXEndNamedEvent(); + + if(dynamicRleBuf != nullptr) delete [] dynamicRleBuf; + LeaveCriticalSection(&rleCompressLock); // printf("Compressed from %d to %d to %d\n",SrcSize,rleSize,*pDestSize); @@ -118,9 +139,26 @@ HRESULT Compression::CompressRLE(void *pDestination, unsigned int *pDestSize, vo EnterCriticalSection(&rleCompressLock); //static unsigned char rleBuf[1024*100]; - unsigned char *pucIn = static_cast(pSource); - unsigned char *pucEnd = pucIn + SrcSize; - unsigned char *pucOut = (unsigned char *)rleCompressBuf; + // RLE can expand data (each 0xFF byte becomes 2 bytes), so worst case is 2*SrcSize. + static const unsigned int staticCompressSize = 1024*100; + unsigned int rleBufSize = SrcSize * 2; + unsigned char *dynamicRleBuf = nullptr; + unsigned char *rleBuf; + + if(rleBufSize <= staticCompressSize) + { + rleBuf = rleCompressBuf; + rleBufSize = staticCompressSize; + } + else + { + dynamicRleBuf = new unsigned char[rleBufSize]; + rleBuf = dynamicRleBuf; + } + + const unsigned char *pucIn = static_cast(pSource); + const unsigned char *pucEnd = pucIn + SrcSize; + unsigned char *pucOut = rleBuf; // Compress with RLE first: // 0 - 254 - encodes a single byte @@ -129,7 +167,7 @@ HRESULT Compression::CompressRLE(void *pDestination, unsigned int *pDestSize, vo PIXBeginNamedEvent(0,"RLE compression"); do { - unsigned char thisOne = *pucIn++; + const unsigned char thisOne = *pucIn++; unsigned int count = 1; while( ( pucIn != pucEnd ) && ( *pucIn == thisOne ) && ( count < 256 ) ) @@ -160,15 +198,14 @@ HRESULT Compression::CompressRLE(void *pDestination, unsigned int *pDestSize, vo *pucOut++ = thisOne; } } while (pucIn != pucEnd); - unsigned int rleSize = static_cast(pucOut - rleCompressBuf); + const unsigned int rleSize = static_cast(pucOut - rleBuf); PIXEndNamedEvent(); - LeaveCriticalSection(&rleCompressLock); // Return if (rleSize <= *pDestSize) { *pDestSize = rleSize; - memcpy(pDestination, rleCompressBuf, *pDestSize); + memcpy(pDestination, rleBuf, *pDestSize); } else { @@ -177,6 +214,9 @@ HRESULT Compression::CompressRLE(void *pDestination, unsigned int *pDestSize, vo #endif } + if(dynamicRleBuf != nullptr) delete [] dynamicRleBuf; + LeaveCriticalSection(&rleCompressLock); + return S_OK; } @@ -195,33 +235,47 @@ HRESULT Compression::DecompressLZXRLE(void *pDestination, unsigned int *pDestSiz //static unsigned char rleBuf[staticRleSize]; unsigned int rleSize = staticRleSize; unsigned char *dynamicRleBuf = nullptr; + HRESULT decompressResult; if(*pDestSize > rleSize) { rleSize = *pDestSize; dynamicRleBuf = new unsigned char[rleSize]; - Decompress(dynamicRleBuf, &rleSize, pSource, SrcSize); + decompressResult = Decompress(dynamicRleBuf, &rleSize, pSource, SrcSize); pucIn = (unsigned char *)dynamicRleBuf; } else { - Decompress(rleDecompressBuf, &rleSize, pSource, SrcSize); - pucIn = static_cast(rleDecompressBuf); + decompressResult = Decompress(rleDecompressBuf, &rleSize, pSource, SrcSize); + pucIn = static_cast(rleDecompressBuf); + } + + if(decompressResult != S_OK) + { + app.DebugPrintf("*** DecompressLZXRLE: zlib Decompress FAILED hr=0x%08X srcSize=%u expectedDest=%u rleSize=%u\n", + decompressResult, SrcSize, *pDestSize, rleSize); + if(dynamicRleBuf != nullptr) delete [] dynamicRleBuf; + *pDestSize = 0; + LeaveCriticalSection(&rleDecompressLock); + return decompressResult; } //unsigned char *pucIn = (unsigned char *)rleDecompressBuf; - unsigned char *pucEnd = pucIn + rleSize; - unsigned char *pucOut = static_cast(pDestination); + const unsigned char *pucEnd = pucIn + rleSize; + unsigned char *pucOut = static_cast(pDestination); + const unsigned char *pucOutEnd = pucOut + *pDestSize; while( pucIn != pucEnd ) { - unsigned char thisOne = *pucIn++; + const unsigned char thisOne = *pucIn++; if( thisOne == 255 ) { + if( pucIn >= pucEnd ) break; unsigned int count = *pucIn++; if( count < 3 ) { count++; + if( pucOut + count > pucOutEnd ) break; for( unsigned int i = 0; i < count; i++ ) { *pucOut++ = 255; @@ -230,7 +284,9 @@ HRESULT Compression::DecompressLZXRLE(void *pDestination, unsigned int *pDestSiz else { count++; - unsigned char data = *pucIn++; + if( pucIn >= pucEnd ) break; + const unsigned char data = *pucIn++; + if( pucOut + count > pucOutEnd ) break; for( unsigned int i = 0; i < count; i++ ) { *pucOut++ = data; @@ -239,10 +295,11 @@ HRESULT Compression::DecompressLZXRLE(void *pDestination, unsigned int *pDestSiz } else { + if( pucOut >= pucOutEnd ) break; *pucOut++ = thisOne; } } - *pDestSize = static_cast(pucOut - (unsigned char *)pDestination); + *pDestSize = static_cast(pucOut - static_cast(pDestination)); // printf("Decompressed from %d to %d to %d\n",SrcSize,rleSize,*pDestSize); @@ -258,18 +315,21 @@ HRESULT Compression::DecompressRLE(void *pDestination, unsigned int *pDestSize, //unsigned char *pucIn = (unsigned char *)rleDecompressBuf; unsigned char *pucIn = static_cast(pSource); - unsigned char *pucEnd = pucIn + SrcSize; - unsigned char *pucOut = static_cast(pDestination); + const unsigned char *pucEnd = pucIn + SrcSize; + unsigned char *pucOut = static_cast(pDestination); + const unsigned char *pucOutEnd = pucOut + *pDestSize; while( pucIn != pucEnd ) { - unsigned char thisOne = *pucIn++; + const unsigned char thisOne = *pucIn++; if( thisOne == 255 ) { + if( pucIn >= pucEnd ) break; unsigned int count = *pucIn++; if( count < 3 ) { count++; + if( pucOut + count > pucOutEnd ) break; for( unsigned int i = 0; i < count; i++ ) { *pucOut++ = 255; @@ -278,7 +338,9 @@ HRESULT Compression::DecompressRLE(void *pDestination, unsigned int *pDestSize, else { count++; - unsigned char data = *pucIn++; + if( pucIn >= pucEnd ) break; + const unsigned char data = *pucIn++; + if( pucOut + count > pucOutEnd ) break; for( unsigned int i = 0; i < count; i++ ) { *pucOut++ = data; @@ -287,10 +349,11 @@ HRESULT Compression::DecompressRLE(void *pDestination, unsigned int *pDestSize, } else { + if( pucOut >= pucOutEnd ) break; *pucOut++ = thisOne; } } - *pDestSize = static_cast(pucOut - (unsigned char *)pDestination); + *pDestSize = static_cast(pucOut - static_cast(pDestination)); LeaveCriticalSection(&rleDecompressLock); return S_OK; @@ -302,7 +365,7 @@ HRESULT Compression::Compress(void *pDestination, unsigned int *pDestSize, void // Using zlib for x64 compression - 360 is using native 360 compression and PS3 a stubbed non-compressing version of this #if defined __ORBIS__ || defined _DURANGO || defined _WIN64 || defined __PSVITA__ SIZE_T destSize = (SIZE_T)(*pDestSize); - int res = ::compress(static_cast(pDestination), (uLongf *)&destSize, static_cast(pSource), SrcSize); + const int res = ::compress(static_cast(pDestination), (uLongf *)&destSize, static_cast(pSource), SrcSize); *pDestSize = static_cast(destSize); return ( ( res == Z_OK ) ? S_OK : -1 ); #elif defined __PS3__ @@ -330,7 +393,7 @@ HRESULT Compression::Decompress(void *pDestination, unsigned int *pDestSize, voi // Using zlib for x64 compression - 360 is using native 360 compression and PS3 a stubbed non-compressing version of this #if defined __ORBIS__ || defined _DURANGO || defined _WIN64 || defined __PSVITA__ SIZE_T destSize = (SIZE_T)(*pDestSize); - int res = ::uncompress(static_cast(pDestination), (uLongf *)&destSize, static_cast(pSource), SrcSize); + const int res = ::uncompress(static_cast(pDestination), (uLongf *)&destSize, static_cast(pSource), SrcSize); *pDestSize = static_cast(destSize); return ( ( res == Z_OK ) ? S_OK : -1 ); #elif defined __PS3__ @@ -350,7 +413,7 @@ HRESULT Compression::Decompress(void *pDestination, unsigned int *pDestSize, voi #ifndef _XBOX VOID Compression::VitaVirtualDecompress(void *pDestination, unsigned int *pDestSize, void *pSource, unsigned int SrcSize) // (LPVOID buf, SIZE_T dwSize, LPVOID dst) { - uint8_t *pSrc = static_cast(pSource); + const uint8_t *pSrc = static_cast(pSource); int Offset = 0; int Page = 0; int Index = 0; @@ -368,7 +431,7 @@ VOID Compression::VitaVirtualDecompress(void *pDestination, unsigned int *pDestS { // how many zeros do we have Index += 1; - int Count = pSrc[Index]; + const int Count = pSrc[Index]; // to do : this should really be a sequence of memsets for( int i = 0;i < Count;i += 1 ) { @@ -396,7 +459,7 @@ HRESULT Compression::DecompressWithType(void *pDestination, unsigned int *pDestS { #if (defined _XBOX || defined _DURANGO || defined _WIN64) SIZE_T destSize = (SIZE_T)(*pDestSize); - HRESULT res = XMemDecompress(decompressionContext, pDestination, (SIZE_T *)&destSize, pSource, SrcSize); + const HRESULT res = XMemDecompress(decompressionContext, pDestination, (SIZE_T *)&destSize, pSource, SrcSize); *pDestSize = static_cast(destSize); return res; #else @@ -420,13 +483,13 @@ HRESULT Compression::DecompressWithType(void *pDestination, unsigned int *pDestS if (pDestination != nullptr) { // Read big-endian srcize from array - PBYTE pbDestSize = (PBYTE) pDestSize; - PBYTE pbSource = static_cast(pSource); + const PBYTE pbDestSize = (PBYTE) pDestSize; + const PBYTE pbSource = static_cast(pSource); for (int i = 3; i >= 0; i--) { pbDestSize[3-i] = pbSource[i]; } - byteArray uncompr = byteArray(*pDestSize); + const byteArray uncompr = byteArray(*pDestSize); // Build decompression stream z_stream strm;