From 341963b9c020d12c19727d2039ad49a836c9dadf Mon Sep 17 00:00:00 2001 From: YkPhysics Date: Tue, 3 Mar 2026 15:18:04 -0500 Subject: [PATCH] a lot --- Minecraft.Client/BufferedImage.cpp | 55 +- Minecraft.Client/ClientConnection.cpp | 141 +- Minecraft.Client/Common/Audio/SoundEngine.cpp | 3 - Minecraft.Client/Common/Consoles_App.cpp | 186 +- .../Common/Network/GameNetworkManager.cpp | 331 ++-- .../Network/PlatformNetworkManagerStub.cpp | 323 +++- .../Network/PlatformNetworkManagerStub.h | 27 +- .../UI/IUIScene_AbstractContainerMenu.cpp | 51 +- .../UI/IUIScene_AbstractContainerMenu.h | 12 - .../Common/UI/IUIScene_PauseMenu.cpp | 13 +- Minecraft.Client/Common/UI/UIBitmapFont.cpp | 55 +- Minecraft.Client/Common/UI/UIController.cpp | 97 +- Minecraft.Client/Common/UI/UIFontData.cpp | 60 +- Minecraft.Client/Common/UI/UIGroup.cpp | 6 +- Minecraft.Client/Common/UI/UIScene.h | 2 - .../UI/UIScene_AbstractContainerMenu.cpp | 45 +- .../Common/UI/UIScene_AbstractContainerMenu.h | 2 - .../Common/UI/UIScene_ConnectingProgress.cpp | 16 + .../Common/UI/UIScene_LoadOrJoinMenu.cpp | 528 ++++- .../Common/UI/UIScene_LoadOrJoinMenu.h | 19 + .../Common/UI/UIScene_MainMenu.cpp | 97 + Minecraft.Client/Common/UI/UIScene_MainMenu.h | 17 +- Minecraft.Client/Common/UI/UIStructs.h | 13 + .../Common/XUI/XUI_ConnectingProgress.cpp | 12 + Minecraft.Client/Common/XUI/XUI_MainMenu.cpp | 89 +- Minecraft.Client/Common/XUI/XUI_MainMenu.h | 25 +- Minecraft.Client/Extrax64Stubs.cpp | 68 +- Minecraft.Client/GameRenderer.cpp | 23 + Minecraft.Client/Gui.cpp | 131 ++ Minecraft.Client/Input.cpp | 31 +- Minecraft.Client/Input.h | 3 +- Minecraft.Client/KeyboardMouseInput.cpp | 59 +- Minecraft.Client/KeyboardMouseInput.h | 7 - Minecraft.Client/LevelRenderer.cpp | 7 +- Minecraft.Client/LocalPlayer.cpp | 11 +- Minecraft.Client/Minecraft.Client.vcxproj | 81 +- Minecraft.Client/Minecraft.cpp | 32 +- Minecraft.Client/Minecraft.h | 1 - Minecraft.Client/MinecraftServer.cpp | 265 ++- Minecraft.Client/MinecraftServer.h | 4 + Minecraft.Client/MultiPlayerChunkCache.cpp | 26 +- Minecraft.Client/PendingConnection.cpp | 50 +- Minecraft.Client/PlayerConnection.cpp | 1722 ++++++++++++++++- Minecraft.Client/PlayerList.cpp | 224 ++- Minecraft.Client/PlayerList.h | 15 +- Minecraft.Client/Screen.cpp | 5 + Minecraft.Client/Screen.h | 2 +- Minecraft.Client/ServerPlayer.cpp | 21 +- Minecraft.Client/ServerPlayer.h | 1 + Minecraft.Client/TextEditScreen.cpp | 6 +- .../Windows64/Network/WinsockNetLayer.cpp | 191 +- .../Windows64/Network/WinsockNetLayer.h | 7 +- Minecraft.Client/Windows64/Windows64_App.cpp | 1 + .../Windows64/Windows64_Minecraft.cpp | 1241 +++++++++++- Minecraft.Client/dedicated-server.properties | 10 + Minecraft.World/ArrayWithLength.h | 16 +- Minecraft.World/CompoundTag.h | 2 +- Minecraft.World/Packet.cpp | 27 +- Minecraft.World/ThreadName.cpp | 13 +- MinecraftConsoles.sln | 4 - proxy-worlds.properties | 5 + 61 files changed, 5764 insertions(+), 773 deletions(-) create mode 100644 Minecraft.Client/dedicated-server.properties create mode 100644 proxy-worlds.properties diff --git a/Minecraft.Client/BufferedImage.cpp b/Minecraft.Client/BufferedImage.cpp index 658e934..fdcb83d 100644 --- a/Minecraft.Client/BufferedImage.cpp +++ b/Minecraft.Client/BufferedImage.cpp @@ -49,12 +49,11 @@ void BufferedImage::ByteFlip4(unsigned int &data) // 24-bits used (ie no alpha channel) whereas method 0 is a full 32-bit image with a valid alpha channel. BufferedImage::BufferedImage(const wstring& File, bool filenameHasExtension /*=false*/, bool bTitleUpdateTexture /*=false*/, const wstring &drive /*=L""*/) { - HRESULT hr; - wstring wDrive; - wstring filePath; - filePath = File; - - wDrive = drive; + HRESULT hr = ERROR_SUCCESS; + wstring wDrive = drive; + wstring filePath = File; + width = 0; + height = 0; if(wDrive.empty()) { #ifdef _XBOX @@ -170,9 +169,53 @@ BufferedImage::BufferedImage(const wstring& File, bool filenameHasExtension /*=f ZeroMemory(&ImageInfo,sizeof(D3DXIMAGE_INFO)); hr=RenderManager.LoadTextureData(pchTextureName,&ImageInfo,&data[l]); +#if defined(_WINDOWS64) + if (hr != ERROR_SUCCESS && l == 0 && drive.empty()) + { + static const wchar_t *fallbackDrives[] = + { + L"Minecraft.Client/Common/", + L"Common/", + L"../Minecraft.Client/Common/", + L"../Common/", + L"..\\Minecraft.Client\\Common\\", + L"..\\Common\\" + }; + const unsigned int fallbackDriveCount = sizeof(fallbackDrives) / sizeof(fallbackDrives[0]); + + for (unsigned int f = 0; f < fallbackDriveCount; ++f) + { + const wstring fallbackDrive = fallbackDrives[f]; + if (fallbackDrive == wDrive) + { + continue; + } + + wstring fallbackName; + if (filenameHasExtension) + { + fallbackName = fallbackDrive + L"res" + filePath.substr(0, filePath.length()); + } + else + { + fallbackName = fallbackDrive + L"res" + filePath.substr(0, filePath.length() - 4) + mipMapPath + L".png"; + } + + const char *fallbackTextureName = wstringtofilename(fallbackName); + hr = RenderManager.LoadTextureData(fallbackTextureName, &ImageInfo, &data[l]); + if (hr == ERROR_SUCCESS) + { + app.DebugPrintf("BufferedImage: fallback load succeeded for '%s'\n", fallbackTextureName); + wDrive = fallbackDrive; + break; + } + } + } +#endif if(hr!=ERROR_SUCCESS) { + app.DebugPrintf("BufferedImage: failed to load texture '%s' (hr=0x%08X)\n", pchTextureName, (unsigned int)hr); // 4J - If we haven't loaded the non-mipmap version then exit the game if( l == 0 ) { diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index b80eaed..4b10a41 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -47,6 +47,9 @@ #else #include "Common\UI\UI.h" #endif +#ifdef _WINDOWS64 +#include "Common\Network\PlatformNetworkManagerStub.h" +#endif #ifdef __PS3__ #include "PS3/Network/SonyVoiceChat.h" #endif @@ -57,6 +60,26 @@ #include "..\Minecraft.World\GenericStats.h" #endif +namespace +{ + const wstring LCE_TRANSFER_PACKET = L"LCE|Xfer"; + + string NarrowAscii(const wstring &value) + { + string out; + out.reserve(value.length()); + for (size_t i = 0; i < value.length(); ++i) + { + const wchar_t ch = value[i]; + if (ch >= 32 && ch <= 126) + { + out.push_back((char)ch); + } + } + return out; + } +} + ClientConnection::ClientConnection(Minecraft *minecraft, const wstring& ip, int port) { // 4J Stu - No longer used as we use the socket version below. @@ -140,6 +163,19 @@ ClientConnection::~ClientConnection() void ClientConnection::tick() { + if (connection == NULL) + { + return; + } + +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + connection->flush(); + return; + } +#endif + if (!done) connection->tick(); connection->flush(); } @@ -704,6 +740,17 @@ void ClientConnection::handleAddPainting(shared_ptr packet) void ClientConnection::handleSetEntityMotion(shared_ptr packet) { +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + return; + } +#endif + if (!started || level == NULL || minecraft == NULL || minecraft->level == NULL) + { + return; + } + shared_ptr e = getEntity(packet->id); if (e == NULL) return; e->lerpMotion(packet->xa / 8000.0, packet->ya / 8000.0, packet->za / 8000.0); @@ -1136,6 +1183,16 @@ void ClientConnection::handleDisconnect(shared_ptr packet) { connection->close(DisconnectPacket::eDisconnect_Kicked); done = true; + +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + level = NULL; + started = false; + app.DebugPrintf("Win64 LAN: Ignoring handleDisconnect during in-progress server transfer\n"); + return; + } +#endif Minecraft *pMinecraft = Minecraft::GetInstance(); pMinecraft->connectionDisconnected( m_userIndex , packet->reason ); @@ -1152,6 +1209,16 @@ void ClientConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, if (done) return; done = true; +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + level = NULL; + started = false; + app.DebugPrintf("Win64 LAN: Ignoring onDisconnect during in-progress server transfer (reason=%d)\n", (int)reason); + return; + } +#endif + Minecraft *pMinecraft = Minecraft::GetInstance(); pMinecraft->connectionDisconnected( m_userIndex , reason ); @@ -1281,6 +1348,20 @@ void ClientConnection::handleChat(shared_ptr packet) switch(packet->m_messageType) { + case ChatPacket::e_ChatCustom: + if(packet->m_stringArgs.size() >= 2) + { + message = L"<" + playerDisplayName + L"> " + packet->m_stringArgs[1]; + } + else if(packet->m_stringArgs.size() >= 1) + { + message = packet->m_stringArgs[0]; + } + else + { + message = L""; + } + break; case ChatPacket::e_ChatBedOccupied: message = app.GetString(IDS_TILE_BED_OCCUPIED); break; @@ -2186,6 +2267,21 @@ void ClientConnection::handleEntityEvent(shared_ptr packet) shared_ptr ClientConnection::getEntity(int entityId) { + if (!started || level == NULL || minecraft == NULL || minecraft->level == NULL) + { + return shared_ptr(); + } + + if (m_userIndex < 0 || m_userIndex >= XUSER_MAX_COUNT) + { + return shared_ptr(); + } + + if (minecraft->localplayers[m_userIndex] == NULL) + { + return shared_ptr(); + } + //if (entityId == minecraft->player->entityId) if(entityId == minecraft->localplayers[m_userIndex]->entityId) { @@ -2841,7 +2937,15 @@ void ClientConnection::handleTileDestruction(shared_ptr p bool ClientConnection::canHandleAsyncPackets() { - return minecraft != NULL && minecraft->level != NULL && minecraft->localplayers[m_userIndex] != NULL && level != NULL; + if (minecraft == NULL || level == NULL || minecraft->level == NULL || !started) + { + return false; + } + if (m_userIndex < 0 || m_userIndex >= XUSER_MAX_COUNT) + { + return false; + } + return minecraft->localplayers[m_userIndex] != NULL; } void ClientConnection::handleGameEvent(shared_ptr gameEventPacket) @@ -3148,6 +3252,41 @@ void ClientConnection::handleSoundEvent(shared_ptr packet) void ClientConnection::handleCustomPayload(shared_ptr customPayloadPacket) { + if (LCE_TRANSFER_PACKET.compare(customPayloadPacket->identifier) == 0) + { +#ifdef _WINDOWS64 + ByteArrayInputStream bais(customPayloadPacket->data); + DataInputStream input(&bais); + const int payloadVersion = (int)input.readUnsignedByte(); + const int queueIndex = (int)input.readUnsignedByte(); + const int hostPort = input.readInt(); + const wstring hostIpW = input.readUTF(); + const wstring queueLabel = input.readUTF(); + const string hostIp = NarrowAscii(hostIpW); + + app.DebugPrintf("Win64 LAN: Received transfer payload v%d queue=%d target=%s:%d label=%ls\n", + payloadVersion, queueIndex, hostIp.c_str(), hostPort, queueLabel.c_str()); + + if (!hostIp.empty() && hostPort > 0) + { + CPlatformNetworkManagerStub::RequestServerTransfer(hostIp.c_str(), hostPort); + wstring message = L"Match found"; + if (!queueLabel.empty()) + { + message += L" for "; + message += queueLabel; + } + message += L". Transferring..."; + minecraft->gui->addMessage(message, m_userIndex); + } + else + { + app.DebugPrintf("Win64 LAN: Ignoring invalid transfer payload target\n"); + } +#endif + return; + } + if (CustomPayloadPacket::TRADER_LIST_PACKET.compare(customPayloadPacket->identifier) == 0) { ByteArrayInputStream bais(customPayloadPacket->data); diff --git a/Minecraft.Client/Common/Audio/SoundEngine.cpp b/Minecraft.Client/Common/Audio/SoundEngine.cpp index ef72dd8..4eaf7df 100644 --- a/Minecraft.Client/Common/Audio/SoundEngine.cpp +++ b/Minecraft.Client/Common/Audio/SoundEngine.cpp @@ -456,9 +456,6 @@ void SoundEngine::updateMiles() if ( SoundInfo.Status != MILESEVENT_SOUND_STATUS_COMPLETE ) { - if (SoundInfo.Sample == NULL) - continue; - // apply the master volume // watch for the 'special' volume levels bool isThunder = false; diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index cf855ca..d2dc069 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -69,6 +69,62 @@ #include "..\Common\Leaderboards\LeaderboardManager.h" +#ifdef _WINDOWS64 +extern void Windows64_DedicatedGuiPushLog(const char *text); + +static void WriteStandaloneDebugLog(const char *text) +{ + if (text == NULL) + { + return; + } + + wchar_t exePath[MAX_PATH]; + if (!GetModuleFileNameW(NULL, exePath, MAX_PATH)) + { + return; + } + + wstring logPath(exePath); + size_t lastSlash = logPath.find_last_of(L"\\/"); + if (lastSlash != wstring::npos) + { + logPath = logPath.substr(0, lastSlash + 1) + L"StandaloneDebug.log"; + } + else + { + logPath = L"StandaloneDebug.log"; + } + + HANDLE file = CreateFileW(logPath.c_str(), FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) + { + return; + } + + SYSTEMTIME st; + GetLocalTime(&st); + + char prefix[64]; + _snprintf_s(prefix, sizeof(prefix), _TRUNCATE, "[%02d:%02d:%02d.%03d] ", + st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + + DWORD bytesWritten = 0; + WriteFile(file, prefix, (DWORD)strlen(prefix), &bytesWritten, NULL); + WriteFile(file, text, (DWORD)strlen(text), &bytesWritten, NULL); + + size_t len = strlen(text); + if (len == 0 || (text[len - 1] != '\n' && text[len - 1] != '\r')) + { + const char *newline = "\r\n"; + WriteFile(file, newline, (DWORD)strlen(newline), &bytesWritten, NULL); + } + + CloseHandle(file); + Windows64_DedicatedGuiPushLog(text); +} +#endif + //CMinecraftApp app; unsigned int CMinecraftApp::m_uiLastSignInData = 0; @@ -233,21 +289,23 @@ CMinecraftApp::CMinecraftApp() void CMinecraftApp::DebugPrintf(const char *szFormat, ...) { - -#ifndef _FINAL_BUILD char buf[1024]; va_list ap; va_start(ap, szFormat); vsnprintf(buf, sizeof(buf), szFormat, ap); va_end(ap); + +#ifndef _FINAL_BUILD OutputDebugStringA(buf); #endif +#ifdef _WINDOWS64 + WriteStandaloneDebugLog(buf); +#endif } void CMinecraftApp::DebugPrintf(int user, const char *szFormat, ...) { -#ifndef _FINAL_BUILD if(user == USER_NONE) return; char buf[1024]; @@ -255,6 +313,7 @@ void CMinecraftApp::DebugPrintf(int user, const char *szFormat, ...) va_start(ap, szFormat); vsnprintf(buf, sizeof(buf), szFormat, ap); va_end(ap); +#ifndef _FINAL_BUILD #ifdef __PS3__ unsigned int writelen; sys_tty_write(SYS_TTYP_USER1 + ( user - 1 ), buf, strlen(buf), &writelen ); @@ -297,12 +356,26 @@ void CMinecraftApp::DebugPrintf(int user, const char *szFormat, ...) } #endif #endif + +#ifdef _WINDOWS64 + WriteStandaloneDebugLog(buf); +#endif } LPCWSTR CMinecraftApp::GetString(int iID) { //return L"DeÄŸiÅŸiklikler ve Yenilikler"; //return L"ÕÕÕÕÖÖÖÖ"; + if (app.m_stringTable == NULL) + { + static int s_missingStringTableLogCount = 0; + if (s_missingStringTableLogCount < 16) + { + app.DebugPrintf("CMinecraftApp::GetString - null m_stringTable for id %d\n", iID); + s_missingStringTableLogCount++; + } + return L""; + } return app.m_stringTable->getString(iID); } @@ -4085,23 +4158,65 @@ int CMinecraftApp::BannedLevelDialogReturned(void *pParam,int iPad,const C4JStor void CMinecraftApp::loadMediaArchive() { - wstring mediapath = L""; + wstring mediaFileName = L""; #ifdef __PS3__ - mediapath = L"Common\\Media\\MediaPS3.arc"; + mediaFileName = L"MediaPS3.arc"; #elif _WINDOWS64 - mediapath = L"Common\\Media\\MediaWindows64.arc"; + mediaFileName = L"MediaWindows64.arc"; #elif __ORBIS__ - mediapath = L"Common\\Media\\MediaOrbis.arc"; + mediaFileName = L"MediaOrbis.arc"; #elif _DURANGO - mediapath = L"Common\\Media\\MediaDurango.arc"; + mediaFileName = L"MediaDurango.arc"; #elif __PSVITA__ - mediapath = L"Common\\Media\\MediaPSVita.arc"; + mediaFileName = L"MediaPSVita.arc"; #endif - if (!mediapath.empty()) + if (!mediaFileName.empty()) { - m_mediaArchive = new ArchiveFile( File(mediapath) ); + if (m_mediaArchive != NULL) + { + delete m_mediaArchive; + m_mediaArchive = NULL; + } + + vector candidates; + candidates.push_back(L"Common\\Media\\" + mediaFileName); + candidates.push_back(L"Minecraft.Client\\Common\\Media\\" + mediaFileName); + +#if defined(_WINDOWS64) || defined(_WIN32) + wchar_t exePath[MAX_PATH]; + if (GetModuleFileNameW(NULL, exePath, MAX_PATH)) + { + wstring exeDir(exePath); + size_t lastSlash = exeDir.find_last_of(L"\\/"); + if (lastSlash != wstring::npos) + { + exeDir = exeDir.substr(0, lastSlash); + wstring root = exeDir; + for (int i = 0; i < 8; ++i) + { + candidates.push_back(root + L"\\Common\\Media\\" + mediaFileName); + candidates.push_back(root + L"\\Minecraft.Client\\Common\\Media\\" + mediaFileName); + root += L"\\.."; + } + } + } +#endif + + for (AUTO_VAR(it, candidates.begin()); it != candidates.end(); ++it) + { + File archiveCandidate(*it); + if (archiveCandidate.exists()) + { + app.DebugPrintf("CMinecraftApp::loadMediaArchive - using '%ls'\n", it->c_str()); + m_mediaArchive = new ArchiveFile(archiveCandidate); + return; + } + } + + app.DebugPrintf("CMinecraftApp::loadMediaArchive - failed to locate '%ls'\n", mediaFileName.c_str()); + m_mediaArchive = NULL; } #if 0 string path = "Common\\media.arc"; @@ -4153,6 +4268,18 @@ void CMinecraftApp::loadStringTable() // we need to unload the current string table, this is a reload delete m_stringTable; } + m_stringTable = NULL; + + if (m_mediaArchive == NULL) + { + loadMediaArchive(); + if (m_mediaArchive == NULL) + { + DebugPrintf("CMinecraftApp::loadStringTable - media archive not loaded\n"); + return; + } + } + wstring localisationFile = L"languages.loc"; if (m_mediaArchive->hasFile(localisationFile)) { @@ -4162,9 +4289,7 @@ void CMinecraftApp::loadStringTable() } else { - m_stringTable = NULL; - assert(false); - // AHHHHHHHHH. + DebugPrintf("CMinecraftApp::loadStringTable - missing file '%ls'\n", localisationFile.c_str()); } #endif } @@ -8864,7 +8989,17 @@ int CMinecraftApp::getArchiveFileSize(const wstring &filename) { return tPack->getArchiveFile()->getFileSize(filename); } - else return m_mediaArchive->getFileSize(filename); + + if (m_mediaArchive == NULL) + { + loadMediaArchive(); + } + if (m_mediaArchive != NULL) + { + return m_mediaArchive->getFileSize(filename); + } + + return -1; } bool CMinecraftApp::hasArchiveFile(const wstring &filename) @@ -8873,7 +9008,12 @@ bool CMinecraftApp::hasArchiveFile(const wstring &filename) Minecraft *pMinecraft = Minecraft::GetInstance(); if(pMinecraft && pMinecraft->skins) tPack = pMinecraft->skins->getSelected(); if(tPack && tPack->hasData() && tPack->getArchiveFile() && tPack->getArchiveFile()->hasFile(filename)) return true; - else return m_mediaArchive->hasFile(filename); + + if (m_mediaArchive == NULL) + { + loadMediaArchive(); + } + return m_mediaArchive != NULL && m_mediaArchive->hasFile(filename); } byteArray CMinecraftApp::getArchiveFile(const wstring &filename) @@ -8885,7 +9025,17 @@ byteArray CMinecraftApp::getArchiveFile(const wstring &filename) { return tPack->getArchiveFile()->getFile(filename); } - else return m_mediaArchive->getFile(filename); + + if (m_mediaArchive == NULL) + { + loadMediaArchive(); + } + if (m_mediaArchive != NULL) + { + return m_mediaArchive->getFile(filename); + } + + return byteArray(); } // DLC @@ -9536,4 +9686,4 @@ bool CMinecraftApp::HasReachedMainMenu() { return m_hasReachedMainMenu; } -#endif \ No newline at end of file +#endif diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index a633359..b4b8ea5 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.cpp +++ b/Minecraft.Client/Common/Network/GameNetworkManager.cpp @@ -40,6 +40,10 @@ #include "..\Minecraft.World\DurangoStats.h" #endif +#ifdef _WINDOWS64 +extern bool g_Win64DedicatedServerMode; +#endif + // Global instance CGameNetworkManager g_NetworkManager; CPlatformNetworkManager *CGameNetworkManager::s_pPlatformNetworkManager; @@ -281,186 +285,199 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame // PRIMARY PLAYER vector createdConnections; - ClientConnection *connection; + ClientConnection *connection = NULL; +#ifdef _WINDOWS64 + const bool dedicatedHeadlessHost = (g_NetworkManager.IsHost() && g_Win64DedicatedServerMode); +#else + const bool dedicatedHeadlessHost = false; +#endif - if( g_NetworkManager.IsHost() ) + if (!dedicatedHeadlessHost) { - connection = new ClientConnection(minecraft, NULL); - } - else - { - INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(ProfileManager.GetLockedProfile()); - if(pNetworkPlayer == NULL) + if( g_NetworkManager.IsHost() ) { - MinecraftServer::HaltServer(); - app.DebugPrintf("%d\n",ProfileManager.GetLockedProfile()); - // If the player is NULL here then something went wrong in the session setup, and continuing will end up in a crash - return false; - } - - Socket *socket = pNetworkPlayer->GetSocket(); - - // Fix for #13259 - CRASH: Gameplay: loading process is halted when player loads saved data - if(socket == NULL) - { - assert(false); - MinecraftServer::HaltServer(); - // If the socket is NULL here then something went wrong in the session setup, and continuing will end up in a crash - return false; - } - - connection = new ClientConnection(minecraft, socket); - } - - if( !connection->createdOk ) - { - assert(false); - delete connection; - connection = NULL; - MinecraftServer::HaltServer(); - return false; - } - - connection->send( shared_ptr( new PreLoginPacket(minecraft->user->name) ) ); - - // Tick connection until we're ready to go. The stages involved in this are: - // (1) Creating the ClientConnection sends a prelogin packet to the server - // (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet - // (3) the server sends a login back, which is handled by the client connection to start the game - if( !g_NetworkManager.IsHost() ) - { - Minecraft::GetInstance()->progressRenderer->progressStart(IDS_PROGRESS_CONNECTING); - } - else - { - // 4J Stu - Host needs to generate a unique multiplayer id for sentient telemetry reporting - INT multiplayerInstanceId = TelemetryManager->GenerateMultiplayerInstanceId(); - TelemetryManager->SetMultiplayerInstanceId(multiplayerInstanceId); - } - TexturePack *tPack = Minecraft::GetInstance()->skins->getSelected(); - do - { - app.DebugPrintf("ticking connection A\n"); - connection->tick(); - - // 4J Stu - We were ticking this way too fast which could cause the connection to time out - // The connections should tick at 20 per second - Sleep(50); - } while ( (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame()) || tPack->isLoadingData() || (Minecraft::GetInstance()->skins->needsUIUpdate() || ui.IsReloadingSkin()) ); - ui.CleanUpSkinReload(); - - // 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash - // We need to break out of the above loop if m_bLeavingGame is set, and close the connection - if( g_NetworkManager.IsLeavingGame() || !IsInSession() ) - { - connection->close(); - } - - if( connection->isStarted() && !connection->isClosed() ) - { - createdConnections.push_back( connection ); - - int primaryPad = ProfileManager.GetPrimaryPad(); - app.SetRichPresenceContext(primaryPad,CONTEXT_GAME_STATE_BLANK); - if (GetPlayerCount() > 1) // Are we offline or online, and how many players are there - { - if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false); - else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER,false); + connection = new ClientConnection(minecraft, NULL); } else { - if(IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1POFFLINE,false); - else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1P,false); - } - - - // ALL OTHER LOCAL PLAYERS - for(int idx = 0; idx < XUSER_MAX_COUNT; ++idx) - { - // Already have setup the primary pad - if(idx == ProfileManager.GetPrimaryPad() ) continue; - - if( GetLocalPlayerByUserIndex(idx) != NULL && !ProfileManager.IsSignedIn(idx) ) + INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(ProfileManager.GetLockedProfile()); + if(pNetworkPlayer == NULL) { - INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx); - Socket *socket = pNetworkPlayer->GetSocket(); - app.DebugPrintf("Closing socket due to player %d not being signed in any more\n"); - if( !socket->close(false) ) socket->close(true); - - continue; + MinecraftServer::HaltServer(); + app.DebugPrintf("%d\n",ProfileManager.GetLockedProfile()); + // If the player is NULL here then something went wrong in the session setup, and continuing will end up in a crash + return false; } - // By default when we host we only have the local player, but currently allow multiple local players to join - // when joining any other way, so just because they are signed in doesn't mean they are in the session - // 4J Stu - If they are in the session, then we should add them to the game. Otherwise we won't be able to add them later - INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx); - if( pNetworkPlayer == NULL ) - continue; - - ClientConnection *connection; - Socket *socket = pNetworkPlayer->GetSocket(); - connection = new ClientConnection(minecraft, socket, idx); - minecraft->addPendingLocalConnection(idx, connection); - //minecraft->createExtraLocalPlayer(idx, (convStringToWstring( ProfileManager.GetGamertag(idx) )).c_str(), idx, connection); + // Fix for #13259 - CRASH: Gameplay: loading process is halted when player loads saved data + if(socket == NULL) + { + assert(false); + MinecraftServer::HaltServer(); + // If the socket is NULL here then something went wrong in the session setup, and continuing will end up in a crash + return false; + } - // Open the socket on the server end to accept incoming data - Socket::addIncomingSocket(socket); + connection = new ClientConnection(minecraft, socket); + } - connection->send( shared_ptr( new PreLoginPacket(convStringToWstring( ProfileManager.GetGamertag(idx) )) ) ); + if( !connection->createdOk ) + { + assert(false); + delete connection; + connection = NULL; + MinecraftServer::HaltServer(); + return false; + } + connection->send( shared_ptr( new PreLoginPacket(minecraft->user->name) ) ); + + // Tick connection until we're ready to go. The stages involved in this are: + // (1) Creating the ClientConnection sends a prelogin packet to the server + // (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet + // (3) the server sends a login back, which is handled by the client connection to start the game + if( !g_NetworkManager.IsHost() ) + { + Minecraft::GetInstance()->progressRenderer->progressStart(IDS_PROGRESS_CONNECTING); + } + else + { + // 4J Stu - Host needs to generate a unique multiplayer id for sentient telemetry reporting + INT multiplayerInstanceId = TelemetryManager->GenerateMultiplayerInstanceId(); + TelemetryManager->SetMultiplayerInstanceId(multiplayerInstanceId); + } + TexturePack *tPack = Minecraft::GetInstance()->skins->getSelected(); + do + { + app.DebugPrintf("ticking connection A\n"); + connection->tick(); + + // 4J Stu - We were ticking this way too fast which could cause the connection to time out + // The connections should tick at 20 per second + Sleep(50); + } while ( (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame()) || tPack->isLoadingData() || (Minecraft::GetInstance()->skins->needsUIUpdate() || ui.IsReloadingSkin()) ); + ui.CleanUpSkinReload(); + + // 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash + // We need to break out of the above loop if m_bLeavingGame is set, and close the connection + if( g_NetworkManager.IsLeavingGame() || !IsInSession() ) + { + connection->close(); + } + + if( connection->isStarted() && !connection->isClosed() ) + { createdConnections.push_back( connection ); - // Tick connection until we're ready to go. The stages involved in this are: - // (1) Creating the ClientConnection sends a prelogin packet to the server - // (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet - // (3) the server sends a login back, which is handled by the client connection to start the game - do + int primaryPad = ProfileManager.GetPrimaryPad(); + app.SetRichPresenceContext(primaryPad,CONTEXT_GAME_STATE_BLANK); + if (GetPlayerCount() > 1) // Are we offline or online, and how many players are there { - // We need to keep ticking the connections for players that already logged in - for(AUTO_VAR(it, createdConnections.begin()); it < createdConnections.end(); ++it) - { - (*it)->tick(); - } - - // 4J Stu - We were ticking this way too fast which could cause the connection to time out - // The connections should tick at 20 per second - Sleep(50); - app.DebugPrintf("<***> %d %d %d %d %d\n",IsInSession(), !connection->isStarted(),!connection->isClosed(),ProfileManager.IsSignedIn(idx),!g_NetworkManager.IsLeavingGame()); -#if defined _XBOX || __PS3__ - } while (IsInSession() && !connection->isStarted() && !connection->isClosed() && ProfileManager.IsSignedIn(idx) && !g_NetworkManager.IsLeavingGame() ); -#else - // TODO - This SHOULD be something just like the code above but temporarily changing here so that we don't have to depend on the profilemanager behaviour - } while (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame() ); -#endif - - // 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash - // We need to break out of the above loop if m_bLeavingGame is set, and stop creating new connections - // The connections in the createdConnections vector get closed at the end of the thread - if( g_NetworkManager.IsLeavingGame() || !IsInSession() ) break; - - if( ProfileManager.IsSignedIn(idx) && !connection->isClosed() ) - { - app.SetRichPresenceContext(idx,CONTEXT_GAME_STATE_BLANK); - if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false); - else ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYER,false); + if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false); + else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER,false); } else { - connection->close(); - AUTO_VAR(it, find( createdConnections.begin(), createdConnections.end(), connection )); - if(it != createdConnections.end() ) createdConnections.erase( it ); + if(IsLocalGame()) ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1POFFLINE,false); + else ProfileManager.SetCurrentGameActivity(primaryPad,CONTEXT_PRESENCE_MULTIPLAYER_1P,false); } - } - app.SetGameMode( eMode_Multiplayer ); - } - else if ( connection->isClosed() || !IsInSession()) - { + + // ALL OTHER LOCAL PLAYERS + for(int idx = 0; idx < XUSER_MAX_COUNT; ++idx) + { + // Already have setup the primary pad + if(idx == ProfileManager.GetPrimaryPad() ) continue; + + if( GetLocalPlayerByUserIndex(idx) != NULL && !ProfileManager.IsSignedIn(idx) ) + { + INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx); + Socket *socket = pNetworkPlayer->GetSocket(); + app.DebugPrintf("Closing socket due to player %d not being signed in any more\n"); + if( !socket->close(false) ) socket->close(true); + + continue; + } + + // By default when we host we only have the local player, but currently allow multiple local players to join + // when joining any other way, so just because they are signed in doesn't mean they are in the session + // 4J Stu - If they are in the session, then we should add them to the game. Otherwise we won't be able to add them later + INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx); + if( pNetworkPlayer == NULL ) + continue; + + ClientConnection *connection; + + Socket *socket = pNetworkPlayer->GetSocket(); + connection = new ClientConnection(minecraft, socket, idx); + + minecraft->addPendingLocalConnection(idx, connection); + //minecraft->createExtraLocalPlayer(idx, (convStringToWstring( ProfileManager.GetGamertag(idx) )).c_str(), idx, connection); + + // Open the socket on the server end to accept incoming data + Socket::addIncomingSocket(socket); + + connection->send( shared_ptr( new PreLoginPacket(convStringToWstring( ProfileManager.GetGamertag(idx) )) ) ); + + createdConnections.push_back( connection ); + + // Tick connection until we're ready to go. The stages involved in this are: + // (1) Creating the ClientConnection sends a prelogin packet to the server + // (2) the server sends a prelogin back, which is handled by the clientConnection, and returns a login packet + // (3) the server sends a login back, which is handled by the client connection to start the game + do + { + // We need to keep ticking the connections for players that already logged in + for(AUTO_VAR(it, createdConnections.begin()); it < createdConnections.end(); ++it) + { + (*it)->tick(); + } + + // 4J Stu - We were ticking this way too fast which could cause the connection to time out + // The connections should tick at 20 per second + Sleep(50); + app.DebugPrintf("<***> %d %d %d %d %d\n",IsInSession(), !connection->isStarted(),!connection->isClosed(),ProfileManager.IsSignedIn(idx),!g_NetworkManager.IsLeavingGame()); +#if defined _XBOX || __PS3__ + } while (IsInSession() && !connection->isStarted() && !connection->isClosed() && ProfileManager.IsSignedIn(idx) && !g_NetworkManager.IsLeavingGame() ); +#else + // TODO - This SHOULD be something just like the code above but temporarily changing here so that we don't have to depend on the profilemanager behaviour + } while (IsInSession() && !connection->isStarted() && !connection->isClosed() && !g_NetworkManager.IsLeavingGame() ); +#endif + + // 4J Stu - Fix for #11279 - CRASH: TCR 001: BAS Game Stability: Signing out of game will cause title to crash + // We need to break out of the above loop if m_bLeavingGame is set, and stop creating new connections + // The connections in the createdConnections vector get closed at the end of the thread + if( g_NetworkManager.IsLeavingGame() || !IsInSession() ) break; + + if( ProfileManager.IsSignedIn(idx) && !connection->isClosed() ) + { + app.SetRichPresenceContext(idx,CONTEXT_GAME_STATE_BLANK); + if (IsLocalGame()) ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYEROFFLINE,false); + else ProfileManager.SetCurrentGameActivity(idx,CONTEXT_PRESENCE_MULTIPLAYER,false); + } + else + { + connection->close(); + AUTO_VAR(it, find( createdConnections.begin(), createdConnections.end(), connection )); + if(it != createdConnections.end() ) createdConnections.erase( it ); + } + } + + app.SetGameMode( eMode_Multiplayer ); + } + else if ( connection->isClosed() || !IsInSession()) + { // assert(false); - MinecraftServer::HaltServer(); - return false; + MinecraftServer::HaltServer(); + return false; + } + } + else + { + app.DebugPrintf("Win64 dedicated: skipping primary local player connection\n"); + app.SetGameMode( eMode_Multiplayer ); } diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index 24d5243..b73bb96 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -7,6 +7,15 @@ #include "..\..\Windows64\Network\WinsockNetLayer.h" #include "..\..\Minecraft.h" #include "..\..\User.h" +extern bool g_Win64DedicatedServerMode; + +namespace +{ + static unsigned long long GetNowMs64() + { + return (unsigned long long)GetTickCount64(); + } +} #endif CPlatformNetworkManagerStub *g_pPlatformNetworkManager; @@ -14,6 +23,12 @@ CPlatformNetworkManagerStub *g_pPlatformNetworkManager; void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) { + if (pQNetPlayer == NULL) + { + app.DebugPrintf("NotifyPlayerJoined called with NULL player\n"); + return; + } + const char * pszDescription; // 4J Stu - We create a fake socket for every where that we need an INBOUND queue of game data. Outbound @@ -94,7 +109,7 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) if( m_pIQNet->IsHost() ) { // 4J-PB - only the host should do this -// g_NetworkManager.UpdateAndSetGameSessionData(); + g_NetworkManager.UpdateAndSetGameSessionData(); SystemFlagAddPlayer( networkPlayer ); } @@ -121,6 +136,12 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) void CPlatformNetworkManagerStub::NotifyPlayerLeaving(IQNetPlayer *pQNetPlayer) { + if (pQNetPlayer == NULL) + { + app.DebugPrintf("NotifyPlayerLeaving called with NULL player\n"); + return; + } + app.DebugPrintf("Player 0x%p \"%ls\" leaving.\n", pQNetPlayer, pQNetPlayer->GetGamertag()); INetworkPlayer *networkPlayer = getNetworkPlayer(pQNetPlayer); @@ -148,6 +169,12 @@ void CPlatformNetworkManagerStub::NotifyPlayerLeaving(IQNetPlayer *pQNetPlayer) } removeNetworkPlayer(pQNetPlayer); + + if (m_pIQNet->IsHost()) + { + // Exclude the leaving player from advertised count immediately. + g_NetworkManager.UpdateAndSetGameSessionData(networkPlayer); + } } bool CPlatformNetworkManagerStub::Initialise(CGameNetworkManager *pGameNetworkManager, int flagIndexSize) @@ -164,6 +191,13 @@ bool CPlatformNetworkManagerStub::Initialise(CGameNetworkManager *pGameNetworkMa m_bLeavingGame = false; m_bLeaveGameOnTick = false; m_bHostChanged = false; +#ifdef _WINDOWS64 + m_bTransferPending = false; + m_bTransferLeaving = false; + m_transferHostIp[0] = 0; + m_transferHostPort = 0; + m_transferSuppressErrorsUntilMs = 0; +#endif m_bSearchResultsReady = false; m_bSearchPending = false; @@ -213,6 +247,46 @@ void CPlatformNetworkManagerStub::DoWork() { #ifdef _WINDOWS64 extern QNET_STATE _iQNetStubState; + if (m_bTransferPending) + { + if (g_NetworkManager.IsInSession()) + { + if (!m_bTransferLeaving) + { + m_bTransferLeaving = true; + app.DebugPrintf("Win64 LAN: Transfer requested to %s:%d, leaving current session\n", + m_transferHostIp, m_transferHostPort); + g_NetworkManager.LeaveGame(false); + } + return; + } + + FriendSessionInfo targetSession; + memset(&targetSession, 0, sizeof(targetSession)); + strncpy_s(targetSession.data.hostIP, sizeof(targetSession.data.hostIP), m_transferHostIp, _TRUNCATE); + targetSession.data.hostPort = m_transferHostPort; + wcsncpy_s(targetSession.data.hostName, XUSER_NAME_SIZE, L"Server Transfer", _TRUNCATE); + targetSession.data.netVersion = MINECRAFT_NET_VERSION; + targetSession.data.isJoinable = true; + targetSession.data.isReadyToJoin = true; + + int primaryPad = ProfileManager.GetPrimaryPad(); + if (primaryPad < 0) + { + primaryPad = 0; + } + const int localUsersMask = GetLocalPlayerMask(primaryPad); + const int joinResult = JoinGame(&targetSession, localUsersMask, ProfileManager.GetLockedProfile()); + app.DebugPrintf("Win64 LAN: Transfer join result=%d for %s:%d\n", + joinResult, m_transferHostIp, m_transferHostPort); + + m_bTransferPending = false; + m_bTransferLeaving = false; + m_transferHostIp[0] = 0; + m_transferHostPort = 0; + m_transferSuppressErrorsUntilMs = GetNowMs64() + 10000ULL; + } + if (_iQNetStubState == QNET_STATE_SESSION_STARTING && app.GetGameStarted()) { _iQNetStubState = QNET_STATE_GAME_PLAY; @@ -236,8 +310,21 @@ void CPlatformNetworkManagerStub::DoWork() qnetPlayer->m_gamertag[0] = 0; qnetPlayer->SetCustomDataValue(0); WinsockNetLayer::PushFreeSmallId(disconnectedSmallId); - if (IQNet::s_playerCount > 1) - IQNet::s_playerCount--; + + // Recompute active slot span instead of blindly decrementing. + // A lower smallId can disconnect while higher smallIds are still active. + DWORD highestActive = 1; + for (DWORD idx = 1; idx < MINECRAFT_NET_MAX_PLAYERS; ++idx) + { + if (IQNet::m_player[idx].GetCustomDataValue() != 0) + { + highestActive = idx + 1; + } + } + IQNet::s_playerCount = highestActive; + app.DebugPrintf("Win64 LAN: Recomputed active player slot span to %u after disconnect smallId=%u\n", + (unsigned int)IQNet::s_playerCount, + (unsigned int)disconnectedSmallId); } } } @@ -321,6 +408,14 @@ bool CPlatformNetworkManagerStub::LeaveGame(bool bMigrateHost) m_machineQNetPrimaryPlayers.clear(); SystemFlagReset(); +#ifdef _WINDOWS64 + // Clear all back-pointers from IQNet slots to deleted network players. + for (int idx = 0; idx < MINECRAFT_NET_MAX_PLAYERS; ++idx) + { + IQNet::m_player[idx].SetCustomDataValue(0); + } +#endif + #ifdef _WINDOWS64 WinsockNetLayer::Shutdown(); WinsockNetLayer::Initialize(); @@ -356,13 +451,26 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame, _HostGame( localUsersMask, publicSlots, privateSlots ); #ifdef _WINDOWS64 - int port = WIN64_NET_DEFAULT_PORT; + unsigned char advertiseMaxPlayers = publicSlots; + if (advertiseMaxPlayers == 0 || advertiseMaxPlayers > MINECRAFT_NET_MAX_PLAYERS) + { + advertiseMaxPlayers = MINECRAFT_NET_MAX_PLAYERS; + } + + g_Win64MultiplayerMaxPlayers = (int)advertiseMaxPlayers; + + int port = g_Win64MultiplayerPort; + if (port <= 0) + { + port = WIN64_NET_DEFAULT_PORT; + } if (!WinsockNetLayer::IsActive()) WinsockNetLayer::HostGame(port); const wchar_t *hostName = IQNet::m_player[0].m_gamertag; unsigned int settings = app.GetGameHostOption(eGameHostOption_All); - WinsockNetLayer::StartAdvertising(port, hostName, settings, 0, 0, MINECRAFT_NET_VERSION); + WinsockNetLayer::StartAdvertising(port, hostName, settings, 0, 0, MINECRAFT_NET_VERSION, advertiseMaxPlayers); + UpdateAndSetGameSessionData(); #endif } @@ -401,6 +509,9 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo *searchResult, int l if (!WinsockNetLayer::JoinGame(hostIP, hostPort)) { app.DebugPrintf("Win64 LAN: Failed to connect to %s:%d\n", hostIP, hostPort); + // Reset transient join state so a failed attempt does not poison subsequent joins. + m_pIQNet->EndGame(); + WinsockNetLayer::StartDiscovery(); return CGameNetworkManager::JOINGAME_FAIL_GENERAL; } @@ -481,44 +592,40 @@ bool CPlatformNetworkManagerStub::_RunNetworkGame() void CPlatformNetworkManagerStub::UpdateAndSetGameSessionData(INetworkPlayer *pNetworkPlayerLeaving /*= NULL*/) { -// DWORD playerCount = m_pIQNet->GetPlayerCount(); -// -// if( this->m_bLeavingGame ) -// return; -// -// if( GetHostPlayer() == NULL ) -// return; -// -// for(unsigned int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; ++i) -// { -// if( i < playerCount ) -// { -// INetworkPlayer *pNetworkPlayer = GetPlayerByIndex(i); -// -// // We can call this from NotifyPlayerLeaving but at that point the player is still considered in the session -// if( pNetworkPlayer != pNetworkPlayerLeaving ) -// { -// m_hostGameSessionData.players[i] = ((NetworkPlayerXbox *)pNetworkPlayer)->GetUID(); -// -// char *temp; -// temp = (char *)wstringtofilename( pNetworkPlayer->GetOnlineName() ); -// memcpy(m_hostGameSessionData.szPlayers[i],temp,XUSER_NAME_SIZE); -// } -// else -// { -// m_hostGameSessionData.players[i] = NULL; -// memset(m_hostGameSessionData.szPlayers[i],0,XUSER_NAME_SIZE); -// } -// } -// else -// { -// m_hostGameSessionData.players[i] = NULL; -// memset(m_hostGameSessionData.szPlayers[i],0,XUSER_NAME_SIZE); -// } -// } -// -// m_hostGameSessionData.hostPlayerUID = ((NetworkPlayerXbox *)GetHostPlayer())->GetQNetPlayer()->GetXuid(); -// m_hostGameSessionData.m_uiGameHostSettings = app.GetGameHostOption(eGameHostOption_All); +#ifdef _WINDOWS64 + if (this->m_bLeavingGame || !m_pIQNet->IsHost() || !WinsockNetLayer::IsHosting()) + { + return; + } + + BYTE advertisedCount = (BYTE)m_pIQNet->GetPlayerCount(); + if (g_Win64DedicatedServerMode && advertisedCount > 0) + { + // Dedicated host keeps slot 0 active internally for network scheduling, + // but should not be shown as an in-game player in server browser counts. + --advertisedCount; + } + if (pNetworkPlayerLeaving != NULL && advertisedCount > 0) + { + --advertisedCount; + } + if (advertisedCount == 0 && !g_Win64DedicatedServerMode) + { + advertisedCount = 1; + } + + extern QNET_STATE _iQNetStubState; + const BYTE maxPlayers = WinsockNetLayer::GetMaxPlayers(); + const bool joinable = (_iQNetStubState == QNET_STATE_GAME_PLAY) && (advertisedCount < maxPlayers); + const unsigned int advertisedSettings = app.GetGameHostOption(eGameHostOption_All); + + WinsockNetLayer::UpdateAdvertiseGameSettings(advertisedSettings); + WinsockNetLayer::UpdateAdvertisePlayerCount(advertisedCount); + WinsockNetLayer::UpdateAdvertiseJoinable(joinable); + + app.DebugPrintf("Win64 LAN: Updated advertised session data (players=%d, joinable=%d, settings=0x%08X)\n", + (int)advertisedCount, joinable ? 1 : 0, advertisedSettings); +#endif } int CPlatformNetworkManagerStub::RemovePlayerOnSocketClosedThreadProc( void* lpParam ) @@ -806,12 +913,102 @@ INetworkPlayer *CPlatformNetworkManagerStub::GetPlayerByIndex(int playerIndex) INetworkPlayer * CPlatformNetworkManagerStub::GetPlayerByXuid(PlayerUID xuid) { - return getNetworkPlayer( m_pIQNet->GetPlayerByXuid(xuid)) ; + IQNetPlayer *qnetPlayer = m_pIQNet->GetPlayerByXuid(xuid); + +#ifdef _WINDOWS64 + if (qnetPlayer == NULL) + { + const unsigned __int64 kWin64StubXuidBase = 0xe000d45248242f2eULL; + const unsigned __int64 uxuid = (unsigned __int64)xuid; + if (uxuid >= kWin64StubXuidBase) + { + const unsigned __int64 delta = uxuid - kWin64StubXuidBase; + if (delta < (unsigned __int64)MINECRAFT_NET_MAX_PLAYERS) + { + const BYTE smallId = (BYTE)delta; + // Clients can legitimately resolve remote players by XUID before the + // platform join callback has materialized the slot's NetworkPlayer. + // Hosts keep the stricter check to avoid creating phantom entries. + const bool slotActive = (!m_pIQNet->IsHost()) || + (smallId == 0) || + (IQNet::m_player[smallId].GetCustomDataValue() != 0); + if (slotActive) + { + qnetPlayer = &IQNet::m_player[smallId]; + qnetPlayer->m_smallId = smallId; + if (smallId == 0) + { + qnetPlayer->m_isHostPlayer = true; + qnetPlayer->m_isRemote = !m_pIQNet->IsHost(); + } + else + { + const BYTE localSmallId = WinsockNetLayer::GetLocalSmallId(); + const bool isLocalSlot = (!m_pIQNet->IsHost() && (smallId == localSmallId)); + qnetPlayer->m_isHostPlayer = false; + qnetPlayer->m_isRemote = !isLocalSlot; + } + if (smallId >= IQNet::s_playerCount) + { + IQNet::s_playerCount = smallId + 1; + } + app.DebugPrintf("Win64 LAN: Materialized player slot from XUID (smallId=%u, isRemote=%d)\n", + (unsigned int)smallId, + qnetPlayer->m_isRemote ? 1 : 0); + } + } + } + } +#endif + + if (qnetPlayer == NULL) + { + return NULL; + } + + INetworkPlayer *networkPlayer = getNetworkPlayer(qnetPlayer); +#ifdef _WINDOWS64 + if (networkPlayer == NULL) + { + NotifyPlayerJoined(qnetPlayer); + networkPlayer = getNetworkPlayer(qnetPlayer); + app.DebugPrintf("Win64 LAN: Lazily created network player from XUID (smallId=%u)\n", + (unsigned int)qnetPlayer->GetSmallId()); + } +#endif + return networkPlayer; } INetworkPlayer * CPlatformNetworkManagerStub::GetPlayerBySmallId(unsigned char smallId) { - return getNetworkPlayer(m_pIQNet->GetPlayerBySmallId(smallId)); + IQNetPlayer *qnetPlayer = m_pIQNet->GetPlayerBySmallId(smallId); + if (qnetPlayer == NULL) + { + return NULL; + } + + INetworkPlayer *networkPlayer = getNetworkPlayer(qnetPlayer); +#ifdef _WINDOWS64 + if (networkPlayer == NULL && smallId != 0 && !m_pIQNet->IsHost()) + { + const BYTE localSmallId = WinsockNetLayer::GetLocalSmallId(); + const bool isLocalSlot = (smallId == localSmallId); + qnetPlayer->m_smallId = smallId; + qnetPlayer->m_isHostPlayer = false; + qnetPlayer->m_isRemote = !isLocalSlot; + if (smallId >= IQNet::s_playerCount) + { + IQNet::s_playerCount = smallId + 1; + } + + // On Win64 clients we may receive player/entity packets for remote users before a + // platform-layer "player joined" callback has been issued for that slot. + NotifyPlayerJoined(qnetPlayer); + networkPlayer = getNetworkPlayer(qnetPlayer); + app.DebugPrintf("Win64 LAN: Lazily created network player for smallId=%u\n", (unsigned int)smallId); + } +#endif + return networkPlayer; } INetworkPlayer *CPlatformNetworkManagerStub::GetHostPlayer() @@ -857,3 +1054,39 @@ bool CPlatformNetworkManagerStub::IsReadyToPlayOrIdle() { return true; } + +#ifdef _WINDOWS64 +void CPlatformNetworkManagerStub::QueueServerTransfer(const char *hostIp, int hostPort) +{ + if (hostIp == NULL || hostIp[0] == 0 || hostPort <= 0) + { + return; + } + + strncpy_s(m_transferHostIp, sizeof(m_transferHostIp), hostIp, _TRUNCATE); + m_transferHostPort = hostPort; + m_bTransferPending = true; + m_bTransferLeaving = false; + m_transferSuppressErrorsUntilMs = GetNowMs64() + 10000ULL; + + app.DebugPrintf("Win64 LAN: Queued server transfer target %s:%d\n", m_transferHostIp, m_transferHostPort); +} + +void CPlatformNetworkManagerStub::RequestServerTransfer(const char *hostIp, int hostPort) +{ + if (g_pPlatformNetworkManager == NULL) + { + return; + } + + g_pPlatformNetworkManager->QueueServerTransfer(hostIp, hostPort); +} + +bool CPlatformNetworkManagerStub::IsServerTransferInProgress() +{ + return (g_pPlatformNetworkManager != NULL) && + (g_pPlatformNetworkManager->m_bTransferPending || + g_pPlatformNetworkManager->m_bTransferLeaving || + (GetNowMs64() < g_pPlatformNetworkManager->m_transferSuppressErrorsUntilMs)); +} +#endif diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h index 919efd7..628f54f 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.h @@ -48,6 +48,10 @@ public: virtual bool IsPrivateGame() { return m_bIsPrivateGame; } virtual bool IsLeavingGame() { return m_bLeavingGame; } virtual void ResetLeavingGame() { m_bLeavingGame = false; } +#ifdef _WINDOWS64 + static void RequestServerTransfer(const char *hostIp, int hostPort); + static bool IsServerTransferInProgress(); +#endif virtual void RegisterPlayerChangedCallback(int iPad, void (*callback)(void *callbackParam, INetworkPlayer *pPlayer, bool leaving), void *callbackParam); virtual void UnRegisterPlayerChangedCallback(int iPad, void (*callback)(void *callbackParam, INetworkPlayer *pPlayer, bool leaving), void *callbackParam); @@ -61,6 +65,9 @@ private: virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom); virtual void _HostGame(int dwUsersMask, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual bool _StartGame(); +#ifdef _WINDOWS64 + void QueueServerTransfer(const char *hostIp, int hostPort); +#endif IQNet * m_pIQNet; // pointer to QNet interface @@ -72,6 +79,13 @@ private: bool m_bLeaveGameOnTick; bool m_migrateHostOnLeave; bool m_bHostChanged; +#ifdef _WINDOWS64 + bool m_bTransferPending; + bool m_bTransferLeaving; + char m_transferHostIp[64]; + int m_transferHostPort; + unsigned long long m_transferSuppressErrorsUntilMs; +#endif bool m_bIsOfflineGame; bool m_bIsPrivateGame; @@ -166,6 +180,17 @@ public: void NotifyPlayerLeaving( IQNetPlayer *pQNetPlayer ); #ifndef _XBOX - void FakeLocalPlayerJoined() { NotifyPlayerJoined(m_pIQNet->GetLocalPlayerByUserIndex(0)); } + void FakeLocalPlayerJoined() + { + IQNetPlayer *player = m_pIQNet->GetLocalPlayerByUserIndex(0); + if (player == NULL) + { + // Dedicated/headless host can suppress slot 0 from "active local player" + // queries. Fall back to the canonical host slot so platform bookkeeping + // can still be initialized. + player = &IQNet::m_player[0]; + } + NotifyPlayerJoined(player); + } #endif }; diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp index dac2f6e..97e7e9d 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp @@ -15,9 +15,6 @@ #ifdef _WINDOWS64 #include "..\..\KeyboardMouseInput.h" -#include "UI.h" - -SavedInventoryCursorPos g_savedInventoryCursorPos = { 0.0f, 0.0f, false }; #endif IUIScene_AbstractContainerMenu::IUIScene_AbstractContainerMenu() @@ -473,26 +470,20 @@ void IUIScene_AbstractContainerMenu::onMouseTick() #ifdef _WINDOWS64 if (!g_KBMInput.IsMouseGrabbed()) { - 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) + int dx = g_KBMInput.GetMouseDeltaX(); + int dy = g_KBMInput.GetMouseDeltaY(); + if (dx != 0 || dy != 0) { - float scaleX = (float)getMovieWidth() / (float)winW; - float scaleY = (float)getMovieHeight() / (float)winH; + float sensitivity = (float)app.GetGameSettings(iPad, eGameSetting_Sensitivity_InMenu) / 100.0f; + float mouseScale = sensitivity * 1.0f; + vPointerPos.x += (float)dx * mouseScale; + vPointerPos.y += (float)dy * mouseScale; + + if (vPointerPos.x < m_fPointerMinX) vPointerPos.x = m_fPointerMinX; + else if (vPointerPos.x > m_fPointerMaxX) vPointerPos.x = m_fPointerMaxX; + if (vPointerPos.y < m_fPointerMinY) vPointerPos.y = m_fPointerMinY; + else if (vPointerPos.y > m_fPointerMaxY) vPointerPos.y = m_fPointerMaxY; - vPointerPos.x += (float)deltaX * scaleX; - vPointerPos.y += (float)deltaY * scaleY; - } - - if (deltaX != 0 || deltaY != 0) - { bStickInput = true; } } @@ -715,11 +706,7 @@ void IUIScene_AbstractContainerMenu::onMouseTick() // If there is no stick input, and we are over a slot, then snap pointer to slot centre. // 4J - TomK - only if this particular component allows so! -#ifdef _WINDOWS64 - if(g_KBMInput.IsMouseGrabbed() && CanHaveFocus(eSectionUnderPointer)) -#else if(CanHaveFocus(eSectionUnderPointer)) -#endif { vPointerPos.x = vSnapPos.x; vPointerPos.y = vSnapPos.y; @@ -727,8 +714,7 @@ void IUIScene_AbstractContainerMenu::onMouseTick() } } - - // Clamp to pointer extents + // Clamp to pointer extents. if ( vPointerPos.x < m_fPointerMinX ) vPointerPos.x = m_fPointerMinX; else if ( vPointerPos.x > m_fPointerMaxX ) vPointerPos.x = m_fPointerMaxX; if ( vPointerPos.y < m_fPointerMinY ) vPointerPos.y = m_fPointerMinY; @@ -1250,15 +1236,8 @@ void IUIScene_AbstractContainerMenu::onMouseTick() // Offset back to image top left. -#ifdef _WINDOWS64 - if (g_KBMInput.IsMouseGrabbed()) - { -#endif - vPointerPos.x -= m_fPointerImageOffsetX; - vPointerPos.y -= m_fPointerImageOffsetY; -#ifdef _WINDOWS64 - } -#endif + vPointerPos.x -= m_fPointerImageOffsetX; + vPointerPos.y -= m_fPointerImageOffsetY; // Update pointer position. // 4J-PB - do not allow sub pixel positions or we get broken lines in box edges diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h index 54e716e..bdb8bb4 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h @@ -1,15 +1,5 @@ #pragma once -#ifdef _WINDOWS64 -struct SavedInventoryCursorPos -{ - float x; - float y; - bool hasSavedPos; -}; -extern SavedInventoryCursorPos g_savedInventoryCursorPos; -#endif - // Uncomment to enable tap input detection to jump 1 slot. Doesn't work particularly well yet, and I feel the system does not need it. // Would probably be required if we decide to slow down the pointer movement. // 4J Stu - There was a request to be able to navigate the scenes with the dpad, so I have used much of the TAP_DETECTION @@ -230,6 +220,4 @@ protected: public: virtual int getPad() = 0; - virtual int getMovieWidth() = 0; - virtual int getMovieHeight() = 0; }; diff --git a/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp index 79203e7..508a017 100644 --- a/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp @@ -10,6 +10,9 @@ #include "..\..\TexturePack.h" #include "..\..\DLCTexturePack.h" #include "..\..\..\Minecraft.World\StringHelpers.h" +#ifdef _WINDOWS64 +#include "..\Network\PlatformNetworkManagerStub.h" +#endif int IUIScene_PauseMenu::ExitGameDialogReturned(void *pParam,int iPad,C4JStorage::EMessageResult result) @@ -393,6 +396,14 @@ int IUIScene_PauseMenu::ExitWorldThreadProc( void* lpParameter ) // This function performs the meat of exiting from a level. It should be called from a thread other than the main thread. void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter) { +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + app.DebugPrintf("Win64 LAN: Skipping _ExitWorld teardown during server transfer\n"); + return; + } +#endif + Minecraft *pMinecraft=Minecraft::GetInstance(); int exitReasonStringId = pMinecraft->progressRenderer->getCurrentTitle(); @@ -688,4 +699,4 @@ int IUIScene_PauseMenu::DisableAutosaveDialogReturned(void *pParam,int iPad,C4JS app.SetAction(iPad,eAppAction_SaveGame); } return 0; -} \ No newline at end of file +} diff --git a/Minecraft.Client/Common/UI/UIBitmapFont.cpp b/Minecraft.Client/Common/UI/UIBitmapFont.cpp index ec49eea..4469e9a 100644 --- a/Minecraft.Client/Common/UI/UIBitmapFont.cpp +++ b/Minecraft.Client/Common/UI/UIBitmapFont.cpp @@ -95,9 +95,14 @@ UIBitmapFont::UIBitmapFont( SFontData &sfontdata ) : UIAbstractBitmapFont( sfontdata.m_strFontName ) { m_numGlyphs = sfontdata.m_uiGlyphCount; + m_cFontData = NULL; BufferedImage bimg(sfontdata.m_wstrFilename); int *bimgData = bimg.getData(); + if (bimgData == NULL) + { + app.DebugPrintf("UIBitmapFont: missing texture data for '%s'\n", sfontdata.m_strFontName.c_str()); + } m_cFontData = new CFontData(sfontdata, bimgData); @@ -106,12 +111,27 @@ UIBitmapFont::UIBitmapFont( SFontData &sfontdata ) UIBitmapFont::~UIBitmapFont() { - m_cFontData->release(); + if (m_cFontData) + { + m_cFontData->release(); + delete m_cFontData; + m_cFontData = NULL; + } } //Callback function type for returning vertical font metrics IggyFontMetrics *UIBitmapFont::GetFontMetrics(IggyFontMetrics *metrics) { + if (metrics == NULL) + { + return NULL; + } + if (m_cFontData == NULL || m_cFontData->getFontData() == NULL) + { + memset(metrics, 0, sizeof(*metrics)); + return metrics; + } + //Description // Vertical metrics for a font //Members @@ -138,6 +158,11 @@ IggyFontMetrics *UIBitmapFont::GetFontMetrics(IggyFontMetrics *metrics) //Callback function type for mapping 32-bit unicode code point to internal font glyph number; use IGGY_GLYPH_INVALID to mean "invalid character" S32 UIBitmapFont::GetCodepointGlyph(U32 codepoint) { + if (m_cFontData == NULL) + { + return 0; + } + // 4J-JEV: Change "right single quotation marks" to apostrophies. if (codepoint == 0x2019) codepoint = 0x27; @@ -147,6 +172,16 @@ S32 UIBitmapFont::GetCodepointGlyph(U32 codepoint) //Callback function type for returning horizontal metrics for each glyph IggyGlyphMetrics * UIBitmapFont::GetGlyphMetrics(S32 glyph,IggyGlyphMetrics *metrics) { + if (metrics == NULL) + { + return NULL; + } + if (m_cFontData == NULL || m_cFontData->getFontData() == NULL) + { + memset(metrics, 0, sizeof(*metrics)); + return metrics; + } + // 4J-JEV: Information about 'Glyph Metrics'. // http://freetype.sourceforge.net/freetype2/docs/glyphs/glyphs-3.html - Overview. // http://en.wikipedia.org/wiki/Kerning#Kerning_values - 'Font Units' @@ -196,6 +231,10 @@ IggyGlyphMetrics * UIBitmapFont::GetGlyphMetrics(S32 glyph,IggyGlyphMetrics *met //Callback function type that should return true iff the glyph has no visible elements rrbool UIBitmapFont::IsGlyphEmpty (S32 glyph) { + if (m_cFontData == NULL) + { + return true; + } if (m_cFontData->glyphIsWhitespace(glyph)) return true; return false;//app.DebugPrintf("Is glyph %d empty? %s\n",glyph,isEmpty?"TRUE":"FALSE"); } @@ -214,6 +253,10 @@ F32 UIBitmapFont::GetKerningForGlyphPair(S32 first_glyph,S32 second_glyph) //Callback function type used for reporting whether a bitmap supports a given glyph at the given scale rrbool UIBitmapFont::CanProvideBitmap(S32 glyph,F32 pixel_scale) { + if (m_cFontData == NULL || m_cFontData->getFontData() == NULL) + { + return false; + } //app.DebugPrintf("Can provide bitmap for glyph %d at scale %f? %s\n",glyph,pixel_scale,canProvideBitmap?"TRUE":"FALSE"); return true; } @@ -226,6 +269,16 @@ rrbool UIBitmapFont::CanProvideBitmap(S32 glyph,F32 pixel_scale) // bitmap The structure to store the bitmap into rrbool UIBitmapFont::GetGlyphBitmap(S32 glyph,F32 pixel_scale,IggyBitmapCharacter *bitmap) { + if (bitmap == NULL) + { + return false; + } + if (m_cFontData == NULL || m_cFontData->getFontData() == NULL) + { + memset(bitmap, 0, sizeof(*bitmap)); + return false; + } + //Description // Data structure used to return to Iggy the bitmap to use for a glyph //Members diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index ffcc293..2978d14 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -10,7 +10,6 @@ #include "..\..\..\Minecraft.World\net.minecraft.world.entity.boss.enderdragon.h" #ifdef _WINDOWS64 #include "..\..\KeyboardMouseInput.h" -#include "UIControl_Slider.h" #endif #include "..\..\EnderDragonRenderer.h" #include "..\..\MultiPlayerLocalPlayer.h" @@ -690,100 +689,6 @@ void UIController::tickInput() else #endif { -#ifdef _WINDOWS64 - if (!g_KBMInput.IsMouseGrabbed()) - { - UIScene *pScene = NULL; - for (int grp = 0; grp < eUIGroup_COUNT && !pScene; ++grp) - { - pScene = m_groups[grp]->GetTopScene(eUILayer_Debug); - if (!pScene) pScene = m_groups[grp]->GetTopScene(eUILayer_Tooltips); - if (!pScene) pScene = m_groups[grp]->GetTopScene(eUILayer_Error); - if (!pScene) pScene = m_groups[grp]->GetTopScene(eUILayer_Alert); - if (!pScene) pScene = m_groups[grp]->GetTopScene(eUILayer_Popup); - if (!pScene) pScene = m_groups[grp]->GetTopScene(eUILayer_Fullscreen); - if (!pScene) pScene = m_groups[grp]->GetTopScene(eUILayer_Scene); - } - if (pScene && pScene->getMovie()) - { - Iggy *movie = pScene->getMovie(); - F32 mouseX = (F32)g_KBMInput.GetMouseX(); - F32 mouseY = (F32)g_KBMInput.GetMouseY(); - - extern HWND g_hWnd; - if (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) - { - mouseX = mouseX * (m_fScreenWidth / (F32)winW); - mouseY = mouseY * (m_fScreenHeight / (F32)winH); - } - } - - IggyFocusHandle currentFocus = IGGY_FOCUS_NULL; - IggyFocusableObject focusables[64]; - S32 numFocusables = 0; - IggyPlayerGetFocusableObjects(movie, ¤tFocus, focusables, 64, &numFocusables); - - IggyFocusHandle hitObject = IGGY_FOCUS_NULL; - for (S32 i = 0; i < numFocusables; ++i) - { - if (mouseX >= focusables[i].x0 && mouseX <= focusables[i].x1 && - mouseY >= focusables[i].y0 && mouseY <= focusables[i].y1) - { - hitObject = focusables[i].object; - break; - } - } - - if (hitObject != IGGY_FOCUS_NULL && hitObject != currentFocus) - { - IggyPlayerSetFocusRS(movie, hitObject, 0); - } - - if (g_KBMInput.IsMouseButtonDown(0) || g_KBMInput.IsMouseButtonPressed(0)) - { - vector *controls = pScene->GetControls(); - if (controls) - { - for (size_t i = 0; i < controls->size(); i++) - { - UIControl *ctrl = (*controls)[i]; - if (ctrl && ctrl->getControlType() == UIControl::eSlider && ctrl->getVisible()) - { - S32 cx = ctrl->getXPos(); - S32 cy = ctrl->getYPos(); - S32 cw = ctrl->getWidth(); - S32 ch = ctrl->getHeight(); - - if (mouseX >= cx && mouseX <= cx + cw && - mouseY >= cy && mouseY <= cy + ch) - { - UIControl_Slider *pSlider = (UIControl_Slider *)ctrl; - float fNewSliderPos = (mouseX - (float)cx) / (float)pSlider->GetRealWidth(); - if (fNewSliderPos < 0.0f) fNewSliderPos = 0.0f; - if (fNewSliderPos > 1.0f) fNewSliderPos = 1.0f; - pSlider->SetSliderTouchPos(fNewSliderPos); - break; - } - } - } - } - } - } - - - int wheel = g_KBMInput.GetMouseWheel(); - if (wheel > 0) - handleKeyPress(0, ACTION_MENU_UP); - else if (wheel < 0) - handleKeyPress(0, ACTION_MENU_DOWN); - } -#endif handleInput(); ++m_accumulatedTicks; } @@ -3145,4 +3050,4 @@ void UIController::SendTouchInput(unsigned int iPad, unsigned int key, bool bPre } -#endif +#endif \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIFontData.cpp b/Minecraft.Client/Common/UI/UIFontData.cpp index c5ad46e..a4bb644 100644 --- a/Minecraft.Client/Common/UI/UIFontData.cpp +++ b/Minecraft.Client/Common/UI/UIFontData.cpp @@ -148,24 +148,41 @@ CFontData::CFontData() m_sFontData = NULL; m_kerningTable = NULL; m_pbRawImage = NULL; + m_pfAdvanceTable = NULL; } CFontData::CFontData(SFontData &sFontData, int *pbRawImage) : m_unicodeMap( sFontData.m_uiGlyphCount + 2 ) { this->m_sFontData = &sFontData; + m_kerningTable = NULL; + m_pbRawImage = NULL; + m_pfAdvanceTable = NULL; // INITIALISE ALPHA CHANNEL // // Glyph Archive (1Byte per pixel). unsigned int archiveSize = sFontData.m_uiGlyphMapX * sFontData.m_uiGlyphMapY; + if (archiveSize == 0) + { + app.DebugPrintf("CFontData: invalid archive size for '%s'\n", sFontData.m_strFontName.c_str()); + return; + } this->m_pbRawImage = new unsigned char[archiveSize]; - // 4J-JEV: Take the alpha channel from each pixel. - for (unsigned int i = 0; i < archiveSize; i++) + if (pbRawImage == NULL) { - this->m_pbRawImage[i] = (pbRawImage[i] & 0xFF000000) >> 24; + app.DebugPrintf("CFontData: raw font image missing for '%s', using blank fallback\n", sFontData.m_strFontName.c_str()); + memset(this->m_pbRawImage, 0, archiveSize); + } + else + { + // 4J-JEV: Take the alpha channel from each pixel. + for (unsigned int i = 0; i < archiveSize; i++) + { + this->m_pbRawImage[i] = (pbRawImage[i] & 0xFF000000) >> 24; + } } // CREATE UNICODE MAP // @@ -267,10 +284,17 @@ void CFontData::release() delete [] m_kerningTable; delete [] m_pfAdvanceTable; delete [] m_pbRawImage; + m_kerningTable = NULL; + m_pfAdvanceTable = NULL; + m_pbRawImage = NULL; } const string CFontData::getFontName() { + if (m_sFontData == NULL) + { + return ""; + } return m_sFontData->m_strFontName; } @@ -289,11 +313,19 @@ unsigned short CFontData::getGlyphId(unsigned int unicodepoint) unsigned int CFontData::getUnicode(unsigned short glyphId) { + if (m_sFontData == NULL || glyphId >= m_sFontData->m_uiGlyphCount) + { + return 0; + } return m_sFontData->Codepoints[glyphId]; } unsigned char *CFontData::topLeftPixel(int row, int col) { + if (m_pbRawImage == NULL || m_sFontData == NULL) + { + return NULL; + } unsigned char *out = m_pbRawImage; moveCursor(out, col * m_sFontData->m_uiGlyphWidth, row* m_sFontData->m_uiGlyphHeight); return out; @@ -301,22 +333,40 @@ unsigned char *CFontData::topLeftPixel(int row, int col) void CFontData::getPos(unsigned short glyphId, int &rowOut, int &colOut) { + if (m_sFontData == NULL || m_sFontData->m_uiGlyphMapCols == 0) + { + rowOut = 0; + colOut = 0; + return; + } rowOut = glyphId / m_sFontData->m_uiGlyphMapCols; colOut = glyphId % m_sFontData->m_uiGlyphMapCols; } float CFontData::getAdvance(unsigned short glyphId) { + if (m_pfAdvanceTable == NULL || m_sFontData == NULL || glyphId >= m_sFontData->m_uiGlyphCount) + { + return 0.0f; + } return m_pfAdvanceTable[glyphId]; } int CFontData::getWidth(unsigned short glyphId) { + if (m_kerningTable == NULL || m_sFontData == NULL || glyphId >= m_sFontData->m_uiGlyphCount) + { + return 0; + } return m_kerningTable[glyphId]; } bool CFontData::glyphIsWhitespace(unsigned short glyphId) { + if (m_sFontData == NULL || glyphId >= m_sFontData->m_uiGlyphCount) + { + return true; + } return unicodeIsWhitespace( getUnicode(glyphId) ); } @@ -337,5 +387,9 @@ bool CFontData::unicodeIsWhitespace(unsigned int unicode) void CFontData::moveCursor(unsigned char *&cursor, unsigned int dx, unsigned int dy) { + if (m_sFontData == NULL || cursor == NULL) + { + return; + } cursor += (dy * m_sFontData->m_uiGlyphMapX) + dx; } diff --git a/Minecraft.Client/Common/UI/UIGroup.cpp b/Minecraft.Client/Common/UI/UIGroup.cpp index 1899d05..1a14361 100644 --- a/Minecraft.Client/Common/UI/UIGroup.cpp +++ b/Minecraft.Client/Common/UI/UIGroup.cpp @@ -12,6 +12,7 @@ UIGroup::UIGroup(EUIGroup group, int iPad) m_bIgnorePlayerJoinMenuDisplayed = false; m_updateFocusStateCountdown = 0; + m_viewportType = C4JRender::VIEWPORT_TYPE_FULLSCREEN; for(unsigned int i = 0; i < eUILayer_COUNT; ++i) { @@ -31,16 +32,12 @@ UIGroup::UIGroup(EUIGroup group, int iPad) m_tutorialPopup = (UIComponent_TutorialPopup *)m_layers[(int)eUILayer_Popup]->addComponent(m_iPad, eUIComponent_TutorialPopup); m_hud = (UIScene_HUD *)m_layers[(int)eUILayer_HUD]->addComponent(m_iPad, eUIScene_HUD); - - //m_layers[(int)eUILayer_Chat]->addComponent(m_iPad, eUIComponent_Chat); } else { m_pressStartToPlay = (UIComponent_PressStartToPlay *)m_layers[(int)eUILayer_Tooltips]->addComponent(0, eUIComponent_PressStartToPlay); } - m_viewportType = C4JRender::VIEWPORT_TYPE_FULLSCREEN; - // 4J Stu - Pre-allocate this for cached rendering in scenes. It's horribly slow to do dynamically, but we should only need one // per group as we will only be displaying one of these types of scenes at a time m_commandBufferList = MemoryTracker::genLists(1); @@ -419,3 +416,4 @@ UIScene *UIGroup::FindScene(EUIScene sceneType) return pScene; } + diff --git a/Minecraft.Client/Common/UI/UIScene.h b/Minecraft.Client/Common/UI/UIScene.h index 982b472..823c510 100644 --- a/Minecraft.Client/Common/UI/UIScene.h +++ b/Minecraft.Client/Common/UI/UIScene.h @@ -109,8 +109,6 @@ public: #ifdef __PSVITA__ UILayer *GetParentLayer() {return m_parentLayer;} EUIGroup GetParentLayerGroup() {return m_parentLayer->m_parentGroup->GetGroup();} -#endif -#if defined(__PSVITA__) || defined(_WINDOWS64) vector *GetControls() {return &m_controls;} #endif diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp index 427567b..a1bd827 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp @@ -2,10 +2,6 @@ #include "UI.h" #include "UIScene_AbstractContainerMenu.h" -#ifdef _WINDOWS64 -#include "..\..\KeyboardMouseInput.h" -#endif - #include "..\..\..\Minecraft.World\net.minecraft.world.inventory.h" #include "..\..\..\Minecraft.World\net.minecraft.world.item.h" #include "..\..\MultiplayerLocalPlayer.h" @@ -39,15 +35,6 @@ UIScene_AbstractContainerMenu::~UIScene_AbstractContainerMenu() void UIScene_AbstractContainerMenu::handleDestroy() { app.DebugPrintf("UIScene_AbstractContainerMenu::handleDestroy\n"); - -#ifdef _WINDOWS64 - g_savedInventoryCursorPos.x = m_pointerPos.x; - g_savedInventoryCursorPos.y = m_pointerPos.y; - g_savedInventoryCursorPos.hasSavedPos = true; - - g_KBMInput.SetCursorHiddenForUI(false); -#endif - Minecraft *pMinecraft = Minecraft::GetInstance(); if( pMinecraft->localgameModes[m_iPad] != NULL ) { @@ -72,7 +59,6 @@ void UIScene_AbstractContainerMenu::handleDestroy() ui.OverrideSFX(m_iPad,ACTION_MENU_RIGHT,false); ui.OverrideSFX(m_iPad,ACTION_MENU_UP,false); ui.OverrideSFX(m_iPad,ACTION_MENU_DOWN,false); - } void UIScene_AbstractContainerMenu::InitDataAssociations(int iPad, AbstractContainerMenu *menu, int startIndex) @@ -81,9 +67,6 @@ void UIScene_AbstractContainerMenu::InitDataAssociations(int iPad, AbstractConta void UIScene_AbstractContainerMenu::PlatformInitialize(int iPad, int startIndex) { -#ifdef _WINDOWS64 - g_KBMInput.SetCursorHiddenForUI(true); -#endif m_labelInventory.init( app.GetString(IDS_INVENTORY) ); @@ -123,8 +106,8 @@ void UIScene_AbstractContainerMenu::PlatformInitialize(int iPad, int startIndex) #ifdef __ORBIS__ // we need to map the touchpad rectangle to the UI rectangle. While it works great for the creative menu, it is much too sensitive for the smaller menus. //X coordinate of the touch point (0 to 1919) - //Y coordinate of the touch point (0 to 941: DUALSHOCK�4 wireless controllers and the CUH-ZCT1J/CAP-ZCT1J/CAP-ZCT1U controllers for the PlayStation�4 development tool, - //0 to 753: JDX-1000x series controllers for the PlayStation�4 development tool,) + //Y coordinate of the touch point (0 to 941: DUALSHOCK®4 wireless controllers and the CUH-ZCT1J/CAP-ZCT1J/CAP-ZCT1U controllers for the PlayStation®4 development tool, + //0 to 753: JDX-1000x series controllers for the PlayStation®4 development tool,) m_fTouchPadMulX=fPanelWidth/1919.0f; m_fTouchPadMulY=fPanelHeight/941.0f; m_fTouchPadDeadZoneX=15.0f*m_fTouchPadMulX; @@ -168,30 +151,17 @@ void UIScene_AbstractContainerMenu::PlatformInitialize(int iPad, int startIndex) //m_pointerControl->SetPosition( &vPointerPos ); m_pointerPos = vPointerPos; -#ifdef _WINDOWS64 - if (g_savedInventoryCursorPos.hasSavedPos) - { - m_pointerPos.x = g_savedInventoryCursorPos.x; - m_pointerPos.y = g_savedInventoryCursorPos.y; - - if (m_pointerPos.x < m_fPointerMinX) m_pointerPos.x = m_fPointerMinX; - if (m_pointerPos.x > m_fPointerMaxX) m_pointerPos.x = m_fPointerMaxX; - if (m_pointerPos.y < m_fPointerMinY) m_pointerPos.y = m_fPointerMinY; - if (m_pointerPos.y > m_fPointerMaxY) m_pointerPos.y = m_fPointerMaxY; - } -#endif - IggyEvent mouseEvent; S32 width, height; m_parentLayer->getRenderDimensions(width, height); S32 x = m_pointerPos.x*((float)width/m_movieWidth); - S32 y = m_pointerPos.y*((float)height/m_movieHeight); + S32 y = m_pointerPos.y*((float)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; @@ -208,10 +178,8 @@ void UIScene_AbstractContainerMenu::tick() IggyEvent mouseEvent; S32 width, height; m_parentLayer->getRenderDimensions(width, height); - - S32 x = (S32)(m_pointerPos.x * ((float)width / m_movieWidth)); - S32 y = (S32)(m_pointerPos.y * ((float)height / m_movieHeight)); - + S32 x = m_pointerPos.x*((float)width/m_movieWidth); + S32 y = m_pointerPos.y*((float)height/m_movieHeight); IggyMakeEventMouseMove( &mouseEvent, x, y); // 4J Stu - This seems to be broken on Durango, so do it ourself @@ -224,7 +192,6 @@ void UIScene_AbstractContainerMenu::tick() IggyPlayerDispatchEventRS ( getMovie() , &mouseEvent , &result ); } - void UIScene_AbstractContainerMenu::render(S32 width, S32 height, C4JRender::eViewportType viewpBort) { m_cacheSlotRenders = true; diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h index c9491d6..b98b376 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h @@ -36,8 +36,6 @@ public: virtual void handleDestroy(); int getPad() { return m_iPad; } - int getMovieWidth() { return m_movieWidth; } - int getMovieHeight() { return m_movieHeight; } bool getIgnoreInput() { return m_bIgnoreInput; } void setIgnoreInput(bool bVal) { m_bIgnoreInput=bVal; } diff --git a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp index 0ea99a3..4116919 100644 --- a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp +++ b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp @@ -2,6 +2,9 @@ #include "UI.h" #include "UIScene_ConnectingProgress.h" #include "..\..\Minecraft.h" +#ifdef _WINDOWS64 +#include "..\Network\PlatformNetworkManagerStub.h" +#endif UIScene_ConnectingProgress::UIScene_ConnectingProgress(int iPad, void *_initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { @@ -113,6 +116,16 @@ void UIScene_ConnectingProgress::handleTimerComplete(int id) // Check if the connection failed Minecraft *pMinecraft = Minecraft::GetInstance(); +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + pMinecraft->m_connectionFailed[m_iPad] = false; + pMinecraft->m_connectionFailedReason[m_iPad] = DisconnectPacket::eDisconnect_None; + addTimer(0, m_timerTime); + return; + } +#endif + if( pMinecraft->m_connectionFailed[m_iPad] || !g_NetworkManager.IsInSession() ) { @@ -132,6 +145,9 @@ void UIScene_ConnectingProgress::handleTimerComplete(int id) case DisconnectPacket::eDisconnect_Kicked: exitReasonStringId = IDS_DISCONNECTED_KICKED; break; + case DisconnectPacket::eDisconnect_Banned: + exitReasonStringId = IDS_DISCONNECTED_BANNED; + break; case DisconnectPacket::eDisconnect_NoUGC_AllLocal: exitReasonStringId = IDS_NO_USER_CREATED_CONTENT_PRIVILEGE_ALL_LOCAL; break; diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index ef5a339..5c0e5ae 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -1,8 +1,10 @@ #include "stdafx.h" #include "UI.h" #include "UIScene_LoadOrJoinMenu.h" +#include #include "..\..\..\Minecraft.World\StringHelpers.h" +#include "..\..\..\Minecraft.World\LevelSettings.h" #include "..\..\..\Minecraft.World\net.minecraft.world.item.h" #include "..\..\..\Minecraft.World\net.minecraft.world.level.h" #include "..\..\..\Minecraft.World\net.minecraft.world.level.chunk.storage.h" @@ -11,9 +13,14 @@ #include "..\..\..\Minecraft.World\ConsoleSaveFileSplit.h" #include "..\..\ProgressRenderer.h" #include "..\..\MinecraftServer.h" +#include "..\..\User.h" +#include "..\..\Options.h" #include "..\..\TexturePackRepository.h" #include "..\..\TexturePack.h" #include "..\Network\SessionInfo.h" +#ifdef _WINDOWS64 +#include "..\..\Windows64\Network\WinsockNetLayer.h" +#endif #if defined(__PS3__) || defined(__ORBIS__) || defined(__PSVITA__) #include "Common\Network\Sony\SonyHttp.h" #include "Common\Network\Sony\SonyRemoteStorage.h" @@ -25,6 +32,79 @@ #include "message_dialog.h" #endif +namespace +{ + // Custom, Windows64-only metadata packed into the host settings bitfield so LAN browser rows + // can show the selected LCE minigame type. + const unsigned int MINIGAME_SETTINGS_FLAG = 0x80000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_MASK = 0x70000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_SHIFT = 28u; + const unsigned int MINIGAME_SETTINGS_ROLE_MASK = 0x0C000000u; + const unsigned int MINIGAME_SETTINGS_ROLE_SHIFT = 26u; + const unsigned int MINIGAME_SETTINGS_QUEUE_MASK = 0x03000000u; + const unsigned int MINIGAME_SETTINGS_QUEUE_SHIFT = 24u; + const unsigned int MINIGAME_ROLE_HUB = 0u; + const unsigned int MINIGAME_ROLE_MATCH = 1u; + + const wchar_t *GetQueueLabelFromSettings(unsigned int gameHostSettings) + { + const unsigned int queueMode = (gameHostSettings & MINIGAME_SETTINGS_QUEUE_MASK) >> MINIGAME_SETTINGS_QUEUE_SHIFT; + switch(queueMode) + { + case 0: return L"Solo"; + case 1: return L"Doubles"; + case 2: return L"Squads"; + case 3: return L"Practice"; + default: return L"Unknown"; + } + } + + const wchar_t *GetMinigameNameFromSettings(unsigned int gameHostSettings) + { + if ((gameHostSettings & MINIGAME_SETTINGS_FLAG) == 0) + { + return L"Unknown"; + } + + const unsigned int minigameIndex = (gameHostSettings & MINIGAME_SETTINGS_TYPE_MASK) >> MINIGAME_SETTINGS_TYPE_SHIFT; + switch(minigameIndex) + { + case 0: return L"Battle"; + case 1: return L"Tumble"; + case 2: return L"Glide"; + case 3: + { + const unsigned int role = (gameHostSettings & MINIGAME_SETTINGS_ROLE_MASK) >> MINIGAME_SETTINGS_ROLE_SHIFT; + if (role == MINIGAME_ROLE_MATCH) + { + static wchar_t bedwarsMatchLabel[64]; + swprintf_s(bedwarsMatchLabel, 64, L"Bedwars %ls Match", GetQueueLabelFromSettings(gameHostSettings)); + return bedwarsMatchLabel; + } + return L"Bedwars Hub"; + } + default: return L"Unknown"; + } + } + + wstring TrimWhitespace(const wstring &input) + { + size_t first = 0; + while(first < input.length() && std::iswspace(input[first])) + { + ++first; + } + + size_t last = input.length(); + while(last > first && std::iswspace(input[last - 1])) + { + --last; + } + + return input.substr(first, last - first); + } +} + #ifdef SONY_REMOTE_STORAGE_DOWNLOAD unsigned long UIScene_LoadOrJoinMenu::m_ulFileSize=0L; @@ -119,6 +199,7 @@ UIScene_LoadOrJoinMenu::UIScene_LoadOrJoinMenu(int iPad, void *initData, UILayer m_bAllLoaded = false; m_bRetrievingSaveThumbnails = false; m_bSaveThumbnailReady = false; + m_bMinigamesMode = false; m_bExitScene=false; m_pSaveDetails=NULL; m_bSavesDisplayed=false; @@ -133,6 +214,22 @@ UIScene_LoadOrJoinMenu::UIScene_LoadOrJoinMenu(int iPad, void *initData, UILayer m_bSaveTransferInProgress=false; #endif m_eAction = eAction_None; + m_pDirectConnectSession = NULL; + + LoadOrJoinMenuInitData *loadOrJoinInit = (LoadOrJoinMenuInitData *)initData; + if(loadOrJoinInit != NULL && + loadOrJoinInit->magic == LOAD_OR_JOIN_MENU_INIT_MAGIC && + loadOrJoinInit->bMinigamesMode) + { + m_bMinigamesMode = true; + } + + if(m_bMinigamesMode) + { + m_labelSavesListTitle.init(L"Create Minigame Server"); + m_labelJoinListTitle.init(L"Server Browser"); + m_labelNoGames.init(L"No servers found"); + } m_bMultiplayerAllowed = ProfileManager.IsSignedInLive( m_iPad ) && ProfileManager.AllowedToPlayMultiplayer(m_iPad); @@ -164,7 +261,11 @@ UIScene_LoadOrJoinMenu::UIScene_LoadOrJoinMenu(int iPad, void *initData, UILayer #endif // block input if we're waiting for DLC to install, and wipe the saves list. The end of dlc mounting custom message will fill the list again - if(app.StartInstallDLCProcess(m_iPad)==true || app.DLCInstallPending()) + if(m_bMinigamesMode) + { + Initialise(); + } + else if(app.StartInstallDLCProcess(m_iPad)==true || app.DLCInstallPending()) { // if we're waiting for DLC to mount, don't fill the save list. The custom message on end of dlc mounting will do that m_bIgnoreInput = true; @@ -192,7 +293,7 @@ UIScene_LoadOrJoinMenu::UIScene_LoadOrJoinMenu(int iPad, void *initData, UILayer MinecraftServer::resetFlags(); // If we're not ignoring input, then we aren't still waiting for the DLC to mount, and can now check for corrupt dlc. Otherwise this will happen when the dlc has finished mounting. - if( !m_bIgnoreInput) + if( !m_bIgnoreInput && !m_bMinigamesMode) { app.m_dlcManager.checkForCorruptDLCAndAlert(); } @@ -299,10 +400,24 @@ UIScene_LoadOrJoinMenu::~UIScene_LoadOrJoinMenu() } delete [] m_saveDetails; } + + delete m_pDirectConnectSession; + m_pDirectConnectSession = NULL; } void UIScene_LoadOrJoinMenu::updateTooltips() { + if(m_bMinigamesMode) + { + int iY = -1; + if(DoesGamesListHaveFocus() && m_buttonListGames.getItemCount() > 0) + { + iY = IDS_TOOLTIPS_VIEW_GAMERCARD; + } + ui.SetTooltips( DEFAULT_XUI_MENU_USER, IDS_TOOLTIPS_SELECT, IDS_TOOLTIPS_BACK, -1, iY); + return; + } + // update the tooltips // if the saves list has focus, then we should show the Delete Save tooltip // if the games list has focus, then we should the the View Gamercard tooltip @@ -391,6 +506,12 @@ void UIScene_LoadOrJoinMenu::updateTooltips() // void UIScene_LoadOrJoinMenu::Initialise() { + if(m_bMinigamesMode) + { + InitialiseMinigamesMode(); + return; + } + m_iSaveListIndex = 0; m_iGameListIndex = 0; @@ -445,6 +566,35 @@ void UIScene_LoadOrJoinMenu::Initialise() app.m_dlcManager.checkForCorruptDLCAndAlert(); } +void UIScene_LoadOrJoinMenu::InitialiseMinigamesMode() +{ + app.DebugPrintf("UIScene_LoadOrJoinMenu::InitialiseMinigamesMode\n"); + + m_iSaveListIndex = 0; + m_iGameListIndex = 0; + m_iDefaultButtonsC = 0; + m_iMashUpButtonsC = 0; + m_generators.clear(); + + m_buttonListSaves.clearList(); + m_buttonListSaves.addItem(L"Battle"); + m_buttonListSaves.addItem(L"Tumble"); + m_buttonListSaves.addItem(L"Glide"); + m_buttonListSaves.addItem(L"Bedwars"); + m_buttonListSaves.addItem(L"Direct Connect"); + + m_iDefaultButtonsC = m_buttonListSaves.getItemCount(); + m_controlSavesTimer.setVisible(false); + m_labelNoGames.setVisible(false); + + m_iRequestingThumbnailId = 0; + m_bAllLoaded = true; + m_bSavesDisplayed = true; + m_bRetrievingSaveThumbnails = false; + m_bSaveThumbnailReady = false; + m_bIgnoreInput = false; +} + void UIScene_LoadOrJoinMenu::updateComponents() { m_parentLayer->showComponent(m_iPad,eUIComponent_Panorama,true); @@ -472,6 +622,16 @@ void UIScene_LoadOrJoinMenu::handleGainFocus(bool navBack) // Add load online timer addTimer(JOIN_LOAD_ONLINE_TIMER_ID,JOIN_LOAD_ONLINE_TIMER_TIME); + if(m_bMinigamesMode) + { + if(navBack) + { + m_bIgnoreInput = false; + UpdateGamesList(); + } + return; + } + if(navBack) { app.SetLiveLinkRequired( true ); @@ -962,6 +1122,15 @@ void UIScene_LoadOrJoinMenu::handleInput(int iPad, int key, bool repeat, bool pr } break; case ACTION_MENU_X: + if(m_bMinigamesMode) + { + if(pressed) + { + StartDirectConnectKeyboard(); + handled = true; + } + break; + } #if TO_BE_IMPLEMENTED // Change device // Fix for #12531 - TCR 001: BAS Game Stability: When a player selects to change a storage @@ -1041,6 +1210,10 @@ void UIScene_LoadOrJoinMenu::handleInput(int iPad, int key, bool repeat, bool pr break; case ACTION_MENU_RIGHT_SCROLL: + if(m_bMinigamesMode) + { + break; + } if(DoesSavesListHaveFocus()) { // 4J-PB - check we are on a valid save @@ -1194,6 +1367,139 @@ int UIScene_LoadOrJoinMenu::KeyboardCompleteWorldNameCallback(LPVOID lpParam,boo return 0; } + +void UIScene_LoadOrJoinMenu::StartDirectConnectKeyboard() +{ +#ifdef _WINDOWS64 + m_bIgnoreInput = true; + ui.PlayUISFX(eSFX_Press); + InputManager.RequestKeyboard(L"Direct Connect", L"", (DWORD)m_iPad, 63, &UIScene_LoadOrJoinMenu::KeyboardCompleteDirectConnectCallback, this, C_4JInput::EKeyboardMode_Default); +#else + UINT uiIDA[1]; + uiIDA[0] = IDS_CONFIRM_OK; + ui.RequestMessageBox(IDS_ERROR_NETWORK_TITLE, IDS_ERROR_NETWORK, uiIDA, 1, m_iPad, NULL, NULL, app.GetStringTable()); +#endif +} + +int UIScene_LoadOrJoinMenu::KeyboardCompleteDirectConnectCallback(LPVOID lpParam, bool bRes) +{ + UIScene_LoadOrJoinMenu *pClass = (UIScene_LoadOrJoinMenu *)lpParam; + pClass->m_bIgnoreInput = false; + + if(!bRes) + { + pClass->updateTooltips(); + return 0; + } + + uint16_t ui16Text[128]; + ZeroMemory(ui16Text, sizeof(ui16Text)); + InputManager.GetText(ui16Text); + pClass->HandleDirectConnectInput((const wchar_t *)ui16Text); + return 0; +} + +void UIScene_LoadOrJoinMenu::HandleDirectConnectInput(const wchar_t *inputText) +{ +#ifdef _WINDOWS64 + char hostIp[64]; + int hostPort = WIN64_NET_DEFAULT_PORT; + + if(!ParseDirectConnectAddress(inputText, hostIp, sizeof(hostIp), hostPort)) + { + app.DebugPrintf("UIScene_LoadOrJoinMenu::HandleDirectConnectInput - invalid address input\n"); + UINT uiIDA[1]; + uiIDA[0] = IDS_CONFIRM_OK; + ui.RequestMessageBox(IDS_ERROR_NETWORK_TITLE, IDS_ERROR_NETWORK, uiIDA, 1, m_iPad, NULL, NULL, app.GetStringTable()); + return; + } + + delete m_pDirectConnectSession; + m_pDirectConnectSession = new FriendSessionInfo(); + + wstring label = TrimWhitespace(inputText != NULL ? inputText : L""); + if(label.length() == 0) + { + label = convStringToWstring(string(hostIp)); + } + + size_t labelLen = label.length(); + m_pDirectConnectSession->displayLabel = new wchar_t[labelLen + 1]; + wcscpy_s(m_pDirectConnectSession->displayLabel, labelLen + 1, label.c_str()); + m_pDirectConnectSession->displayLabelLength = (unsigned char)((labelLen > 255) ? 255 : labelLen); + m_pDirectConnectSession->displayLabelViewableStartIndex = 0; + m_pDirectConnectSession->data.isJoinable = true; + m_pDirectConnectSession->data.isReadyToJoin = true; + m_pDirectConnectSession->data.hostPort = hostPort; + m_pDirectConnectSession->data.playerCount = 0; + m_pDirectConnectSession->data.maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + strncpy_s(m_pDirectConnectSession->data.hostIP, sizeof(m_pDirectConnectSession->data.hostIP), hostIp, _TRUNCATE); + + wstring hostName = convStringToWstring(string(hostIp)); + wcsncpy_s(m_pDirectConnectSession->data.hostName, XUSER_NAME_SIZE, hostName.c_str(), _TRUNCATE); + m_pDirectConnectSession->sessionId = 0; + + app.DebugPrintf("UIScene_LoadOrJoinMenu::HandleDirectConnectInput - joining %s:%d\n", hostIp, hostPort); + + m_initData->iPad = 0; + m_initData->selectedSession = m_pDirectConnectSession; + m_controlJoinTimer.setVisible(false); + + m_bIgnoreInput = true; + ui.NavigateToScene(ProfileManager.GetPrimaryPad(), eUIScene_JoinMenu, m_initData); +#else + (void)inputText; +#endif +} + +#ifdef _WINDOWS64 +bool UIScene_LoadOrJoinMenu::ParseDirectConnectAddress(const wchar_t *inputText, char *outHostIp, size_t outHostIpSize, int &outHostPort) +{ + if(inputText == NULL || outHostIp == NULL || outHostIpSize == 0) + { + return false; + } + + outHostIp[0] = '\0'; + outHostPort = WIN64_NET_DEFAULT_PORT; + + wstring trimmedInput = TrimWhitespace(inputText); + if(trimmedInput.length() == 0) + { + return false; + } + + wstring hostPart = trimmedInput; + wstring portPart; + size_t separatorPos = trimmedInput.rfind(L':'); + if(separatorPos != wstring::npos) + { + hostPart = TrimWhitespace(trimmedInput.substr(0, separatorPos)); + portPart = TrimWhitespace(trimmedInput.substr(separatorPos + 1)); + if(hostPart.length() == 0 || portPart.length() == 0) + { + return false; + } + + int parsedPort = _wtoi(portPart.c_str()); + if(parsedPort <= 0 || parsedPort > 65535) + { + return false; + } + outHostPort = parsedPort; + } + + size_t convertedCount = 0; + errno_t convertResult = wcstombs_s(&convertedCount, outHostIp, outHostIpSize, hostPart.c_str(), _TRUNCATE); + if(convertResult != 0 || outHostIp[0] == '\0') + { + return false; + } + + return true; +} +#endif + void UIScene_LoadOrJoinMenu::handleInitFocus(F64 controlId, F64 childId) { app.DebugPrintf(app.USER_SR, "UIScene_LoadOrJoinMenu::handleInitFocus - %d , %d\n", (int)controlId, (int)childId); @@ -1235,11 +1541,24 @@ void UIScene_LoadOrJoinMenu::handlePress(F64 controlId, F64 childId) { m_bIgnoreInput=true; - int lGenID = (int)childId - 1; - //CD - Added for audio ui.PlayUISFX(eSFX_Press); + if(m_bMinigamesMode) + { + if((int)childId == (int)eMinigame_Count) + { + StartDirectConnectKeyboard(); + } + else + { + StartMinigameServer((int)childId); + } + break; + } + + int lGenID = (int)childId - 1; + if((int)childId == JOIN_LOAD_CREATE_BUTTON_INDEX) { app.SetTutorialMode( false ); @@ -1354,8 +1673,10 @@ void UIScene_LoadOrJoinMenu::handlePress(F64 controlId, F64 childId) void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) { - if( m_buttonListGames.getItemCount() > 0 && gameIndex < m_currentSessions->size() ) + if( m_currentSessions != NULL && m_buttonListGames.getItemCount() > 0 && gameIndex >= 0 && gameIndex < (int)m_currentSessions->size() ) { + FriendSessionInfo *selectedSession = m_currentSessions->at(gameIndex); + #if defined(__PS3__) || defined(__ORBIS__) || defined(__PSVITA__) // 4J-PB - is the player allowed to join games? bool noUGC=false; @@ -1472,7 +1793,7 @@ void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) //CScene_MultiGameInfo::JoinMenuInitData *initData = new CScene_MultiGameInfo::JoinMenuInitData(); m_initData->iPad = 0;; - m_initData->selectedSession = m_currentSessions->at( gameIndex ); + m_initData->selectedSession = selectedSession; // check that we have the texture pack available // If it's not the default texture pack @@ -1526,6 +1847,165 @@ void UIScene_LoadOrJoinMenu::CheckAndJoinGame(int gameIndex) } } +void UIScene_LoadOrJoinMenu::StartMinigameServer(int minigameIndex) +{ + static const wchar_t *minigameNames[eMinigame_Count] = + { + L"Battle", + L"Tumble", + L"Glide", + L"Bedwars", + }; + + if(minigameIndex < 0 || minigameIndex >= (int)eMinigame_Count) + { + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - invalid minigame index %d\n", minigameIndex); + m_bIgnoreInput = false; + return; + } + + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - selected %ls (%d)\n", + minigameNames[minigameIndex], minigameIndex); + + int hostPad = m_iPad; + if(hostPad < 0 || !ProfileManager.IsSignedIn(hostPad)) + { + hostPad = ProfileManager.GetPrimaryPad(); + } + + UINT uiIDA[1]; + uiIDA[0] = IDS_OK; + + if(hostPad < 0) + { + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - no signed in host pad\n"); + m_bIgnoreInput = false; + ui.RequestMessageBox(IDS_MUST_SIGN_IN_TITLE, IDS_MUST_SIGN_IN_TEXT, uiIDA, 1, m_iPad, NULL, NULL, app.GetStringTable()); + return; + } + + if(ProfileManager.IsGuest(hostPad)) + { + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - host pad %d is guest\n", hostPad); + m_bIgnoreInput = false; + ui.RequestMessageBox(IDS_PRO_GUESTPROFILE_TITLE, IDS_PRO_GUESTPROFILE_TEXT, uiIDA, 1); + return; + } + + bool isClientSide = ProfileManager.IsSignedInLive(hostPad); +#ifdef __PSVITA__ + if(app.GetGameSettings(ProfileManager.GetPrimaryPad(),eGameSetting_PSVita_NetworkModeAdhoc) == true) + { + CGameNetworkManager::setAdhocMode(true); + isClientSide = SQRNetworkManager_AdHoc_Vita::GetAdhocStatus(); + } + else + { + CGameNetworkManager::setAdhocMode(false); + } +#endif + + if(!isClientSide) + { + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - host pad %d is offline\n", hostPad); + m_bIgnoreInput = false; + ui.RequestMessageBox(IDS_PRO_NOTONLINE_TITLE, IDS_PRO_NOTONLINE_TEXT, uiIDA, 1, hostPad, NULL, NULL, app.GetStringTable()); + return; + } + + Minecraft *pMinecraft = Minecraft::GetInstance(); + ProfileManager.SetLockedProfile(hostPad); + ProfileManager.QuerySigninStatus(); + + pMinecraft->user->name = convStringToWstring(ProfileManager.GetGamertag(hostPad)); + app.ApplyGameSettingsChanged(hostPad); + + const bool launchingBedwarsHub = (minigameIndex == (int)eMinigame_Bedwars); + wstring saveTitle = L"LCE "; + saveTitle += launchingBedwarsHub ? L"Bedwars Hub" : minigameNames[minigameIndex]; + + app.ClearTerrainFeaturePosition(); + StorageManager.ResetSaveData(); + StorageManager.SetSaveTitle(saveTitle.c_str()); + + app.SetTutorialMode(false); + app.setLevelGenerationOptions(NULL); + + int difficulty = 2; + if(pMinecraft->options != NULL) + { + difficulty = pMinecraft->options->difficulty; + } + + app.SetGameHostOption(eGameHostOption_Difficulty, difficulty); + app.SetGameHostOption(eGameHostOption_FriendsOfFriends, 1); + app.SetGameHostOption(eGameHostOption_Gamertags, app.GetGameSettings(hostPad, eGameSetting_GamertagsVisible) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_BedrockFog, app.GetGameSettings(hostPad, eGameSetting_BedrockFog) ? 1 : 0); + + app.SetGameHostOption(eGameHostOption_GameType, GameType::SURVIVAL->getId()); + app.SetGameHostOption(eGameHostOption_LevelType, 1); + app.SetGameHostOption(eGameHostOption_Structures, 0); + app.SetGameHostOption(eGameHostOption_BonusChest, 0); + app.SetGameHostOption(eGameHostOption_DisableSaving, 1); + StorageManager.SetSaveDisabled(true); + + app.SetGameHostOption(eGameHostOption_PvP, launchingBedwarsHub ? 0 : 1); + app.SetGameHostOption(eGameHostOption_TrustPlayers, 0); + app.SetGameHostOption(eGameHostOption_FireSpreads, launchingBedwarsHub ? 0 : 1); + app.SetGameHostOption(eGameHostOption_TNT, launchingBedwarsHub ? 0 : 1); + app.SetGameHostOption(eGameHostOption_HostCanFly, 0); + app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, 0); + app.SetGameHostOption(eGameHostOption_HostCanBeInvisible, 0); + + // Tag the session with a minigame type so the server browser can show it. + unsigned int hostSettings = app.GetGameHostOption(eGameHostOption_All); + hostSettings |= MINIGAME_SETTINGS_FLAG; + hostSettings &= ~MINIGAME_SETTINGS_TYPE_MASK; + hostSettings |= ((unsigned int)(minigameIndex & 0x7) << MINIGAME_SETTINGS_TYPE_SHIFT); + hostSettings &= ~MINIGAME_SETTINGS_ROLE_MASK; + hostSettings |= (MINIGAME_ROLE_HUB << MINIGAME_SETTINGS_ROLE_SHIFT); + hostSettings &= ~MINIGAME_SETTINGS_QUEUE_MASK; + hostSettings |= (0u << MINIGAME_SETTINGS_QUEUE_SHIFT); + app.SetGameHostOption(eGameHostOption_All, hostSettings); + + DWORD dwLocalUsersMask = CGameNetworkManager::GetLocalPlayerMask(hostPad); + if(dwLocalUsersMask == 0) + { + dwLocalUsersMask = CGameNetworkManager::GetLocalPlayerMask(ProfileManager.GetPrimaryPad()); + } + g_NetworkManager.HostGame(dwLocalUsersMask, isClientSide, false, MINECRAFT_NET_MAX_PLAYERS, 0); + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - HostGame called (mask=0x%08X)\n", dwLocalUsersMask); + +#ifndef _XBOX + g_NetworkManager.FakeLocalPlayerJoined(); +#endif + + NetworkGameInitData *param = new NetworkGameInitData(); + param->seed = 0; + param->findSeed = true; + param->saveData = NULL; + param->settings = app.GetGameHostOption(eGameHostOption_All); + param->texturePackId = pMinecraft->getCurrentTexturePackId(); + param->xzSize = LEVEL_MAX_WIDTH; + param->hellScale = HELL_LEVEL_MAX_SCALE; + + LoadingInputParams *loadingParams = new LoadingInputParams(); + loadingParams->func = &CGameNetworkManager::RunNetworkGameThreadProc; + loadingParams->lpParam = (LPVOID)param; + + app.SetAutosaveTimerTime(); + + UIFullscreenProgressCompletionData *completionData = new UIFullscreenProgressCompletionData(); + completionData->bShowBackground = TRUE; + completionData->bShowLogo = TRUE; + completionData->type = e_ProgressCompletion_CloseAllPlayersUIScenes; + completionData->iPad = DEFAULT_XUI_MENU_USER; + loadingParams->completionData = completionData; + + ui.NavigateToScene(hostPad, eUIScene_FullscreenProgress, loadingParams); + app.DebugPrintf("UIScene_LoadOrJoinMenu::StartMinigameServer - navigating to fullscreen progress\n"); +} + void UIScene_LoadOrJoinMenu::LoadLevelGen(LevelGenerationOptions *levelGen) { // Load data from disc @@ -1610,10 +2090,13 @@ void UIScene_LoadOrJoinMenu::UpdateGamesList() FriendSessionInfo *pSelectedSession = NULL; - if(DoesGamesListHaveFocus() && m_buttonListGames.getItemCount() > 0) + if(DoesGamesListHaveFocus() && m_buttonListGames.getItemCount() > 0 && m_currentSessions != NULL) { unsigned int nIndex = m_buttonListGames.getCurrentSelection(); - pSelectedSession = m_currentSessions->at( nIndex ); + if(nIndex < m_currentSessions->size()) + { + pSelectedSession = m_currentSessions->at( nIndex ); + } } SessionID selectedSessionId; @@ -1729,7 +2212,32 @@ void UIScene_LoadOrJoinMenu::UpdateGamesList() } } +#ifdef _WINDOWS64 + wstring rowLabel = sessionInfo->displayLabel != NULL ? sessionInfo->displayLabel : L"Unknown Host"; + + int playerCount = (int)sessionInfo->data.playerCount; + int maxPlayers = (int)sessionInfo->data.maxPlayers; + if(maxPlayers <= 0) + { + maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + } + + wchar_t sessionDetail[96]; + if(m_bMinigamesMode) + { + const wchar_t *minigameName = GetMinigameNameFromSettings(sessionInfo->data.m_uiGameHostSettings); + swprintf(sessionDetail, 96, L" [%d/%d] - %ls", playerCount, maxPlayers, minigameName); + } + else + { + swprintf(sessionDetail, 96, L" [%d/%d]", playerCount, maxPlayers); + } + + rowLabel += sessionDetail; + m_buttonListGames.addItem(rowLabel, textureName); +#else m_buttonListGames.addItem( sessionInfo->displayLabel, textureName ); +#endif if(memcmp( &selectedSessionId, &sessionInfo->sessionId, sizeof(SessionID) ) == 0) { @@ -3529,3 +4037,7 @@ int UIScene_LoadOrJoinMenu::CopySaveErrorDialogFinishedCallback(void *pParam,int } #endif // _XBOX_ONE + + + + diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h index 01d94b0..2118ffe 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.h @@ -42,6 +42,15 @@ private: }; eActions m_eAction; + enum EMinigameButtons + { + eMinigame_Battle = 0, + eMinigame_Tumble, + eMinigame_Glide, + eMinigame_Bedwars, + eMinigame_Count, + }; + static const int JOIN_LOAD_CREATE_BUTTON_INDEX = 0; SaveListDetails *m_saveDetails; @@ -85,6 +94,7 @@ private: bool m_bAllLoaded; bool m_bRetrievingSaveThumbnails; bool m_bSaveThumbnailReady; + bool m_bMinigamesMode; bool m_bShowingPartyGamesOnly; bool m_bInParty; JoinMenuInitData *m_initData; @@ -105,6 +115,7 @@ private: bool m_bSaveTransferCancelled; #endif bool m_bUpdateSaveSize; + FriendSessionInfo *m_pDirectConnectSession; public: UIScene_LoadOrJoinMenu(int iPad, void *initData, UILayer *parentLayer); @@ -132,6 +143,7 @@ public: private: void Initialise(); + void InitialiseMinigamesMode(); void GetSaveInfo(); void UpdateGamesList(); void AddDefaultButtons(); @@ -153,8 +165,15 @@ public: static int DeleteSaveDataReturned(LPVOID lpParam,bool bRes); static int RenameSaveDataReturned(LPVOID lpParam,bool bRes); static int KeyboardCompleteWorldNameCallback(LPVOID lpParam,bool bRes); + static int KeyboardCompleteDirectConnectCallback(LPVOID lpParam,bool bRes); protected: void handlePress(F64 controlId, F64 childId); + void StartMinigameServer(int minigameIndex); + void StartDirectConnectKeyboard(); + void HandleDirectConnectInput(const wchar_t *inputText); +#ifdef _WINDOWS64 + bool ParseDirectConnectAddress(const wchar_t *inputText, char *outHostIp, size_t outHostIpSize, int &outHostPort); +#endif void LoadLevelGen(LevelGenerationOptions *levelGen); void LoadSaveFromDisk(File *saveFile, ESavePlatform savePlatform = SAVE_FILE_PLATFORM_LOCAL); #if defined(__PS3__) || defined(__PSVITA__) || defined(__ORBIS__) diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 7724aaa..f25099b 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -2,8 +2,10 @@ #include "..\..\..\Minecraft.World\Mth.h" #include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\..\Minecraft.World\Random.h" +#include "..\..\..\Minecraft.World\LevelSettings.h" #include "..\..\User.h" #include "..\..\MinecraftServer.h" +#include "..\..\Options.h" #include "UI.h" #include "UIScene_MainMenu.h" #ifdef __ORBIS__ @@ -51,6 +53,7 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye m_bTrialVersion=true; m_buttons[(int)eControl_UnlockOrDLC].init(app.GetString(IDS_UNLOCK_FULL_GAME),eControl_UnlockOrDLC); } + m_buttons[(int)eControl_Minigames].init(L"LCE Minigames", eControl_Minigames); #ifndef _DURANGO m_buttons[(int)eControl_Exit].init(app.GetString(IDS_EXIT_GAME),eControl_Exit); @@ -334,6 +337,12 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId) m_eAction=eAction_RunUnlockOrDLC; signInReturnedFunc = &UIScene_MainMenu::UnlockFullGame_SignInReturned; break; + case eControl_Minigames: + // Launch the dedicated LCE minigames world flow. + ui.PlayUISFX(eSFX_Press); + m_eAction = eAction_RunMinigames; + signInReturnedFunc = &UIScene_MainMenu::Minigames_SignInReturned; + break; #if defined _XBOX case eControl_Exit: if( ProfileManager.IsFullVersion() ) @@ -400,6 +409,9 @@ void UIScene_MainMenu::RunAction(int iPad) case eAction_RunGame: RunPlayGame(iPad); break; + case eAction_RunMinigames: + RunMinigames(iPad); + break; case eAction_RunLeaderboards: RunLeaderboards(iPad); break; @@ -493,6 +505,7 @@ int UIScene_MainMenu::MustSignInReturned(void *pParam, int iPad, C4JStorage::EMe switch(pClass->m_eAction) { case eAction_RunGame: ProfileManager.RequestSignInUI(false, true, false, false, true, &UIScene_MainMenu::CreateLoad_SignInReturned, pClass, iPad ); break; + case eAction_RunMinigames: ProfileManager.RequestSignInUI(false, false, true, false, true, &UIScene_MainMenu::Minigames_SignInReturned, pClass, iPad ); break; case eAction_RunHelpAndOptions: ProfileManager.RequestSignInUI(false, false, true, false, true, &UIScene_MainMenu::HelpAndOptions_SignInReturned, pClass, iPad ); break; case eAction_RunLeaderboards: ProfileManager.RequestSignInUI(false, false, true, false, true, &UIScene_MainMenu::Leaderboards_SignInReturned, pClass, iPad ); break; case eAction_RunAchievements: ProfileManager.RequestSignInUI(false, false, true, false, true, &UIScene_MainMenu::Achievements_SignInReturned, pClass, iPad ); break; @@ -658,6 +671,30 @@ int UIScene_MainMenu::HelpAndOptions_SignInReturned(void *pParam,bool bContinue, return 0; } +int UIScene_MainMenu::Minigames_SignInReturned(void *pParam,bool bContinue,int iPad) +{ + UIScene_MainMenu *pClass = (UIScene_MainMenu *)pParam; + + if(bContinue) + { + pClass->RunMinigames(iPad); + } + else + { + pClass->m_bIgnorePress=false; + ProfileManager.SetLockedProfile(-1); + for(int i=0;iuser->name = convStringToWstring(ProfileManager.GetGamertag(ProfileManager.GetPrimaryPad())); + app.ApplyGameSettingsChanged(hostPad); + + LoadOrJoinMenuInitData *params = new LoadOrJoinMenuInitData(); + params->bMinigamesMode = TRUE; + app.DebugPrintf("UIScene_MainMenu::RunMinigames - navigating to LoadOrJoin minigames hub (hostPad=%d)\n", hostPad); + ui.NavigateToScene(hostPad, eUIScene_LoadOrJoinMenu, params); +} + void UIScene_MainMenu::RunLeaderboards(int iPad) { UINT uiIDA[1]; diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index f1b358d..bcaf616 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -8,6 +8,7 @@ private: enum EControls { eControl_PlayGame, + eControl_Minigames, eControl_Leaderboards, eControl_Achievements, eControl_HelpAndOptions, @@ -33,14 +34,15 @@ private: UIControl m_controlTimer; UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) UI_MAP_ELEMENT( m_buttons[(int)eControl_PlayGame], "Button1") - UI_MAP_ELEMENT( m_buttons[(int)eControl_Leaderboards], "Button2") - UI_MAP_ELEMENT( m_buttons[(int)eControl_Achievements], "Button3") - UI_MAP_ELEMENT( m_buttons[(int)eControl_HelpAndOptions], "Button4") - UI_MAP_ELEMENT( m_buttons[(int)eControl_UnlockOrDLC], "Button5") + UI_MAP_ELEMENT( m_buttons[(int)eControl_Minigames], "Button2") + UI_MAP_ELEMENT( m_buttons[(int)eControl_Leaderboards], "Button3") + UI_MAP_ELEMENT( m_buttons[(int)eControl_Achievements], "Button4") + UI_MAP_ELEMENT( m_buttons[(int)eControl_HelpAndOptions], "Button5") + UI_MAP_ELEMENT( m_buttons[(int)eControl_UnlockOrDLC], "Button6") #ifndef _DURANGO - UI_MAP_ELEMENT( m_buttons[(int)eControl_Exit], "Button6") + UI_MAP_ELEMENT( m_buttons[(int)eControl_Exit], "Button7") #else - UI_MAP_ELEMENT( m_buttons[(int)eControl_XboxHelp], "Button6") + UI_MAP_ELEMENT( m_buttons[(int)eControl_XboxHelp], "Button7") #endif UI_MAP_ELEMENT( m_controlTimer, "Timer") UI_END_MAP_ELEMENTS_AND_NAMES() @@ -76,6 +78,7 @@ private: { eAction_None=0, eAction_RunGame, + eAction_RunMinigames, eAction_RunLeaderboards, eAction_RunAchievements, eAction_RunHelpAndOptions, @@ -127,6 +130,7 @@ protected: private: void RunPlayGame(int iPad); + void RunMinigames(int iPad); void RunLeaderboards(int iPad); void RunUnlockOrDLC(int iPad); void RunAchievements(int iPad); @@ -141,6 +145,7 @@ private: #endif static int CreateLoad_SignInReturned(void *pParam,bool bContinue, int iPad); static int HelpAndOptions_SignInReturned(void *pParam,bool bContinue,int iPad); + static int Minigames_SignInReturned(void *pParam,bool bContinue,int iPad); static int Achievements_SignInReturned(void *pParam,bool bContinue,int iPad); static int MustSignInReturned(void *pParam,int iPad,C4JStorage::EMessageResult result); diff --git a/Minecraft.Client/Common/UI/UIStructs.h b/Minecraft.Client/Common/UI/UIStructs.h index 2dd03c8..e617681 100644 --- a/Minecraft.Client/Common/UI/UIStructs.h +++ b/Minecraft.Client/Common/UI/UIStructs.h @@ -216,6 +216,19 @@ typedef struct _SaveListDetails } SaveListDetails; +#define LOAD_OR_JOIN_MENU_INIT_MAGIC 0x4C4F4A4D +typedef struct _LoadOrJoinMenuInitData +{ + DWORD magic; + BOOL bMinigamesMode; + + _LoadOrJoinMenuInitData() + { + magic = LOAD_OR_JOIN_MENU_INIT_MAGIC; + bMinigamesMode = FALSE; + } +} LoadOrJoinMenuInitData; + // Load world typedef struct _LoadMenuInitData { diff --git a/Minecraft.Client/Common/XUI/XUI_ConnectingProgress.cpp b/Minecraft.Client/Common/XUI/XUI_ConnectingProgress.cpp index 9a82a7b..603a534 100644 --- a/Minecraft.Client/Common/XUI/XUI_ConnectingProgress.cpp +++ b/Minecraft.Client/Common/XUI/XUI_ConnectingProgress.cpp @@ -6,6 +6,9 @@ #include #include "..\..\Minecraft.h" #include "..\..\..\Minecraft.World\DisconnectPacket.h" +#ifdef _WINDOWS64 +#include "..\Network\PlatformNetworkManagerStub.h" +#endif //---------------------------------------------------------------------------------- // Performs initialization tasks - retrieves controls. @@ -144,6 +147,15 @@ HRESULT CScene_ConnectingProgress::OnTimer( XUIMessageTimer *pTimer, BOOL& bHand // Check if the connection failed Minecraft *pMinecraft = Minecraft::GetInstance(); +#ifdef _WINDOWS64 + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + pMinecraft->m_connectionFailed[m_iPad] = false; + pMinecraft->m_connectionFailedReason[m_iPad] = DisconnectPacket::eDisconnect_None; + return S_OK; + } +#endif + if( pMinecraft->m_connectionFailed[m_iPad] || !g_NetworkManager.IsInSession() ) { app.RemoveBackScene(m_iPad); diff --git a/Minecraft.Client/Common/XUI/XUI_MainMenu.cpp b/Minecraft.Client/Common/XUI/XUI_MainMenu.cpp index 7b9c1a5..5888af0 100644 --- a/Minecraft.Client/Common/XUI/XUI_MainMenu.cpp +++ b/Minecraft.Client/Common/XUI/XUI_MainMenu.cpp @@ -14,10 +14,12 @@ //#include "XUI_CreateLoad.h" #include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\..\Minecraft.World\Random.h" +#include "..\..\..\Minecraft.World\LevelSettings.h" #include "..\..\MinecraftServer.h" #include "..\..\Minecraft.h" #include "..\..\Options.h" #include "..\..\Font.h" +#include "..\UI\UIStructs.h" #include "..\..\Common\GameRules\ConsoleGameRules.h" #define DLC_INSTALLED_TIMER_ID 1 @@ -38,6 +40,7 @@ HRESULT CScene_Main::OnInit( XUIMessageInit* pInitData, BOOL& bHandled ) XuiControlSetText(m_Buttons[BUTTON_ACHIEVEMENTS],app.GetString(IDS_ACHIEVEMENTS)); XuiControlSetText(m_Buttons[BUTTON_HELPANDOPTIONS],app.GetString(IDS_HELP_AND_OPTIONS)); XuiControlSetText(m_Buttons[BUTTON_UNLOCKFULLGAME],app.GetString(IDS_UNLOCK_FULL_GAME)); + XuiControlSetText(m_Buttons[BUTTON_MINIGAMES],L"LCE Minigames"); XuiControlSetText(m_Buttons[BUTTON_EXITGAME],app.GetString(IDS_EXIT_GAME)); m_Timer.SetShow(FALSE); @@ -261,6 +264,20 @@ HRESULT CScene_Main::OnNotifyPressEx(HXUIOBJ hObjPressed, XUINotifyPress* pNotif } } break; + case BUTTON_MINIGAMES: + m_eAction = eAction_RunMinigames; + if(ProfileManager.IsSignedIn(pNotifyPressData->UserIndex)) + { + RunMinigames(pNotifyPressData->UserIndex); + } + else + { + UINT uiIDA[2]; + uiIDA[0]=IDS_CONFIRM_OK; + uiIDA[1]=IDS_CONFIRM_CANCEL; + StorageManager.RequestMessageBox(IDS_MUST_SIGN_IN_TITLE, IDS_MUST_SIGN_IN_TEXT, uiIDA, 2, pNotifyPressData->UserIndex,&CScene_Main::MustSignInReturned,this, app.GetStringTable()); + } + break; case BUTTON_EXITGAME: if( ProfileManager.IsFullVersion() ) { @@ -672,6 +689,9 @@ int CScene_Main::MustSignInReturned(void *pParam,int iPad,C4JStorage::EMessageRe case eAction_RunGame: ProfileManager.RequestSignInUI(false, true, false,false,true,&CScene_Main::CreateLoad_SignInReturned,pClass ,iPad); break; + case eAction_RunMinigames: + ProfileManager.RequestSignInUI(false, false, true,false,true,&CScene_Main::Minigames_SignInReturned,pClass,iPad ); + break; case eAction_RunLeaderboards: ProfileManager.RequestSignInUI(false, false, true,false,true, &CScene_Main::Leaderboards_SignInReturned, pClass,iPad); break; @@ -704,6 +724,29 @@ int CScene_Main::MustSignInReturned(void *pParam,int iPad,C4JStorage::EMessageRe return 0; } +int CScene_Main::Minigames_SignInReturned(void *pParam,bool bContinue,int iPad) +{ + CScene_Main* pClass = (CScene_Main*)pParam; + + if(bContinue==true) + { + pClass->RunMinigames(iPad); + } + else + { + ProfileManager.SetLockedProfile(-1); + for(int i=0;iuser->name = convStringToWstring(ProfileManager.GetGamertag(ProfileManager.GetPrimaryPad())); + app.ApplyGameSettingsChanged(hostPad); + + LoadOrJoinMenuInitData *params = new LoadOrJoinMenuInitData(); + params->bMinigamesMode = TRUE; + app.DebugPrintf("CScene_Main::RunMinigames - navigating to LoadOrJoin minigames hub (hostPad=%d)\n", hostPad); + app.NavigateToScene(hostPad, eUIScene_LoadOrJoinMenu, params); +} + HRESULT CScene_Main::OnTMSBanFileRetrieved() { Minecraft *pMinecraft=Minecraft::GetInstance(); @@ -1285,4 +1372,4 @@ HRESULT CScene_Main::OnTimer( XUIMessageTimer *pTimer, BOOL& bHandled ) } return S_OK; -} \ No newline at end of file +} diff --git a/Minecraft.Client/Common/XUI/XUI_MainMenu.h b/Minecraft.Client/Common/XUI/XUI_MainMenu.h index 2e4279e..8d5a54a 100644 --- a/Minecraft.Client/Common/XUI/XUI_MainMenu.h +++ b/Minecraft.Client/Common/XUI/XUI_MainMenu.h @@ -4,11 +4,12 @@ #include "XUI_CustomMessages.h" #define BUTTON_PLAYGAME 0 -#define BUTTON_LEADERBOARDS 1 -#define BUTTON_ACHIEVEMENTS 2 -#define BUTTON_HELPANDOPTIONS 3 -#define BUTTON_UNLOCKFULLGAME 4 -#define BUTTON_EXITGAME 5 +#define BUTTON_MINIGAMES 1 +#define BUTTON_LEADERBOARDS 2 +#define BUTTON_ACHIEVEMENTS 3 +#define BUTTON_HELPANDOPTIONS 4 +#define BUTTON_UNLOCKFULLGAME 5 +#define BUTTON_EXITGAME 6 #define BUTTONS_MAX BUTTON_EXITGAME + 1 #define MAIN_MENU_MAX_TEXT_SCALE 1.5f @@ -41,6 +42,7 @@ private: { eAction_None=0, eAction_RunGame, + eAction_RunMinigames, eAction_RunLeaderboards, eAction_RunAchievements, eAction_RunHelpAndOptions, @@ -70,11 +72,12 @@ protected: // Control mapping to objects BEGIN_CONTROL_MAP() MAP_CONTROL(IDC_XuiButton1, m_Buttons[BUTTON_PLAYGAME]) - MAP_CONTROL(IDC_XuiButton2, m_Buttons[BUTTON_LEADERBOARDS ]) - MAP_CONTROL(IDC_XuiButton3, m_Buttons[BUTTON_ACHIEVEMENTS ]) - MAP_CONTROL(IDC_XuiButton4, m_Buttons[BUTTON_HELPANDOPTIONS]) - MAP_CONTROL(IDC_XuiButton5, m_Buttons[BUTTON_UNLOCKFULLGAME]) - MAP_CONTROL(IDC_XuiButton6, m_Buttons[BUTTON_EXITGAME]) + MAP_CONTROL(IDC_XuiButton2, m_Buttons[BUTTON_MINIGAMES]) + MAP_CONTROL(IDC_XuiButton3, m_Buttons[BUTTON_LEADERBOARDS ]) + MAP_CONTROL(IDC_XuiButton4, m_Buttons[BUTTON_ACHIEVEMENTS ]) + MAP_CONTROL(IDC_XuiButton5, m_Buttons[BUTTON_HELPANDOPTIONS]) + MAP_CONTROL(IDC_XuiButton6, m_Buttons[BUTTON_UNLOCKFULLGAME]) + MAP_CONTROL(IDC_XuiButton7, m_Buttons[BUTTON_EXITGAME]) MAP_CONTROL(IDC_XuiSplash, m_Subtitle) MAP_CONTROL(IDC_XuiSplashMCFont, m_SubtitleMCFont) MAP_CONTROL(IDC_Timer, m_Timer) @@ -95,6 +98,7 @@ protected: static void LoadTrial(); void RunPlayGame(int iPad); + void RunMinigames(int iPad); void RunLeaderboards(int iPad); void RunAchievements(int iPad); void RunHelpAndOptions(int iPad); @@ -114,6 +118,7 @@ public: static int DeviceSelectReturned(void *pParam,bool bContinue); static int SaveGameReturned(void *pParam,bool bContinue); static int HelpAndOptions_SignInReturned(void *pParam,bool bContinue,int iPad); + static int Minigames_SignInReturned(void *pParam,bool bContinue,int iPad); static int ExitGameReturned(void *pParam,int iPad,C4JStorage::EMessageResult result); static int AchievementsDeviceSelectReturned(void *pParam,bool bContinue); static int Achievements_SignInReturned(void *pParam,bool bContinue,int iPad); diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index 4ebd89e..35f5559 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -168,7 +168,7 @@ void PIXSetMarkerDeprecated(int a, char *b, ...) {} bool IsEqualXUID(PlayerUID a, PlayerUID b) { -#if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) || defined(_DURANGO) +#if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) || defined(_DURANGO) || defined(_WINDOWS64) return (a == b); #else return false; @@ -187,6 +187,8 @@ D3DXVECTOR3& D3DXVECTOR3::operator += ( CONST D3DXVECTOR3& add ) { x += add.x; y #include "Windows64\Network\WinsockNetLayer.h" +extern bool g_Win64DedicatedServerMode; + BYTE IQNetPlayer::GetSmallId() { return m_smallId; } void IQNetPlayer::SendData(IQNetPlayer *player, const void *pvData, DWORD dwDataSize, DWORD dwFlags) { @@ -195,7 +197,14 @@ void IQNetPlayer::SendData(IQNetPlayer *player, const void *pvData, DWORD dwData WinsockNetLayer::SendToSmallId(player->m_smallId, pvData, dwDataSize); } } -bool IQNetPlayer::IsSameSystem(IQNetPlayer *player) { return (this == player) || (!m_isRemote && !player->m_isRemote); } +bool IQNetPlayer::IsSameSystem(IQNetPlayer *player) +{ + if (player == NULL) + { + return false; + } + return (this == player) || (!m_isRemote && !player->m_isRemote); +} DWORD IQNetPlayer::GetSendQueueSize( IQNetPlayer *player, DWORD dwFlags ) { return 0; } DWORD IQNetPlayer::GetCurrentRtt() { return 0; } bool IQNetPlayer::IsHost() { return m_isHostPlayer; } @@ -232,13 +241,17 @@ void Win64_SetupRemoteQNetPlayer(IQNetPlayer *player, BYTE smallId, bool isHost, IQNet::s_playerCount = smallId + 1; } +static bool Win64_IsActivePlayer(IQNetPlayer *p, DWORD index); + HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex){ return S_OK; } IQNetPlayer *IQNet::GetHostPlayer() { return &m_player[0]; } IQNetPlayer *IQNet::GetLocalPlayerByUserIndex(DWORD dwUserIndex) { if (s_isHosting) { - if (dwUserIndex < MINECRAFT_NET_MAX_PLAYERS && !m_player[dwUserIndex].m_isRemote) + if (dwUserIndex < MINECRAFT_NET_MAX_PLAYERS && + !m_player[dwUserIndex].m_isRemote && + Win64_IsActivePlayer(&m_player[dwUserIndex], dwUserIndex)) return &m_player[dwUserIndex]; return NULL; } @@ -246,14 +259,20 @@ IQNetPlayer *IQNet::GetLocalPlayerByUserIndex(DWORD dwUserIndex) return NULL; for (DWORD i = 0; i < s_playerCount; i++) { - if (!m_player[i].m_isRemote) + if (!m_player[i].m_isRemote && Win64_IsActivePlayer(&m_player[i], i)) return &m_player[i]; } return NULL; } static bool Win64_IsActivePlayer(IQNetPlayer *p, DWORD index) { - if (index == 0) return true; + if (index == 0) + { + // Keep host slot active for index/session-id consistency in server queue logic. + // Dedicated mode suppresses host "player count" through advertising logic instead. + (void)g_Win64DedicatedServerMode; + return true; + } return (p->GetCustomDataValue() != 0); } @@ -272,19 +291,24 @@ IQNetPlayer *IQNet::GetPlayerByIndex(DWORD dwPlayerIndex) } IQNetPlayer *IQNet::GetPlayerBySmallId(BYTE SmallId) { - for (DWORD i = 0; i < s_playerCount; i++) - { - if (m_player[i].m_smallId == SmallId && Win64_IsActivePlayer(&m_player[i], i)) return &m_player[i]; - } - return NULL; + if (SmallId >= MINECRAFT_NET_MAX_PLAYERS) + return NULL; + + // On Win64 LAN, clients can receive packets for a newly-joined higher smallId + // before local span bookkeeping catches up (e.g. local smallId=1 receiving smallId=2). + // Always provide the slot and grow the span so downstream code can materialize it. + m_player[SmallId].m_smallId = SmallId; + if (SmallId >= s_playerCount) + s_playerCount = SmallId + 1; + return &m_player[SmallId]; } IQNetPlayer *IQNet::GetPlayerByXuid(PlayerUID xuid) { - for (DWORD i = 0; i < s_playerCount; i++) + for (DWORD i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) { if (Win64_IsActivePlayer(&m_player[i], i) && m_player[i].GetXuid() == xuid) return &m_player[i]; } - return &m_player[0]; + return NULL; } DWORD IQNet::GetPlayerCount() { @@ -299,15 +323,29 @@ QNET_STATE IQNet::GetState() { return _iQNetStubState; } bool IQNet::IsHost() { return s_isHosting; } HRESULT IQNet::JoinGameFromInviteInfo(DWORD dwUserIndex, DWORD dwUserMask, const INVITE_INFO *pInviteInfo) { return S_OK; } void IQNet::HostGame() { _iQNetStubState = QNET_STATE_SESSION_STARTING; s_isHosting = true; } -void IQNet::ClientJoinGame() { _iQNetStubState = QNET_STATE_SESSION_STARTING; s_isHosting = false; } +void IQNet::ClientJoinGame() +{ + _iQNetStubState = QNET_STATE_SESSION_STARTING; + s_isHosting = false; + + // Reset all slots so no stale network-player pointers survive between sessions. + for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) + { + m_player[i].m_smallId = (BYTE)i; + m_player[i].m_isRemote = true; + m_player[i].m_isHostPlayer = false; + m_player[i].m_gamertag[0] = 0; + m_player[i].SetCustomDataValue(0); + } +} void IQNet::EndGame() { _iQNetStubState = QNET_STATE_IDLE; s_isHosting = false; s_playerCount = 1; - for (int i = 1; i < MINECRAFT_NET_MAX_PLAYERS; i++) + for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) { - m_player[i].m_smallId = 0; + m_player[i].m_smallId = (BYTE)i; m_player[i].m_isRemote = false; m_player[i].m_isHostPlayer = false; m_player[i].m_gamertag[0] = 0; diff --git a/Minecraft.Client/GameRenderer.cpp b/Minecraft.Client/GameRenderer.cpp index 508b3a1..940b6c1 100644 --- a/Minecraft.Client/GameRenderer.cpp +++ b/Minecraft.Client/GameRenderer.cpp @@ -48,6 +48,9 @@ #include "TexturePackRepository.h" #include "TexturePack.h" +#ifdef _WINDOWS64 +#include "Common\Network\PlatformNetworkManagerStub.h" +#endif bool GameRenderer::anaglyph3d = false; int GameRenderer::anaglyphPass = 0; @@ -1164,6 +1167,26 @@ int GameRenderer::runUpdate(LPVOID lpParam) m_updateEvents->Set(eUpdateCanRun); +#ifdef _WINDOWS64 + // Cross-server transfer can overlap old-level teardown and new-level connect. + // Do not touch chunk rebuild queues in this transition window. + if (CPlatformNetworkManagerStub::IsServerTransferInProgress()) + { + RenderManager.CBuffDeferredModeEnd(); + m_updateEvents->Set(eUpdateEventIsFinished); + Sleep(1); + continue; + } +#endif + + if (minecraft == NULL || minecraft->levelRenderer == NULL || minecraft->level == NULL) + { + RenderManager.CBuffDeferredModeEnd(); + m_updateEvents->Set(eUpdateEventIsFinished); + Sleep(1); + continue; + } + // PIXBeginNamedEvent(0,"Updating dirty chunks %d",(count++)&7); // Update chunks atomically until there aren't any very near ones left - they will be deferred for rendering diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index e876579..fb05766 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -27,6 +27,8 @@ #include "..\Minecraft.World\net.minecraft.world.h" #include "..\Minecraft.World\LevelChunk.h" #include "..\Minecraft.World\Biome.h" +#include +#include #define RENDER_HUD 0 //#ifndef _XBOX @@ -34,6 +36,50 @@ //#define RENDER_HUD 1 //#endif +namespace +{ + const unsigned int MINIGAME_SETTINGS_FLAG = 0x80000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_MASK = 0x70000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_SHIFT = 28u; + const unsigned int MINIGAME_SETTINGS_ROLE_MASK = 0x0C000000u; + const unsigned int MINIGAME_SETTINGS_ROLE_SHIFT = 26u; + const unsigned int MINIGAME_SETTINGS_QUEUE_MASK = 0x03000000u; + const unsigned int MINIGAME_SETTINGS_QUEUE_SHIFT = 24u; + const unsigned int MINIGAME_TYPE_BEDWARS = 3u; + const unsigned int MINIGAME_ROLE_HUB = 0u; + + struct BedwarsHudRow + { + wstring name; + int health; + int distanceBlocks; + bool isSelf; + }; + + bool IsBedwarsSettings(unsigned int hostSettings) + { + if ((hostSettings & MINIGAME_SETTINGS_FLAG) == 0) + { + return false; + } + const unsigned int minigameType = (hostSettings & MINIGAME_SETTINGS_TYPE_MASK) >> MINIGAME_SETTINGS_TYPE_SHIFT; + return (minigameType == MINIGAME_TYPE_BEDWARS); + } + + wstring GetBedwarsQueueLabel(unsigned int hostSettings) + { + const unsigned int queueMode = (hostSettings & MINIGAME_SETTINGS_QUEUE_MASK) >> MINIGAME_SETTINGS_QUEUE_SHIFT; + switch (queueMode) + { + case 0u: return L"Solo"; + case 1u: return L"Doubles"; + case 2u: return L"Squads"; + case 3u: return L"Practice"; + default: return L"Unknown"; + } + } +} + float Gui::currentGuiBlendFactor = 1.0f; // 4J added float Gui::currentGuiScaleFactor = 1.0f; // 4J added ItemRenderer *Gui::itemRenderer = new ItemRenderer(); @@ -735,6 +781,91 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) glDisable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // Bedwars party/status panel (mid-right) + if (bDisplayGui && !bTwoPlayerSplitscreen && minecraft->level != NULL && minecraft->player != NULL) + { + const unsigned int hostSettings = app.GetGameHostOption(eGameHostOption_All); + if (IsBedwarsSettings(hostSettings)) + { + vector rows; + rows.reserve(minecraft->level->players.size()); + + for (AUTO_VAR(itP, minecraft->level->players.begin()); itP != minecraft->level->players.end(); ++itP) + { + shared_ptr p = *itP; + if (p == NULL) + { + continue; + } + + BedwarsHudRow row; + row.name = p->displayName.empty() ? p->name : p->displayName; + row.health = p->getHealth(); + const double dx = p->x - minecraft->player->x; + const double dy = p->y - minecraft->player->y; + const double dz = p->z - minecraft->player->z; + row.distanceBlocks = (int)std::sqrt(dx * dx + dy * dy + dz * dz); + row.isSelf = (p.get() == minecraft->player.get()); + rows.push_back(row); + } + + std::sort(rows.begin(), rows.end(), [](const BedwarsHudRow &a, const BedwarsHudRow &b) + { + if (a.isSelf != b.isSelf) + { + return a.isSelf; + } + if (a.distanceBlocks != b.distanceBlocks) + { + return a.distanceBlocks < b.distanceBlocks; + } + return a.name < b.name; + }); + + const int shownCount = (int)std::min(rows.size(), 6); + const int panelWidth = 144; + const int headerHeight = 12; + const int lineHeight = 9; + const int bodyTop = headerHeight + 12; + const int bodyHeight = shownCount * lineHeight; + const int footerHeight = 10; + const int panelHeight = bodyTop + bodyHeight + footerHeight; + const int panelX = screenWidth - panelWidth - 2; + const int panelY = (screenHeight / 2) - (panelHeight / 2); + + fill(panelX - 1, panelY - 1, panelX + panelWidth + 1, panelY + panelHeight + 1, 0xA0101010); + fill(panelX, panelY, panelX + panelWidth, panelY + headerHeight, 0xB0303030); + fill(panelX, panelY + headerHeight, panelX + panelWidth, panelY + panelHeight, 0x70101010); + + drawString(font, L"Party", panelX + 3, panelY + 2, 0x55FFFF); + + const bool isHubRole = (((hostSettings & MINIGAME_SETTINGS_ROLE_MASK) >> MINIGAME_SETTINGS_ROLE_SHIFT) == MINIGAME_ROLE_HUB); + const wstring modeText = isHubRole ? L"Hub" : L"Match"; + const wstring queueText = GetBedwarsQueueLabel(hostSettings); + wchar_t infoLine[96]; + swprintf_s(infoLine, L"%ls | %ls | %d online", modeText.c_str(), queueText.c_str(), (int)rows.size()); + drawString(font, infoLine, panelX + 3, panelY + headerHeight + 1, 0xCFCFCF); + + for (int i = 0; i < shownCount; ++i) + { + const BedwarsHudRow &r = rows[(size_t)i]; + const int y = panelY + bodyTop + i * lineHeight; + const int nameColor = r.isSelf ? 0xFFFF55 : 0xE0E0E0; + drawString(font, r.name, panelX + 3, y, nameColor); + + wchar_t rightText[48]; + const int clampedHp = (r.health < 0) ? 0 : r.health; + swprintf_s(rightText, L"%dHP %dm", clampedHp, r.distanceBlocks); + int hpColor = 0x55FF55; + if (clampedHp <= 10) hpColor = 0xFFAA55; + if (clampedHp <= 4) hpColor = 0xFF5555; + drawString(font, rightText, panelX + panelWidth - font->width(rightText) - 3, y, hpColor); + } + + drawString(font, L"/party /queue", panelX + 3, panelY + panelHeight - 9, 0x909090); + } + } + // if the player is falling asleep we render a dark overlay if (minecraft->player->getSleepTimer() > 0) { diff --git a/Minecraft.Client/Input.cpp b/Minecraft.Client/Input.cpp index 6772b3c..bc95ad5 100644 --- a/Minecraft.Client/Input.cpp +++ b/Minecraft.Client/Input.cpp @@ -18,7 +18,6 @@ Input::Input() wasJumping = false; jumping = false; sneaking = false; - sprinting = false; lReset = false; rReset = false; @@ -94,35 +93,11 @@ void Input::tick(LocalPlayer *player) } #ifdef _WINDOWS64 - if (iPad == 0 && g_KBMInput.IsMouseGrabbed()) + if (iPad == 0 && g_KBMInput.IsMouseGrabbed() && pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_SNEAK_TOGGLE)) { - // Left Shift = sneak (hold to crouch) - if (pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_SNEAK_TOGGLE)) - { - if (!player->abilities.flying) - { - sneaking = g_KBMInput.IsKeyDown(KeyboardMouseInput::KEY_SNEAK); - } - } - - // Left Ctrl + forward = sprint (hold to sprint) if (!player->abilities.flying) { - bool ctrlHeld = g_KBMInput.IsKeyDown(KeyboardMouseInput::KEY_SPRINT); - bool movingForward = (kbYA > 0.0f); - - if (ctrlHeld && movingForward) - { - sprinting = true; - } - else - { - sprinting = false; - } - } - else - { - sprinting = false; + sneaking = g_KBMInput.IsKeyDown(KeyboardMouseInput::KEY_SNEAK); } } #endif @@ -169,7 +144,7 @@ void Input::tick(LocalPlayer *player) if (iPad == 0 && g_KBMInput.IsMouseGrabbed()) { float mouseSensitivity = ((float)app.GetGameSettings(iPad,eGameSetting_Sensitivity_InGame)) / 100.0f; - float mouseLookScale = 5.0f; + float mouseLookScale = 1.0f; float mx = g_KBMInput.GetLookX(mouseSensitivity * mouseLookScale); float my = g_KBMInput.GetLookY(mouseSensitivity * mouseLookScale); diff --git a/Minecraft.Client/Input.h b/Minecraft.Client/Input.h index 1110981..0a44d76 100644 --- a/Minecraft.Client/Input.h +++ b/Minecraft.Client/Input.h @@ -10,8 +10,7 @@ public: bool wasJumping; bool jumping; bool sneaking; - bool sprinting; - + Input(); // 4J - added virtual void tick(LocalPlayer *player); diff --git a/Minecraft.Client/KeyboardMouseInput.cpp b/Minecraft.Client/KeyboardMouseInput.cpp index 4d5855c..9824581 100644 --- a/Minecraft.Client/KeyboardMouseInput.cpp +++ b/Minecraft.Client/KeyboardMouseInput.cpp @@ -6,9 +6,6 @@ KeyboardMouseInput g_KBMInput; extern HWND g_hWnd; -// Forward declaration -static void ClipCursorToWindow(HWND hWnd); - // coded by notpies fr void KeyboardMouseInput::Init() { @@ -33,7 +30,6 @@ void KeyboardMouseInput::Init() m_mouseWheel = 0; m_mouseWheelAccum = 0; m_mouseGrabbed = false; - m_cursorHiddenForUI = false; m_windowFocused = true; m_hasInput = false; @@ -44,6 +40,10 @@ void KeyboardMouseInput::Init() rid.hwndTarget = g_hWnd; RegisterRawInputDevices(&rid, 1, sizeof(rid)); + if (g_hWnd) + { + while (ShowCursor(FALSE) >= 0) {} + } } void KeyboardMouseInput::ClearAllState() @@ -107,7 +107,7 @@ void KeyboardMouseInput::Tick() } } - if ((m_mouseGrabbed || m_cursorHiddenForUI) && g_hWnd) + if (m_mouseGrabbed && g_hWnd) { RECT rc; GetClientRect(g_hWnd, &rc); @@ -226,9 +226,6 @@ void KeyboardMouseInput::SetMouseGrabbed(bool grabbed) m_mouseGrabbed = grabbed; if (grabbed && g_hWnd) { - while (ShowCursor(FALSE) >= 0) {} - ClipCursorToWindow(g_hWnd); - RECT rc; GetClientRect(g_hWnd, &rc); POINT center; @@ -240,40 +237,6 @@ void KeyboardMouseInput::SetMouseGrabbed(bool grabbed) m_mouseDeltaAccumX = 0; m_mouseDeltaAccumY = 0; } - else if (!grabbed && !m_cursorHiddenForUI && g_hWnd) - { - while (ShowCursor(TRUE) < 0) {} - ClipCursor(NULL); - } -} - -void KeyboardMouseInput::SetCursorHiddenForUI(bool hidden) -{ - if (m_cursorHiddenForUI == hidden) - return; - - m_cursorHiddenForUI = hidden; - if (hidden && g_hWnd) - { - while (ShowCursor(FALSE) >= 0) {} - ClipCursorToWindow(g_hWnd); - - RECT rc; - GetClientRect(g_hWnd, &rc); - POINT center; - center.x = (rc.right - rc.left) / 2; - center.y = (rc.bottom - rc.top) / 2; - ClientToScreen(g_hWnd, ¢er); - SetCursorPos(center.x, center.y); - - m_mouseDeltaAccumX = 0; - m_mouseDeltaAccumY = 0; - } - else if (!hidden && !m_mouseGrabbed && g_hWnd) - { - while (ShowCursor(TRUE) < 0) {} - ClipCursor(NULL); - } } static void ClipCursorToWindow(HWND hWnd) @@ -294,16 +257,8 @@ void KeyboardMouseInput::SetWindowFocused(bool focused) m_windowFocused = focused; if (focused) { - if (m_mouseGrabbed || m_cursorHiddenForUI) - { - while (ShowCursor(FALSE) >= 0) {} - ClipCursorToWindow(g_hWnd); - } - else - { - while (ShowCursor(TRUE) < 0) {} - ClipCursor(NULL); - } + while (ShowCursor(FALSE) >= 0) {} + ClipCursorToWindow(g_hWnd); } else { diff --git a/Minecraft.Client/KeyboardMouseInput.h b/Minecraft.Client/KeyboardMouseInput.h index 03e4e29..098e6e9 100644 --- a/Minecraft.Client/KeyboardMouseInput.h +++ b/Minecraft.Client/KeyboardMouseInput.h @@ -18,11 +18,9 @@ public: static const int KEY_RIGHT = 'D'; static const int KEY_JUMP = VK_SPACE; static const int KEY_SNEAK = VK_LSHIFT; - static const int KEY_SPRINT = VK_LCONTROL; static const int KEY_INVENTORY = 'E'; static const int KEY_DROP = 'Q'; static const int KEY_CRAFTING = VK_TAB; - static const int KEY_CRAFTING_ALT = 'R'; static const int KEY_PAUSE = VK_ESCAPE; static const int KEY_THIRD_PERSON = VK_F5; static const int KEY_DEBUG_INFO = VK_F3; @@ -58,9 +56,6 @@ public: void SetMouseGrabbed(bool grabbed); bool IsMouseGrabbed() const { return m_mouseGrabbed; } - void SetCursorHiddenForUI(bool hidden); - bool IsCursorHiddenForUI() const { return m_cursorHiddenForUI; } - void SetWindowFocused(bool focused); bool IsWindowFocused() const { return m_windowFocused; } @@ -102,8 +97,6 @@ private: bool m_mouseGrabbed; - bool m_cursorHiddenForUI; - bool m_windowFocused; bool m_hasInput; diff --git a/Minecraft.Client/LevelRenderer.cpp b/Minecraft.Client/LevelRenderer.cpp index 9836007..940d3c7 100644 --- a/Minecraft.Client/LevelRenderer.cpp +++ b/Minecraft.Client/LevelRenderer.cpp @@ -1794,6 +1794,11 @@ void LevelRenderer::renderAdvancedClouds(float alpha) bool LevelRenderer::updateDirtyChunks() { + if (level == NULL || mc == NULL || mc->level == NULL || globalChunkFlags == NULL) + { + return false; + } + #ifdef _LARGE_WORLDS std::list< std::pair > nearestClipChunks; #endif @@ -3641,4 +3646,4 @@ int LevelRenderer::rebuildChunkThreadProc(LPVOID lpParam) void LevelRenderer::nonStackDirtyChunksAdded() { dirtyChunksLockFreeStack.Push((int *)1); -} \ No newline at end of file +} diff --git a/Minecraft.Client/LocalPlayer.cpp b/Minecraft.Client/LocalPlayer.cpp index 747d30a..2d02f58 100644 --- a/Minecraft.Client/LocalPlayer.cpp +++ b/Minecraft.Client/LocalPlayer.cpp @@ -328,12 +328,6 @@ void LocalPlayer::aiStep() } } if (isSneaking()) sprintTriggerTime = 0; -#ifdef _WINDOWS64 - if (input->sprinting && onGround && enoughFoodToSprint && !isUsingItem() && !hasEffect(MobEffect::blindness) && !isSneaking()) - { - setSprinting(true); - } -#endif // 4J-PB - try not stopping sprint on collision //if (isSprinting() && (input->ya < runTreshold || horizontalCollision || !enoughFoodToSprint)) if (isSprinting() && (input->ya < runTreshold || !enoughFoodToSprint)) @@ -591,9 +585,7 @@ void LocalPlayer::closeContainer() void LocalPlayer::openTextEdit(shared_ptr sign) { - bool success = app.LoadSignEntryMenu(GetXboxPad(), sign ); - if( success ) ui.PlayUISFX(eSFX_Press); - //minecraft->setScreen(new TextEditScreen(sign)); + minecraft->setScreen(new TextEditScreen(sign)); } bool LocalPlayer::openContainer(shared_ptr container) @@ -1619,4 +1611,3 @@ void LocalPlayer::SetPlayerAdditionalModelParts(vectorpAdditionalMo { m_pAdditionalModelParts=pAdditionalModelParts; } - diff --git a/Minecraft.Client/Minecraft.Client.vcxproj b/Minecraft.Client/Minecraft.Client.vcxproj index 078a606..310464b 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj +++ b/Minecraft.Client/Minecraft.Client.vcxproj @@ -1,4 +1,4 @@ - + @@ -1311,17 +1311,6 @@ if not exist "$(TargetDir)\savedata" mkdir "$(TargetDir)\savedata" CopyToHardDrive $(RemoteRoot)=$(ImagePath);$(RemoteRoot)\res=Xbox\res;$(RemoteRoot)=Xbox\AvatarAwards;$(RemoteRoot)\Tutorial=Xbox\Tutorial\Tutorial;$(RemoteRoot)=Xbox\584111F70AAAAAAA;$(RemoteRoot)=Xbox\kinect\speech;$(RemoteRoot)=Xbox\XZP\TMSFiles.xzp - - copy /Y "$(TargetPath)" "$(ProjectDir)" -copy /Y "$(ProjectDir)Windows64\Iggy\lib\redist64\iggy_w64.dll" "$(ProjectDir)" -copy /Y "$(ProjectDir)Windows64\Miles\lib\redist64\mss64.dll" "$(ProjectDir)" -copy /Y "$(ProjectDir)Windows64\Iggy\lib\redist64\iggy_w64.dll" "$(OutDir)" -copy /Y "$(ProjectDir)Windows64\Miles\lib\redist64\mss64.dll" "$(OutDir)" -xcopy /Y /I /E "$(ProjectDir)redist64" "$(OutDir)redist64\" -if not exist "$(OutDir)Durango\Sound\" mkdir "$(OutDir)Durango\Sound\" -copy /Y "$(ProjectDir)Durango\Sound\Minecraft.msscmp" "$(OutDir)Durango\Sound\" - Copying exe and DLLs to Minecraft.Client and output folders - @@ -1432,7 +1421,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU true $(OutDir)$(ProjectName).pdb - d3d11.lib;..\Minecraft.World\x64_Release\Minecraft.World.lib;XInput9_1_0.lib;Windows64\Iggy\lib\iggy_w64.lib;..\Minecraft.Client\Windows64\Miles\Lib\mss64.lib;%(AdditionalDependencies) + d3d11.lib;..\Minecraft.World\x64_Release\Minecraft.World.lib;XInput9_1_0.lib;Windows64\Iggy\lib\iggy_w64.lib;%(AdditionalDependencies) NotSet false @@ -1450,17 +1439,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUCopyToHardDrive $(RemoteRoot)=$(ImagePath);$(RemoteRoot)\res=Xbox\res;$(RemoteRoot)=Xbox\AvatarAwards;$(RemoteRoot)\Tutorial=Xbox\Tutorial\Tutorial;$(RemoteRoot)=Xbox\584111F70AAAAAAA;$(RemoteRoot)=Xbox\kinect\speech;$(RemoteRoot)=Xbox\XZP\TMSFiles.xzp - - copy /Y "$(TargetPath)" "$(ProjectDir)" -copy /Y "$(ProjectDir)Windows64\Iggy\lib\redist64\iggy_w64.dll" "$(ProjectDir)" -copy /Y "$(ProjectDir)Windows64\Miles\lib\redist64\mss64.dll" "$(ProjectDir)" -copy /Y "$(ProjectDir)Windows64\Iggy\lib\redist64\iggy_w64.dll" "$(OutDir)" -copy /Y "$(ProjectDir)Windows64\Miles\lib\redist64\mss64.dll" "$(OutDir)" -xcopy /Y /I /E "$(ProjectDir)redist64" "$(OutDir)redist64\" -if not exist "$(OutDir)Durango\Sound\" mkdir "$(OutDir)Durango\Sound\" -copy /Y "$(ProjectDir)Durango\Sound\Minecraft.msscmp" "$(OutDir)Durango\Sound\" - Copying exe and DLLs to Minecraft.Client and output folders - @@ -2850,7 +2828,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true true - true + false true true true @@ -2894,8 +2872,8 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true true - true - true + false + false true true true @@ -2939,7 +2917,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true true - true + false true true true @@ -28935,7 +28913,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true true - true + false true true true @@ -28943,7 +28921,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true true - true + false true @@ -34103,7 +34081,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true false true true @@ -34148,7 +34125,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34193,7 +34169,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34238,7 +34213,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true false true true @@ -34283,7 +34257,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34328,7 +34301,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34373,7 +34345,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true false true true @@ -34418,7 +34389,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34463,7 +34433,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34508,7 +34477,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true false true true @@ -34553,7 +34521,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34598,7 +34565,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34643,21 +34609,14 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true - - true - + - true true - - true - + - true true true true @@ -34695,7 +34654,6 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue - true true true true @@ -34732,18 +34690,10 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true - - true - - - true - - - true - - - true - + + + + true true @@ -36889,4 +36839,5 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU - \ No newline at end of file + + diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index 5631d90..4b6a36e 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -29,6 +29,7 @@ #include "TitleScreen.h" #include "InventoryScreen.h" #include "InBedChatScreen.h" +#include "ChatScreen.h" #include "AchievementPopup.h" #include "Input.h" #include "FrustumCuller.h" @@ -172,7 +173,6 @@ Minecraft::Minecraft(Component *mouseComponent, Canvas *parent, MinecraftApplet //lastTickTime = System::currentTimeMillis(); recheckPlayerIn = 0; running = true; - showFpsCounter = false; unoccupiedQuadrant = -1; Stats::init(); @@ -1481,7 +1481,7 @@ void Minecraft::run_middle() if(g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_DROP)) localplayers[i]->ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL< 0) @@ -1529,7 +1521,7 @@ void Minecraft::run_middle() localplayers[i]->ullDpad_filtered = 0; if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_DPAD_RIGHT)) localplayers[i]->ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<drop(); } - if (isClientSide() && Keyboard.getEventKey() == options.keyChat.key) { + if ((isClientSide() || g_NetworkManager.IsInSession()) && Keyboard.getEventKey() == options.keyChat.key) { setScreen(new ChatScreen()); } } @@ -4989,4 +4996,3 @@ int Minecraft::MustSignInReturnedPSN(void *pParam, int iPad, C4JStorage::EMessag return 0; } #endif - diff --git a/Minecraft.Client/Minecraft.h b/Minecraft.Client/Minecraft.h index 8cec348..3076030 100644 --- a/Minecraft.Client/Minecraft.h +++ b/Minecraft.Client/Minecraft.h @@ -211,7 +211,6 @@ private: public: void destroy(); volatile bool running; - bool showFpsCounter; wstring fpsString; void run(); // 4J-PB - split the run into 3 parts so we can run it from our xbox game loop diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index ceb9554..b82edbc 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -22,6 +22,7 @@ #include "..\Minecraft.World\net.minecraft.world.h" #include "..\Minecraft.World\net.minecraft.world.level.h" #include "..\Minecraft.World\net.minecraft.world.level.tile.h" +#include "..\Minecraft.World\net.minecraft.world.level.tile.entity.h" #include "..\Minecraft.World\Pos.h" #include "..\Minecraft.World\System.h" #include "..\Minecraft.World\StringHelpers.h" @@ -66,6 +67,227 @@ bool MinecraftServer::s_slowQueuePacketSent = false; unordered_map MinecraftServer::ironTimers; +namespace +{ + const unsigned int MINIGAME_SETTINGS_FLAG = 0x80000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_MASK = 0x70000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_SHIFT = 28u; + const unsigned int MINIGAME_SETTINGS_ROLE_MASK = 0x0C000000u; + const unsigned int MINIGAME_SETTINGS_ROLE_SHIFT = 26u; + const unsigned int MINIGAME_TYPE_BEDWARS = 3u; + const unsigned int MINIGAME_ROLE_HUB = 0u; + + struct BedwarsNpcDef + { + const wchar_t *label; + int offX; + int offZ; + float yRot; + }; + + static const BedwarsNpcDef kBedwarsNpcDefs[] = + { + { L"Solo Queue", -3, 0, 90.0f }, + { L"Doubles Queue", 0, 3, 180.0f }, + { L"Squads Queue", 3, 0, 270.0f }, + { L"Practice NPC", 0, -3, 0.0f }, + }; + static const wchar_t *kBedwarsNpcRainbowLabels[] = + { + L"\u00A7cS\u00A76o\u00A7el\u00A7ao \u00A7bQ\u00A7du\u00A7ce\u00A76u\u00A7ee", + L"\u00A7cD\u00A76o\u00A7eu\u00A7ab\u00A7bl\u00A7de\u00A7cs", + L"\u00A7cS\u00A76q\u00A7eu\u00A7aa\u00A7bd\u00A7ds", + L"\u00A7cP\u00A76r\u00A7ea\u00A7ac\u00A7bt\u00A7di\u00A7cc\u00A76e" + }; + + bool IsBedwarsMinigameSettings(unsigned int hostSettings) + { + if ((hostSettings & MINIGAME_SETTINGS_FLAG) == 0) + { + return false; + } + + const unsigned int minigameType = (hostSettings & MINIGAME_SETTINGS_TYPE_MASK) >> MINIGAME_SETTINGS_TYPE_SHIFT; + return (minigameType == MINIGAME_TYPE_BEDWARS); + } + + bool IsBedwarsHubSettings(unsigned int hostSettings) + { + if (!IsBedwarsMinigameSettings(hostSettings)) + { + return false; + } + const unsigned int role = (hostSettings & MINIGAME_SETTINGS_ROLE_MASK) >> MINIGAME_SETTINGS_ROLE_SHIFT; + return (role == MINIGAME_ROLE_HUB); + } + + void SpawnBedwarsQueueSign(ServerLevel *level, int x, int y, int z, const wchar_t *line1, const wchar_t *line2) + { + if (level == NULL) + { + return; + } + + level->setTile(x, y - 1, z, Tile::cloth_Id); + level->setTileAndData(x, y, z, Tile::sign_Id, 8); + + shared_ptr sign = dynamic_pointer_cast(level->getTileEntity(x, y, z)); + if (sign != NULL) + { + wstring s1 = line1 != NULL ? line1 : L""; + wstring s2 = line2 != NULL ? line2 : L""; + wstring s3 = L"\u00A7aClick NPC"; + wstring s4 = L"to join"; + sign->SetMessage(0, s1); + sign->SetMessage(1, s2); + sign->SetMessage(2, s3); + sign->SetMessage(3, s4); + sign->setChanged(); + } + } + + void KeepBedwarsLobbyNpcsStationary(ServerLevel *level) + { + if (level == NULL) + { + return; + } + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos == NULL) + { + return; + } + const double spawnX = (double)spawnPos->x + 0.5; + const double spawnY = (double)spawnPos->y + 1.0; + const double spawnZ = (double)spawnPos->z + 0.5; + delete spawnPos; + vector > entities = level->getAllEntities(); + for (size_t i = 0; i < entities.size(); ++i) + { + shared_ptr entity = entities[i]; + if (entity == NULL || entity->GetType() != eTYPE_VILLAGER) + { + continue; + } + for (size_t j = 0; j < (sizeof(kBedwarsNpcDefs) / sizeof(kBedwarsNpcDefs[0])); ++j) + { + const BedwarsNpcDef &def = kBedwarsNpcDefs[j]; + const double anchorX = spawnX + (double)def.offX; + const double anchorZ = spawnZ + (double)def.offZ; + const double dx = entity->x - anchorX; + const double dz = entity->z - anchorZ; + if ((dx * dx + dz * dz) <= (3.0 * 3.0)) + { + entity->xd = 0.0; + entity->yd = 0.0; + entity->zd = 0.0; + entity->moveTo(anchorX, spawnY, anchorZ, def.yRot, 0.0f); + break; + } + } + } + } + void BuildBedwarsHub(ServerLevel *level) + { + if (level == NULL) + { + return; + } + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos == NULL) + { + return; + } + const int sx = spawnPos->x; + const int sy = spawnPos->y; + const int sz = spawnPos->z; + delete spawnPos; + for (int dx = -14; dx <= 14; ++dx) + { + for (int dz = -14; dz <= 14; ++dz) + { + const int x = sx + dx; + const int z = sz + dz; + const int ax = abs(dx); + const int az = abs(dz); + for (int y = sy + 1; y <= sy + 6; ++y) + { + level->setTile(x, y, z, 0); + } + if (ax == 14 || az == 14) + { + level->setTile(x, sy, z, Tile::stoneBrick_Id); + if (((dx + dz) & 1) == 0) + { + level->setTile(x, sy + 1, z, Tile::glass_Id); + } + } + else if (ax <= 2 || az <= 2) + { + level->setTile(x, sy, z, Tile::stoneBrick_Id); + } + } + } + for (size_t i = 0; i < (sizeof(kBedwarsNpcDefs) / sizeof(kBedwarsNpcDefs[0])); ++i) + { + const BedwarsNpcDef &def = kBedwarsNpcDefs[i]; + const int px = sx + def.offX; + const int pz = sz + def.offZ; + const int woolColor = (int)i + 1; + for (int ox = -1; ox <= 1; ++ox) + { + for (int oz = -1; oz <= 1; ++oz) + { + level->setTileAndData(px + ox, sy, pz + oz, Tile::cloth_Id, woolColor); + } + } + } + } + void SpawnBedwarsLobbyNpcs(ServerLevel *level) + { + if (level == NULL) + { + return; + } + + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos == NULL) + { + return; + } + + const int spawnXi = spawnPos->x; + const int spawnYi = spawnPos->y; + const int spawnZi = spawnPos->z; + const double spawnX = (double)spawnXi + 0.5; + const double spawnY = (double)spawnYi + 1.0; + const double spawnZ = (double)spawnZi + 0.5; + delete spawnPos; + + for (size_t i = 0; i < (sizeof(kBedwarsNpcDefs) / sizeof(kBedwarsNpcDefs[0])); ++i) + { + const BedwarsNpcDef &def = kBedwarsNpcDefs[i]; + shared_ptr npc(EntityIO::newByEnumType(eTYPE_VILLAGER, level)); + if (npc == NULL) + { + continue; + } + + npc->moveTo(spawnX + (double)def.offX, spawnY, spawnZ + (double)def.offZ, def.yRot, 0.0f); + npc->setDespawnProtected(); + level->addEntity(npc); + + const wchar_t *rainbowLabel = kBedwarsNpcRainbowLabels[i]; + SpawnBedwarsQueueSign(level, spawnXi + def.offX, spawnYi + 2, spawnZi + def.offZ, L"\u00A7cB\u00A76e\u00A7ed\u00A7aw\u00A7ba\u00A7dr\u00A7cs", rainbowLabel); + + app.DebugPrintf("Bedwars lobby NPC spawned: %ls at (%.1f, %.1f, %.1f)\n", + def.label, + spawnX + (double)def.offX, + spawnY, + spawnZ + (double)def.offZ); + } + } +} MinecraftServer::MinecraftServer() { // 4J - added initialisers @@ -91,6 +313,9 @@ MinecraftServer::MinecraftServer() m_texturePackId = 0; maxBuildHeight = Level::maxBuildHeight; m_postUpdateThread = NULL; + m_recentTps = (float)SharedConstants::TICKS_PER_SECOND; + m_lastTpsSampleMs = 0; + m_lastTpsSampleTick = 0; commandDispatcher = new ServerCommandDispatcher(); } @@ -1060,6 +1285,12 @@ void MinecraftServer::run(__int64 seed, void *lpParameter) if (initServer(seed, initData, initSettings,findSeed)) { ServerLevel *levelNormalDimension = levels[0]; + if (IsBedwarsHubSettings(initSettings)) + { + app.DebugPrintf("Bedwars mode detected - building hub and spawning lobby NPCs\n"); + BuildBedwarsHub(levelNormalDimension); + SpawnBedwarsLobbyNpcs(levelNormalDimension); + } // 4J-PB - Set the Stronghold position in the leveldata if there isn't one in there Minecraft *pMinecraft = Minecraft::GetInstance(); LevelData *pLevelData=levelNormalDimension->getLevelData(); @@ -1077,6 +1308,8 @@ void MinecraftServer::run(__int64 seed, void *lpParameter) __int64 lastTime = System::currentTimeMillis(); __int64 unprocessedTime = 0; + m_lastTpsSampleMs = lastTime; + m_lastTpsSampleTick = tickCount; while (running && !s_bServerHalted) { __int64 now = System::currentTimeMillis(); @@ -1152,6 +1385,21 @@ void MinecraftServer::run(__int64 seed, void *lpParameter) connection->tick(); } } + __int64 tpsNow = System::currentTimeMillis(); + __int64 tpsElapsed = tpsNow - m_lastTpsSampleMs; + if (tpsElapsed >= 1000) + { + int tickDelta = tickCount - m_lastTpsSampleTick; + if (tpsElapsed > 0) + { + m_recentTps = ((float)tickDelta * 1000.0f) / (float)tpsElapsed; + if (m_recentTps < 0.0f) m_recentTps = 0.0f; + float maxTps = (float)SharedConstants::TICKS_PER_SECOND; + if (m_recentTps > maxTps) m_recentTps = maxTps; + } + m_lastTpsSampleMs = tpsNow; + m_lastTpsSampleTick = tickCount; + } if(MinecraftServer::setTimeAtEndOfTick) { MinecraftServer::setTimeAtEndOfTick = false; @@ -1468,6 +1716,11 @@ void MinecraftServer::tick() // 4J Stu - We set the levels difficulty based on the minecraft options level->difficulty = app.GetGameHostOption(eGameHostOption_Difficulty); //pMinecraft->options->difficulty; + if (i == 0 && IsBedwarsHubSettings(app.GetGameHostOption(eGameHostOption_All))) + { + KeepBedwarsLobbyNpcsStationary(level); + } + #if DEBUG_SERVER_DONT_SPAWN_MOBS level->setSpawnSettings(false, false); #else @@ -1682,4 +1935,14 @@ bool MinecraftServer::flagEntitiesToBeRemoved(unsigned int *flags) } } return removedFound; -} \ No newline at end of file +} + + + + + + + + + + diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index e61001a..c9cd151 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -111,6 +111,9 @@ public: private: // 4J Added //int m_lastSentDifficulty; + float m_recentTps; + __int64 m_lastTpsSampleMs; + int m_lastTpsSampleTick; public: // 4J Stu - This value should be incremented every time the list of players with friends-only UGC settings changes @@ -236,6 +239,7 @@ public: //static int getSlowQueueIndex() { return s_slowQueuePlayerIndex; } static bool canSendOnSlowQueue(INetworkPlayer *player); static void cycleSlowQueueIndex(); + float getRecentTps() const { return m_recentTps; } void setSaveOnExit(bool save) { m_saveOnExit = save; s_bSaveOnExitAnswered = true; } void Suspend(); diff --git a/Minecraft.Client/MultiPlayerChunkCache.cpp b/Minecraft.Client/MultiPlayerChunkCache.cpp index 8c6c90e..6f938ef 100644 --- a/Minecraft.Client/MultiPlayerChunkCache.cpp +++ b/Minecraft.Client/MultiPlayerChunkCache.cpp @@ -102,12 +102,20 @@ MultiPlayerChunkCache::~MultiPlayerChunkCache() { delete emptyChunk; delete waterChunk; - delete cache; - delete hasData; + // Defensive: avoid deleting loaded chunk objects here. + // During transfer/exit there can still be shared or duplicated references to chunk internals + // (especially light storage) across teardown paths, which can lead to double-free crashes. + // We release cache containers and clear references; chunk memory is reclaimed with process lifetime. + EnterCriticalSection(&m_csLoadCreate); + if (cache != NULL) + { + memset(cache, 0, XZSIZE * XZSIZE * sizeof(LevelChunk *)); + } + loadedChunkList.clear(); + LeaveCriticalSection(&m_csLoadCreate); - AUTO_VAR(itEnd, loadedChunkList.end()); - for (AUTO_VAR(it, loadedChunkList.begin()); it != itEnd; it++) - delete *it; + delete[] cache; + delete[] hasData; DeleteCriticalSection(&m_csLoadCreate); } @@ -172,7 +180,11 @@ LevelChunk *MultiPlayerChunkCache::create(int x, int z) if( g_NetworkManager.IsHost() ) // force here to disable sharing of data { // 4J-JEV: We are about to use shared data, abort if the server is stopped and the data is deleted. - if (MinecraftServer::getInstance()->serverHalted()) return NULL; + if (MinecraftServer::getInstance()->serverHalted()) + { + LeaveCriticalSection(&m_csLoadCreate); + return NULL; + } // If we're the host, then don't create the chunk, share data from the server's copy #ifdef _LARGE_WORLDS @@ -303,4 +315,4 @@ void MultiPlayerChunkCache::dataReceived(int x, int z) if( ( iz < 0 ) || ( iz >= XZSIZE ) ) return; int idx = ix * XZSIZE + iz; hasData[idx] = true; -} \ No newline at end of file +} diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 2bb5106..4eaebde 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -14,6 +14,7 @@ #include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\SharedConstants.h" #include "Settings.h" +#include "Common\Network\NetworkPlayerInterface.h" // #ifdef __PS3__ // #include "PS3\Network\NetworkPlayerSony.h" // #endif @@ -21,7 +22,9 @@ Random *PendingConnection::random = new Random(); #ifdef _WINDOWS64 -bool g_bRejectDuplicateNames = true; +// Windows64 LAN/minigame testing commonly uses repeated display names across clients. +// Keep joins permissive by default; duplicate names can still be rejected by toggling this. +bool g_bRejectDuplicateNames = false; #endif PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id) @@ -161,13 +164,51 @@ void PendingConnection::handleLogin(shared_ptr packet) //if (true)// 4J removed !server->onlineMode) bool sentDisconnect = false; + if (server->getPlayers()->isNameBanned(name)) + { + app.DebugPrintf("Rejecting banned player name: %ls\n", name.c_str()); + disconnect(DisconnectPacket::eDisconnect_Banned); + return; + } + if (server->getPlayers()->isWhitelistEnabled() && !server->getPlayers()->isWhiteListed(name)) + { + app.DebugPrintf("Rejecting non-whitelisted player name: %ls\n", name.c_str()); + disconnect(DisconnectPacket::eDisconnect_Kicked); + return; + } + PlayerUID onlineXuidForBanCheck = packet->m_onlineXuid; +#ifdef _WINDOWS64 + BYTE onlineSmallIdForBanCheck = 255; + if (connection != NULL && connection->getSocket() != NULL) + { + INetworkPlayer *networkPlayer = connection->getSocket()->getPlayer(); + if (networkPlayer != NULL) + { + onlineXuidForBanCheck = networkPlayer->GetUID(); + onlineSmallIdForBanCheck = networkPlayer->GetSmallId(); + } + } +#endif + bool rejectAsBanned = server->getPlayers()->isXuidBanned(onlineXuidForBanCheck); +#ifdef _WINDOWS64 + if (rejectAsBanned) + { + // Windows64 stub player identity is slot-based, so suppress persistent ban rejections + // to avoid false "previously kicked" failures for new joiners. + app.DebugPrintf("PendingConnection::handleLogin - suppressed stale ban for %ls (smallId=%u)\n", + name.c_str(), + (unsigned int)onlineSmallIdForBanCheck); + rejectAsBanned = false; + } +#endif if( sentDisconnect ) { // Do nothing } - else if( server->getPlayers()->isXuidBanned( packet->m_onlineXuid ) ) + else if( rejectAsBanned ) { + app.DebugPrintf("Rejecting banned XUID for player: %ls\n", name.c_str()); disconnect(DisconnectPacket::eDisconnect_Banned); } #ifdef _WINDOWS64 @@ -186,7 +227,8 @@ void PendingConnection::handleLogin(shared_ptr packet) if (nameTaken) { app.DebugPrintf("Rejecting duplicate name: %ls\n", name.c_str()); - disconnect(DisconnectPacket::eDisconnect_Banned); + // Don't map duplicate-name rejection to "banned by host". + disconnect(DisconnectPacket::eDisconnect_Kicked); } else { @@ -294,4 +336,4 @@ wstring PendingConnection::getName() bool PendingConnection::isServerPacketListener() { return true; -} \ No newline at end of file +} diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index f0e538a..42f1ac1 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -27,13 +27,1003 @@ #include "ServerConnection.h" #include "..\Minecraft.World\GenericStats.h" #include "..\Minecraft.World\JavaMath.h" - +#include +#include +#include +#include +#include +#include +#include +#include "..\Minecraft.World\InputOutputStream.h" +#ifdef _WINDOWS64 +#include "Windows64\Network\WinsockNetLayer.h" +#endif // 4J Added #include "..\Minecraft.World\net.minecraft.world.item.crafting.h" #include "Options.h" Random PlayerConnection::random; +namespace +{ + const wstring LCE_TRANSFER_PACKET = L"LCE|Xfer"; + const unsigned int MINIGAME_SETTINGS_FLAG = 0x80000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_MASK = 0x70000000u; + const unsigned int MINIGAME_SETTINGS_TYPE_SHIFT = 28u; + const unsigned int MINIGAME_SETTINGS_ROLE_MASK = 0x0C000000u; + const unsigned int MINIGAME_SETTINGS_ROLE_SHIFT = 26u; + const unsigned int MINIGAME_SETTINGS_QUEUE_MASK = 0x03000000u; + const unsigned int MINIGAME_SETTINGS_QUEUE_SHIFT = 24u; + const unsigned int MINIGAME_TYPE_BEDWARS = 3u; + const unsigned int MINIGAME_ROLE_HUB = 0u; + const unsigned int MINIGAME_ROLE_MATCH = 1u; + const int PARTY_INVITE_TICKS = SharedConstants::TICKS_PER_SECOND * 30; + + struct BedwarsQueueDef + { + const wchar_t *name; + int npcOffX; + int npcOffZ; + int queueOffX; + int queueOffZ; + float queueYaw; + int partyLimit; + int requiredPlayers; + }; + + static const BedwarsQueueDef BEDWARS_QUEUE_DEFS[] = + { + { L"Solo", -3, 0, -8, 0, 90.0f, 1, 2 }, + { L"Doubles", 0, 3, 0, 8, 180.0f, 2, 4 }, + { L"Squads", 3, 0, 8, 0, 270.0f, 4, 8 }, + { L"Practice", 0, -3, 0, -8, 0.0f, 1, 1 }, + }; + + struct PartyInvite + { + BYTE leaderSmallId; + int expireTick; + }; + + struct BedwarsQueueEntry + { + BYTE leaderSmallId; + vector members; + int queuedTick; + }; + + static map s_partyLeaderByMember; + static map > s_partyMembersByLeader; + static map s_partyInvites; + static vector s_bedwarsQueueEntries[4]; + static map s_queueIndexByMember; + static int s_lastQueueProcessTick = -1; + + struct ProxyRoute + { + string hostIp; + int hostPort; + wstring displayName; + }; + + static map s_proxyRoutes; + static bool s_proxyRoutesLoaded = false; + + wstring ToLowerText(const wstring &value) + { + wstring result = value; + for (size_t i = 0; i < result.length(); ++i) + { + result[i] = (wchar_t)std::towlower(result[i]); + } + return result; + } + + vector SplitCommandTokens(const wstring &message) + { + vector tokens; + size_t pos = 0; + while (pos < message.length()) + { + while (pos < message.length() && std::iswspace(message[pos])) + { + ++pos; + } + if (pos >= message.length()) + { + break; + } + size_t end = pos; + while (end < message.length() && !std::iswspace(message[end])) + { + ++end; + } + tokens.push_back(message.substr(pos, end - pos)); + pos = end; + } + return tokens; + } + + string TrimAsciiString(const string &value) + { + size_t first = 0; + while (first < value.length() && std::isspace((unsigned char)value[first])) + { + ++first; + } + + size_t last = value.length(); + while (last > first && std::isspace((unsigned char)value[last - 1])) + { + --last; + } + + return value.substr(first, last - first); + } + + wstring AsciiToWide(const string &value) + { + wstring out; + out.reserve(value.length()); + for (size_t i = 0; i < value.length(); ++i) + { + out.push_back((wchar_t)(unsigned char)value[i]); + } + return out; + } + + bool OpenProxyRoutesFile(std::ifstream &in, string &outPath) + { + vector candidates; + candidates.push_back("proxy-worlds.properties"); + candidates.push_back("..\\proxy-worlds.properties"); + candidates.push_back("..\\..\\proxy-worlds.properties"); + candidates.push_back("..\\..\\..\\proxy-worlds.properties"); + + char modulePath[MAX_PATH] = {0}; + if (::GetModuleFileNameA(NULL, modulePath, MAX_PATH) > 0) + { + string exePath(modulePath); + size_t slash = exePath.find_last_of("\\/"); + if (slash != string::npos) + { + string exeDir = exePath.substr(0, slash); + candidates.push_back(exeDir + "\\proxy-worlds.properties"); + candidates.push_back(exeDir + "\\..\\proxy-worlds.properties"); + candidates.push_back(exeDir + "\\..\\..\\proxy-worlds.properties"); + } + } + + for (size_t i = 0; i < candidates.size(); ++i) + { + in.clear(); + in.open(candidates[i].c_str(), std::ios::in); + if (in.good()) + { + outPath = candidates[i]; + return true; + } + } + + return false; + } + + void LoadProxyRoutes(bool forceReload) + { + if (s_proxyRoutesLoaded && !forceReload) + { + return; + } + + s_proxyRoutesLoaded = true; + s_proxyRoutes.clear(); + + std::ifstream in; + string loadedFromPath; + if (!OpenProxyRoutesFile(in, loadedFromPath)) + { + return; + } + + string line; + while (std::getline(in, line)) + { + const size_t commentPos = line.find('#'); + if (commentPos != string::npos) + { + line = line.substr(0, commentPos); + } + + line = TrimAsciiString(line); + if (line.empty()) + { + continue; + } + + const size_t equalsPos = line.find('='); + if (equalsPos == string::npos) + { + continue; + } + + string routeNameRaw = TrimAsciiString(line.substr(0, equalsPos)); + string routeSpecRaw = TrimAsciiString(line.substr(equalsPos + 1)); + if (routeNameRaw.empty() || routeSpecRaw.empty()) + { + continue; + } + + string displayNameRaw = routeNameRaw; + const size_t pipePos = routeSpecRaw.find('|'); + if (pipePos != string::npos) + { + displayNameRaw = TrimAsciiString(routeSpecRaw.substr(pipePos + 1)); + routeSpecRaw = TrimAsciiString(routeSpecRaw.substr(0, pipePos)); + } + + const size_t colonPos = routeSpecRaw.rfind(':'); + if (colonPos == string::npos) + { + continue; + } + + string ip = TrimAsciiString(routeSpecRaw.substr(0, colonPos)); + string portText = TrimAsciiString(routeSpecRaw.substr(colonPos + 1)); + if (ip.empty() || portText.empty()) + { + continue; + } + + char *portEnd = NULL; + long parsedPort = std::strtol(portText.c_str(), &portEnd, 10); + if (portEnd == NULL || *portEnd != '\0' || parsedPort <= 0 || parsedPort > 65535) + { + continue; + } + + ProxyRoute route; + route.hostIp = ip; + route.hostPort = (int)parsedPort; + route.displayName = AsciiToWide(displayNameRaw); + if (route.displayName.empty()) + { + route.displayName = AsciiToWide(routeNameRaw); + } + + s_proxyRoutes[ToLowerText(AsciiToWide(routeNameRaw))] = route; + } + } + + bool TryGetProxyRoute(const wstring &routeName, ProxyRoute &outRoute) + { + LoadProxyRoutes(false); + auto it = s_proxyRoutes.find(ToLowerText(routeName)); + if (it == s_proxyRoutes.end()) + { + return false; + } + outRoute = it->second; + return true; + } + + int GetQueueIndexFromName(const wstring &name) + { + const wstring lower = ToLowerText(name); + if (lower == L"solo") return 0; + if (lower == L"double" || lower == L"doubles") return 1; + if (lower == L"squad" || lower == L"squads") return 2; + if (lower == L"practice") return 3; + return -1; + } + + wstring GetQueueDisplayName(int queueIndex) + { + if (queueIndex < 0 || queueIndex >= (int)(sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0]))) + { + return L"Unknown"; + } + return BEDWARS_QUEUE_DEFS[queueIndex].name; + } + + BYTE GetSmallIdForPlayer(shared_ptr serverPlayer) + { + if (serverPlayer == NULL || serverPlayer->connection == NULL) + { + return 0xFF; + } + INetworkPlayer *networkPlayer = serverPlayer->connection->getNetworkPlayer(); + if (networkPlayer == NULL) + { + return 0xFF; + } + return networkPlayer->GetSmallId(); + } + + shared_ptr FindPlayerBySmallId(MinecraftServer *server, BYTE smallId) + { + if (server == NULL || server->getPlayers() == NULL) + { + return nullptr; + } + for (AUTO_VAR(it, server->getPlayers()->players.begin()); it != server->getPlayers()->players.end(); ++it) + { + shared_ptr check = *it; + if (GetSmallIdForPlayer(check) == smallId) + { + return check; + } + } + return nullptr; + } + + shared_ptr FindPlayerByNameInsensitive(MinecraftServer *server, const wstring &name) + { + if (server == NULL || server->getPlayers() == NULL) + { + return nullptr; + } + const wstring wanted = ToLowerText(name); + for (AUTO_VAR(it, server->getPlayers()->players.begin()); it != server->getPlayers()->players.end(); ++it) + { + shared_ptr check = *it; + if (ToLowerText(check->name) == wanted) + { + return check; + } + } + return nullptr; + } + + wstring GetPlayerNameBySmallId(MinecraftServer *server, BYTE smallId) + { + shared_ptr target = FindPlayerBySmallId(server, smallId); + if (target != NULL) + { + return target->name; + } + wchar_t fallback[32]; + swprintf_s(fallback, L"Player%u", (unsigned int)smallId); + return fallback; + } + + void SendPlayerMessage(shared_ptr target, const wstring &message) + { + if (target != NULL) + { + target->sendMessage(message, ChatPacket::e_ChatCustom); + } + } + + bool IsBedwarsHostSettings(unsigned int hostSettings) + { + if ((hostSettings & MINIGAME_SETTINGS_FLAG) == 0) + { + return false; + } + return (((hostSettings & MINIGAME_SETTINGS_TYPE_MASK) >> MINIGAME_SETTINGS_TYPE_SHIFT) == MINIGAME_TYPE_BEDWARS); + } + + unsigned int GetBedwarsRoleFromSettings(unsigned int hostSettings) + { + return (hostSettings & MINIGAME_SETTINGS_ROLE_MASK) >> MINIGAME_SETTINGS_ROLE_SHIFT; + } + + unsigned int GetBedwarsQueueModeFromSettings(unsigned int hostSettings) + { + return (hostSettings & MINIGAME_SETTINGS_QUEUE_MASK) >> MINIGAME_SETTINGS_QUEUE_SHIFT; + } + + bool IsBedwarsHubSettings(unsigned int hostSettings) + { + if (!IsBedwarsHostSettings(hostSettings)) + { + return false; + } + return GetBedwarsRoleFromSettings(hostSettings) == MINIGAME_ROLE_HUB; + } + + unsigned int ApplyBedwarsSessionMetadata(unsigned int hostSettings, unsigned int role, unsigned int queueMode) + { + hostSettings |= MINIGAME_SETTINGS_FLAG; + hostSettings &= ~MINIGAME_SETTINGS_TYPE_MASK; + hostSettings |= (MINIGAME_TYPE_BEDWARS << MINIGAME_SETTINGS_TYPE_SHIFT); + hostSettings &= ~MINIGAME_SETTINGS_ROLE_MASK; + hostSettings |= ((role & 0x3u) << MINIGAME_SETTINGS_ROLE_SHIFT); + hostSettings &= ~MINIGAME_SETTINGS_QUEUE_MASK; + hostSettings |= ((queueMode & 0x3u) << MINIGAME_SETTINGS_QUEUE_SHIFT); + return hostSettings; + } + + BYTE GetPartyLeaderForMember(BYTE memberSmallId) + { + auto it = s_partyLeaderByMember.find(memberSmallId); + if (it == s_partyLeaderByMember.end()) + { + return memberSmallId; + } + return it->second; + } + + void EnsurePartyLeaderEntry(BYTE leaderSmallId) + { + if (s_partyMembersByLeader.find(leaderSmallId) == s_partyMembersByLeader.end()) + { + s_partyMembersByLeader[leaderSmallId] = set(); + } + s_partyMembersByLeader[leaderSmallId].insert(leaderSmallId); + s_partyLeaderByMember[leaderSmallId] = leaderSmallId; + } + + vector GetPartyMembersForPlayer(BYTE memberSmallId) + { + vector members; + const BYTE leader = GetPartyLeaderForMember(memberSmallId); + auto it = s_partyMembersByLeader.find(leader); + if (it == s_partyMembersByLeader.end()) + { + members.push_back(memberSmallId); + return members; + } + for (AUTO_VAR(itM, it->second.begin()); itM != it->second.end(); ++itM) + { + members.push_back(*itM); + } + if (members.empty()) + { + members.push_back(memberSmallId); + } + return members; + } + + void RemoveQueueGroupContainingMember(MinecraftServer *server, BYTE memberSmallId, bool announce, const wchar_t *reason) + { + auto queueIt = s_queueIndexByMember.find(memberSmallId); + if (queueIt == s_queueIndexByMember.end()) + { + return; + } + const int queueIndex = queueIt->second; + if (queueIndex < 0 || queueIndex >= (int)(sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0]))) + { + s_queueIndexByMember.erase(memberSmallId); + return; + } + + vector &entries = s_bedwarsQueueEntries[queueIndex]; + for (size_t i = 0; i < entries.size(); ++i) + { + bool contains = false; + for (size_t m = 0; m < entries[i].members.size(); ++m) + { + if (entries[i].members[m] == memberSmallId) + { + contains = true; + break; + } + } + if (!contains) + { + continue; + } + + for (size_t m = 0; m < entries[i].members.size(); ++m) + { + const BYTE removeId = entries[i].members[m]; + s_queueIndexByMember.erase(removeId); + if (announce) + { + shared_ptr target = FindPlayerBySmallId(server, removeId); + if (target != NULL) + { + wstring msg = L"Left Bedwars "; + msg += GetQueueDisplayName(queueIndex); + msg += L" queue"; + if (reason != NULL && reason[0] != 0) + { + msg += L" ("; + msg += reason; + msg += L")"; + } + msg += L"."; + SendPlayerMessage(target, msg); + } + } + } + + entries.erase(entries.begin() + i); + return; + } + + // Defensive cleanup: stale map entry with no corresponding queue group. + s_queueIndexByMember.erase(memberSmallId); + } + + void RemovePlayerFromAllQueues(MinecraftServer *server, BYTE memberSmallId, bool announce, const wchar_t *reason) + { + if (memberSmallId == 0xFF) + { + return; + } + while (s_queueIndexByMember.find(memberSmallId) != s_queueIndexByMember.end()) + { + RemoveQueueGroupContainingMember(server, memberSmallId, announce, reason); + } + } + + void LeaveParty(MinecraftServer *server, BYTE memberSmallId, bool announce) + { + if (memberSmallId == 0xFF) + { + return; + } + + const BYTE leaderSmallId = GetPartyLeaderForMember(memberSmallId); + auto partyIt = s_partyMembersByLeader.find(leaderSmallId); + if (partyIt == s_partyMembersByLeader.end()) + { + s_partyLeaderByMember.erase(memberSmallId); + s_partyInvites.erase(memberSmallId); + return; + } + + if (leaderSmallId == memberSmallId) + { + for (AUTO_VAR(it, partyIt->second.begin()); it != partyIt->second.end(); ++it) + { + const BYTE other = *it; + s_partyLeaderByMember.erase(other); + if (announce) + { + shared_ptr target = FindPlayerBySmallId(server, other); + if (target != NULL) + { + SendPlayerMessage(target, L"Party disbanded."); + } + } + } + s_partyMembersByLeader.erase(leaderSmallId); + } + else + { + partyIt->second.erase(memberSmallId); + s_partyLeaderByMember.erase(memberSmallId); + if (announce) + { + shared_ptr leader = FindPlayerBySmallId(server, leaderSmallId); + if (leader != NULL) + { + wstring leftMsg = GetPlayerNameBySmallId(server, memberSmallId); + leftMsg += L" left your party."; + SendPlayerMessage(leader, leftMsg); + } + shared_ptr member = FindPlayerBySmallId(server, memberSmallId); + if (member != NULL) + { + SendPlayerMessage(member, L"You left the party."); + } + } + if (partyIt->second.size() <= 1) + { + s_partyMembersByLeader.erase(leaderSmallId); + s_partyLeaderByMember.erase(leaderSmallId); + } + } + + s_partyInvites.erase(memberSmallId); + for (AUTO_VAR(itI, s_partyInvites.begin()); itI != s_partyInvites.end();) + { + if (itI->second.leaderSmallId == memberSmallId) + { + itI = s_partyInvites.erase(itI); + } + else + { + ++itI; + } + } + } + + void ClearPlayerStateOnDisconnect(MinecraftServer *server, shared_ptr leavingPlayer) + { + const BYTE smallId = GetSmallIdForPlayer(leavingPlayer); + if (smallId == 0xFF) + { + return; + } + RemovePlayerFromAllQueues(server, smallId, false, NULL); + LeaveParty(server, smallId, false); + s_partyInvites.erase(smallId); + } + + vector BuildQueueGroupMembers(MinecraftServer *server, BYTE requestorSmallId) + { + vector members; + vector partyMembers = GetPartyMembersForPlayer(requestorSmallId); + for (size_t i = 0; i < partyMembers.size(); ++i) + { + if (FindPlayerBySmallId(server, partyMembers[i]) != NULL) + { + members.push_back(partyMembers[i]); + } + } + if (members.empty()) + { + members.push_back(requestorSmallId); + } + std::sort(members.begin(), members.end()); + members.erase(std::unique(members.begin(), members.end()), members.end()); + return members; + } + + bool JoinBedwarsQueue(MinecraftServer *server, shared_ptr requestingPlayer, int queueIndex) + { + if (server == NULL || requestingPlayer == NULL) + { + return false; + } + if (queueIndex < 0 || queueIndex >= (int)(sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0]))) + { + return false; + } + + const BYTE requestorSmallId = GetSmallIdForPlayer(requestingPlayer); + if (requestorSmallId == 0xFF) + { + return false; + } + + vector members = BuildQueueGroupMembers(server, requestorSmallId); + const BedwarsQueueDef &queue = BEDWARS_QUEUE_DEFS[queueIndex]; + if ((int)members.size() > queue.partyLimit) + { + wstring msg = L"Your party is too large for "; + msg += queue.name; + msg += L" queue."; + SendPlayerMessage(requestingPlayer, msg); + return false; + } + + for (size_t i = 0; i < members.size(); ++i) + { + RemovePlayerFromAllQueues(server, members[i], false, NULL); + } + + BedwarsQueueEntry entry; + entry.leaderSmallId = GetPartyLeaderForMember(requestorSmallId); + entry.members = members; + entry.queuedTick = server->tickCount; + s_bedwarsQueueEntries[queueIndex].push_back(entry); + for (size_t i = 0; i < members.size(); ++i) + { + s_queueIndexByMember[members[i]] = queueIndex; + } + + for (size_t i = 0; i < members.size(); ++i) + { + shared_ptr target = FindPlayerBySmallId(server, members[i]); + if (target != NULL) + { + wstring queuedMsg = L"Queued for Bedwars "; + queuedMsg += queue.name; + queuedMsg += L"."; + SendPlayerMessage(target, queuedMsg); + } + } + return true; + } + + bool IsBedwarsHubProtectedArea(ServerLevel *level, int x, int y, int z) + { + if (level == NULL || !IsBedwarsHubSettings(app.GetGameHostOption(eGameHostOption_All))) + { + return false; + } + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos == NULL) + { + return false; + } + const int dx = (int)Mth::abs((float)(x - spawnPos->x)); + const int dz = (int)Mth::abs((float)(z - spawnPos->z)); + const int minY = spawnPos->y - 3; + const int maxY = spawnPos->y + 12; + delete spawnPos; + return (dx <= 24 && dz <= 24 && y >= minY && y <= maxY); + } + + int FindBedwarsQueueIndexForTarget(ServerLevel *level, shared_ptr target) + { + if (level == NULL || target == NULL || target->GetType() != eTYPE_VILLAGER) + { + return -1; + } + + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos == NULL) + { + return -1; + } + + const double spawnX = (double)spawnPos->x + 0.5; + const double spawnZ = (double)spawnPos->z + 0.5; + delete spawnPos; + + const double maxMatchDistSqr = 2.5 * 2.5; + for (size_t i = 0; i < (sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0])); ++i) + { + const double npcX = spawnX + (double)BEDWARS_QUEUE_DEFS[i].npcOffX; + const double npcZ = spawnZ + (double)BEDWARS_QUEUE_DEFS[i].npcOffZ; + const double dx = target->x - npcX; + const double dz = target->z - npcZ; + if ((dx * dx + dz * dz) <= maxMatchDistSqr) + { + return (int)i; + } + } + + return -1; + } + +#ifdef _WINDOWS64 + bool PickQueueEntriesRecursive(const vector &entries, size_t startIndex, int remainingPlayers, vector &pickedEntries) + { + if (remainingPlayers == 0) + { + return true; + } + if (startIndex >= entries.size() || remainingPlayers < 0) + { + return false; + } + + for (size_t i = startIndex; i < entries.size(); ++i) + { + const int groupSize = (int)entries[i].members.size(); + if (groupSize <= 0 || groupSize > remainingPlayers) + { + continue; + } + + pickedEntries.push_back(i); + if (PickQueueEntriesRecursive(entries, i + 1, remainingPlayers - groupSize, pickedEntries)) + { + return true; + } + pickedEntries.pop_back(); + } + + return false; + } + + bool PickQueueEntriesForMatch(const vector &entries, int requiredPlayers, vector &pickedEntries) + { + pickedEntries.clear(); + if (requiredPlayers <= 0) + { + return false; + } + return PickQueueEntriesRecursive(entries, 0, requiredPlayers, pickedEntries); + } + + bool IsRemoteMatchServerCandidate(const Win64LANSession &session, int queueIndex, int requiredPlayers) + { + if (!session.isJoinable) + { + return false; + } + if (!IsBedwarsHostSettings(session.gameHostSettings)) + { + return false; + } + if (GetBedwarsRoleFromSettings(session.gameHostSettings) != MINIGAME_ROLE_MATCH) + { + return false; + } + if ((int)GetBedwarsQueueModeFromSettings(session.gameHostSettings) != queueIndex) + { + return false; + } + int maxPlayers = (int)session.maxPlayers; + if (maxPlayers <= 0) + { + maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + } + const int slotsAvailable = maxPlayers - (int)session.playerCount; + if (slotsAvailable < requiredPlayers) + { + return false; + } + return true; + } + + bool FindBestQueueTarget(int queueIndex, int requiredPlayers, string &outIp, int &outPort) + { + std::vector sessions = WinsockNetLayer::GetDiscoveredSessions(); + int bestFreeSlots = -1; + int bestPlayers = 9999; + for (size_t i = 0; i < sessions.size(); ++i) + { + const Win64LANSession &session = sessions[i]; + if (!IsRemoteMatchServerCandidate(session, queueIndex, requiredPlayers)) + { + continue; + } + int maxPlayers = (int)session.maxPlayers; + if (maxPlayers <= 0) + { + maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + } + const int freeSlots = maxPlayers - (int)session.playerCount; + if (freeSlots > bestFreeSlots || (freeSlots == bestFreeSlots && (int)session.playerCount < bestPlayers)) + { + bestFreeSlots = freeSlots; + bestPlayers = (int)session.playerCount; + outIp = session.hostIP; + outPort = session.hostPort; + } + } + return (bestFreeSlots >= 0 && !outIp.empty() && outPort > 0); + } + + void SendTransferPayloadEx(shared_ptr player, const string &hostIp, int hostPort, int queueIndex, const wstring &destinationName) + { + if (player == NULL || player->connection == NULL) + { + return; + } + + wstring hostIpW; + for (size_t i = 0; i < hostIp.length(); ++i) + { + hostIpW.push_back((wchar_t)hostIp[i]); + } + + ByteArrayOutputStream rawOutput; + DataOutputStream output(&rawOutput); + output.writeByte((byte)1); + output.writeByte((byte)queueIndex); + output.writeInt(hostPort); + output.writeUTF(hostIpW); + output.writeUTF(destinationName); + + player->connection->send(shared_ptr(new CustomPayloadPacket(LCE_TRANSFER_PACKET, rawOutput.toByteArray()))); + } + + void SendTransferPayload(shared_ptr player, const string &hostIp, int hostPort, int queueIndex) + { + SendTransferPayloadEx(player, hostIp, hostPort, queueIndex, GetQueueDisplayName(queueIndex)); + } +#endif + + void ProcessBedwarsQueueSystem(MinecraftServer *server) + { + if (server == NULL) + { + return; + } + if (server->getPlayers() == NULL || server->getPlayers()->players.empty()) + { + for (int i = 0; i < (int)(sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0])); ++i) + { + s_bedwarsQueueEntries[i].clear(); + } + s_queueIndexByMember.clear(); + s_partyInvites.clear(); + s_partyLeaderByMember.clear(); + s_partyMembersByLeader.clear(); + return; + } + + if (s_lastQueueProcessTick == server->tickCount) + { + return; + } + s_lastQueueProcessTick = server->tickCount; + + for (AUTO_VAR(itInvite, s_partyInvites.begin()); itInvite != s_partyInvites.end();) + { + if (server->tickCount >= itInvite->second.expireTick) + { + itInvite = s_partyInvites.erase(itInvite); + } + else + { + ++itInvite; + } + } + + for (int queueIndex = 0; queueIndex < (int)(sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0])); ++queueIndex) + { + vector &entries = s_bedwarsQueueEntries[queueIndex]; + for (size_t i = 0; i < entries.size();) + { + bool valid = true; + for (size_t m = 0; m < entries[i].members.size(); ++m) + { + if (FindPlayerBySmallId(server, entries[i].members[m]) == NULL) + { + valid = false; + break; + } + } + if (!valid) + { + for (size_t m = 0; m < entries[i].members.size(); ++m) + { + s_queueIndexByMember.erase(entries[i].members[m]); + } + entries.erase(entries.begin() + i); + continue; + } + ++i; + } + } + +#ifdef _WINDOWS64 + if (!IsBedwarsHubSettings(app.GetGameHostOption(eGameHostOption_All))) + { + return; + } + + for (int queueIndex = 0; queueIndex < (int)(sizeof(BEDWARS_QUEUE_DEFS) / sizeof(BEDWARS_QUEUE_DEFS[0])); ++queueIndex) + { + vector &entries = s_bedwarsQueueEntries[queueIndex]; + if (entries.empty()) + { + continue; + } + + const int requiredPlayers = BEDWARS_QUEUE_DEFS[queueIndex].requiredPlayers; + int totalQueued = 0; + for (size_t i = 0; i < entries.size(); ++i) + { + totalQueued += (int)entries[i].members.size(); + } + if (totalQueued < requiredPlayers) + { + continue; + } + + string targetIp; + int targetPort = 0; + if (!FindBestQueueTarget(queueIndex, requiredPlayers, targetIp, targetPort)) + { + continue; + } + + vector pickedEntries; + if (!PickQueueEntriesForMatch(entries, requiredPlayers, pickedEntries)) + { + continue; + } + + app.DebugPrintf("Bedwars queue %d matched %d players -> %s:%d\n", queueIndex, requiredPlayers, targetIp.c_str(), targetPort); + + for (int pick = (int)pickedEntries.size() - 1; pick >= 0; --pick) + { + const size_t entryIndex = pickedEntries[pick]; + if (entryIndex >= entries.size()) + { + continue; + } + const BedwarsQueueEntry entry = entries[entryIndex]; + for (size_t m = 0; m < entry.members.size(); ++m) + { + const BYTE memberId = entry.members[m]; + s_queueIndexByMember.erase(memberId); + shared_ptr target = FindPlayerBySmallId(server, memberId); + if (target != NULL) + { + SendPlayerMessage(target, L"Match found. Transferring to game server..."); + SendTransferPayload(target, targetIp, targetPort, queueIndex); + } + } + entries.erase(entries.begin() + entryIndex); + } + } +#endif + } +} PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connection, shared_ptr player) { // 4J - added initialisers @@ -107,6 +1097,8 @@ void PlayerConnection::tick() { dropSpamTickCount--; } + + ProcessBedwarsQueueSystem(server); } void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason) @@ -119,6 +1111,7 @@ void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason) } app.DebugPrintf("PlayerConnection disconect reason: %d\n", reason ); + ClearPlayerStateOnDisconnect(server, player); player->disconnect(); // 4J Stu - Need to remove the player from the receiving list before their socket is NULLed so that we can find another player on their system @@ -417,6 +1410,11 @@ void PlayerConnection::handlePlayerAction(shared_ptr packet) int x = packet->x; int y = packet->y; int z = packet->z; + if (packet->action == PlayerActionPacket::START_DESTROY_BLOCK && IsBedwarsHubProtectedArea(level, x, y, z)) + { + player->connection->send(shared_ptr(new TileUpdatePacket(x, y, z, level))); + return; + } if (shouldVerifyLocation) { double xDist = player->x - (x + 0.5); @@ -482,6 +1480,11 @@ void PlayerConnection::handleUseItem(shared_ptr packet) int y = packet->getY(); int z = packet->getZ(); int face = packet->getFace(); + if (face != 255 && IsBedwarsHubProtectedArea(level, x, y, z)) + { + player->connection->send(shared_ptr(new TileUpdatePacket(x, y, z, level))); + return; + } // 4J Stu - We don't have ops, so just use the levels setting bool canEditSpawn = level->canEditSpawn; // = level->dimension->id != 0 || server->players->isOp(player->name); @@ -636,48 +1639,677 @@ void PlayerConnection::handleSetCarriedItem(shared_ptr pac void PlayerConnection::handleChat(shared_ptr packet) { - // 4J - TODO -#if 0 - wstring message = packet->message; - if (message.length() > SharedConstants::maxChatLength) + if(packet == NULL || packet->m_stringArgs.size() == 0) { - disconnect(L"Chat message too long"); return; } - message = message.trim(); - for (int i = 0; i < message.length(); i++) + + wstring message = packet->m_stringArgs[0]; + if(message.length() > SharedConstants::maxChatLength) { - if (SharedConstants.acceptableLetters.indexOf(message.charAt(i)) < 0 && (int) message.charAt(i) < 32) + disconnect(DisconnectPacket::eDisconnect_Overflow); + return; + } + + size_t first = 0; + while(first < message.length() && std::iswspace(message[first])) + { + ++first; + } + + size_t last = message.length(); + while(last > first && std::iswspace(message[last - 1])) + { + --last; + } + + message = message.substr(first, last - first); + if(message.length() == 0) + { + return; + } + + for(size_t i = 0; i < message.length(); ++i) + { + wchar_t ch = message[i]; + if(ch < 32 && SharedConstants::acceptableLetters.find(ch) == wstring::npos) { - disconnect(L"Illegal characters in chat"); return; } } - if (message.startsWith("/")) + if(message[0] == L'/') { handleCommand(message); - } else { - message = "<" + player.name + "> " + message; - logger.info(message); - server.players.broadcastAll(new ChatPacket(message)); + return; } + + server->getPlayers()->broadcastAll(shared_ptr(new ChatPacket(player->name, ChatPacket::e_ChatCustom, -1, message))); + chatSpamTickCount += SharedConstants::TICKS_PER_SECOND; - if (chatSpamTickCount > SharedConstants::TICKS_PER_SECOND * 10) + if(chatSpamTickCount > SharedConstants::TICKS_PER_SECOND * 10) { - disconnect("disconnect.spam"); + disconnect(DisconnectPacket::eDisconnect_Overflow); } -#endif } void PlayerConnection::handleCommand(const wstring& message) { - // 4J - TODO -#if 0 - server.getCommandDispatcher().performCommand(player, message); -#endif -} + if (message.length() < 2 || message[0] != L'/') + { + return; + } + vector tokens = SplitCommandTokens(message.substr(1)); + if (tokens.empty()) + { + return; + } + + const wstring command = ToLowerText(tokens[0]); + const BYTE selfSmallId = GetSmallIdForPlayer(player); + INetworkPlayer *requestPlayer = getNetworkPlayer(); + const bool isHost = (requestPlayer != NULL && requestPlayer->IsHost()); + const bool isPersistedOp = (server != NULL && server->getPlayers() != NULL && server->getPlayers()->isOp(player->name)); + const bool hasAdminPermission = isHost || player->isModerator() || isPersistedOp; + + if (command == L"tps") + { + float tps = server->getRecentTps(); + wchar_t tpsText[64]; + swprintf_s(tpsText, L"TPS: %.2f", tps); + player->sendMessage(tpsText, ChatPacket::e_ChatCustom); + return; + } + + if (command == L"help") + { + player->sendMessage(L"/tps, /list, /party, /queue, /queuehost, /hub, /server", ChatPacket::e_ChatCustom); + if (hasAdminPermission) + { + player->sendMessage(L"Admin: /kick /ban /pardon /op /deop /tp /gamemode /save-on /save-off /save-all /whitelist /send", ChatPacket::e_ChatCustom); + } + return; + } + + if (command == L"list") + { + wchar_t listInfo[80]; + const int count = (server != NULL && server->getPlayers() != NULL) ? server->getPlayers()->getPlayerCount() : 0; + const int maxPlayers = (server != NULL && server->getPlayers() != NULL) ? server->getPlayers()->getMaxPlayers() : MINECRAFT_NET_MAX_PLAYERS; + swprintf_s(listInfo, L"Players: %d/%d", count, maxPlayers); + player->sendMessage(listInfo, ChatPacket::e_ChatCustom); + if (server != NULL && server->getPlayers() != NULL) + { + const wstring names = server->getPlayers()->getPlayerNames(); + if (!names.empty()) + { + player->sendMessage(names, ChatPacket::e_ChatCustom); + } + } + return; + } + + if (command == L"server") + { + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /server ", ChatPacket::e_ChatCustom); + return; + } + + const wstring sub = ToLowerText(tokens[1]); + if (sub == L"list") + { + LoadProxyRoutes(false); + if (s_proxyRoutes.empty()) + { + player->sendMessage(L"No proxy routes configured. Create proxy-worlds.properties.", ChatPacket::e_ChatCustom); + return; + } + + player->sendMessage(L"Available servers:", ChatPacket::e_ChatCustom); + for (AUTO_VAR(itRoute, s_proxyRoutes.begin()); itRoute != s_proxyRoutes.end(); ++itRoute) + { + const ProxyRoute &route = itRoute->second; + wstring line = L" - "; + line += itRoute->first; + line += L" ("; + line += route.displayName; + line += L")"; + player->sendMessage(line, ChatPacket::e_ChatCustom); + } + return; + } + + if (sub == L"reload") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + LoadProxyRoutes(true); + player->sendMessage(L"Proxy route config reloaded.", ChatPacket::e_ChatCustom); + return; + } + + ProxyRoute route; + if (!TryGetProxyRoute(sub, route)) + { + player->sendMessage(L"Unknown server route. Use /server list.", ChatPacket::e_ChatCustom); + return; + } + + SendTransferPayloadEx(player, route.hostIp, route.hostPort, 0xFF, route.displayName); + wstring msg = L"Transferring to "; + msg += route.displayName; + msg += L"..."; + player->sendMessage(msg, ChatPacket::e_ChatCustom); + return; + } + + if (command == L"send") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 3) + { + player->sendMessage(L"Usage: /send ", ChatPacket::e_ChatCustom); + return; + } + + shared_ptr target = FindPlayerByNameInsensitive(server, tokens[1]); + if (target == NULL || target->connection == NULL) + { + player->sendMessage(L"Player not found.", ChatPacket::e_ChatCustom); + return; + } + + ProxyRoute route; + if (!TryGetProxyRoute(tokens[2], route)) + { + player->sendMessage(L"Unknown server route. Use /server list.", ChatPacket::e_ChatCustom); + return; + } + + SendTransferPayloadEx(target, route.hostIp, route.hostPort, 0xFF, route.displayName); + wstring adminMsg = L"Sent "; + adminMsg += target->name; + adminMsg += L" to "; + adminMsg += route.displayName; + player->sendMessage(adminMsg, ChatPacket::e_ChatCustom); + return; + } + + if (command == L"whitelist") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /whitelist ", ChatPacket::e_ChatCustom); + return; + } + const wstring sub = ToLowerText(tokens[1]); + if (sub == L"on") + { + server->getPlayers()->setWhitelistEnabled(true); + player->sendMessage(L"Whitelist enabled.", ChatPacket::e_ChatCustom); + return; + } + if (sub == L"off") + { + server->getPlayers()->setWhitelistEnabled(false); + player->sendMessage(L"Whitelist disabled.", ChatPacket::e_ChatCustom); + return; + } + if (sub == L"reload") + { + server->getPlayers()->reloadWhitelist(); + player->sendMessage(L"Whitelist reloaded.", ChatPacket::e_ChatCustom); + return; + } + if (sub == L"add" && tokens.size() >= 3) + { + server->getPlayers()->whiteList(tokens[2]); + wstring msg = L"Added to whitelist: "; + msg += tokens[2]; + player->sendMessage(msg, ChatPacket::e_ChatCustom); + return; + } + if ((sub == L"remove" || sub == L"del") && tokens.size() >= 3) + { + server->getPlayers()->blackList(tokens[2]); + wstring msg = L"Removed from whitelist: "; + msg += tokens[2]; + player->sendMessage(msg, ChatPacket::e_ChatCustom); + return; + } + + player->sendMessage(L"Usage: /whitelist ", ChatPacket::e_ChatCustom); + return; + } + + if (command == L"op" || command == L"deop") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 2) + { + player->sendMessage(command == L"op" ? L"Usage: /op " : L"Usage: /deop ", ChatPacket::e_ChatCustom); + return; + } + + const bool isAdd = (command == L"op"); + const wstring &targetName = tokens[1]; + bool changed = isAdd ? server->getPlayers()->addOp(targetName) : server->getPlayers()->removeOp(targetName); + + shared_ptr target = FindPlayerByNameInsensitive(server, targetName); + if (target != NULL) + { + target->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_Op, isAdd ? 1 : 0); + server->getPlayers()->broadcastAll(shared_ptr(new PlayerInfoPacket(target))); + } + + if (changed) + { + wstring ok = (isAdd ? L"Granted op to " : L"Removed op from "); + ok += targetName; + player->sendMessage(ok, ChatPacket::e_ChatCustom); + } + else + { + wstring msg = (isAdd ? L"Already op: " : L"Not op: "); + msg += targetName; + player->sendMessage(msg, ChatPacket::e_ChatCustom); + } + return; + } + + if (command == L"kick" || command == L"ban" || command == L"pardon") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + + if ((command == L"kick" || command == L"ban") && tokens.size() < 2) + { + player->sendMessage(command == L"kick" ? L"Usage: /kick " : L"Usage: /ban ", ChatPacket::e_ChatCustom); + return; + } + + if (command == L"pardon") + { + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /pardon ", ChatPacket::e_ChatCustom); + return; + } + if (server->getPlayers()->unbanName(tokens[1])) + { + wstring msg = L"Unbanned "; + msg += tokens[1]; + player->sendMessage(msg, ChatPacket::e_ChatCustom); + } + else + { + player->sendMessage(L"Player was not banned.", ChatPacket::e_ChatCustom); + } + return; + } + + shared_ptr target = FindPlayerByNameInsensitive(server, tokens[1]); + if (target == NULL || target->connection == NULL) + { + player->sendMessage(L"Player not found.", ChatPacket::e_ChatCustom); + return; + } + if (target == player) + { + player->sendMessage(L"You cannot target yourself.", ChatPacket::e_ChatCustom); + return; + } + + if (command == L"ban") + { + server->getPlayers()->banName(target->name); + } + + target->connection->setWasKicked(); + target->connection->disconnect(command == L"ban" ? DisconnectPacket::eDisconnect_Banned : DisconnectPacket::eDisconnect_Kicked); + return; + } + + if (command == L"save-on" || command == L"save-off" || command == L"save-all") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + if (command == L"save-all") + { + server->getPlayers()->saveAll(NULL, false); + player->sendMessage(L"Saved all player data.", ChatPacket::e_ChatCustom); + return; + } + + const bool disableSaving = (command == L"save-off"); + app.SetGameHostOption(eGameHostOption_DisableSaving, disableSaving ? 1 : 0); + player->sendMessage(disableSaving ? L"World saving disabled." : L"World saving enabled.", ChatPacket::e_ChatCustom); + return; + } + + if (command == L"gamemode" || command == L"gm") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /gamemode [player]", ChatPacket::e_ChatCustom); + return; + } + const wstring gmArg = ToLowerText(tokens[1]); + GameType *gameType = NULL; + if (gmArg == L"0" || gmArg == L"s" || gmArg == L"survival") + { + gameType = GameType::SURVIVAL; + } + else if (gmArg == L"1" || gmArg == L"c" || gmArg == L"creative") + { + gameType = GameType::CREATIVE; + } + if (gameType == NULL) + { + player->sendMessage(L"Unknown gamemode.", ChatPacket::e_ChatCustom); + return; + } + + shared_ptr target = player; + if (tokens.size() >= 3) + { + target = FindPlayerByNameInsensitive(server, tokens[2]); + if (target == NULL) + { + player->sendMessage(L"Player not found.", ChatPacket::e_ChatCustom); + return; + } + } + + target->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_CreativeMode, gameType == GameType::CREATIVE ? 1 : 0); + target->gameMode->setGameModeForPlayer(gameType); + target->connection->send(shared_ptr(new GameEventPacket(GameEventPacket::CHANGE_GAME_MODE, gameType->getId()))); + server->getPlayers()->broadcastAll(shared_ptr(new PlayerInfoPacket(target))); + player->sendMessage(L"Gamemode updated.", ChatPacket::e_ChatCustom); + return; + } + + if (command == L"tp") + { + if (!hasAdminPermission) + { + player->sendMessage(L"You do not have permission.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /tp [destination]", ChatPacket::e_ChatCustom); + return; + } + + shared_ptr toMove = player; + shared_ptr destination = NULL; + if (tokens.size() == 2) + { + destination = FindPlayerByNameInsensitive(server, tokens[1]); + } + else + { + toMove = FindPlayerByNameInsensitive(server, tokens[1]); + destination = FindPlayerByNameInsensitive(server, tokens[2]); + } + if (toMove == NULL || destination == NULL || toMove->connection == NULL) + { + player->sendMessage(L"Player not found.", ChatPacket::e_ChatCustom); + return; + } + + toMove->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot); + player->sendMessage(L"Teleported.", ChatPacket::e_ChatCustom); + return; + } + if (command == L"hub" && IsBedwarsHubSettings(app.GetGameHostOption(eGameHostOption_All))) + { + ServerLevel *level = server->getLevel(player->dimension); + if (level != NULL) + { + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos != NULL) + { + teleport((double)spawnPos->x + 0.5, (double)spawnPos->y + 1.0, (double)spawnPos->z + 0.5, 0.0f, 0.0f); + delete spawnPos; + } + } + player->sendMessage(L"Returned to Bedwars hub.", ChatPacket::e_ChatCustom); + return; + } + + if (command == L"queue" && IsBedwarsHostSettings(app.GetGameHostOption(eGameHostOption_All))) + { + if (!IsBedwarsHubSettings(app.GetGameHostOption(eGameHostOption_All))) + { + player->sendMessage(L"Queues are only available in Bedwars hub mode.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /queue ", ChatPacket::e_ChatCustom); + return; + } + const wstring queueArg = ToLowerText(tokens[1]); + if (queueArg == L"leave") + { + RemovePlayerFromAllQueues(server, selfSmallId, true, L"left"); + return; + } + const int queueIndex = GetQueueIndexFromName(queueArg); + if (queueIndex < 0) + { + player->sendMessage(L"Unknown queue. Use solo, doubles, squads or practice.", ChatPacket::e_ChatCustom); + return; + } + JoinBedwarsQueue(server, player, queueIndex); + return; + } + + if (command == L"queuehost" && IsBedwarsHostSettings(app.GetGameHostOption(eGameHostOption_All))) + { + INetworkPlayer *networkPlayer = getNetworkPlayer(); + if (networkPlayer == NULL || !networkPlayer->IsHost()) + { + player->sendMessage(L"Only the host can change queue hosting mode.", ChatPacket::e_ChatCustom); + return; + } + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /queuehost ", ChatPacket::e_ChatCustom); + return; + } + + const wstring hostMode = ToLowerText(tokens[1]); + unsigned int hostSettings = app.GetGameHostOption(eGameHostOption_All); + if (hostMode == L"hub") + { + hostSettings = ApplyBedwarsSessionMetadata(hostSettings, MINIGAME_ROLE_HUB, 0u); + app.SetGameHostOption(eGameHostOption_All, hostSettings); + g_NetworkManager.UpdateAndSetGameSessionData(); + player->sendMessage(L"This server is now a Bedwars hub.", ChatPacket::e_ChatCustom); + return; + } + + const int queueIndex = GetQueueIndexFromName(hostMode); + if (queueIndex < 0) + { + player->sendMessage(L"Unknown queue mode for /queuehost.", ChatPacket::e_ChatCustom); + return; + } + + hostSettings = ApplyBedwarsSessionMetadata(hostSettings, MINIGAME_ROLE_MATCH, (unsigned int)queueIndex); + app.SetGameHostOption(eGameHostOption_All, hostSettings); + g_NetworkManager.UpdateAndSetGameSessionData(); + + wstring modeMsg = L"This server now accepts "; + modeMsg += GetQueueDisplayName(queueIndex); + modeMsg += L" queue transfers."; + player->sendMessage(modeMsg, ChatPacket::e_ChatCustom); + return; + } + + if (command == L"party") + { + if (selfSmallId == 0xFF) + { + return; + } + + if (tokens.size() < 2) + { + player->sendMessage(L"Usage: /party ", ChatPacket::e_ChatCustom); + return; + } + + const wstring sub = ToLowerText(tokens[1]); + if (sub == L"invite") + { + if (tokens.size() < 3) + { + player->sendMessage(L"Usage: /party invite ", ChatPacket::e_ChatCustom); + return; + } + + const BYTE currentLeader = GetPartyLeaderForMember(selfSmallId); + if (currentLeader != selfSmallId) + { + player->sendMessage(L"Only the party leader can invite players.", ChatPacket::e_ChatCustom); + return; + } + + shared_ptr target = FindPlayerByNameInsensitive(server, tokens[2]); + if (target == NULL) + { + player->sendMessage(L"Player not found.", ChatPacket::e_ChatCustom); + return; + } + + const BYTE targetSmallId = GetSmallIdForPlayer(target); + if (targetSmallId == 0xFF || targetSmallId == selfSmallId) + { + player->sendMessage(L"Invalid party target.", ChatPacket::e_ChatCustom); + return; + } + + PartyInvite invite; + invite.leaderSmallId = selfSmallId; + invite.expireTick = server->tickCount + PARTY_INVITE_TICKS; + s_partyInvites[targetSmallId] = invite; + + wstring sentMsg = L"Invited "; + sentMsg += target->name; + sentMsg += L" to your party."; + player->sendMessage(sentMsg, ChatPacket::e_ChatCustom); + + wstring recvMsg = player->name; + recvMsg += L" invited you to a party. Type /party accept."; + SendPlayerMessage(target, recvMsg); + return; + } + + if (sub == L"accept") + { + auto inviteIt = s_partyInvites.find(selfSmallId); + if (inviteIt == s_partyInvites.end()) + { + player->sendMessage(L"You have no pending party invite.", ChatPacket::e_ChatCustom); + return; + } + + const BYTE leaderSmallId = inviteIt->second.leaderSmallId; + shared_ptr leader = FindPlayerBySmallId(server, leaderSmallId); + if (leader == NULL) + { + s_partyInvites.erase(selfSmallId); + player->sendMessage(L"That party invite has expired.", ChatPacket::e_ChatCustom); + return; + } + + RemovePlayerFromAllQueues(server, selfSmallId, true, L"party updated"); + LeaveParty(server, selfSmallId, false); + EnsurePartyLeaderEntry(leaderSmallId); + s_partyMembersByLeader[leaderSmallId].insert(selfSmallId); + s_partyLeaderByMember[selfSmallId] = leaderSmallId; + s_partyInvites.erase(selfSmallId); + + wstring joinedMsg = L"You joined "; + joinedMsg += leader->name; + joinedMsg += L"'s party."; + player->sendMessage(joinedMsg, ChatPacket::e_ChatCustom); + + wstring leaderMsg = player->name; + leaderMsg += L" joined your party."; + SendPlayerMessage(leader, leaderMsg); + return; + } + + if (sub == L"leave") + { + RemovePlayerFromAllQueues(server, selfSmallId, true, L"party left"); + LeaveParty(server, selfSmallId, true); + return; + } + + if (sub == L"list") + { + const BYTE leaderSmallId = GetPartyLeaderForMember(selfSmallId); + auto partyIt = s_partyMembersByLeader.find(leaderSmallId); + if (partyIt == s_partyMembersByLeader.end() || partyIt->second.size() <= 1) + { + player->sendMessage(L"You are not in a party.", ChatPacket::e_ChatCustom); + return; + } + + wstring listMsg = L"Party: "; + bool first = true; + for (AUTO_VAR(itM, partyIt->second.begin()); itM != partyIt->second.end(); ++itM) + { + if (!first) listMsg += L", "; + listMsg += GetPlayerNameBySmallId(server, *itM); + first = false; + } + player->sendMessage(listMsg, ChatPacket::e_ChatCustom); + return; + } + + player->sendMessage(L"Usage: /party ", ChatPacket::e_ChatCustom); + return; + } + + player->sendMessage(L"Unknown command. Use /help.", ChatPacket::e_ChatCustom); +} void PlayerConnection::handleAnimate(shared_ptr packet) { if (packet->action == AnimatePacket::SWING) @@ -728,6 +2360,7 @@ void PlayerConnection::setShowOnMaps(bool bVal) void PlayerConnection::handleDisconnect(shared_ptr packet) { // 4J Stu - Need to remove the player from the receiving list before their socket is NULLed so that we can find another player on their system + ClearPlayerStateOnDisconnect(server, player); server->getPlayers()->removePlayerFromReceiving( player ); connection->close(DisconnectPacket::eDisconnect_Quitting); } @@ -765,6 +2398,34 @@ void PlayerConnection::handleInteract(shared_ptr packet) // even though the ray is blocked. if (target != NULL) // && player->canSee(target) && player->distanceToSqr(target) < 6 * 6) { + if (packet->action == InteractPacket::INTERACT && + IsBedwarsHubSettings(app.GetGameHostOption(eGameHostOption_All)) && + target->GetType() == eTYPE_VILLAGER) + { + const int queueIndex = FindBedwarsQueueIndexForTarget(level, target); + if (queueIndex >= 0) + { + Pos *spawnPos = level->getSharedSpawnPos(); + if (spawnPos != NULL) + { + const BedwarsQueueDef &queue = BEDWARS_QUEUE_DEFS[queueIndex]; + const double queueX = (double)spawnPos->x + 0.5 + (double)queue.queueOffX; + const double queueY = (double)spawnPos->y + 1.0; + const double queueZ = (double)spawnPos->z + 0.5 + (double)queue.queueOffZ; + delete spawnPos; + + teleport(queueX, queueY, queueZ, queue.queueYaw, 0.0f); + } + + JoinBedwarsQueue(server, player, queueIndex); + } + else + { + send(shared_ptr(new ChatPacket(L"Bedwars", ChatPacket::e_ChatCustom, -1, L"This Bedwars NPC is unavailable."))); + } + return; + } + //boole canSee = player->canSee(target); //double maxDist = 6 * 6; //if (!canSee) @@ -1715,3 +3376,18 @@ bool PlayerConnection::isGuest() return isGuest; } } + + + + + + + + + + + + + + + diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 4e5334a..bab1bed 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -24,6 +24,9 @@ #include "..\Minecraft.World\net.minecraft.world.level.storage.h" #include "..\Minecraft.World\net.minecraft.world.level.saveddata.h" #include "..\Minecraft.World\JavaMath.h" +#include +#include +#include #if defined(_XBOX) || defined(_WINDOWS64) #include "Xbox\Network\NetworkPlayerXbox.h" #elif defined(__PS3__) || defined(__ORBIS__) @@ -31,6 +34,68 @@ #endif // 4J - this class is fairly substantially altered as there didn't seem any point in porting code for banning, whitelisting, ops etc. +namespace +{ + static const wchar_t *kWhitelistFile = L"whitelist.txt"; + static const wchar_t *kOpsFile = L"ops.txt"; + static const wchar_t *kBansFile = L"banned-players.txt"; + + static wstring NormalizePlayerName(const wstring &name) + { + wstring out = name; + std::transform(out.begin(), out.end(), out.begin(), [](wchar_t c) { return (wchar_t)std::towlower(c); }); + return out; + } + + static void LoadNameSetFromFile(const wchar_t *fileName, set &outSet) + { + outSet.clear(); + std::wifstream in(fileName); + if (!in.good()) + { + return; + } + + wstring line; + while (std::getline(in, line)) + { + // Strip comments. + size_t hashPos = line.find(L'#'); + if (hashPos != wstring::npos) + { + line = line.substr(0, hashPos); + } + + while (!line.empty() && iswspace(line.front())) + { + line.erase(line.begin()); + } + while (!line.empty() && iswspace(line.back())) + { + line.pop_back(); + } + + if (!line.empty()) + { + outSet.insert(NormalizePlayerName(line)); + } + } + } + + static void SaveNameSetToFile(const wchar_t *fileName, const set &values) + { + std::wofstream out(fileName, std::ios::out | std::ios::trunc); + if (!out.good()) + { + return; + } + + for (AUTO_VAR(it, values.begin()); it != values.end(); ++it) + { + out << *it << L"\n"; + } + } +} PlayerList::PlayerList(MinecraftServer *server) { @@ -58,6 +123,10 @@ PlayerList::PlayerList(MinecraftServer *server) maxPlayers = server->settings->getInt(L"max-players", 20); #endif doWhiteList = false; + + reloadWhitelist(); + reloadOps(); + reloadBans(); InitializeCriticalSection(&m_kickPlayersCS); InitializeCriticalSection(&m_closePlayersCS); @@ -90,6 +159,12 @@ void PlayerList::placeNewPlayer(Connection *connection, shared_ptr player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_HOST,1); } + // Apply persisted operator status on join (dedicated and local-host modes). + if (isOp(player->name)) + { + player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_Op, 1); + } + #if defined(__PS3__) || defined(__ORBIS__) // PS3 networking library doesn't automatically assign PlayerUIDs to the network players for anything remote, so need to tell it what to set from the data in this packet now if( !g_NetworkManager.IsLocalGame() ) @@ -467,8 +542,64 @@ shared_ptr PlayerList::getPlayerForLogin(PendingConnection *pendin return shared_ptr(); } #endif + + if (isNameBanned(userName)) + { + pendingConnection->disconnect(DisconnectPacket::eDisconnect_Banned); + return shared_ptr(); + } + + if (doWhiteList) + { + INetworkPlayer *joiningNetworkPlayer = NULL; + if (pendingConnection != NULL && pendingConnection->connection != NULL && pendingConnection->connection->getSocket() != NULL) + { + joiningNetworkPlayer = pendingConnection->connection->getSocket()->getPlayer(); + } + + if (joiningNetworkPlayer == NULL || !joiningNetworkPlayer->IsHost()) + { + if (!isWhiteListed(userName)) + { + app.DebugPrintf("PlayerList::getPlayerForLogin - rejecting non-whitelisted player %ls\n", userName.c_str()); + pendingConnection->disconnect(DisconnectPacket::eDisconnect_Kicked); + return shared_ptr(); + } + } + } - shared_ptr player = shared_ptr(new ServerPlayer(server, server->getLevel(0), userName, new ServerPlayerGameMode(server->getLevel(0)) )); + wstring resolvedName = userName; +#ifdef _WINDOWS64 + // Win64 testing often has multiple clients with the same OS username. + // Keep server-side player names unique to avoid legacy name-based collisions. + if (!resolvedName.empty()) + { + const wstring baseName = resolvedName; + int suffix = 2; + bool unique = false; + while (!unique) + { + unique = true; + for (AUTO_VAR(it, players.begin()); it != players.end(); ++it) + { + shared_ptr existing = *it; + if (existing != NULL && existing->name == resolvedName) + { + const int currentSuffix = suffix++; + wstring trimmedBase = baseName; + if (trimmedBase.length() > 56) + { + trimmedBase = trimmedBase.substr(0, 56); + } + resolvedName = trimmedBase + L"_" + to_wstring(currentSuffix); + unique = false; + break; + } + } + } + } +#endif + shared_ptr player = shared_ptr(new ServerPlayer(server, server->getLevel(0), resolvedName, new ServerPlayerGameMode(server->getLevel(0)) )); player->gameMode->player = player; // 4J added as had to remove this assignment from ServerPlayer ctor player->setXuid( xuid ); // 4J Added player->setOnlineXuid( onlineXuid ); // 4J Added @@ -930,7 +1061,14 @@ void PlayerList::tick() if (player != NULL) { +#ifdef _WINDOWS64 + // Keep kicks session-scoped on Windows64. The stub identity maps to slot ids, so + // persisting bans can incorrectly block later joiners that reuse a slot. + app.DebugPrintf("PlayerList::tick - kicking smallId=%u (no persistent ban on Windows64)\n", + (unsigned int)smallId); +#else m_bannedXuids.push_back( player->getOnlineXuid() ); +#endif // 4J Stu - If we have kicked a player, make sure that they have no privileges if they later try to join the world when trust players is off player->enableAllPlayerPrivileges( false ); player->connection->setWasKicked(); @@ -1003,12 +1141,16 @@ wstring PlayerList::getPlayerNames() bool PlayerList::isWhiteListed(const wstring& name) { - return true; + if (!doWhiteList) + { + return true; + } + return (m_whitelistNames.find(NormalizePlayerName(name)) != m_whitelistNames.end()); } bool PlayerList::isOp(const wstring& name) { - return false; + return (m_operatorNames.find(NormalizePlayerName(name)) != m_operatorNames.end()); } bool PlayerList::isOp(shared_ptr player) @@ -1177,14 +1319,90 @@ void PlayerList::saveAll(ProgressListener *progressListener, bool bDeleteGuestMa void PlayerList::whiteList(const wstring& playerName) { + m_whitelistNames.insert(NormalizePlayerName(playerName)); + SaveNameSetToFile(kWhitelistFile, m_whitelistNames); } void PlayerList::blackList(const wstring& playerName) { + m_whitelistNames.erase(NormalizePlayerName(playerName)); + SaveNameSetToFile(kWhitelistFile, m_whitelistNames); } void PlayerList::reloadWhitelist() { + LoadNameSetFromFile(kWhitelistFile, m_whitelistNames); +} + +void PlayerList::setWhitelistEnabled(bool enabled) +{ + doWhiteList = enabled; +} + +bool PlayerList::isWhitelistEnabled() const +{ + return doWhiteList; +} + +bool PlayerList::addOp(const wstring &playerName) +{ + const size_t oldSize = m_operatorNames.size(); + m_operatorNames.insert(NormalizePlayerName(playerName)); + if (m_operatorNames.size() != oldSize) + { + SaveNameSetToFile(kOpsFile, m_operatorNames); + return true; + } + return false; +} + +bool PlayerList::removeOp(const wstring &playerName) +{ + const size_t erased = m_operatorNames.erase(NormalizePlayerName(playerName)); + if (erased != 0) + { + SaveNameSetToFile(kOpsFile, m_operatorNames); + return true; + } + return false; +} + +void PlayerList::reloadOps() +{ + LoadNameSetFromFile(kOpsFile, m_operatorNames); +} + +bool PlayerList::banName(const wstring &playerName) +{ + const size_t oldSize = m_bannedNames.size(); + m_bannedNames.insert(NormalizePlayerName(playerName)); + if (m_bannedNames.size() != oldSize) + { + SaveNameSetToFile(kBansFile, m_bannedNames); + return true; + } + return false; +} + +bool PlayerList::unbanName(const wstring &playerName) +{ + const size_t erased = m_bannedNames.erase(NormalizePlayerName(playerName)); + if (erased != 0) + { + SaveNameSetToFile(kBansFile, m_bannedNames); + return true; + } + return false; +} + +void PlayerList::reloadBans() +{ + LoadNameSetFromFile(kBansFile, m_bannedNames); +} + +bool PlayerList::isNameBanned(const wstring &name) const +{ + return (m_bannedNames.find(NormalizePlayerName(name)) != m_bannedNames.end()); } void PlayerList::sendLevelInfo(shared_ptr player, ServerLevel *level) diff --git a/Minecraft.Client/PlayerList.h b/Minecraft.Client/PlayerList.h index 14bd6b2..5c32f62 100644 --- a/Minecraft.Client/PlayerList.h +++ b/Minecraft.Client/PlayerList.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "..\Minecraft.World\ArrayWithLength.h" class ServerPlayer; @@ -34,6 +35,9 @@ private: CRITICAL_SECTION m_kickPlayersCS; deque m_smallIdsToClose; CRITICAL_SECTION m_closePlayersCS; + set m_whitelistNames; + set m_operatorNames; + set m_bannedNames; /* 4J - removed Set bans = new HashSet(); Set ipBans = new HashSet(); @@ -99,10 +103,19 @@ public: bool sendTo(const wstring& name, shared_ptr packet); // 4J Added ProgressListener *progressListener param and bDeleteGuestMaps param void saveAll(ProgressListener *progressListener, bool bDeleteGuestMaps = false); - void whiteList(const wstring& playerName); + void whiteList(const wstring& playerName); void blackList(const wstring& playerName); // Set getWhiteList(); / 4J removed void reloadWhitelist(); + void setWhitelistEnabled(bool enabled); + bool isWhitelistEnabled() const; + bool addOp(const wstring &playerName); + bool removeOp(const wstring &playerName); + void reloadOps(); + bool banName(const wstring &playerName); + bool unbanName(const wstring &playerName); + void reloadBans(); + bool isNameBanned(const wstring &name) const; void sendLevelInfo(shared_ptr player, ServerLevel *level); void sendAllPlayerInfo(shared_ptr player); int getPlayerCount(); diff --git a/Minecraft.Client/Screen.cpp b/Minecraft.Client/Screen.cpp index 0019b54..f01aeec 100644 --- a/Minecraft.Client/Screen.cpp +++ b/Minecraft.Client/Screen.cpp @@ -38,6 +38,11 @@ void Screen::keyPressed(wchar_t eventCharacter, int eventKey) } } +void Screen::HandleKeyPressed(wchar_t eventCharacter, int eventKey) +{ + keyPressed(eventCharacter, eventKey); +} + wstring Screen::getClipboard() { // 4J - removed diff --git a/Minecraft.Client/Screen.h b/Minecraft.Client/Screen.h index 50deb1d..aedbe84 100644 --- a/Minecraft.Client/Screen.h +++ b/Minecraft.Client/Screen.h @@ -28,6 +28,7 @@ protected: public: static wstring getClipboard(); static void setClipboard(const wstring& str); + void HandleKeyPressed(wchar_t eventCharacter, int eventKey); private: Button *clickedButton; @@ -51,4 +52,3 @@ public: virtual void confirmResult(bool result, int id); virtual void tabPressed(); }; - diff --git a/Minecraft.Client/ServerPlayer.cpp b/Minecraft.Client/ServerPlayer.cpp index 489e6f3..b194689 100644 --- a/Minecraft.Client/ServerPlayer.cpp +++ b/Minecraft.Client/ServerPlayer.cpp @@ -8,6 +8,7 @@ #include "Settings.h" #include "PlayerList.h" #include "MultiPlayerLevel.h" +#include "TextEditScreen.h" #include "..\Minecraft.World\Pos.h" #include "..\Minecraft.World\net.minecraft.world.level.h" #include "..\Minecraft.World\net.minecraft.world.level.storage.h" @@ -269,6 +270,24 @@ void ServerPlayer::flushEntitiesToRemove() } +void ServerPlayer::openTextEdit(shared_ptr sign) +{ + if (sign == NULL || connection == NULL) + { + return; + } + + if (connection->isLocal()) + { + Minecraft *mc = Minecraft::GetInstance(); + if (mc != NULL) + { + app.DebugPrintf("ServerPlayer::openTextEdit - opening TextEditScreen for local player"); + mc->setScreen(new TextEditScreen(sign)); + } + } +} + // 4J - have split doTick into 3 bits, so that we can call the doChunkSendingTick separately, but still do the equivalent of what calling a full doTick used to do, by calling this method void ServerPlayer::doTick(bool sendChunks, bool dontDelayChunks/*=false*/, bool ignorePortal/*=false*/) { @@ -445,7 +464,7 @@ void ServerPlayer::doChunkSendingTick(bool dontDelayChunks) for (unsigned int i = 0; i < tes->size(); i++) { // 4J Stu - Added delay param to ensure that these arrive after the BRUPs from above - // Fix for #9169 - ART : Sign text is replaced with the words “Awaiting approval”. + // Fix for #9169 - ART : Sign text is replaced with the words ?Awaiting approval?. broadcast(tes->at(i), !connection->isLocal() && !dontDelayChunks); } delete tes; diff --git a/Minecraft.Client/ServerPlayer.h b/Minecraft.Client/ServerPlayer.h index a9b3759..df2f091 100644 --- a/Minecraft.Client/ServerPlayer.h +++ b/Minecraft.Client/ServerPlayer.h @@ -97,6 +97,7 @@ public: virtual bool startCrafting(int x, int y, int z); // 4J added bool return virtual bool startEnchanting(int x, int y, int z); // 4J added bool return virtual bool startRepairing(int x, int y, int z); // 4J added bool return + virtual void openTextEdit(shared_ptr sign); virtual bool openContainer(shared_ptr container); // 4J added bool return virtual bool openFurnace(shared_ptr furnace); // 4J added bool return virtual bool openTrap(shared_ptr trap); // 4J added bool return diff --git a/Minecraft.Client/TextEditScreen.cpp b/Minecraft.Client/TextEditScreen.cpp index 9537b96..7e00f53 100644 --- a/Minecraft.Client/TextEditScreen.cpp +++ b/Minecraft.Client/TextEditScreen.cpp @@ -62,6 +62,10 @@ void TextEditScreen::keyPressed(wchar_t ch, int eventKey) if (eventKey == Keyboard::KEY_DOWN || eventKey == Keyboard::KEY_RETURN) line = (line + 1) & 3; wstring temp=sign->GetMessage(line); + while (!temp.empty() && temp[temp.length() - 1] == L' ') + { + temp = temp.substr(0, temp.length() - 1); + } if (eventKey == Keyboard::KEY_BACK && temp.length() > 0) { temp = temp.substr(0, temp.length() - 1); @@ -116,4 +120,4 @@ void TextEditScreen::render(int xm, int ym, float a) Screen::render(xm, ym, a); -} \ No newline at end of file +} diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 19fc259..9e84eff 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -47,7 +47,28 @@ std::vector WinsockNetLayer::s_freeSmallIds; bool g_Win64MultiplayerHost = false; bool g_Win64MultiplayerJoin = false; int g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; -char g_Win64MultiplayerIP[256] = "127.0.0.1"; +int g_Win64MultiplayerMaxPlayers = MINECRAFT_NET_MAX_PLAYERS; +char g_Win64MultiplayerIP[256] = "0.0.0.0"; +BYTE WinsockNetLayer::s_maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + +static void ConfigureConnectedSocket(SOCKET sock) +{ + if (sock == INVALID_SOCKET) + { + return; + } + + int noDelay = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&noDelay, sizeof(noDelay)); + + int keepAlive = 1; + setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (const char *)&keepAlive, sizeof(keepAlive)); + + int recvTimeoutMs = 30000; + int sendTimeoutMs = 30000; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&recvTimeoutMs, sizeof(recvTimeoutMs)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char *)&sendTimeoutMs, sizeof(sendTimeoutMs)); +} bool WinsockNetLayer::Initialize() { @@ -145,6 +166,14 @@ bool WinsockNetLayer::HostGame(int port) s_hostSmallId = 0; s_nextSmallId = 1; s_hostGamePort = port; + if (g_Win64MultiplayerMaxPlayers >= 1 && g_Win64MultiplayerMaxPlayers <= MINECRAFT_NET_MAX_PLAYERS) + { + s_maxPlayers = (BYTE)g_Win64MultiplayerMaxPlayers; + } + else + { + s_maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + } EnterCriticalSection(&s_freeSmallIdLock); s_freeSmallIds.clear(); @@ -161,7 +190,15 @@ bool WinsockNetLayer::HostGame(int port) char portStr[16]; sprintf_s(portStr, "%d", port); - int iResult = getaddrinfo(NULL, portStr, &hints, &result); + const char *bindIp = NULL; + if (g_Win64MultiplayerIP[0] != 0 && + strcmp(g_Win64MultiplayerIP, "*") != 0 && + strcmp(g_Win64MultiplayerIP, "0.0.0.0") != 0) + { + bindIp = g_Win64MultiplayerIP; + } + + int iResult = getaddrinfo(bindIp, portStr, &hints, &result); if (iResult != 0) { app.DebugPrintf("getaddrinfo failed: %d\n", iResult); @@ -189,7 +226,8 @@ bool WinsockNetLayer::HostGame(int port) return false; } - iResult = listen(s_listenSocket, SOMAXCONN); + int backlog = (s_maxPlayers > 1) ? (int)s_maxPlayers : 1; + iResult = listen(s_listenSocket, backlog); if (iResult == SOCKET_ERROR) { app.DebugPrintf("listen() failed: %d\n", WSAGetLastError()); @@ -203,7 +241,10 @@ bool WinsockNetLayer::HostGame(int port) s_acceptThread = CreateThread(NULL, 0, AcceptThreadProc, NULL, 0, NULL); - app.DebugPrintf("Win64 LAN: Hosting on port %d\n", port); + app.DebugPrintf("Win64 LAN: Hosting on %s:%d (maxPlayers=%u)\n", + (bindIp != NULL) ? bindIp : "0.0.0.0", + port, + (unsigned int)s_maxPlayers); return true; } @@ -213,6 +254,14 @@ bool WinsockNetLayer::JoinGame(const char *ip, int port) s_isHost = false; s_hostSmallId = 0; + s_connected = false; + s_active = false; + + if (s_hostConnectionSocket != INVALID_SOCKET) + { + closesocket(s_hostConnectionSocket); + s_hostConnectionSocket = INVALID_SOCKET; + } struct addrinfo hints = {}; struct addrinfo *result = NULL; @@ -231,37 +280,54 @@ bool WinsockNetLayer::JoinGame(const char *ip, int port) return false; } - s_hostConnectionSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); - if (s_hostConnectionSocket == INVALID_SOCKET) + bool connected = false; + BYTE assignedSmallId = 0; + const int maxAttempts = 12; + + for (int attempt = 0; attempt < maxAttempts; ++attempt) { - app.DebugPrintf("socket() failed: %d\n", WSAGetLastError()); - freeaddrinfo(result); - return false; + s_hostConnectionSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (s_hostConnectionSocket == INVALID_SOCKET) + { + app.DebugPrintf("socket() failed: %d\n", WSAGetLastError()); + break; + } + + ConfigureConnectedSocket(s_hostConnectionSocket); + + iResult = connect(s_hostConnectionSocket, result->ai_addr, (int)result->ai_addrlen); + if (iResult == SOCKET_ERROR) + { + int err = WSAGetLastError(); + app.DebugPrintf("connect() to %s:%d failed (attempt %d/%d): %d\n", ip, port, attempt + 1, maxAttempts, err); + closesocket(s_hostConnectionSocket); + s_hostConnectionSocket = INVALID_SOCKET; + Sleep(200); + continue; + } + + BYTE assignBuf[1]; + int bytesRecv = recv(s_hostConnectionSocket, (char *)assignBuf, 1, 0); + if (bytesRecv != 1) + { + app.DebugPrintf("Failed to receive small ID assignment from host (attempt %d/%d)\n", attempt + 1, maxAttempts); + closesocket(s_hostConnectionSocket); + s_hostConnectionSocket = INVALID_SOCKET; + Sleep(200); + continue; + } + + assignedSmallId = assignBuf[0]; + connected = true; + break; } - - int noDelay = 1; - setsockopt(s_hostConnectionSocket, IPPROTO_TCP, TCP_NODELAY, (const char *)&noDelay, sizeof(noDelay)); - - iResult = connect(s_hostConnectionSocket, result->ai_addr, (int)result->ai_addrlen); freeaddrinfo(result); - if (iResult == SOCKET_ERROR) - { - app.DebugPrintf("connect() to %s:%d failed: %d\n", ip, port, WSAGetLastError()); - closesocket(s_hostConnectionSocket); - s_hostConnectionSocket = INVALID_SOCKET; - return false; - } - BYTE assignBuf[1]; - int bytesRecv = recv(s_hostConnectionSocket, (char *)assignBuf, 1, 0); - if (bytesRecv != 1) + if (!connected) { - app.DebugPrintf("Failed to receive small ID assignment from host\n"); - closesocket(s_hostConnectionSocket); - s_hostConnectionSocket = INVALID_SOCKET; return false; } - s_localSmallId = assignBuf[0]; + s_localSmallId = assignedSmallId; app.DebugPrintf("Win64 LAN: Connected to %s:%d, assigned smallId=%d\n", ip, port, s_localSmallId); @@ -363,16 +429,22 @@ void WinsockNetLayer::HandleDataReceived(BYTE fromSmallId, BYTE toSmallId, unsig INetworkPlayer *pPlayerFrom = g_NetworkManager.GetPlayerBySmallId(fromSmallId); INetworkPlayer *pPlayerTo = g_NetworkManager.GetPlayerBySmallId(toSmallId); - if (pPlayerFrom == NULL || pPlayerTo == NULL) return; - if (s_isHost) { + if (pPlayerFrom == NULL) + { + return; + } ::Socket *pSocket = pPlayerFrom->GetSocket(); if (pSocket != NULL) pSocket->pushDataToQueue(data, dataSize, false); } else { + if (pPlayerTo == NULL) + { + return; + } ::Socket *pSocket = pPlayerTo->GetSocket(); if (pSocket != NULL) pSocket->pushDataToQueue(data, dataSize, true); @@ -391,8 +463,7 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) break; } - int noDelay = 1; - setsockopt(clientSocket, IPPROTO_TCP, TCP_NODELAY, (const char *)&noDelay, sizeof(noDelay)); + ConfigureConnectedSocket(clientSocket); extern QNET_STATE _iQNetStubState; if (_iQNetStubState != QNET_STATE_GAME_PLAY) @@ -409,7 +480,7 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) assignedSmallId = s_freeSmallIds.back(); s_freeSmallIds.pop_back(); } - else if (s_nextSmallId < MINECRAFT_NET_MAX_PLAYERS) + else if (s_nextSmallId < s_maxPlayers) { assignedSmallId = s_nextSmallId++; } @@ -479,7 +550,8 @@ DWORD WINAPI WinsockNetLayer::RecvThreadProc(LPVOID param) BYTE clientSmallId = s_connections[connIdx].smallId; LeaveCriticalSection(&s_connectionsLock); - BYTE *recvBuf = new BYTE[WIN64_NET_RECV_BUFFER_SIZE]; + std::vector recvBuf; + recvBuf.resize(WIN64_NET_RECV_BUFFER_SIZE); while (s_active) { @@ -492,23 +564,30 @@ DWORD WINAPI WinsockNetLayer::RecvThreadProc(LPVOID param) int packetSize = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; - if (packetSize <= 0 || packetSize > WIN64_NET_RECV_BUFFER_SIZE) + if (packetSize <= 0 || packetSize > WIN64_NET_MAX_PACKET_SIZE) { - app.DebugPrintf("Win64 LAN: Invalid packet size %d from client smallId=%d\n", packetSize, clientSmallId); + app.DebugPrintf("Win64 LAN: Invalid packet size %d from client smallId=%d (max=%d)\n", + packetSize, + clientSmallId, + (int)WIN64_NET_MAX_PACKET_SIZE); break; } - if (!RecvExact(sock, recvBuf, packetSize)) + if ((int)recvBuf.size() < packetSize) + { + recvBuf.resize(packetSize); + app.DebugPrintf("Win64 LAN: Resized host recv buffer to %d bytes for client smallId=%d\n", packetSize, clientSmallId); + } + + if (!RecvExact(sock, &recvBuf[0], packetSize)) { app.DebugPrintf("Win64 LAN: Client smallId=%d disconnected (body)\n", clientSmallId); break; } - HandleDataReceived(clientSmallId, s_hostSmallId, recvBuf, packetSize); + HandleDataReceived(clientSmallId, s_hostSmallId, &recvBuf[0], packetSize); } - delete[] recvBuf; - EnterCriticalSection(&s_connectionsLock); for (size_t i = 0; i < s_connections.size(); i++) { @@ -552,7 +631,8 @@ void WinsockNetLayer::PushFreeSmallId(BYTE smallId) DWORD WINAPI WinsockNetLayer::ClientRecvThreadProc(LPVOID param) { - BYTE *recvBuf = new BYTE[WIN64_NET_RECV_BUFFER_SIZE]; + std::vector recvBuf; + recvBuf.resize(WIN64_NET_RECV_BUFFER_SIZE); while (s_active && s_hostConnectionSocket != INVALID_SOCKET) { @@ -565,28 +645,34 @@ DWORD WINAPI WinsockNetLayer::ClientRecvThreadProc(LPVOID param) int packetSize = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; - if (packetSize <= 0 || packetSize > WIN64_NET_RECV_BUFFER_SIZE) + if (packetSize <= 0 || packetSize > WIN64_NET_MAX_PACKET_SIZE) { - app.DebugPrintf("Win64 LAN: Invalid packet size %d from host\n", packetSize); + app.DebugPrintf("Win64 LAN: Invalid packet size %d from host (max=%d)\n", + packetSize, + (int)WIN64_NET_MAX_PACKET_SIZE); break; } - if (!RecvExact(s_hostConnectionSocket, recvBuf, packetSize)) + if ((int)recvBuf.size() < packetSize) + { + recvBuf.resize(packetSize); + app.DebugPrintf("Win64 LAN: Resized client recv buffer to %d bytes\n", packetSize); + } + + if (!RecvExact(s_hostConnectionSocket, &recvBuf[0], packetSize)) { app.DebugPrintf("Win64 LAN: Disconnected from host (body)\n"); break; } - HandleDataReceived(s_hostSmallId, s_localSmallId, recvBuf, packetSize); + HandleDataReceived(s_hostSmallId, s_localSmallId, &recvBuf[0], packetSize); } - delete[] recvBuf; - s_connected = false; return 0; } -bool WinsockNetLayer::StartAdvertising(int gamePort, const wchar_t *hostName, unsigned int gameSettings, unsigned int texPackId, unsigned char subTexId, unsigned short netVer) +bool WinsockNetLayer::StartAdvertising(int gamePort, const wchar_t *hostName, unsigned int gameSettings, unsigned int texPackId, unsigned char subTexId, unsigned short netVer, unsigned char maxPlayers) { if (s_advertising) return true; if (!s_initialized) return false; @@ -598,7 +684,7 @@ bool WinsockNetLayer::StartAdvertising(int gamePort, const wchar_t *hostName, un s_advertiseData.gamePort = (WORD)gamePort; wcsncpy_s(s_advertiseData.hostName, 32, hostName, _TRUNCATE); s_advertiseData.playerCount = 1; - s_advertiseData.maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + s_advertiseData.maxPlayers = (maxPlayers > 0) ? maxPlayers : MINECRAFT_NET_MAX_PLAYERS; s_advertiseData.gameHostSettings = gameSettings; s_advertiseData.texturePackParentId = texPackId; s_advertiseData.subTexturePackId = subTexId; @@ -648,6 +734,13 @@ void WinsockNetLayer::UpdateAdvertisePlayerCount(BYTE count) LeaveCriticalSection(&s_advertiseLock); } +void WinsockNetLayer::UpdateAdvertiseGameSettings(unsigned int gameSettings) +{ + EnterCriticalSection(&s_advertiseLock); + s_advertiseData.gameHostSettings = gameSettings; + LeaveCriticalSection(&s_advertiseLock); +} + void WinsockNetLayer::UpdateAdvertiseJoinable(bool joinable) { EnterCriticalSection(&s_advertiseLock); diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index 96b03c9..6d1b3b2 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -12,6 +12,7 @@ #define WIN64_NET_DEFAULT_PORT 25565 #define WIN64_NET_MAX_CLIENTS 7 #define WIN64_NET_RECV_BUFFER_SIZE 65536 +#define WIN64_NET_MAX_PACKET_SIZE (4 * 1024 * 1024) #define WIN64_LAN_DISCOVERY_PORT 25566 #define WIN64_LAN_BROADCAST_MAGIC 0x4D434C4E @@ -82,8 +83,9 @@ public: static bool PopDisconnectedSmallId(BYTE *outSmallId); static void PushFreeSmallId(BYTE smallId); - static bool StartAdvertising(int gamePort, const wchar_t *hostName, unsigned int gameSettings, unsigned int texPackId, unsigned char subTexId, unsigned short netVer); + static bool StartAdvertising(int gamePort, const wchar_t *hostName, unsigned int gameSettings, unsigned int texPackId, unsigned char subTexId, unsigned short netVer, unsigned char maxPlayers = MINECRAFT_NET_MAX_PLAYERS); static void StopAdvertising(); + static void UpdateAdvertiseGameSettings(unsigned int gameSettings); static void UpdateAdvertisePlayerCount(BYTE count); static void UpdateAdvertiseJoinable(bool joinable); @@ -92,6 +94,7 @@ public: static std::vector GetDiscoveredSessions(); static int GetHostPort() { return s_hostGamePort; } + static BYTE GetMaxPlayers() { return s_maxPlayers; } private: static DWORD WINAPI AcceptThreadProc(LPVOID param); @@ -125,6 +128,7 @@ private: static Win64LANBroadcast s_advertiseData; static CRITICAL_SECTION s_advertiseLock; static int s_hostGamePort; + static BYTE s_maxPlayers; static SOCKET s_discoverySock; static HANDLE s_discoveryThread; @@ -142,6 +146,7 @@ private: extern bool g_Win64MultiplayerHost; extern bool g_Win64MultiplayerJoin; extern int g_Win64MultiplayerPort; +extern int g_Win64MultiplayerMaxPlayers; extern char g_Win64MultiplayerIP[256]; #endif diff --git a/Minecraft.Client/Windows64/Windows64_App.cpp b/Minecraft.Client/Windows64/Windows64_App.cpp index 133049d..eb05f14 100644 --- a/Minecraft.Client/Windows64/Windows64_App.cpp +++ b/Minecraft.Client/Windows64/Windows64_App.cpp @@ -31,6 +31,7 @@ void CConsoleMinecraftApp::ExitGame() } void CConsoleMinecraftApp::FatalLoadError() { + app.DebugPrintf("CConsoleMinecraftApp::FatalLoadError\n"); } void CConsoleMinecraftApp::CaptureSaveThumbnail() diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index f3bc185..6598176 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -4,8 +4,12 @@ #include "stdafx.h" #include +#include #include "GameConfig\Minecraft.spa.h" #include "..\MinecraftServer.h" +#include "..\PlayerList.h" +#include "..\ServerPlayer.h" +#include "..\PlayerConnection.h" #include "..\LocalPlayer.h" #include "..\..\Minecraft.World\ItemInstance.h" #include "..\..\Minecraft.World\MapItem.h" @@ -20,6 +24,8 @@ #include "..\ClientConnection.h" #include "..\User.h" +#include "..\ChatScreen.h" +#include "..\TextEditScreen.h" #include "..\..\Minecraft.World\Socket.h" #include "..\KeyboardMouseInput.h" #include "..\..\Minecraft.World\ThreadName.h" @@ -38,6 +44,9 @@ #include "..\..\Minecraft.World\compression.h" #include "..\..\Minecraft.World\OldChunkStorage.h" #include "Network\WinsockNetLayer.h" +#include +#include +#include #include "Xbox/resource.h" @@ -83,6 +92,867 @@ int g_iScreenHeight = 1080; char g_Win64Username[17] = {0}; wchar_t g_Win64UsernameW[17] = {0}; +bool g_Win64DedicatedServerMode = false; +extern HINSTANCE g_hInst; +extern HWND g_hWnd; + +static bool g_dedicatedGuiEnabled = false; +static HWND g_hDedicatedStatus = NULL; +static HWND g_hDedicatedDetails = NULL; +static HWND g_hDedicatedLog = NULL; +static HWND g_hDedicatedStopButton = NULL; +static HWND g_hDedicatedRefreshButton = NULL; +static HWND g_hDedicatedCopyButton = NULL; +static HWND g_hDedicatedSaveToggleButton = NULL; +static HWND g_hDedicatedWhitelistToggleButton = NULL; +static HWND g_hDedicatedKickAllButton = NULL; +static HFONT g_hDedicatedFont = NULL; +static HBRUSH g_hDedicatedBgBrush = NULL; +static HBRUSH g_hDedicatedPanelBrush = NULL; +static HBRUSH g_hDedicatedLogBrush = NULL; +static DWORD g_dedicatedServerStartTick = 0; +static int g_dedicatedPort = WIN64_NET_DEFAULT_PORT; +static unsigned int g_dedicatedMaxPlayers = MINECRAFT_NET_MAX_PLAYERS; +static wstring g_dedicatedWorldName = L"Dedicated Server"; +static wstring g_dedicatedBindAddress = L"0.0.0.0"; +static bool g_networkManagerReady = false; + +static const UINT WM_APP_DEDICATED_APPEND_LOG = WM_APP + 101; +static const UINT ID_DEDICATED_TIMER = 9001; +static const int ID_DEDICATED_STOP_BUTTON = 9002; +static const int ID_DEDICATED_REFRESH_BUTTON = 9003; +static const int ID_DEDICATED_COPY_BUTTON = 9004; +static const int ID_DEDICATED_SAVE_TOGGLE_BUTTON = 9005; +static const int ID_DEDICATED_WHITELIST_TOGGLE_BUTTON = 9006; +static const int ID_DEDICATED_KICKALL_BUTTON = 9007; + +static const COLORREF DEDICATED_BG_COLOR = RGB(16, 18, 22); +static const COLORREF DEDICATED_PANEL_COLOR = RGB(28, 31, 36); +static const COLORREF DEDICATED_TEXT_COLOR = RGB(225, 232, 240); +static const COLORREF DEDICATED_MUTED_TEXT_COLOR = RGB(170, 182, 198); +static const COLORREF DEDICATED_LOG_BG_COLOR = RGB(12, 14, 18); + +void Windows64_DedicatedGuiPushLog(const char *text); +static void DedicatedGuiUpdateStatus(); +static void DedicatedGuiAppendLogNow(const char *text); +static void DedicatedGuiCreateControls(HWND hWnd); +static void DedicatedGuiLayout(HWND hWnd); +static void DedicatedGuiUpdateControlLabels(); +static void DedicatedGuiCopyConnectInfo(HWND hWnd); +static void DedicatedGuiKickAllPlayers(); + +static void StandaloneWriteLogLine(const char *text) +{ + if (text == NULL) + { + return; + } + + wchar_t exePath[MAX_PATH]; + if (!GetModuleFileNameW(NULL, exePath, MAX_PATH)) + { + return; + } + + wstring logPath(exePath); + size_t lastSlash = logPath.find_last_of(L"\\/"); + if (lastSlash != wstring::npos) + { + logPath = logPath.substr(0, lastSlash + 1) + L"StandaloneDebug.log"; + } + else + { + logPath = L"StandaloneDebug.log"; + } + + HANDLE file = CreateFileW(logPath.c_str(), FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) + { + return; + } + + SYSTEMTIME st; + GetLocalTime(&st); + + char prefix[64]; + _snprintf_s(prefix, sizeof(prefix), _TRUNCATE, "[%02d:%02d:%02d.%03d] ", + st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + + DWORD bytesWritten = 0; + WriteFile(file, prefix, (DWORD)strlen(prefix), &bytesWritten, NULL); + WriteFile(file, text, (DWORD)strlen(text), &bytesWritten, NULL); + + size_t len = strlen(text); + if (len == 0 || (text[len - 1] != '\n' && text[len - 1] != '\r')) + { + const char *newline = "\r\n"; + WriteFile(file, newline, (DWORD)strlen(newline), &bytesWritten, NULL); + } + + CloseHandle(file); +} + +static void StandaloneLog(const char *format, ...) +{ + char buffer[1024]; + va_list ap; + va_start(ap, format); + vsnprintf(buffer, sizeof(buffer), format, ap); + va_end(ap); + + StandaloneWriteLogLine(buffer); + Windows64_DedicatedGuiPushLog(buffer); +} + +void Windows64_DedicatedGuiPushLog(const char *text) +{ + if (!g_dedicatedGuiEnabled || text == NULL || text[0] == 0 || g_hWnd == NULL) + { + return; + } + + size_t len = strlen(text); + char *copy = new char[len + 1]; + strcpy_s(copy, len + 1, text); + + if (!PostMessage(g_hWnd, WM_APP_DEDICATED_APPEND_LOG, 0, (LPARAM)copy)) + { + delete[] copy; + } +} + +static void DedicatedGuiAppendLogNow(const char *text) +{ + if (g_hDedicatedLog == NULL || text == NULL || text[0] == 0) + { + return; + } + + char line[1200]; + _snprintf_s(line, sizeof(line), _TRUNCATE, "%s\r\n", text); + + int wideLen = MultiByteToWideChar(CP_ACP, 0, line, -1, NULL, 0); + if (wideLen <= 1) + { + return; + } + + wchar_t *wide = new wchar_t[wideLen]; + MultiByteToWideChar(CP_ACP, 0, line, -1, wide, wideLen); + + int curLen = GetWindowTextLengthW(g_hDedicatedLog); + if (curLen > 120000) + { + SendMessageW(g_hDedicatedLog, WM_SETTEXT, 0, (LPARAM)L""); + curLen = 0; + } + + SendMessageW(g_hDedicatedLog, EM_SETSEL, curLen, curLen); + SendMessageW(g_hDedicatedLog, EM_REPLACESEL, FALSE, (LPARAM)wide); + + delete[] wide; +} + +static void DedicatedGuiUpdateStatus() +{ + if (!g_dedicatedGuiEnabled) + { + return; + } + + if (g_hDedicatedStatus != NULL) + { + const bool gameStarted = app.GetGameStarted(); + bool inSession = false; + int players = 0; + bool whitelistEnabled = false; + if (MinecraftServer::getInstance() != NULL && MinecraftServer::getInstance()->getPlayers() != NULL) + { + whitelistEnabled = MinecraftServer::getInstance()->getPlayers()->isWhitelistEnabled(); + } + if (g_networkManagerReady) + { + inSession = g_NetworkManager.IsInSession(); + players = g_NetworkManager.GetPlayerCount(); + } + const wchar_t *state = gameStarted ? L"Online" : (inSession ? L"Starting" : L"Offline"); + const bool saveDisabled = app.GetGameHostOption(eGameHostOption_DisableSaving) != 0; + + wchar_t status[256]; + swprintf_s(status, L"Status: %ls Players: %d/%u Saving: %ls Whitelist: %ls", + state, + players, + g_dedicatedMaxPlayers, + saveDisabled ? L"Off" : L"On", + whitelistEnabled ? L"On" : L"Off"); + SetWindowTextW(g_hDedicatedStatus, status); + } + + if (g_hDedicatedDetails != NULL) + { + DWORD uptimeSeconds = 0; + if (g_dedicatedServerStartTick != 0) + { + uptimeSeconds = (GetTickCount() - g_dedicatedServerStartTick) / 1000; + } + + const DWORD hours = uptimeSeconds / 3600; + const DWORD minutes = (uptimeSeconds % 3600) / 60; + const DWORD seconds = uptimeSeconds % 60; + + wchar_t details[512]; + swprintf_s(details, + L"World: %ls Bind: %ls Port: %d Uptime: %02lu:%02lu:%02lu", + g_dedicatedWorldName.c_str(), + g_dedicatedBindAddress.c_str(), + g_dedicatedPort, + hours, + minutes, + seconds); + SetWindowTextW(g_hDedicatedDetails, details); + } + + DedicatedGuiUpdateControlLabels(); +} + +static void DedicatedGuiUpdateControlLabels() +{ + if (!g_dedicatedGuiEnabled) + { + return; + } + + const bool saveDisabled = app.GetGameHostOption(eGameHostOption_DisableSaving) != 0; + if (g_hDedicatedSaveToggleButton != NULL) + { + SetWindowTextW(g_hDedicatedSaveToggleButton, saveDisabled ? L"Enable Saving" : L"Disable Saving"); + } + + bool whitelistEnabled = false; + MinecraftServer *server = MinecraftServer::getInstance(); + if (server != NULL && server->getPlayers() != NULL) + { + whitelistEnabled = server->getPlayers()->isWhitelistEnabled(); + } + if (g_hDedicatedWhitelistToggleButton != NULL) + { + SetWindowTextW(g_hDedicatedWhitelistToggleButton, whitelistEnabled ? L"Whitelist: ON" : L"Whitelist: OFF"); + } +} + +static void DedicatedGuiCopyConnectInfo(HWND hWnd) +{ + wchar_t joinText[256]; + swprintf_s(joinText, L"%ls:%d", g_dedicatedBindAddress.c_str(), g_dedicatedPort); + + if (!OpenClipboard(hWnd)) + { + StandaloneLog("Dedicated GUI: failed to open clipboard"); + return; + } + + EmptyClipboard(); + const size_t chars = wcslen(joinText) + 1; + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, chars * sizeof(wchar_t)); + if (hMem != NULL) + { + void *dst = GlobalLock(hMem); + if (dst != NULL) + { + memcpy(dst, joinText, chars * sizeof(wchar_t)); + GlobalUnlock(hMem); + SetClipboardData(CF_UNICODETEXT, hMem); + char joinTextA[256] = { 0 }; + wcstombs(joinTextA, joinText, sizeof(joinTextA) - 1); + StandaloneLog("Dedicated GUI: copied connect address %s", joinTextA); + } + else + { + GlobalFree(hMem); + } + } + CloseClipboard(); +} + +static void DedicatedGuiKickAllPlayers() +{ + MinecraftServer *server = MinecraftServer::getInstance(); + if (server == NULL || server->getPlayers() == NULL) + { + return; + } + + vector > toKick; + toKick.reserve(server->getPlayers()->players.size()); + for (size_t i = 0; i < server->getPlayers()->players.size(); ++i) + { + shared_ptr p = server->getPlayers()->players[i]; + if (p == NULL || p->connection == NULL) + { + continue; + } + INetworkPlayer *np = p->connection->getNetworkPlayer(); + if (np != NULL && np->IsLocal()) + { + continue; + } + toKick.push_back(p); + } + + for (size_t i = 0; i < toKick.size(); ++i) + { + toKick[i]->connection->setWasKicked(); + toKick[i]->connection->disconnect(DisconnectPacket::eDisconnect_Kicked); + } + StandaloneLog("Dedicated GUI: kicked %u players", (unsigned int)toKick.size()); +} + +static void DedicatedGuiLayout(HWND hWnd) +{ + if (!g_dedicatedGuiEnabled || hWnd == NULL) + { + return; + } + + RECT rc; + GetClientRect(hWnd, &rc); + const int width = rc.right - rc.left; + const int height = rc.bottom - rc.top; + + const int margin = 14; + const int statusH = 32; + const int detailsH = 24; + const int buttonH = 32; + const int buttonGap = 8; + + const int logTop = margin + statusH + detailsH + margin; + const int logBottom = height - margin - buttonH - margin; + const int logH = (logBottom > logTop) ? (logBottom - logTop) : 40; + + if (g_hDedicatedStatus != NULL) + { + SetWindowPos(g_hDedicatedStatus, NULL, margin, margin, width - (margin * 2), statusH, SWP_NOZORDER); + } + if (g_hDedicatedDetails != NULL) + { + SetWindowPos(g_hDedicatedDetails, NULL, margin, margin + statusH, width - (margin * 2), detailsH, SWP_NOZORDER); + } + if (g_hDedicatedLog != NULL) + { + SetWindowPos(g_hDedicatedLog, NULL, margin, logTop, width - (margin * 2), logH, SWP_NOZORDER); + } + const int controlCount = 6; + const int buttonW = (width - (margin * 2) - (buttonGap * (controlCount - 1))) / controlCount; + int x = margin; + if (g_hDedicatedRefreshButton != NULL) + { + SetWindowPos(g_hDedicatedRefreshButton, NULL, x, height - margin - buttonH, buttonW, buttonH, SWP_NOZORDER); + x += buttonW + buttonGap; + } + if (g_hDedicatedCopyButton != NULL) + { + SetWindowPos(g_hDedicatedCopyButton, NULL, x, height - margin - buttonH, buttonW, buttonH, SWP_NOZORDER); + x += buttonW + buttonGap; + } + if (g_hDedicatedSaveToggleButton != NULL) + { + SetWindowPos(g_hDedicatedSaveToggleButton, NULL, x, height - margin - buttonH, buttonW, buttonH, SWP_NOZORDER); + x += buttonW + buttonGap; + } + if (g_hDedicatedWhitelistToggleButton != NULL) + { + SetWindowPos(g_hDedicatedWhitelistToggleButton, NULL, x, height - margin - buttonH, buttonW, buttonH, SWP_NOZORDER); + x += buttonW + buttonGap; + } + if (g_hDedicatedKickAllButton != NULL) + { + SetWindowPos(g_hDedicatedKickAllButton, NULL, x, height - margin - buttonH, buttonW, buttonH, SWP_NOZORDER); + x += buttonW + buttonGap; + } + if (g_hDedicatedStopButton != NULL) + { + SetWindowPos(g_hDedicatedStopButton, NULL, x, height - margin - buttonH, buttonW, buttonH, SWP_NOZORDER); + } +} + +static void DedicatedGuiCreateControls(HWND hWnd) +{ + if (!g_dedicatedGuiEnabled || hWnd == NULL) + { + return; + } + + g_hDedicatedFont = CreateFontW( + -17, 0, 0, 0, FW_MEDIUM, FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, + DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + + if (g_hDedicatedBgBrush == NULL) g_hDedicatedBgBrush = CreateSolidBrush(DEDICATED_BG_COLOR); + if (g_hDedicatedPanelBrush == NULL) g_hDedicatedPanelBrush = CreateSolidBrush(DEDICATED_PANEL_COLOR); + if (g_hDedicatedLogBrush == NULL) g_hDedicatedLogBrush = CreateSolidBrush(DEDICATED_LOG_BG_COLOR); + + g_hDedicatedStatus = CreateWindowExW( + 0, L"STATIC", L"Status: Starting", + WS_CHILD | WS_VISIBLE, + 10, 10, 100, 20, + hWnd, NULL, g_hInst, NULL); + + g_hDedicatedDetails = CreateWindowExW( + 0, L"STATIC", L"", + WS_CHILD | WS_VISIBLE, + 10, 34, 100, 20, + hWnd, NULL, g_hInst, NULL); + + g_hDedicatedLog = CreateWindowExW( + WS_EX_CLIENTEDGE, L"EDIT", L"", + WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY, + 10, 60, 100, 100, + hWnd, NULL, g_hInst, NULL); + + g_hDedicatedRefreshButton = CreateWindowExW( + 0, L"BUTTON", L"Refresh", + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 10, 10, 100, 28, + hWnd, (HMENU)ID_DEDICATED_REFRESH_BUTTON, g_hInst, NULL); + + g_hDedicatedCopyButton = CreateWindowExW( + 0, L"BUTTON", L"Copy Join IP", + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 10, 10, 100, 28, + hWnd, (HMENU)ID_DEDICATED_COPY_BUTTON, g_hInst, NULL); + + g_hDedicatedSaveToggleButton = CreateWindowExW( + 0, L"BUTTON", L"Disable Saving", + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 10, 10, 100, 28, + hWnd, (HMENU)ID_DEDICATED_SAVE_TOGGLE_BUTTON, g_hInst, NULL); + + g_hDedicatedWhitelistToggleButton = CreateWindowExW( + 0, L"BUTTON", L"Whitelist: OFF", + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 10, 10, 100, 28, + hWnd, (HMENU)ID_DEDICATED_WHITELIST_TOGGLE_BUTTON, g_hInst, NULL); + + g_hDedicatedKickAllButton = CreateWindowExW( + 0, L"BUTTON", L"Kick All", + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 10, 10, 100, 28, + hWnd, (HMENU)ID_DEDICATED_KICKALL_BUTTON, g_hInst, NULL); + + g_hDedicatedStopButton = CreateWindowExW( + 0, L"BUTTON", L"Stop Server", + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 10, 10, 100, 28, + hWnd, (HMENU)ID_DEDICATED_STOP_BUTTON, g_hInst, NULL); + + if (g_hDedicatedFont != NULL) + { + if (g_hDedicatedStatus != NULL) SendMessage(g_hDedicatedStatus, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedDetails != NULL) SendMessage(g_hDedicatedDetails, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedLog != NULL) SendMessage(g_hDedicatedLog, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedRefreshButton != NULL) SendMessage(g_hDedicatedRefreshButton, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedCopyButton != NULL) SendMessage(g_hDedicatedCopyButton, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedSaveToggleButton != NULL) SendMessage(g_hDedicatedSaveToggleButton, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedWhitelistToggleButton != NULL) SendMessage(g_hDedicatedWhitelistToggleButton, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedKickAllButton != NULL) SendMessage(g_hDedicatedKickAllButton, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + if (g_hDedicatedStopButton != NULL) SendMessage(g_hDedicatedStopButton, WM_SETFONT, (WPARAM)g_hDedicatedFont, TRUE); + } + + SetWindowTextW(hWnd, L"Minecraft Dedicated Server"); + SetTimer(hWnd, ID_DEDICATED_TIMER, 1000, NULL); + DedicatedGuiLayout(hWnd); + DedicatedGuiUpdateStatus(); + DedicatedGuiAppendLogNow("Dedicated server GUI ready"); +} + +static LONG WINAPI StandaloneUnhandledExceptionFilter(EXCEPTION_POINTERS *exceptionInfo) +{ + if (exceptionInfo && exceptionInfo->ExceptionRecord) + { + StandaloneLog( + "Unhandled exception: code=0x%08X address=0x%p flags=0x%08X", + exceptionInfo->ExceptionRecord->ExceptionCode, + exceptionInfo->ExceptionRecord->ExceptionAddress, + exceptionInfo->ExceptionRecord->ExceptionFlags + ); + } + else + { + StandaloneLog("Unhandled exception: exception info unavailable"); + } + return EXCEPTION_CONTINUE_SEARCH; +} + +static bool IsFilePath(const wstring &path) +{ + DWORD attrs = GetFileAttributesW(path.c_str()); + return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +static void ConfigureStandaloneWorkingDirectory() +{ + wchar_t exePath[MAX_PATH]; + if (!GetModuleFileNameW(NULL, exePath, MAX_PATH)) + { + StandaloneLog("ConfigureStandaloneWorkingDirectory: GetModuleFileNameW failed"); + return; + } + + wstring exeDir(exePath); + size_t lastSlash = exeDir.find_last_of(L"\\/"); + if (lastSlash == wstring::npos) + { + StandaloneLog("ConfigureStandaloneWorkingDirectory: executable path has no directory separator"); + return; + } + exeDir = exeDir.substr(0, lastSlash); + + wstring root = exeDir; + for (int i = 0; i < 8; ++i) + { + wstring mediaFromClient = root + L"\\Minecraft.Client\\Common\\Media\\MediaWindows64.arc"; + wstring mediaLocal = root + L"\\Common\\Media\\MediaWindows64.arc"; + wstring fontFromClient = root + L"\\Minecraft.Client\\Common\\res\\font\\Mojangles_7.png"; + wstring fontLocal = root + L"\\Common\\res\\font\\Mojangles_7.png"; + + if (IsFilePath(mediaFromClient) || IsFilePath(fontFromClient)) + { + wstring clientRoot = root + L"\\Minecraft.Client"; + if (!SetCurrentDirectoryW(clientRoot.c_str())) + { + char pathA[MAX_PATH * 2] = { 0 }; + wcstombs(pathA, clientRoot.c_str(), sizeof(pathA) - 1); + StandaloneLog("ConfigureStandaloneWorkingDirectory: failed to set cwd to '%s' (err=%lu)", pathA, GetLastError()); + } + else + { + char pathA[MAX_PATH * 2] = { 0 }; + wcstombs(pathA, clientRoot.c_str(), sizeof(pathA) - 1); + StandaloneLog("ConfigureStandaloneWorkingDirectory: cwd set to '%s'", pathA); + } + return; + } + if (IsFilePath(mediaLocal) || IsFilePath(fontLocal)) + { + if (!SetCurrentDirectoryW(root.c_str())) + { + char pathA[MAX_PATH * 2] = { 0 }; + wcstombs(pathA, root.c_str(), sizeof(pathA) - 1); + StandaloneLog("ConfigureStandaloneWorkingDirectory: failed to set cwd to '%s' (err=%lu)", pathA, GetLastError()); + } + else + { + char pathA[MAX_PATH * 2] = { 0 }; + wcstombs(pathA, root.c_str(), sizeof(pathA) - 1); + StandaloneLog("ConfigureStandaloneWorkingDirectory: cwd set to '%s'", pathA); + } + return; + } + root += L"\\.."; + } + + char exeDirA[MAX_PATH * 2] = { 0 }; + wcstombs(exeDirA, exeDir.c_str(), sizeof(exeDirA) - 1); + StandaloneLog("ConfigureStandaloneWorkingDirectory: no media/font root found from '%s'", exeDirA); +} + +struct Win64LaunchConfig +{ + bool dedicated; + bool flatWorld; + bool disableSaving; + unsigned char maxPlayers; + int port; + string worldName; + string bindAddress; + string serverName; + bool whitelistEnabled; +}; + +static string TrimAscii(const string &in) +{ + size_t start = 0; + while (start < in.size() && isspace((unsigned char)in[start])) + { + ++start; + } + size_t end = in.size(); + while (end > start && isspace((unsigned char)in[end - 1])) + { + --end; + } + return in.substr(start, end - start); +} + +static bool ParseBoolValue(const string &value, bool defaultValue) +{ + const string v = TrimAscii(value); + if (_stricmp(v.c_str(), "1") == 0 || _stricmp(v.c_str(), "true") == 0 || _stricmp(v.c_str(), "yes") == 0 || _stricmp(v.c_str(), "on") == 0) + { + return true; + } + if (_stricmp(v.c_str(), "0") == 0 || _stricmp(v.c_str(), "false") == 0 || _stricmp(v.c_str(), "no") == 0 || _stricmp(v.c_str(), "off") == 0) + { + return false; + } + return defaultValue; +} + +static bool LoadDedicatedServerProperties(Win64LaunchConfig &cfg) +{ + const char *fileName = "dedicated-server.properties"; + std::ifstream in(fileName); + if (!in.good()) + { + std::ofstream out(fileName, std::ios::out | std::ios::trunc); + if (out.good()) + { + out << "# LCE Dedicated Server Properties\n"; + out << "dedicated=true\n"; + out << "server-name=" << cfg.serverName << "\n"; + out << "world-name=" << cfg.worldName << "\n"; + out << "bind-address=" << cfg.bindAddress << "\n"; + out << "server-port=" << cfg.port << "\n"; + out << "max-players=" << (int)cfg.maxPlayers << "\n"; + out << "level-type=" << (cfg.flatWorld ? "flat" : "normal") << "\n"; + out << "save-world=" << (cfg.disableSaving ? "false" : "true") << "\n"; + out << "whitelist=" << (cfg.whitelistEnabled ? "true" : "false") << "\n"; + } + return false; + } + + std::map kv; + std::string line; + while (std::getline(in, line)) + { + line = TrimAscii(line); + if (line.empty() || line[0] == '#' || line[0] == ';') + { + continue; + } + const size_t eq = line.find('='); + if (eq == std::string::npos) + { + continue; + } + std::string key = TrimAscii(line.substr(0, eq)); + std::string value = TrimAscii(line.substr(eq + 1)); + kv[key] = value; + } + + if (kv.find("dedicated") != kv.end()) cfg.dedicated = ParseBoolValue(kv["dedicated"], cfg.dedicated); + if (kv.find("server-name") != kv.end() && !kv["server-name"].empty()) cfg.serverName = kv["server-name"]; + if (kv.find("world-name") != kv.end() && !kv["world-name"].empty()) cfg.worldName = kv["world-name"]; + if (kv.find("bind-address") != kv.end() && !kv["bind-address"].empty()) cfg.bindAddress = kv["bind-address"]; + if (kv.find("server-port") != kv.end()) + { + const int p = atoi(kv["server-port"].c_str()); + if (p > 0 && p <= 65535) cfg.port = p; + } + if (kv.find("max-players") != kv.end()) + { + const int mp = atoi(kv["max-players"].c_str()); + if (mp >= 1 && mp <= MINECRAFT_NET_MAX_PLAYERS) cfg.maxPlayers = (unsigned char)mp; + } + if (kv.find("level-type") != kv.end()) + { + cfg.flatWorld = (_stricmp(kv["level-type"].c_str(), "flat") == 0); + } + if (kv.find("save-world") != kv.end()) + { + const bool saveWorld = ParseBoolValue(kv["save-world"], !cfg.disableSaving); + cfg.disableSaving = !saveWorld; + } + if (kv.find("whitelist") != kv.end()) + { + cfg.whitelistEnabled = ParseBoolValue(kv["whitelist"], cfg.whitelistEnabled); + } + + return true; +} + +static vector TokenizeCommandLine(const char *cmdLine) +{ + vector tokens; + if (cmdLine == NULL || cmdLine[0] == 0) + { + return tokens; + } + + string current; + bool inQuotes = false; + for (const char *p = cmdLine; *p != 0; ++p) + { + const char ch = *p; + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (!inQuotes && isspace((unsigned char)ch)) + { + if (!current.empty()) + { + tokens.push_back(current); + current.clear(); + } + } + else + { + current.push_back(ch); + } + } + + if (!current.empty()) + { + tokens.push_back(current); + } + return tokens; +} + +static bool HasCommandToken(const vector &tokens, const char *flag) +{ + for (size_t i = 0; i < tokens.size(); ++i) + { + if (_stricmp(tokens[i].c_str(), flag) == 0) + { + return true; + } + } + return false; +} + +static bool GetCommandValue(const vector &tokens, const char *flag, string &value) +{ + for (size_t i = 0; i < tokens.size(); ++i) + { + if (_stricmp(tokens[i].c_str(), flag) == 0) + { + if ((i + 1) < tokens.size()) + { + value = tokens[i + 1]; + return true; + } + return false; + } + + const size_t flagLen = strlen(flag); + if (_strnicmp(tokens[i].c_str(), flag, flagLen) == 0 && tokens[i].size() > (flagLen + 1) && tokens[i][flagLen] == '=') + { + value = tokens[i].substr(flagLen + 1); + return true; + } + } + return false; +} + +static bool StartDedicatedServer(const Win64LaunchConfig &launchConfig) +{ + Minecraft *pMinecraft = Minecraft::GetInstance(); + if (pMinecraft == NULL) + { + StandaloneLog("Dedicated: Minecraft instance is null"); + return false; + } + + app.DebugPrintf("Dedicated: starting with world=\"%s\" flat=%d disableSaving=%d maxPlayers=%u bind=%s port=%d\n", + launchConfig.worldName.c_str(), + launchConfig.flatWorld ? 1 : 0, + launchConfig.disableSaving ? 1 : 0, + (unsigned int)launchConfig.maxPlayers, + g_Win64MultiplayerIP, + launchConfig.port); + + StorageManager.SetSaveDisabled(launchConfig.disableSaving); + app.SetGameHostOption(eGameHostOption_DisableSaving, launchConfig.disableSaving ? 1 : 0); + + app.setLevelGenerationOptions(NULL); + app.ReleaseSaveThumbnail(); + ProfileManager.SetLockedProfile(0); + ProfileManager.SetPrimaryPad(0); + pMinecraft->user->name = g_Win64UsernameW; + app.ApplyGameSettingsChanged(0); + + MinecraftServer::resetFlags(); + app.SetTutorialMode(false); + app.SetCorruptSaveDeleted(false); + app.ClearTerrainFeaturePosition(); + + wstring worldNameW = convStringToWstring(launchConfig.worldName); + StorageManager.ResetSaveData(); + StorageManager.SetSaveTitle(worldNameW.c_str()); + + NetworkGameInitData *param = new NetworkGameInitData(); + ZeroMemory(param, sizeof(NetworkGameInitData)); + param->seed = 0; + param->saveData = NULL; + + app.SetGameHostOption(eGameHostOption_Difficulty, pMinecraft->options->difficulty); + app.SetGameHostOption(eGameHostOption_FriendsOfFriends, 1); + app.SetGameHostOption(eGameHostOption_Gamertags, 1); + app.SetGameHostOption(eGameHostOption_BedrockFog, 1); + app.SetGameHostOption(eGameHostOption_GameType, 0); + app.SetGameHostOption(eGameHostOption_LevelType, launchConfig.flatWorld ? 1 : 0); + app.SetGameHostOption(eGameHostOption_Structures, launchConfig.flatWorld ? 0 : 1); + app.SetGameHostOption(eGameHostOption_BonusChest, 0); + app.SetGameHostOption(eGameHostOption_PvP, 1); + app.SetGameHostOption(eGameHostOption_TrustPlayers, 1); + app.SetGameHostOption(eGameHostOption_FireSpreads, 1); + app.SetGameHostOption(eGameHostOption_TNT, 1); + app.SetGameHostOption(eGameHostOption_HostCanFly, 1); + app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, 1); + app.SetGameHostOption(eGameHostOption_HostCanBeInvisible, 1); + param->settings = app.GetGameHostOption(eGameHostOption_All); + + unsigned char publicSlots = launchConfig.maxPlayers; + if (publicSlots == 0 || publicSlots > MINECRAFT_NET_MAX_PLAYERS) + { + publicSlots = MINECRAFT_NET_MAX_PLAYERS; + } + + g_NetworkManager.SetPrivateGame(false); + g_NetworkManager.HostGame(0, true, false, publicSlots, 0); + // Keep a host network-player object for server-side queue/system bookkeeping. + // The dedicated path now suppresses slot 0 from active gameplay player lists. + g_NetworkManager.FakeLocalPlayerJoined(); + + app.SetAutosaveTimerTime(); + + C4JThread *thread = new C4JThread(&CGameNetworkManager::RunNetworkGameThreadProc, (LPVOID)param, "RunNetworkGame"); + thread->Run(); + + const DWORD waitStart = GetTickCount(); + while (!app.GetGameStarted()) + { + g_NetworkManager.DoWork(); + app.HandleXuiActions(); + Sleep(10); + + if (MinecraftServer::serverHalted()) + { + StandaloneLog("Dedicated: server halted during startup"); + return false; + } + + if ((GetTickCount() - waitStart) > 45000) + { + StandaloneLog("Dedicated: startup timed out"); + return false; + } + } + + g_dedicatedServerStartTick = GetTickCount(); + MinecraftServer *serverInstance = MinecraftServer::getInstance(); + if (serverInstance != NULL && serverInstance->getPlayers() != NULL) + { + serverInstance->getPlayers()->setWhitelistEnabled(launchConfig.whitelistEnabled); + app.DebugPrintf("Dedicated: whitelist %s\n", launchConfig.whitelistEnabled ? "enabled" : "disabled"); + } + DedicatedGuiUpdateStatus(); + StandaloneLog("Dedicated: server is online"); + return true; +} void DefineActions(void) { @@ -300,6 +1170,43 @@ static bool g_isFullscreen = false; static RECT g_windowedRect = {}; static LONG g_windowedStyle = 0; +static void ForwardPrintableKeyToTextEdit(Minecraft *mc, WPARAM wParam, LPARAM lParam) +{ + if (mc == NULL || mc->screen == NULL || dynamic_cast(mc->screen) == NULL) + { + return; + } + + // Ignore ctrl/alt combos so gameplay shortcuts do not inject text. + if ((GetKeyState(VK_CONTROL) & 0x8000) || (GetKeyState(VK_MENU) & 0x8000)) + { + return; + } + + BYTE keyState[256]; + if (!GetKeyboardState(keyState)) + { + return; + } + + const UINT vkCode = (UINT)wParam; + const UINT scanCode = (UINT)((lParam >> 16) & 0xFF); + wchar_t chars[4] = {}; + const int produced = ToUnicode(vkCode, scanCode, keyState, chars, 4, 0); + if (produced <= 0) + { + return; + } + + for (int i = 0; i < produced; ++i) + { + if (chars[i] >= 32) + { + mc->screen->HandleKeyPressed(chars[i], 0); + } + } +} + void ToggleFullscreen() { if (!g_hWnd) return; @@ -416,6 +1323,42 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) // Parse the menu selections: switch (wmId) { + case ID_DEDICATED_REFRESH_BUTTON: + DedicatedGuiUpdateStatus(); + break; + case ID_DEDICATED_COPY_BUTTON: + DedicatedGuiCopyConnectInfo(hWnd); + break; + case ID_DEDICATED_SAVE_TOGGLE_BUTTON: + { + const bool disableSaving = app.GetGameHostOption(eGameHostOption_DisableSaving) == 0; + StorageManager.SetSaveDisabled(disableSaving); + app.SetGameHostOption(eGameHostOption_DisableSaving, disableSaving ? 1 : 0); + DedicatedGuiUpdateStatus(); + StandaloneLog("Dedicated GUI: world saving %s", disableSaving ? "disabled" : "enabled"); + break; + } + case ID_DEDICATED_WHITELIST_TOGGLE_BUTTON: + { + MinecraftServer *server = MinecraftServer::getInstance(); + if (server != NULL && server->getPlayers() != NULL) + { + const bool enabled = !server->getPlayers()->isWhitelistEnabled(); + server->getPlayers()->setWhitelistEnabled(enabled); + StandaloneLog("Dedicated GUI: whitelist %s", enabled ? "enabled" : "disabled"); + } + DedicatedGuiUpdateStatus(); + break; + } + case ID_DEDICATED_KICKALL_BUTTON: + DedicatedGuiKickAllPlayers(); + DedicatedGuiUpdateStatus(); + break; + case ID_DEDICATED_STOP_BUTTON: + StandaloneLog("Dedicated: stop requested from GUI"); + DestroyWindow(hWnd); + break; + case IDM_EXIT: DestroyWindow(hWnd); break; @@ -426,12 +1369,71 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); - // TODO: Add any drawing code here... + if (g_hDedicatedBgBrush != NULL) + { + RECT rc; + GetClientRect(hWnd, &rc); + FillRect(hdc, &rc, g_hDedicatedBgBrush); + } EndPaint(hWnd, &ps); break; + case WM_ERASEBKGND: + return 1; + case WM_SIZE: + DedicatedGuiLayout(hWnd); + break; + case WM_TIMER: + if (wParam == ID_DEDICATED_TIMER) + { + DedicatedGuiUpdateStatus(); + } + break; + case WM_APP_DEDICATED_APPEND_LOG: + { + char *line = (char *)lParam; + if (line != NULL) + { + DedicatedGuiAppendLogNow(line); + delete[] line; + } + break; + } case WM_DESTROY: + KillTimer(hWnd, ID_DEDICATED_TIMER); + if (g_hDedicatedBgBrush != NULL) { DeleteObject(g_hDedicatedBgBrush); g_hDedicatedBgBrush = NULL; } + if (g_hDedicatedPanelBrush != NULL) { DeleteObject(g_hDedicatedPanelBrush); g_hDedicatedPanelBrush = NULL; } + if (g_hDedicatedLogBrush != NULL) { DeleteObject(g_hDedicatedLogBrush); g_hDedicatedLogBrush = NULL; } + if (g_hDedicatedFont != NULL) { DeleteObject(g_hDedicatedFont); g_hDedicatedFont = NULL; } PostQuitMessage(0); break; + case WM_CTLCOLORSTATIC: + { + HDC controlDc = (HDC)wParam; + HWND controlWnd = (HWND)lParam; + if (controlWnd == g_hDedicatedLog) + { + SetTextColor(controlDc, DEDICATED_MUTED_TEXT_COLOR); + SetBkColor(controlDc, DEDICATED_LOG_BG_COLOR); + return (LRESULT)(g_hDedicatedLogBrush != NULL ? g_hDedicatedLogBrush : GetStockObject(BLACK_BRUSH)); + } + SetTextColor(controlDc, DEDICATED_TEXT_COLOR); + SetBkMode(controlDc, TRANSPARENT); + return (LRESULT)(g_hDedicatedBgBrush != NULL ? g_hDedicatedBgBrush : GetStockObject(BLACK_BRUSH)); + } + case WM_CTLCOLOREDIT: + { + HDC controlDc = (HDC)wParam; + SetTextColor(controlDc, DEDICATED_MUTED_TEXT_COLOR); + SetBkColor(controlDc, DEDICATED_LOG_BG_COLOR); + return (LRESULT)(g_hDedicatedLogBrush != NULL ? g_hDedicatedLogBrush : GetStockObject(BLACK_BRUSH)); + } + case WM_CTLCOLORBTN: + { + HDC controlDc = (HDC)wParam; + SetTextColor(controlDc, DEDICATED_TEXT_COLOR); + SetBkColor(controlDc, DEDICATED_PANEL_COLOR); + return (LRESULT)(g_hDedicatedPanelBrush != NULL ? g_hDedicatedPanelBrush : GetStockObject(GRAY_BRUSH)); + } case WM_KILLFOCUS: g_KBMInput.ClearAllState(); @@ -447,6 +1449,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_KEYDOWN: case WM_SYSKEYDOWN: { + const int rawVk = (int)wParam; int vk = (int)wParam; if (vk == VK_F11) { @@ -460,6 +1463,36 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) else if (vk == VK_MENU) vk = (lParam & (1 << 24)) ? VK_RMENU : VK_LMENU; g_KBMInput.OnKeyDown(vk); + + // The old Keyboard.next() event pump is disabled on this platform build. + // Forward key controls directly to chat so Enter/Backspace/Escape work. + Minecraft *mc = Minecraft::GetInstance(); + if (mc != NULL && mc->screen != NULL && + (dynamic_cast(mc->screen) != NULL || dynamic_cast(mc->screen) != NULL)) + { + if (vk == VK_RETURN) + { + mc->screen->HandleKeyPressed(0, Keyboard::KEY_RETURN); + } + else if (vk == VK_BACK) + { + mc->screen->HandleKeyPressed(0, Keyboard::KEY_BACK); + } + else if (vk == VK_ESCAPE) + { + mc->screen->HandleKeyPressed(0, Keyboard::KEY_ESCAPE); + } + else if (vk == VK_UP) + { + mc->screen->HandleKeyPressed(0, Keyboard::KEY_UP); + } + else if (vk == VK_DOWN) + { + mc->screen->HandleKeyPressed(0, Keyboard::KEY_DOWN); + } + + ForwardPrintableKeyToTextEdit(mc, (WPARAM)rawVk, lParam); + } break; } case WM_KEYUP: @@ -503,6 +1536,24 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) g_KBMInput.OnMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); break; + case WM_CHAR: + { + // Route printable text to chat input. + // Sign editing receives text from WM_KEYDOWN+ToUnicode to avoid missing WM_CHAR cases. + Minecraft *mc = Minecraft::GetInstance(); + if (mc != NULL && mc->screen != NULL && + (dynamic_cast(mc->screen) != NULL)) + { + wchar_t ch = (wchar_t)wParam; + if (ch >= 32) + { + mc->screen->HandleKeyPressed(ch, 0); + } + return 0; + } + break; + } + case WM_INPUT: { UINT dwSize = 0; @@ -660,7 +1711,7 @@ app.DebugPrintf("width: %d, height: %d\n", width, height); UINT createDeviceFlags = 0; #ifdef _DEBUG - createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; + //createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; #endif D3D_DRIVER_TYPE driverTypes[] = @@ -793,6 +1844,32 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); + SetUnhandledExceptionFilter(StandaloneUnhandledExceptionFilter); + StandaloneLog("==== Standalone startup begin ===="); + + ConfigureStandaloneWorkingDirectory(); + wchar_t cwd[MAX_PATH]; + if (GetCurrentDirectoryW(MAX_PATH, cwd)) + { + char cwdA[MAX_PATH * 2]; + ZeroMemory(cwdA, sizeof(cwdA)); + wcstombs(cwdA, cwd, sizeof(cwdA) - 1); + StandaloneLog("Current directory: %s", cwdA); + } + + Win64LaunchConfig launchConfig; + launchConfig.dedicated = false; + launchConfig.flatWorld = true; + launchConfig.disableSaving = true; + launchConfig.maxPlayers = MINECRAFT_NET_MAX_PLAYERS; + launchConfig.port = WIN64_NET_DEFAULT_PORT; + launchConfig.worldName = "LCE Dedicated Server"; + launchConfig.bindAddress = "0.0.0.0"; + launchConfig.serverName = "LCE Dedicated"; + launchConfig.whitelistEnabled = false; + + LoadDedicatedServerProperties(launchConfig); + if(lpCmdLine) { if(lpCmdLine[0] == '1') @@ -815,28 +1892,101 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, //g_iScreenWidth = 960; //g_iScreenHeight = 544; } + } - char cmdLineA[1024]; - strncpy_s(cmdLineA, sizeof(cmdLineA), lpCmdLine, _TRUNCATE); + vector cmdTokens = TokenizeCommandLine(lpCmdLine); - char *nameArg = strstr(cmdLineA, "-name "); - if (nameArg) + launchConfig.dedicated = HasCommandToken(cmdTokens, "-dedicated"); + g_Win64DedicatedServerMode = launchConfig.dedicated; + if (HasCommandToken(cmdTokens, "-normal")) + { + launchConfig.flatWorld = false; + } + if (HasCommandToken(cmdTokens, "-flat")) + { + launchConfig.flatWorld = true; + } + if (HasCommandToken(cmdTokens, "-save")) + { + launchConfig.disableSaving = false; + } + if (HasCommandToken(cmdTokens, "-nosave")) + { + launchConfig.disableSaving = true; + } + + string value; + if (GetCommandValue(cmdTokens, "-name", value) && !value.empty()) + { + strncpy_s(g_Win64Username, 17, value.c_str(), _TRUNCATE); + } + if (GetCommandValue(cmdTokens, "-servername", value) && !value.empty()) + { + launchConfig.serverName = value; + } + if (GetCommandValue(cmdTokens, "-world", value) && !value.empty()) + { + launchConfig.worldName = value; + } + if (GetCommandValue(cmdTokens, "-port", value) && !value.empty()) + { + int port = atoi(value.c_str()); + if (port > 0 && port <= 65535) { - nameArg += 6; - while (*nameArg == ' ') nameArg++; - char nameBuf[17]; - int n = 0; - while (nameArg[n] && nameArg[n] != ' ' && n < 16) { nameBuf[n] = nameArg[n]; n++; } - nameBuf[n] = 0; - strncpy_s(g_Win64Username, 17, nameBuf, _TRUNCATE); + launchConfig.port = port; } } + if (GetCommandValue(cmdTokens, "-bind", value) && !value.empty()) + { + launchConfig.bindAddress = value; + } + if (GetCommandValue(cmdTokens, "-maxplayers", value) && !value.empty()) + { + int maxPlayers = atoi(value.c_str()); + if (maxPlayers >= 1 && maxPlayers <= MINECRAFT_NET_MAX_PLAYERS) + { + launchConfig.maxPlayers = (unsigned char)maxPlayers; + } + } + if (HasCommandToken(cmdTokens, "-whitelist")) + { + launchConfig.whitelistEnabled = true; + } + + strncpy_s(g_Win64MultiplayerIP, sizeof(g_Win64MultiplayerIP), launchConfig.bindAddress.c_str(), _TRUNCATE); + g_Win64MultiplayerPort = launchConfig.port; + g_Win64MultiplayerMaxPlayers = (int)launchConfig.maxPlayers; + + if (launchConfig.dedicated) + { + g_dedicatedGuiEnabled = true; + g_dedicatedPort = launchConfig.port; + g_dedicatedMaxPlayers = (unsigned int)launchConfig.maxPlayers; + g_dedicatedWorldName = convStringToWstring(launchConfig.worldName); + g_dedicatedBindAddress = convStringToWstring(g_Win64MultiplayerIP); + } + + StandaloneLog("Launch config: dedicated=%d, bind=%s, port=%d, maxPlayers=%u, flat=%d, disableSaving=%d, world=\"%s\"", + launchConfig.dedicated ? 1 : 0, + g_Win64MultiplayerIP, + launchConfig.port, + (unsigned int)launchConfig.maxPlayers, + launchConfig.flatWorld ? 1 : 0, + launchConfig.disableSaving ? 1 : 0, + launchConfig.worldName.c_str()); if (g_Win64Username[0] == 0) { + if (!launchConfig.serverName.empty()) + { + strncpy_s(g_Win64Username, 17, launchConfig.serverName.c_str(), _TRUNCATE); + } + else + { DWORD sz = 17; if (!GetUserNameA(g_Win64Username, &sz)) strncpy_s(g_Win64Username, 17, "Player", _TRUNCATE); + } g_Win64Username[16] = 0; } @@ -846,18 +1996,31 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, MyRegisterClass(hInstance); // Perform application initialization: + StandaloneLog("Calling InitInstance"); if (!InitInstance (hInstance, nCmdShow)) { + StandaloneLog("InitInstance failed"); return FALSE; } + StandaloneLog("InitInstance succeeded"); hMyInst=hInstance; + if (launchConfig.dedicated) + { + SetWindowPos(g_hWnd, NULL, CW_USEDEFAULT, CW_USEDEFAULT, 900, 620, SWP_NOZORDER | SWP_NOMOVE); + DedicatedGuiCreateControls(g_hWnd); + StandaloneLog("Dedicated mode enabled (GUI active)"); + } + + StandaloneLog("Calling InitDevice"); if( FAILED( InitDevice() ) ) { + StandaloneLog("InitDevice failed"); CleanupDevice(); return 0; } + StandaloneLog("InitDevice succeeded"); #if 0 // Main message loop @@ -911,12 +2074,17 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } #endif + StandaloneLog("Calling app.loadMediaArchive"); app.loadMediaArchive(); + StandaloneLog("Finished app.loadMediaArchive"); RenderManager.Initialise(g_pd3dDevice, g_pSwapChain); + StandaloneLog("Calling app.loadStringTable"); app.loadStringTable(); + StandaloneLog("Finished app.loadStringTable"); ui.init(g_pd3dDevice,g_pImmediateContext,g_pRenderTargetView,g_pDepthStencilView,g_iScreenWidth,g_iScreenHeight); + StandaloneLog("Finished ui.init"); //////////////// // Initialise // @@ -1020,6 +2188,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // XN_SYS_SIGNINCHANGED notifications. This does mean that we need to have a callback in the // ProfileManager for XN_LIVE_INVITE_ACCEPTED for QNet. g_NetworkManager.Initialise(); + g_networkManagerReady = true; for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) { @@ -1125,6 +2294,22 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, pMinecraft->options->set(Options::Option::MUSIC,1.0f); pMinecraft->options->set(Options::Option::SOUND,1.0f); + if (launchConfig.dedicated) + { + pMinecraft->noRender = true; + pMinecraft->options->set(Options::Option::MUSIC, 0.0f); + pMinecraft->options->set(Options::Option::SOUND, 0.0f); + + if (!StartDedicatedServer(launchConfig)) + { + StandaloneLog("Dedicated startup failed"); + g_NetworkManager.Terminate(); + g_networkManagerReady = false; + CleanupDevice(); + return -1; + } + } + //app.TemporaryCreateGameStart(); //Sleep(10000); @@ -1163,6 +2348,20 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } if (msg.message == WM_QUIT) break; + if (launchConfig.dedicated) + { + app.UpdateTime(); + StorageManager.Tick(); + g_NetworkManager.DoWork(); + if (app.GetGameStarted()) + { + pMinecraft->tickAllConnections(); + } + app.HandleXuiActions(); + Sleep(10); + continue; + } + RenderManager.StartFrame(); #if 0 if(pMinecraft->soundEngine->isStreamingWavebankReady() && @@ -1407,7 +2606,19 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // Free resources, unregister custom classes, and exit. // app.Uninit(); - g_pd3dDevice->Release(); + if (launchConfig.dedicated && g_NetworkManager.IsInSession()) + { + g_NetworkManager.LeaveGame(false); + } + g_NetworkManager.Terminate(); + g_networkManagerReady = false; + + if (g_pd3dDevice) + { + g_pd3dDevice->Release(); + } + + return (int)msg.wParam; } #ifdef MEMORY_TRACKING @@ -1597,4 +2808,4 @@ void MemPixStuff() PIXAddNamedCounter(((float)allSectsTotal)/(4096.0f),"MemSect total pages"); } -#endif \ No newline at end of file +#endif diff --git a/Minecraft.Client/dedicated-server.properties b/Minecraft.Client/dedicated-server.properties new file mode 100644 index 0000000..4e2a727 --- /dev/null +++ b/Minecraft.Client/dedicated-server.properties @@ -0,0 +1,10 @@ +# LCE Dedicated Server Properties +dedicated=true +server-name=LCE Dedicated +world-name=LCE Dedicated Server +bind-address=0.0.0.0 +server-port=25565 +max-players=8 +level-type=flat +save-world=false +whitelist=false diff --git a/Minecraft.World/ArrayWithLength.h b/Minecraft.World/ArrayWithLength.h index 4e9f994..6babd07 100644 --- a/Minecraft.World/ArrayWithLength.h +++ b/Minecraft.World/ArrayWithLength.h @@ -11,7 +11,21 @@ public: T *data; unsigned int length; arrayWithLength() { data = NULL; length = 0; } - arrayWithLength(unsigned int elements, bool bClearArray=true) { assert(elements!=0); data = new T[elements]; if(bClearArray){ memset( data,0,sizeof(T)*elements); } this->length = elements; } + arrayWithLength(unsigned int elements, bool bClearArray=true) + { + if(elements == 0) + { + // Keep a valid pointer even for empty arrays to avoid null dereferences in + // legacy code paths that assume data is always non-null. + data = new T[1]; + if(bClearArray){ memset(data,0,sizeof(T)); } + length = 0; + return; + } + data = new T[elements]; + if(bClearArray){ memset( data,0,sizeof(T)*elements); } + this->length = elements; + } // 4J Stu Added this ctor so I static init arrays in the Item derivation tree arrayWithLength( T data[], unsigned int elements) { this->data = data; this->length = elements; } diff --git a/Minecraft.World/CompoundTag.h b/Minecraft.World/CompoundTag.h index 32c3158..cec148c 100644 --- a/Minecraft.World/CompoundTag.h +++ b/Minecraft.World/CompoundTag.h @@ -182,7 +182,7 @@ public: intArray getIntArray(wchar_t * name) { - if (tags.find(name) == tags.end()) return intArray(0); + if (tags.find(name) == tags.end()) return intArray(); return ((IntArrayTag *) tags[name])->data; } diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 0a59204..d27c7da 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -421,26 +421,29 @@ void Packet::writeUtf(const wstring& value, DataOutputStream *dos) // throws IOE wstring Packet::readUtf(DataInputStream *dis, int maxLength) // throws IOException TODO 4J JEV, should this declare a throws? { - short stringLength = dis->readShort(); - if (stringLength > maxLength) - { - wstringstream stream; - stream << L"Received string length longer than maximum allowed (" << stringLength << " > " << maxLength << ")"; - assert(false); - // throw new IOException( stream.str() ); - } if (stringLength < 0) { - assert(false); - // throw new IOException(L"Received string length is less than zero! Weird string!"); + app.DebugPrintf("Packet::readUtf - invalid negative length %d (max=%d)\n", (int)stringLength, maxLength); + return L""; + } + + const int sourceLength = (int)stringLength; + const int safeLength = sourceLength > maxLength ? maxLength : sourceLength; + if (sourceLength > maxLength) + { + app.DebugPrintf("Packet::readUtf - clamping overlong string (%d > %d)\n", sourceLength, maxLength); } wstring builder = L""; - for (int i = 0; i < stringLength; i++) + builder.reserve(safeLength); + for (int i = 0; i < sourceLength; i++) { wchar_t rc = dis->readChar(); - builder.push_back( rc ); + if (i < safeLength) + { + builder.push_back(rc); + } } return builder; diff --git a/Minecraft.World/ThreadName.cpp b/Minecraft.World/ThreadName.cpp index f41beb6..0b3431e 100644 --- a/Minecraft.World/ThreadName.cpp +++ b/Minecraft.World/ThreadName.cpp @@ -18,11 +18,20 @@ void SetThreadName( DWORD dwThreadID, LPCSTR szThreadName ) info.szName = szThreadName; info.dwThreadID = dwThreadID; info.dwFlags = 0; + +#if defined(_WINDOWS64) + // The 0x406D1388 thread-name exception is debugger-only metadata. + // Skip raising it when no debugger is attached to avoid standalone exits. + if (!IsDebuggerPresent()) + { + return; + } +#endif -#if ( defined _WINDOWS64 | defined _DURANGO ) +#if ( defined _WINDOWS64 || defined _DURANGO ) __try { - RaiseException( 0x406D1388, 0, sizeof(info)/sizeof(DWORD), (ULONG_PTR *)&info ); + RaiseException( 0x406D1388, 0, 4, (ULONG_PTR *)&info ); } __except( GetExceptionCode()==0x406D1388 ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_EXECUTE_HANDLER ) { diff --git a/MinecraftConsoles.sln b/MinecraftConsoles.sln index f132503..fd95ada 100644 --- a/MinecraftConsoles.sln +++ b/MinecraftConsoles.sln @@ -68,7 +68,6 @@ Global {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.ContentPackage|Windows64.Build.0 = ContentPackage|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.ContentPackage|Xbox 360.ActiveCfg = ContentPackage|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|Durango.ActiveCfg = Debug|x64 - {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|Durango.Build.0 = Debug|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|ORBIS.ActiveCfg = Debug|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|PS3.ActiveCfg = Debug|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|PSVita.ActiveCfg = Debug|x64 @@ -76,7 +75,6 @@ Global {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|Windows64.Build.0 = Debug|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Debug|Xbox 360.ActiveCfg = Debug|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Release|Durango.ActiveCfg = Release|x64 - {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Release|Durango.Build.0 = Release|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Release|ORBIS.ActiveCfg = Release|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Release|PS3.ActiveCfg = Release|x64 {F046C3CE-9749-4823-B32B-D9CC10B1A2C8}.Release|PSVita.ActiveCfg = Release|x64 @@ -108,7 +106,6 @@ Global {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.ContentPackage|Windows64.ActiveCfg = ContentPackage|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.ContentPackage|Xbox 360.ActiveCfg = ContentPackage|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|Durango.ActiveCfg = Debug|x64 - {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|Durango.Build.0 = Debug|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|ORBIS.ActiveCfg = Debug|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|PS3.ActiveCfg = Debug|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|PSVita.ActiveCfg = Debug|x64 @@ -116,7 +113,6 @@ Global {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|Windows64.Build.0 = Debug|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Debug|Xbox 360.ActiveCfg = Debug|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Release|Durango.ActiveCfg = Release|x64 - {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Release|Durango.Build.0 = Release|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Release|ORBIS.ActiveCfg = Release|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Release|PS3.ActiveCfg = Release|x64 {1B9A8C38-DD48-448C-AA24-E1A35E0089A3}.Release|PSVita.ActiveCfg = Release|x64 diff --git a/proxy-worlds.properties b/proxy-worlds.properties new file mode 100644 index 0000000..1ff82ac --- /dev/null +++ b/proxy-worlds.properties @@ -0,0 +1,5 @@ +# Bungee-like route table for /server and /send +# format: name=host:port|Display Name +hub=127.0.0.1:25565|Hub +bedwars=127.0.0.1:25566|Bedwars Match +practice=127.0.0.1:25567|Practice