From 63e590d783cc3302ec6a5b639dd831e930a557c6 Mon Sep 17 00:00:00 2001 From: dtentiion Date: Mon, 2 Mar 2026 21:13:11 +0000 Subject: [PATCH 01/32] Win64: show actual world names in save list, sort newest-first, preserve level name on load/resave --- .../Common/UI/UIScene_CreateWorldMenu.cpp | 1 + .../Common/UI/UIScene_LoadMenu.cpp | 13 +- Minecraft.Client/Common/UI/UIScene_LoadMenu.h | 1 + .../Common/UI/UIScene_LoadOrJoinMenu.cpp | 150 +++++++++++++++++- Minecraft.Client/MinecraftServer.cpp | 4 +- Minecraft.Client/MinecraftServer.h | 1 + Minecraft.World/ConsoleSaveFileOriginal.cpp | 2 +- README.md | 19 +++ 8 files changed, 183 insertions(+), 8 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp index 6be67bed6..a9ff02f5f 100644 --- a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp @@ -1110,6 +1110,7 @@ void UIScene_CreateWorldMenu::CreateGame(UIScene_CreateWorldMenu* pClass, DWORD __int64 seedValue = 0; NetworkGameInitData *param = new NetworkGameInitData(); + param->levelName = wWorldName; if (wSeed.length() != 0) { diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp index 1c07e540f..bb399b973 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp @@ -231,12 +231,22 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye #endif m_bShowTimer = true; } -#if defined(_DURANGO) +#if defined(_DURANGO) m_labelGameName.init(params->saveDetails->UTF16SaveName); #else m_labelGameName.init(params->saveDetails->UTF8SaveName); #endif +#endif +#ifdef _WINDOWS64 + if (params->saveDetails != NULL && params->saveDetails->UTF8SaveName[0] != '\0') + { + wchar_t wSaveName[128]; + ZeroMemory(wSaveName, sizeof(wSaveName)); + mbstowcs(wSaveName, params->saveDetails->UTF8SaveName, 127); + m_levelName = wstring(wSaveName); + m_labelGameName.init(m_levelName); + } #endif } @@ -1448,6 +1458,7 @@ void UIScene_LoadMenu::StartGameFromSave(UIScene_LoadMenu* pClass, DWORD dwLocal param->seed = pClass->m_seed; param->saveData = NULL; param->texturePackId = pClass->m_MoreOptionsParams.dwTexturePack; + param->levelName = pClass->m_levelName; Minecraft *pMinecraft = Minecraft::GetInstance(); pMinecraft->skins->selectTexturePackById(pClass->m_MoreOptionsParams.dwTexturePack); diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.h b/Minecraft.Client/Common/UI/UIScene_LoadMenu.h index e45fa09c9..e245e3beb 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.h @@ -62,6 +62,7 @@ private: bool m_bIsCorrupt; bool m_bThumbnailGetFailed; __int64 m_seed; + wstring m_levelName; #ifdef __PS3__ std::vector*m_pvProductInfo; diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index 1d90da775..e3749dcb2 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -25,6 +25,98 @@ #include "message_dialog.h" #endif +#ifdef _WINDOWS64 +#include "..\..\..\Minecraft.World\NbtIo.h" +#include "..\..\..\Minecraft.World\compression.h" + +static wstring ReadLevelNameFromSaveFile(const wstring& filePath) +{ + HANDLE hFile = CreateFileW(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); + if (hFile == INVALID_HANDLE_VALUE) return L""; + + DWORD fileSize = GetFileSize(hFile, NULL); + if (fileSize < 12 || fileSize == INVALID_FILE_SIZE) { CloseHandle(hFile); return L""; } + + unsigned char *rawData = new unsigned char[fileSize]; + DWORD bytesRead = 0; + if (!ReadFile(hFile, rawData, fileSize, &bytesRead, NULL) || bytesRead != fileSize) + { + CloseHandle(hFile); + delete[] rawData; + return L""; + } + CloseHandle(hFile); + + unsigned char *saveData = NULL; + unsigned int saveSize = 0; + bool freeSaveData = false; + + if (*(unsigned int*)rawData == 0) + { + // Compressed format: bytes 0-3=0, bytes 4-7=decompressed size, bytes 8+=compressed data + unsigned int decompSize = *(unsigned int*)(rawData + 4); + if (decompSize == 0 || decompSize > 128 * 1024 * 1024) + { + delete[] rawData; + return L""; + } + saveData = new unsigned char[decompSize]; + Compression::getCompression()->Decompress(saveData, &decompSize, rawData + 8, fileSize - 8); + saveSize = decompSize; + freeSaveData = true; + } + else + { + saveData = rawData; + saveSize = fileSize; + } + + wstring result = L""; + if (saveSize >= 12) + { + unsigned int headerOffset = *(unsigned int*)saveData; + unsigned int numEntries = *(unsigned int*)(saveData + 4); + const unsigned int entrySize = sizeof(FileEntrySaveData); + + if (headerOffset < saveSize && numEntries > 0 && numEntries < 10000 && + headerOffset + numEntries * entrySize <= saveSize) + { + FileEntrySaveData *table = (FileEntrySaveData *)(saveData + headerOffset); + for (unsigned int i = 0; i < numEntries; i++) + { + if (wcscmp(table[i].filename, L"level.dat") == 0) + { + unsigned int off = table[i].startOffset; + unsigned int len = table[i].length; + if (off >= 12 && off + len <= saveSize && len > 0 && len < 4 * 1024 * 1024) + { + byteArray ba; + ba.data = (byte*)(saveData + off); + ba.length = len; + CompoundTag *root = NbtIo::decompress(ba); + if (root != NULL) + { + CompoundTag *dataTag = root->getCompound(L"Data"); + if (dataTag != NULL) + result = dataTag->getString(L"LevelName"); + delete root; + } + } + break; + } + } + } + } + + if (freeSaveData) delete[] saveData; + delete[] rawData; + // "world" is the engine default — it means no real name was ever set, so + // return empty to let the caller fall back to the save filename (timestamp). + if (result == L"world") result = L""; + return result; +} +#endif + #ifdef SONY_REMOTE_STORAGE_DOWNLOAD unsigned long UIScene_LoadOrJoinMenu::m_ulFileSize=0L; @@ -158,7 +250,7 @@ UIScene_LoadOrJoinMenu::UIScene_LoadOrJoinMenu(int iPad, void *initData, UILayer } #endif -#if defined(__PS3__) || defined(__ORBIS__) || defined(__PSVITA__) || defined(_DURANGO) +#if defined(__PS3__) || defined(__ORBIS__) || defined(__PSVITA__) || defined(_DURANGO) || defined(_WINDOWS64) // Always clear the saves when we enter this menu StorageManager.ClearSavesInfo(); #endif @@ -603,6 +695,22 @@ void UIScene_LoadOrJoinMenu::tick() m_saveDetails = new SaveListDetails[m_pSaveDetails->iSaveC]; m_iSaveDetailsCount = m_pSaveDetails->iSaveC; +#ifdef _WINDOWS64 + // Build sorted index array (newest-first by filename timestamp YYYYMMDDHHMMSS) + int *sortedIdx = new int[m_pSaveDetails->iSaveC]; + for (int si = 0; si < (int)m_pSaveDetails->iSaveC; ++si) sortedIdx[si] = si; + for (int si = 1; si < (int)m_pSaveDetails->iSaveC; ++si) + { + int key = sortedIdx[si]; + int sj = si - 1; + while (sj >= 0 && strcmp(m_pSaveDetails->SaveInfoA[sortedIdx[sj]].UTF8SaveFilename, m_pSaveDetails->SaveInfoA[key].UTF8SaveFilename) < 0) + { + sortedIdx[sj + 1] = sortedIdx[sj]; + --sj; + } + sortedIdx[sj + 1] = key; + } +#endif for(unsigned int i = 0; i < m_pSaveDetails->iSaveC; ++i) { #if defined(_XBOX_ONE) @@ -616,14 +724,40 @@ void UIScene_LoadOrJoinMenu::tick() m_saveDetails[i].saveId = i; memcpy(m_saveDetails[i].UTF16SaveName, m_pSaveDetails->SaveInfoA[i].UTF16SaveTitle, 128); memcpy(m_saveDetails[i].UTF16SaveFilename, m_pSaveDetails->SaveInfoA[i].UTF16SaveFilename, MAX_SAVEFILENAME_LENGTH); +#else +#ifdef _WINDOWS64 + { + int origIdx = sortedIdx[i]; + wchar_t wFilename[MAX_SAVEFILENAME_LENGTH]; + ZeroMemory(wFilename, sizeof(wFilename)); + mbstowcs(wFilename, m_pSaveDetails->SaveInfoA[origIdx].UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1); + wstring filePath = wstring(L"Windows64\\GameHDD\\") + wstring(wFilename) + wstring(L"\\saveData.ms"); + wstring levelName = ReadLevelNameFromSaveFile(filePath); + if (!levelName.empty()) + { + m_buttonListSaves.addItem(levelName, wstring(L"")); + wcstombs(m_saveDetails[i].UTF8SaveName, levelName.c_str(), 127); + m_saveDetails[i].UTF8SaveName[127] = '\0'; + } + else + { + m_buttonListSaves.addItem(m_pSaveDetails->SaveInfoA[origIdx].UTF8SaveTitle, L""); + memcpy(m_saveDetails[i].UTF8SaveName, m_pSaveDetails->SaveInfoA[origIdx].UTF8SaveTitle, 128); + } + m_saveDetails[i].saveId = origIdx; + memcpy(m_saveDetails[i].UTF8SaveFilename, m_pSaveDetails->SaveInfoA[origIdx].UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH); + } #else m_buttonListSaves.addItem(m_pSaveDetails->SaveInfoA[i].UTF8SaveTitle, L""); - - m_saveDetails[i].saveId = i; memcpy(m_saveDetails[i].UTF8SaveName, m_pSaveDetails->SaveInfoA[i].UTF8SaveTitle, 128); + m_saveDetails[i].saveId = i; memcpy(m_saveDetails[i].UTF8SaveFilename, m_pSaveDetails->SaveInfoA[i].UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH); +#endif #endif } +#ifdef _WINDOWS64 + delete[] sortedIdx; +#endif m_controlSavesTimer.setVisible( false ); // set focus on the first button @@ -639,7 +773,11 @@ void UIScene_LoadOrJoinMenu::tick() app.DebugPrintf("Requesting the first thumbnail\n"); // set the save to load PSAVE_DETAILS pSaveDetails=StorageManager.ReturnSavesInfo(); +#ifdef _WINDOWS64 + C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[m_saveDetails[m_iRequestingThumbnailId].saveId],&LoadSaveDataThumbnailReturned,this); +#else C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[(int)m_iRequestingThumbnailId],&LoadSaveDataThumbnailReturned,this); +#endif if(eLoadStatus!=C4JStorage::ESaveGame_GetSaveThumbnail) { @@ -702,7 +840,11 @@ void UIScene_LoadOrJoinMenu::tick() app.DebugPrintf("Requesting another thumbnail\n"); // set the save to load PSAVE_DETAILS pSaveDetails=StorageManager.ReturnSavesInfo(); +#ifdef _WINDOWS64 + C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[m_saveDetails[m_iRequestingThumbnailId].saveId],&LoadSaveDataThumbnailReturned,this); +#else C4JStorage::ESaveGameState eLoadStatus=StorageManager.LoadSaveDataThumbnail(&pSaveDetails->SaveInfoA[(int)m_iRequestingThumbnailId],&LoadSaveDataThumbnailReturned,this); +#endif if(eLoadStatus!=C4JStorage::ESaveGame_GetSaveThumbnail) { // something went wrong @@ -1310,7 +1452,7 @@ void UIScene_LoadOrJoinMenu::handlePress(F64 controlId, F64 childId) LoadMenuInitData *params = new LoadMenuInitData(); params->iPad = m_iPad; // need to get the iIndex from the list item, since the position in the list doesn't correspond to the GetSaveGameInfo list because of sorting - params->iSaveGameInfoIndex=((int)childId)-m_iDefaultButtonsC; + params->iSaveGameInfoIndex=m_saveDetails[((int)childId)-m_iDefaultButtonsC].saveId; //params->pbSaveRenamed=&m_bSaveRenamed; params->levelGen = NULL; params->saveDetails = &m_saveDetails[ ((int)childId)-m_iDefaultButtonsC ]; diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index ceb9554b2..5e7ce7948 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -149,7 +149,7 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW //localIp = settings->getString(L"server-ip", L""); //onlineMode = settings->getBoolean(L"online-mode", true); //motd = settings->getString(L"motd", L"A Minecraft Server"); - //motd.replace('§', '$'); + //motd.replace('�', '$'); setAnimals(settings->getBoolean(L"spawn-animals", true)); setNpcsEnabled(settings->getBoolean(L"spawn-npcs", true)); @@ -203,7 +203,7 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW __int64 levelNanoTime = System::nanoTime(); - wstring levelName = settings->getString(L"level-name", L"world"); + wstring levelName = (initData && !initData->levelName.empty()) ? initData->levelName : settings->getString(L"level-name", L"world"); wstring levelTypeString; bool gameRuleUseFlatWorld = false; diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index e61001a3d..ac99a4154 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -39,6 +39,7 @@ typedef struct _NetworkGameInitData unsigned int xzSize; unsigned char hellScale; ESavePlatform savePlatform; + wstring levelName; _NetworkGameInitData() { diff --git a/Minecraft.World/ConsoleSaveFileOriginal.cpp b/Minecraft.World/ConsoleSaveFileOriginal.cpp index 139d99acf..7a11b5e12 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.cpp +++ b/Minecraft.World/ConsoleSaveFileOriginal.cpp @@ -740,7 +740,7 @@ void ConsoleSaveFileOriginal::Flush(bool autosave, bool updateThumbnail ) PBYTE pbDataSaveImage=NULL; DWORD dwDataSizeSaveImage=0; -#if ( defined _XBOX || defined _DURANGO ) +#if ( defined _XBOX || defined _DURANGO || defined _WINDOWS64 ) app.GetSaveThumbnail(&pbThumbnailData,&dwThumbnailDataSize); #elif ( defined __PS3__ || defined __ORBIS__ || defined __PSVITA__ ) app.GetSaveThumbnail(&pbThumbnailData,&dwThumbnailDataSize,&pbDataSaveImage,&dwDataSizeSaveImage); diff --git a/README.md b/README.md index dd9a7965f..5be0c8fbb 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,32 @@ This project contains the source code of Minecraft Legacy Console Edition v1.3.0 - Disabled V-Sync for better performance - Auto-detect native monitor resolution with DPI awareness, resulting in sharper visuals on high-resolution displays - Full support for keyboard and mouse input +- **Configurable player username/nametag** — edit `username.txt` next to the exe to set your in-game name +- **Persistent game settings** — gamma, music, sound, difficulty, HUD options, debug flags and all other settings now survive restarts (saved to `settings.dat` next to the exe) +- **Correct world save names** — save slots now display the actual world name instead of a raw timestamp; save list is sorted newest-first and refreshes without restarting ## Controls (Keyboard & Mouse) - **Movement**: `W` `A` `S` `D` - **Jump / Fly (Up)**: `Space` - **Sneak / Fly (Down)**: `Shift` (Hold) +- **Toggle Fly**: `F` - **Sprint**: `Ctrl` (Hold) or Double-tap `W` - **Inventory**: `E` - **Drop Item**: `Q` - **Crafting**: `C` - **Toggle View (FPS/TPS)**: `F5` +- **Toggle Debug Info**: `F3` +- **Open Debug Overlay**: `F4` (Debug builds only) - **Fullscreen**: `F11` - **Pause Menu**: `Esc` - **Toggle Mouse Capture**: `Left Alt` (for debugging) - **Attack / Destroy**: `Left Click` - **Use / Place**: `Right Click` - **Select Item**: `Mouse Wheel` or keys `1` to `9` +- **Accept Tutorial Hint**: `Enter` +- **Decline Tutorial Hint**: `B` +- **Host Options / Player List**: `Tab` ## Build & Run @@ -49,7 +58,17 @@ cmake -S . -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Debug --target MinecraftClient ``` +## Runtime Files + +Some features require files placed next to the built executable (`x64\Debug\` or `x64\Release\`): + +| File | Purpose | +|------|---------| +| `username.txt` | Plain text file — first line becomes your in-game name and nametag. Created automatically with default value `Windows` on first run if absent. | +| `settings.dat` | Binary save of all game settings. Written automatically whenever you change a setting; loaded on startup. Delete it to reset all settings to defaults. | + ## Known Issues - Builds for other platforms have not been tested and are most likely non-functional - There are some render bugs in the Release mode build +- Changing the resource pack on an existing world while loading it may crash (`reloadAll` called during world load) — use the default resource pack or select it when creating a new world From 2d6a62a0e0c14bd435ef3d760ba413506029eff2 Mon Sep 17 00:00:00 2001 From: dtentiion Date: Mon, 2 Mar 2026 23:00:42 +0000 Subject: [PATCH 02/32] Refactor README for consistent formatting Updated formatting for clarity and consistency in the README. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5be0c8fbb..3b7372993 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ This project contains the source code of Minecraft Legacy Console Edition v1.3.0 - Disabled V-Sync for better performance - Auto-detect native monitor resolution with DPI awareness, resulting in sharper visuals on high-resolution displays - Full support for keyboard and mouse input -- **Configurable player username/nametag** — edit `username.txt` next to the exe to set your in-game name -- **Persistent game settings** — gamma, music, sound, difficulty, HUD options, debug flags and all other settings now survive restarts (saved to `settings.dat` next to the exe) -- **Correct world save names** — save slots now display the actual world name instead of a raw timestamp; save list is sorted newest-first and refreshes without restarting +- **Configurable player username/nametag** - edit `username.txt` next to the exe to set your in-game name +- **Persistent game settings** - gamma, music, sound, difficulty, HUD options, debug flags and all other settings now survive restarts (saved to `settings.dat` next to the exe) +- **Correct world save names** - save slots now display the actual world name instead of a raw timestamp; save list is sorted newest-first and refreshes without restarting ## Controls (Keyboard & Mouse) @@ -64,11 +64,11 @@ Some features require files placed next to the built executable (`x64\Debug\` or | File | Purpose | |------|---------| -| `username.txt` | Plain text file — first line becomes your in-game name and nametag. Created automatically with default value `Windows` on first run if absent. | +| `username.txt` | Plain text file - first line becomes your in-game name and nametag. Created automatically with default value `Windows` on first run if absent. | | `settings.dat` | Binary save of all game settings. Written automatically whenever you change a setting; loaded on startup. Delete it to reset all settings to defaults. | ## Known Issues - Builds for other platforms have not been tested and are most likely non-functional - There are some render bugs in the Release mode build -- Changing the resource pack on an existing world while loading it may crash (`reloadAll` called during world load) — use the default resource pack or select it when creating a new world +- Changing the resource pack on an existing world while loading it may crash (`reloadAll` called during world load), use the default resource pack or select it when creating a new world From d31d261ffd707aabfd8715d3bc2cbf2e6fbc3fcd Mon Sep 17 00:00:00 2001 From: Alezito2008 <92759854+Alezito2008@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:29:29 -0300 Subject: [PATCH 03/32] Prevent world input from affecting inventory (#354) --- Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp index 5ad275c39..1a5772bcb 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp @@ -33,6 +33,11 @@ UIScene_AbstractContainerMenu::UIScene_AbstractContainerMenu(int iPad, UILayer * m_bHasMousePosition = false; m_lastMouseX = 0; m_lastMouseY = 0; + + for (int btn = 0; btn < 3; btn++) + { + KMInput.ConsumeMousePress(btn); + } #endif } From f216abca4217b3fe4497d5aea5e4a3d6cf679b46 Mon Sep 17 00:00:00 2001 From: 4win <4winyt@gmail.com> Date: Tue, 3 Mar 2026 22:30:14 -0600 Subject: [PATCH 04/32] fix: properly offset the mouse position in containers (#327) --- .../Common/UI/UIScene_AbstractContainerMenu.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp index 1a5772bcb..bb56c7809 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp @@ -214,8 +214,8 @@ void UIScene_AbstractContainerMenu::tick() scrollDelta = KMInput.ConsumeScrollDelta(); // Convert mouse position to movie coordinates using the movie/client ratio - float mx = (float)mouseX * ((float)m_movieWidth / (float)clientWidth); - float my = (float)mouseY * ((float)m_movieHeight / (float)clientHeight); + float mx = (float)mouseX * ((float)m_movieWidth / (float)clientWidth) - (float)m_controlMainPanel.getXPos(); + float my = (float)mouseY * ((float)m_movieHeight / (float)clientHeight) - (float)m_controlMainPanel.getYPos(); rawMouseMovieX = mx; rawMouseMovieY = my; @@ -306,8 +306,13 @@ void UIScene_AbstractContainerMenu::tick() // Scale mouse client coords to the Iggy display space (which was set to getRenderDimensions()) RECT clientRect; GetClientRect(KMInput.GetHWnd(), &clientRect); - x = (S32)((float)KMInput.GetMouseX() * ((float)width / (float)clientRect.right)); - y = (S32)((float)KMInput.GetMouseY() * ((float)height / (float)clientRect.bottom)); + float mouseMovieX = (float)KMInput.GetMouseX() * ((float)m_movieWidth / (float)clientRect.right); + float mouseMovieY = (float)KMInput.GetMouseY() * ((float)m_movieHeight / (float)clientRect.bottom); + float mouseLocalX = mouseMovieX - (float)m_controlMainPanel.getXPos(); + float mouseLocalY = mouseMovieY - (float)m_controlMainPanel.getYPos(); + + x = (S32)(mouseLocalX * ((float)width / m_movieWidth)); + y = (S32)(mouseLocalY * ((float)height / m_movieHeight)); } else { From b1b622c303a40a5533962fc63559a0346cfbca04 Mon Sep 17 00:00:00 2001 From: rtm516 Date: Wed, 4 Mar 2026 04:31:47 +0000 Subject: [PATCH 05/32] Fix overlapping debug menus and screens (#294) * Fix overlapping debug menus and screens Also resolves a formatting issue with clang-format * Update readme --- .clang-format | 4 +- Minecraft.Client/Minecraft.cpp | 14 ++-- .../Windows64/Windows64_Minecraft.cpp | 67 +++++++++---------- README.md | 1 + 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/.clang-format b/.clang-format index 383e62104..a4d4a4c2a 100644 --- a/.clang-format +++ b/.clang-format @@ -39,9 +39,7 @@ RemoveSemicolon: false SeparateDefinitionBlocks: Leave ShortNamespaceLines: 1 SkipMacroDefinitionBody: false -SortIncludes: - Enabled: true - IgnoreCase: false +SortIncludes: CaseSensitive SpacesInParens: Never SpacesInParensOptions: ExceptDoubleParentheses: false diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index 164417abe..bd75a61a9 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -3618,8 +3618,6 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) if((player->ullButtonsPressed&(1LL<renderDebug = !options->renderDebug; #ifdef _XBOX app.EnableDebugOverlay(options->renderDebug,iPad); #else @@ -3629,13 +3627,11 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) #endif } - if((player->ullButtonsPressed&(1LL< mob = dynamic_pointer_cast(Creeper::_class->newInstance( level )); - //shared_ptr mob = dynamic_pointer_cast(Wolf::_class->newInstance( level )); - shared_ptr mob = dynamic_pointer_cast(shared_ptr(new Spider( level ))); - mob->moveTo(player->x+1, player->y, player->z+1, level->random->nextFloat() * 360, 0); - level->addEntity(mob); + if((player->ullButtonsPressed&(1LL<renderDebug = !options->renderDebug; +#endif } } diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 4a3d835ca..9d9537c58 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1278,7 +1278,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } } - // F1 toggles the HUD, F3 toggles the debug console overlay, F11 toggles fullscreen + // F1 toggles the HUD if (KMInput.IsKeyPressed(VK_F1)) { int primaryPad = ProfileManager.GetPrimaryPad(); @@ -1286,21 +1286,43 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, app.SetGameSettings(primaryPad, eGameSetting_DisplayHUD, displayHud ? 0 : 1); app.SetGameSettings(primaryPad, eGameSetting_DisplayHand, displayHud ? 0 : 1); } - + + // F3 toggles onscreen debug info if (KMInput.IsKeyPressed(VK_F3)) { - static bool s_debugConsole = false; - s_debugConsole = !s_debugConsole; - ui.ShowUIDebugConsole(s_debugConsole); + if (Minecraft* pMinecraft = Minecraft::GetInstance()) + { + if (pMinecraft->options) + { + pMinecraft->options->renderDebug = !pMinecraft->options->renderDebug; + } + } } #ifdef _DEBUG_MENUS_ENABLED - if (KMInput.IsKeyPressed(VK_F4)) - { - ui.NavigateToScene(ProfileManager.GetPrimaryPad(), eUIScene_DebugOverlay, NULL, eUILayer_Debug); - } + // F4 Open debug overlay + if (KMInput.IsKeyPressed(VK_F4)) + { + if (Minecraft *pMinecraft = Minecraft::GetInstance()) + { + if (pMinecraft->options && + app.GetGameStarted() && !ui.GetMenuDisplayed(0) && pMinecraft->screen == NULL) + { + ui.NavigateToScene(0, eUIScene_DebugOverlay, NULL, eUILayer_Debug); + } + } + } + + // F6 Open debug console + if (KMInput.IsKeyPressed(VK_F6)) + { + static bool s_debugConsole = false; + s_debugConsole = !s_debugConsole; + ui.ShowUIDebugConsole(s_debugConsole); + } #endif + // F11 Toggle fullscreen if (KMInput.IsKeyPressed(VK_F11)) { ToggleFullscreen(); @@ -1318,33 +1340,6 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } } -#ifdef _DEBUG_MENUS_ENABLED - // F3 toggles onscreen debug info - if (KMInput.IsKeyPressed(VK_F3)) - { - if (Minecraft* pMinecraft = Minecraft::GetInstance()) - { - if (pMinecraft->options && app.DebugSettingsOn()) - { - pMinecraft->options->renderDebug = !pMinecraft->options->renderDebug; - } - } - } - - // F4 opens debug overlay - if (KMInput.IsKeyPressed(VK_F4)) - { - if (Minecraft* pMinecraft = Minecraft::GetInstance()) - { - if (pMinecraft->options && app.DebugSettingsOn() && - app.GetGameStarted() && !ui.GetMenuDisplayed(0) && pMinecraft->screen == NULL) - { - ui.NavigateToScene(0, eUIScene_DebugOverlay, NULL, eUILayer_Debug); - } - } - } -#endif - #if 0 // has the game defined profile data been changed (by a profile load) if(app.uiGameDefinedDataChangedBitmask!=0) diff --git a/README.md b/README.md index bc45ef229..71c067b04 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ This feature is based on [LCEMP](https://github.com/LCEMP/LCEMP/) - **Toggle HUD**: `F1` - **Toggle Debug Info**: `F3` - **Open Debug Overlay**: `F4` +- **Toggle Debug Console**: `F6` ## Build & Run From c8a8f9dd6cc120758ea2501ef5a94320225dd7de Mon Sep 17 00:00:00 2001 From: Mykey Date: Wed, 4 Mar 2026 12:32:51 +0800 Subject: [PATCH 06/32] Update crafting controls description (#359) Clarified crafting controls in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71c067b04..6e9f18aed 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This feature is based on [LCEMP](https://github.com/LCEMP/LCEMP/) - **Sprint**: `Ctrl` (Hold) or Double-tap `W` - **Inventory**: `E` - **Drop Item**: `Q` -- **Crafting**: `C` +- **Crafting**: `C` Use `Q` and `E` to move through tabs (cycles Left/Right) - **Toggle View (FPS/TPS)**: `F5` - **Fullscreen**: `F11` - **Pause Menu**: `Esc` From 6c842b2854fb8c1d0c87ffcbf0bb5c297b883226 Mon Sep 17 00:00:00 2001 From: Alezito2008 <92759854+Alezito2008@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:21:21 -0300 Subject: [PATCH 07/32] Disable flight state when riding entities (#368) --- Minecraft.World/Player.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Minecraft.World/Player.cpp b/Minecraft.World/Player.cpp index 700935f6e..e0fa26ad4 100644 --- a/Minecraft.World/Player.cpp +++ b/Minecraft.World/Player.cpp @@ -627,6 +627,7 @@ void Player::ride(shared_ptr e) return; } + this->abilities.flying = false; LivingEntity::ride(e); } From bfcc4aa10c7a56efa275652f6c7b59b5cd5e8f8d Mon Sep 17 00:00:00 2001 From: ModMaker101 <119018978+ModMaker101@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:37:04 -0500 Subject: [PATCH 08/32] renderer: frustum test new chunks so newly generated chunks render to full view distance #175 (#344) --- Minecraft.Client/LevelRenderer.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/LevelRenderer.cpp b/Minecraft.Client/LevelRenderer.cpp index fc56494f9..222a5c189 100644 --- a/Minecraft.Client/LevelRenderer.cpp +++ b/Minecraft.Client/LevelRenderer.cpp @@ -2557,13 +2557,19 @@ void LevelRenderer::cull(Culler *culler, float a) { unsigned char flags = pClipChunk->globalIdx == -1 ? 0 : globalChunkFlags[ pClipChunk->globalIdx ]; + // Always perform frustum cull test + bool clipres = clip(pClipChunk->aabb, fdraw); + if ( (flags & CHUNK_FLAG_COMPILED ) && ( ( flags & CHUNK_FLAG_EMPTYBOTH ) != CHUNK_FLAG_EMPTYBOTH ) ) { - bool clipres = clip(pClipChunk->aabb, fdraw); pClipChunk->visible = clipres; if( pClipChunk->visible ) vis++; total++; } + else if (clipres) + { + pClipChunk->visible = true; + } else { pClipChunk->visible = false; @@ -2572,6 +2578,7 @@ void LevelRenderer::cull(Culler *culler, float a) } } + void LevelRenderer::playStreamingMusic(const wstring& name, int x, int y, int z) { if (name != L"") From 779cf56f4b9c767df37a7d10569114f8588d9329 Mon Sep 17 00:00:00 2001 From: Tygo de Vries <108730722+TygodeVries@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:39:16 +0100 Subject: [PATCH 09/32] Removed private information (#333) Maybe we should not have this out there to find so easily? --- .../Xbox/Docs/Ratings_Submission_Form.doc | Bin 209408 -> 213504 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Minecraft.Client/Xbox/Docs/Ratings_Submission_Form.doc b/Minecraft.Client/Xbox/Docs/Ratings_Submission_Form.doc index 91d965bd3d1212d9e166c24f8aeb5d9efe7ecd31..2b87aa85425370f0aedffb7a0b001fd6be87bce3 100644 GIT binary patch delta 21638 zcmcKC30w}_Q$HWxw`|Yu|U-voBBRT0Y&QlM$-{1H9|NUOC-*0%&a^`tv&N*}D%$YOu0Q|~;$xD;cNt72DJw~q zETrM%hmSY+H22hpelsdzYAv~?%F-kk#|gq#wTse2H?m^Ehs1PHje`0AG_@+SO`@ug z>(s*U6`eNab9)FEX>40dl1>>(l3~5?3`y#*kls3yPI_oANspRIQdPcCta-uK|2fSd z{4yKb`<*1E@sXu1C20!N>C+{N?MO42is8RIO_(J~QGC&VE}1#H@Qc4_M`kJI%nXVD ztEIZm*jgucEe6B%FLo^k!_=_Lg3y1>Lqk#w>&5b27Tm}KYo<%R3fBB{Dz@KKk{WzD zD25Fg;%8VEc~Ep%>ZLBK*4WJuzkgl*b${c@f>RSE>3fB*bke<^RtD_kBa zNk-D^X~d(emtlRur@m&Xh@TIEhNKy$hO9Fz>yMms)vt>M8!{{#j@XRp9AM{S%{c&_ znXA^^@PU6$4e|eH_}69CuK2e4hG>5c{&v6r8cu3G-2XMW;A`T9Jj1zB%~_I~lgq`V zvf>ChMz&5KEUSUlIvh1yELR%PbJWDiGe?bd96rZ!#N>$+XG|LBJIB#?)Ub(z)a4}| z+Wv?2rFxC>X*zanl;f167Jtb{Esguj z7OFhHiF{FQG`@yzLJ1BKd@%=eF%OHe1Y58b+pr5q5Q$?riBq_VTeyuh+{0gZi;wsO zOEXC-3oE>RoBJg9k^Y~HnB?BSmHRgL%DEFqj)>XZ{rmUp!^F%j{Zh!S)gjv1o^m;g zp60kpZd{^-_1M<)drBTI+OQpRIn{T2Sohe;L%nnO17|sqSjp z)M{F@>T;P<=HePMrKY`>)tbHy>;Jp>FXv+`5r8#_LJTrXs%L$FQxkm$sO{gEQq>s~bt1X{ z{P?kNzCs3aZkeGR+)*D5&=6kmMjP}-AB@B(j7A*J;u;c>gcm4Tio#h+YAcmtrV{L6 z55+=~jA4R2h8KB{9;KzFrKF`@O*?ZnEjltHnz_wa30eKqjMXDXjG%1hI?2vzyE&aq zD=DUnjK-%cL7#1vS&Dih-&xI=(p=4$V6WDiTC1GooJraY@fY!pi!3wqrCdzx%~WkR zyK?;)=1$=~u9TLf1c;V{1ts2d#tYvspXgSRpk~c>%3J{}(VnoGXK~0!zqWGBxJ+@V!uCP|! zADgJjbAM6y%qyu#b;9PGhpkl9n#*gJ5!Y+lP*Mq)p`_Yx`H!k`prggBtn{q(Br!_= zH7i{`wcMTqA{89A-KE5a&#x=kaOUzbHFkxI8nD7zk!ptp7?)VemX=|;T0L-{A5 zj@$UXY8+};_G{dTT2}gT;&*m=#jo*;ub@x(cKw!uHWfrKVv|04n@v=^P#gKBS}$~- z@dz8D6X94VKPNvUC!g{497gBGC`ybX#Ave^1?X2V6tjl>RnPA!jB04C`i!W@t|%qf z2@7Kf5d-Bkbw)%h)%dWLdU$z-;>2=OR(ha%CdXEPXeL(?G0cfLrq8*EJ^HBaHBqe& zHLXOyxvpGL{(NK zTh%RUu-sQ&5miGqjBYkQWZI9@MWd15`hcENKH-1)aU;BYvlg5`X+L?TIbe0I~#jRst8-ugecePVw6PrKC`j1 zQDHd`y8T&pJDM`ZL|&s#ykA4zc;BuBn`H;{rwxrfec!uS`Qv;&3W@_2Elgml%9+!H zp4!xSbx#bhZYKPiaQs(ByTxde7_Abc1-~XF%$zuJq*`%mRqbVI*;0L*c~-8W=HIs~ zIkAIeJP9*QW%b>Cd%4EZEK_6E_{C5~a#V-E=;WH0{m1?6+gEPKUSV|j%I)kwc8Nvp z%5BCgMBqFTI7I|TzIkCL)4-@VfwqTh*fZs5j-8_OtwOY`5^b2k6lO4oCCXwnBqvGw z9f_z>U6N{}4}QcbOhg1E8jQn{2q*5qYGNBg5Cy3wttfnkNiE4JbNyY?sjzqK3Jrl&7j#5N>VdG%4Q zANHql&!214^n>IoN#!vad8k&02%Q|V-6BFQr@Vb(;AL53ho4ac9mqVSg z%#Oo&EWmZ#Kr(LPHQpc(AJp>iyt_B7M|%k^;epQhATG!HAvgDVZq{@DWf?;5F#|El zyO|V!Gw)>Hfs>3vPKuzLNow3&E9G!cHJ6HNX?}k-@N?OQv?ZID>P=fxW^Q)v)rE7_ zyId1<-?W(|lY+L>cVT}iVegIAq3@a&^Ce0@sThZ^qAHO&fE3~H%V!d(8;*#&V#k|Cgl*fzur6TTI@WGmr=R%yXZ!gP$AV0jN*8UeHa1

#)8l7P_4$duUW#a~J;udZr1rq%bO|b&2uv!~tA(yKX%B&+@3{_AS)zBJV z@Wudb5g%)vpO>GX|3d#iKR+|$W@i57{8N`19lXqnJ=?c$XQH3J5Fa&s@^Zdf(7vmx zl_@QkFKEp6FD-A>Qku!-<>uj*vZ)qmAve^i>`U^beuoKcVFw2|qAHx=jGAzP8|tGW z8lx$iqa7qK?kV7a%5a1STEP?D&>cN6#7k-^4P~bA(t7*G#oK2%MUL*>cyy^4j_%#N zH#>VVK?I96oKe2)qabaoN2{T8th8rNa(!)YCAot(vbt=elpU#ARFYja%hGbulcr9% z4JVDAtY{r<<%ZptvDa)wdUJ1t9OU8^{={p%fz*ae9v&eRkCBC9ZOJZFgA;NnXxEt>h0=38CvtOjPAXld1ns#j@l zQL~H5W+p)kPE%aIj$S&c|vPsp0?C&s+;uxav9v|=tQU}e&Rc>CaQ3vX4ZJ4X(b^YGG^Vb1#6QoX59tdtITWQPPWy`)&XQ~yrpewqgCwiebe#hI~$GNv3GrIKnEcyI6 zH&QI@dYt=qd+=LA)~!2xR!joKH6=j5;IIAZF8`%S4a4ipCgsIOPIRH)3a4=faayUy za)l~c%t~D)X+9QUA+}&Ew&6Fed{eoV{6Xu{RIX6RryB<(k0P9v^)RboxFaSBS#b%B zjwWR74gY!XojX};vgfa%4AjsPddenR*=F(!Yii!A^4!8|TI3n^vQ#U)xooTxn^n3~ zRAB;Bn86&Du!1#gU<;)OpM?! z7j(r~t(ceG#Dj#29)y&Xq@<*b6hlu`!7@QfQDU?&Y3n{lYxgBBTf%0)4%*OWl>d$1 zR7I11kjt7@)fckL^F%+Mo#r=zm0vm(6sca6hiq&dZQMrwAeY)g9JXQ`LbTRia)s)R zejwewNH~L$hIz@AoQ!+(5tzaZ=CD9nSi=Uv9b}un9(|~V;f0YHjj`~@ zQoMOV&(F0B$FJ=W!?gyNVgD!=p9O1nl`(zB7W#cMeL#_c*((r^IOZ-WV6cR z@N-%foNVC>3QxAeP^oTsJBo^NXRboY?Azx@dU>!I{#c6TScw3vK_G$l5df#k`#Tpv&Vd$At}aTrH&3{i-|Da7F%&LbZ0@E%fsatNa_24k@l%di}qHT#}&i^g2& zv-9&avJ)~+i9vLiibjBbh!o?9{LK-J0wVJL{R(Tk4a+niZ?1~YKgj(>hn9=>@CVtx zpkMT$CRi@1y{NI)X4BN-_Jc@USz%zb3wAu^GLcep-?58@V5APr`Ru!ar3g9kh@ z5N~syJW0JCmwHePsn@Sxe9IV@oXL&cnwMD=`8 z)WoNg4xMJS{dCf%4J$Vhl#w#K@aaKHigtCloK&>Jts6d4Ht8U?WDOhGVlVb14(D(l z@wkY~xPk;EA{i-2MX---qkA`sRF5W_uz)4X!V1=~fi3Ld2q!qBCS2eNFSJ2lNMkWu zmQrySW9GTmxoLQy*=>Hu-tvD z+m75#xob6t3G#3)x3j1WCKOf%q6}*}&Ezt2({LZzIBJ?)%&KZ(WDF5-m?uk>;fN~m zoF{9om&%Q_ZcAlbb>OU0YV-G{q63%8Q)Hcp-#!S1Vb?ilQ9KTF%5oL zgvD3_e*{6rdTc;2r1`Q`4}&lcw{d5_+)uj8Od9UtK0+7B(st~_ejI?pb7EuUzsk>l zDk{)>`Pc3-Ix9wz_e9XaZ3kbyVs-wySGsi*Cl);aaMc`E$xXG>gZHX*( zM}K^6l!>$clD-0n6SK#Yb{}W7>3Gt*)r;2=bcrfJ+=>L{n`ptRTvL0f%68&VDC$Oi zBSkdKw4=pj2bCuM*V@5Rax0nxhSYyeZM4H0`H^f*#EPc4Fw$Bj2Qq)qPPtTct3dgz z>>v^%5@6}i0fG%|u@Eo(qaD`E4`rQ5*#HcL!U|)Uz!YXMhhYQO%o{f1%)Ho;9~NU9 zLJ@|Y2*){`M?5a#GP3X#&+x)u?jq$d>ns9*!avoe0!vk7^ad9zm zaR=f;4lr7GAa2D^b5~5&hs9iYaDhy>)`oYHZM0)SJUCmqgNx$jAj3VB{{HWGO)f*O z_tKnI*}ig3qM(mK(Y^L$kA2X8X|%&ekw+p5p725&v_l7ULT7Y^;Y)p)?~j4#w2bu_ zis2ZE(HM*I@WCWZfiGrbF6Lt)0I0vO+Q6|?Al;oQ(%m=9M`Zi2nWE1V zkspS1o?9O6u!Xb3hA@%VqUjWAJf>A~l&QXNWJ!C=E-LL1l2$Dpf&MR6{+~M*}oOBY2|?+M*rWV=#tbC?>6v zM@W;InS!a920tvqVl06_f}mnOHh{LQzBiC&YgYOZ@tAIh2-$f1j6P(g2$?TJrrtio z#rRV={Wjxwb1`0chCaX{yR`W`WM?f_6>Xa^*}5`Mci5^SnMI>2_Et2{BIKR2j<8@z z0f-?<7z$s^#B9vPd@O_?7Q-LQ5r8!aL=ZatLT!Yh7><+F$T z4a=4E0T1&-L!UidV+a&s6i?0J_?K4WaaxVMZBx6h5qJ6DG;5lOY!z83vd)kthV0Nc zZA1&DhNe?!DTNwWRo8wf7QK0|Ji3H~A;UyQh|l@Da77)sp)Oj&1FhhR z)~BdG<}F2e5QZ4@CZMTqV=0h;?6xvO}NrUjo74WToI4WSbUzBGh9|6|isJe+DV+eE4_ zGO=g|e$BjS+Y|B=*}6yuip(p_fTFFUQ?!bzktzB-DVnY_r)8-uT4VNUnz3if-DAWpDIAPyB#h=#3E=iBTAhF_>{$ z?jg-&W)_H-GzU-d2VUb1{yTO1!kHrrFU6OS72arj%kIVhiqv<8X`6nPs~1%9zf!x4 z%1e71YiK`xij^DijI(N`8bxDZnMj)wHn2_9XeZUux}O)<>GSeoStlZv2Z_}Rj9~&( zn86%|O;|Huuo)VC%)7uVrE!^Oa`e=y8Xo}|e4j%AAJ9I#&Buaf}X1by~dSW5` zuo(VWise{|0IWeEg0LRJ*oe&tM+Ej_KMvycD=q=)>923!zMYtuh}XAcUw;X*CoC~B z>R@=7^yUA?pp8Lm)`&~1cHpY)T78&nX-WFhn=h8AzP_9a(khdJvX(Y+$1VqmKA$)=%2Y|$c0Wx(GC}>pjr~qe`bWp1VaWG5^qSjA<+d1jrO?A z31DwX@qNCSfrrS1c$fa8R%N7IQQUIVT1ud`)I#JFBP@xYNhY^QF z_#p$ikc{XXfg{}EiT}lQy8nxf{tvdP`#(?Ne~wI*|8JIR9xD}VCr9Ght&@?|9X&9l zgKW$@M2@^e{zCr0?R`<`( z!{qL~@M@}6Ue7M9gO$3hYZt8l_U{E_+(g!GJvH|A<7Jvd_(mqyb?+ZCCzcWGDdccUcOy1 zH!?dr%OR(Iar&4YwQV~U2i^CRD>boWWPIr%vYGd&s@R9Svvpsueb{My3 zP-@!a#XEhT_i@@i*vzWKDhJ!pXor^5Qv>TJR^NH;X;g*#5w~l9|8ueV0gK1h-0}Uyq%oVe zl%G(m`EcIpHtVp?sh{JrW;r_?+Pv!Tw#l^dii@>um$qJ0-+AaSJ@@sg^2XZb_@6@t z*c});vSaPRacw%+spYZg>C~X#N53;Yw$wWFVfNkEPbMdXd|vqJ;cxk`hb*x#TjP4f zG+ky?59_%^&*6qD=Ew*{2uPzwrBk))viz-}Rd0l(Kz}XAkEd0S#P@^6Xp0r%XRF#=q&z zd(~Se7(f3#qhg6$WnOLG>3yPW!__}Ubu?R8@xAHdGxm31c$B(TF>mYKav?DtVk#}4 zdvTWgj4SUfOF4B~;L@Vzt|;BvS%IDBJKgoZA33#7m9sbMg}aVz+t)PYipR3o{k!S{ z98b=<7yJFPJ*zTj zz0Pr_*Sz8*R(9>-KQ%XVdg&!If?G7MtS(&d+2>AtY}{P`w7$KsRkTj5y(epQ^KLno z5Bz;9UGOmpx%Z&W!-`?9ZRZDPwah&+@pT{V*HLe}r|dpGqEbxz(Bl5HU)hct;%8Ai zX3Y4O9Rh!dOODLF@I0`^r<0fMm&}`!nHY2Kv~k7x6D#UEyokC|eBzDP$#;g@x!bo{ zY(L~gu`(AvjwsW6;PI5W+P7!b%}yzmXy2?$w@ydDZyx&j;Oj-79zI<5<2K7vTPIyO zl+v;H?HAKW?+VIZku%zQ($U>_hJIJIvhMrH6()D&r7rci9?EIH`Q#es59zZSM&`U8 zRJp;vyD`s4cs;F~^M2#-W7Bjk7F>y$vFKpC)fKL6Y+ri8^-T>zJhmUJv&`zHY0JOL z>Kdl5663>)>WMG>`u?$di7b9Is5ccQ|}s)79(87CFboK-s9Dc*S#|K-HD%j zrMa$Z=*UCf=Tfg!ulKUbk%s+G`TK9)e9vOl(q+FL?b&MD&GohX`u|b>-7ofWYu}8H zi3v458!+%qXrs03Ljv}-zkf5~RF`ho>W7z;-TO4Gv1t4JKmJ(yu=(rR&wQPe4`z&8 z=09^~iQ1ZfRKTR%HcG3-tDg4S_v7}dgN8JTcboCF)(=lVI_hFCGz<=Ib^c*^tjRBz z(mWfq_#x4?=}#>NFZsTLNz3(3e)YH#@LbMpP?4c-HBFzZaybQRxTD zj~e&dwAOv6k}sqCf5;kI{gO$BE@x)httNw-XDl=I@|n74@2N^< z=SmmS+l+EKdgf#OP8S0EwQtz1dyk$kd?sa_2wm^{=7(`c&-XNKV3S46>kC0yS8DD01&<$E{&IPS`wVZDkw`^<@SnS8`{lTFa(K@Yp+ zY`gM{Zgr2NKaaJ#%u6A4vu8!7PVP}@a#r^r0(QSX+$r_9y#u~$SNxQJYVW=N8~%D; z?RdGFwO9GiYSH&pk3Wk4aw+1cU7sS3BnRKBdo}xQ>i&7gmqwIJO_LilCxpLDgguDr*6Mf|&gjdq!JjPy<`-NE;k`GnYnq*D_QSl{d7*RFQG-lsaXo*eze z%zAe9@oqk~e3Vn>F4JobdeGDBrTOVuwf5Io>5=-UPn&*u&-`vXTV*8=O+D8$cUG*v zes(;qv*%Sxq&e`MlQz_VD*Wn|SBAwKy3ZkA!xV22`n2L%t(PQ;m$by2P2#mA@g@#$ zyXYkGhKG2?LcD1po~DZj(c+=1cpfR9l8Fa2;)#Ih@fIDQqT5b%qKIA$aVIS9>qHYj z3k_+|Ywe%ot3-WK6>h%))HU zgJ{Sufo3dX-oPZUBGKm zQa2R7MI`;ed~a|#r617`98!sQ5u`!j&`Luw48y@8mPWzwgm)bC_!CkVm}VxAP(aQj^Y@i5RDj|z)8g8A}--~T)|abLlUmz29j}0l)eD}s&GbQ95LfpT}XhA z-!Ojp3FDv4DUnzk%Sv~gvZFkTNF1Z zA?Dh{O`}qK=@!17Ge?BAq0nT1ZVLpZr~;!;|bp4Jv_va(Lk|qY2u#OJEJZNF5rG(-z-5Tv z9Hv2cpHU88LAK<$paLqQ7HXpfTA~ZOVF-p}8vGE5AcSEjjv)&1xQGgbU;rG#}xQsB{pLxj^P}x<34gAez{p5l~EVX(GH!_ zrW(h;7o$EHjj@;wf2>C^!V!TOT*W=4<0W1}cH%W4IKUC^s1Gl6#b6A@6imZnEORAi zvB{O=zlWK9IEh3zDkD6^pLhe~x}59vxYpu0e!~UaM#uWpco+b!0o6A4;1`_4@0ie- zvV#R!jkO5Ddzdug_?vT8+=r7mjjOnZbQI%iSsoQo1GUf$qcIb+u^cP08R3XR3@+j_ zQjvwf@D8S2ea&Ett2z&|6zO=0SCCt=15Cqgyn*CNY>|c+$iXK_ttlt?8M|=+c3xZ) z;D)<+4)NmuM|?(UZw?E_LpO!dEj+|iOl?C}VF|q3O41MLjZqkbS-5~3xP#j5IBn4q zuVF-Eq8Q4f0%~9umS6?$;t8@*r6b3`UPsPHc%vV$B*cT zahQO4s6ZRH8fsxRHX#IcXpc2REBpzCwwW=kU=3%C!VJtp?`4cuU=x1D4cx?IJi%MI z3?_H*9Xg{c1|tz^c!+UB*eK>9F}mbw#j3bYG*(0-rM?kub8LlZkT^lqx}sJTHKM2u zcS2Nxq5>3^pQ!jmr6(#pQQ3)#PE>B9A`_LEsJuiyBB}y$$|td3F|bRzZ}q+J+P3q` zAe|*kwa|v0i`G#Rsu6)E7)~U9V24w%499T|f1nIItAmahfkpTkH{ebLx}Z0a*hnN& zh=ll!ogoq}XyABZ>kuBohY+(+gkcAEAsi9dg9L~#7MJX_(0HYiZYpV@A6C+VyCZ2f zv0sK+#keqGqX`?pfD$ybF~^P)kM+>tXV1k284jE&SW<;Df(2FC4qiFYoPEQQL zFvQfQ2-M}2Y}JrtBLOM6(3}{;`#X+50y@xy#_}#~7}`LNA8Jun>S7DR@E+pEfNc05 z%72Nemw~&Un5Qlj*Y{(oHqMV7tEXtTT z#G-78Lo3RbIJB>;6cA-f98z)p+W>LByAN@FtL#L^hzr^f>~^N^fGB|C_>1e-1Bk*X z%7Q46qCALeO(?|WBm?5|(H!D(@iWBbA{ydyPzjS;P!@3sm*Gxk*Mqn!D6MqlW@`#2 zL^W>dT~H&>rWO`8ZB2*@v=2l@*Ak*G8w61=xj@uI+i?p|AgY-<5Y@_jh$^B6M2)Z+ zq6T;eamEjEWVD4tDGsM7xJM9&%g`5M&HbWsMC)=t@#YFr4Y0(clYg2CjV?&R=u79L>eBHgXHt5;cRU&$m zGYwyjc1Tpl7yr6{;`7L`PDI^V^UI;oTcjx7ML)y_i=@w43uRfm{f`f|y<_-L@N=cR z*5tZkrL9dRC0FiziB9a@Z*yyM>X+QouPTyTta8={-!1yp!bC>PX-aiPbI(vLwAmSo zO?2n`%4p-F@%>-PD~;VaYTeUG!KC}_<#MJn@PB_VQO}iY-;|~Qow)CLsRS0y5src3 z2rkO~lIQxuTj$l6Rr(Sz<<*xwPki;IU{P=!wV}5Zljy``<%Jww{f#nPHV$;U6sc{` zQ@o8c%3q3%ewwHF8a3gn#0f)V0*@gEsyIo!)gm7IiysEBXy+k`Uzm#@sEJ<{^S73? zRryLYouQ%@U!oqBK6@(1#8eF8EH>gcnvL6Y5xT*p^9FTkdv@5~8Z^PhPtA){%@wCg zo8AhZT;-^(BB-q*sI4N}aEnH5mD~>0R>@pL(y6V|JK=jScmX}Qxb&nUO4aq0YU?S@ zt*3oK)%0`#xX9e2783oOmL`&=#15F65j8QjDO|QFTy`v6b|_r-EL^TyxZI&|*|Tt2 z=U*5={PY_PEL`qhxV*D)xmV#bQPh8qDC(!j3fB`&{W4M2Plpyx&lj#AS-3p9aGI-l ziqCb@|MfPJ63Iv(Akp`y-vIr8`YHW?`l(gndiwtK%k=%}r?U&g6SYHsE1fEejA?Hj z$Vx>qF)C(UOffS4>?UOl(8_*L##=e-*OW9dR>V-aCh&ve=vc6(bTJWZq+gTydB1dy zYpH(ttR;R>?Dg4R5TL#Npg8Ni-NXjJ4m4UEDKcM%kixaC>G|`eLw364$d8T3F`Yuc z;4ppHUxWYgHfZd&EJ#RkqY{czOfe=UTHTMz6l?yh45>B|DKL>TK08TX#6{7Le^gvr zPBrzjk#Z9iZhQ67dT~x}FF8IMBVubT-6X6n*X^SBBH9$=k5G}`Wy(oeRo z?=C+5b(l!GoH#`M6*=wFXQgDx21}+&xt5%ZA3ln|WoXTTl}tUmYkz-MM#?2N3^lSZ z)9KNrCbSgwLmFdw!%CyN)yvY)I*pterO%`O^lbMcuST-$%Cr|bHAb4u`{(74KQ^+K zgE_25W+d!o6{GT+onmAq`)KtPBk$H9*diyYWJ+?x;i}2hoM}U*bXiHBOvTyKnQ3XJ zy_nKLBMoHA$t{i4jw?n(s)!1t&$Mx)raSiU)yr|@C|Ac`bH+^Ork`+8*)-JZ7#Y=U z>T$-uHA|k5QlhPXO1&cM=WY7U6o0Nz6a}$!F%`KerXqih^nXDwvU_qj`F}8K_f4{` zHT$wg`$~!fAT~=Xw7X@EDn`F3Yjn7*@lyh{(CS83#^SGeM#oe);s<_hNTX(1qcBjb zS;HvV*pIN_nns@W)-e+V73;A9!O$SK@$nzFM)#;`v{nvwG4d%fW%367pJxowX1EyX n#>aw>Ckk+WXo@bLHjQ3|E!{qw`#iN*4Qni=voo delta 17434 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 Ti70()Gh$29es5+dVfjA*gIMYR From c9d58eeac7c72f0b3038e084667b4d89a6249fce Mon Sep 17 00:00:00 2001 From: dtentiion Date: Wed, 4 Mar 2026 06:05:24 +0000 Subject: [PATCH 10/32] Delete README.md Unnecessary --- README.md | 74 ------------------------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 3b7372993..000000000 --- a/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# MinecraftConsoles - -[![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white)](https://discord.gg/5CSzhc9t) - -![img.png](.github/IMG_8725.png) - -## Introduction - -This project contains the source code of Minecraft Legacy Console Edition v1.3.0494.0, with some fixes and improvements applied. - -## Features - -- Fixed compilation and execution in both Debug and Release mode on Windows using Visual Studio 2022 -- Added support for keyboard and mouse input -- Added fullscreen mode support (toggle using F11) -- Disabled V-Sync for better performance -- Auto-detect native monitor resolution with DPI awareness, resulting in sharper visuals on high-resolution displays -- Full support for keyboard and mouse input -- **Configurable player username/nametag** - edit `username.txt` next to the exe to set your in-game name -- **Persistent game settings** - gamma, music, sound, difficulty, HUD options, debug flags and all other settings now survive restarts (saved to `settings.dat` next to the exe) -- **Correct world save names** - save slots now display the actual world name instead of a raw timestamp; save list is sorted newest-first and refreshes without restarting - -## Controls (Keyboard & Mouse) - -- **Movement**: `W` `A` `S` `D` -- **Jump / Fly (Up)**: `Space` -- **Sneak / Fly (Down)**: `Shift` (Hold) -- **Toggle Fly**: `F` -- **Sprint**: `Ctrl` (Hold) or Double-tap `W` -- **Inventory**: `E` -- **Drop Item**: `Q` -- **Crafting**: `C` -- **Toggle View (FPS/TPS)**: `F5` -- **Toggle Debug Info**: `F3` -- **Open Debug Overlay**: `F4` (Debug builds only) -- **Fullscreen**: `F11` -- **Pause Menu**: `Esc` -- **Toggle Mouse Capture**: `Left Alt` (for debugging) -- **Attack / Destroy**: `Left Click` -- **Use / Place**: `Right Click` -- **Select Item**: `Mouse Wheel` or keys `1` to `9` -- **Accept Tutorial Hint**: `Enter` -- **Decline Tutorial Hint**: `B` -- **Host Options / Player List**: `Tab` - -## Build & Run - -1. Install Visual Studio 2022 -2. Clone the repository -3. Open the project by double-clicking `MinecraftConsoles.sln` -4. Make sure `Minecraft.Client` is set as the Startup Project -5. Set the build configuration to **Debug** (Release is also OK but has some bugs) and the target platform to **Windows64**, then build and run - -### CMake (Windows x64) - -```powershell -cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -cmake --build build --config Debug --target MinecraftClient -``` - -## Runtime Files - -Some features require files placed next to the built executable (`x64\Debug\` or `x64\Release\`): - -| File | Purpose | -|------|---------| -| `username.txt` | Plain text file - first line becomes your in-game name and nametag. Created automatically with default value `Windows` on first run if absent. | -| `settings.dat` | Binary save of all game settings. Written automatically whenever you change a setting; loaded on startup. Delete it to reset all settings to defaults. | - -## Known Issues - -- Builds for other platforms have not been tested and are most likely non-functional -- There are some render bugs in the Release mode build -- Changing the resource pack on an existing world while loading it may crash (`reloadAll` called during world load), use the default resource pack or select it when creating a new world From b1b4435c0101341b1aa196c7b13c939c02a9eaec Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Wed, 4 Mar 2026 14:40:52 +0800 Subject: [PATCH 11/32] fix: fix inverted pitch in the second third person view --- Minecraft.Client/GameRenderer.cpp | 41 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Minecraft.Client/GameRenderer.cpp b/Minecraft.Client/GameRenderer.cpp index 08c20cfb4..d3c5e8d39 100644 --- a/Minecraft.Client/GameRenderer.cpp +++ b/Minecraft.Client/GameRenderer.cpp @@ -501,23 +501,26 @@ void GameRenderer::moveCameraToPlayer(float a) else { // 4J - corrected bug where this used to just take player->xRot & yRot directly and so wasn't taking into account interpolation, allowing camera to go through walls - float playerYRot = player->yRotO + (player->yRot - player->yRotO) * a; - float playerXRot = player->xRotO + (player->xRot - player->xRotO) * a; - float yRot = playerYRot; - float xRot = playerXRot; + float yRot = player->yRotO + (player->yRot - player->yRotO) * a; + float xRot = player->xRotO + (player->xRot - player->xRotO) * a; // Thirdperson view values are now 0 for disabled, 1 for original mode, 2 for reversed. if( localplayer->ThirdPersonView() == 2 ) { - // Reverse x rotation - note that this is only used in doing collision to calculate our view + // Reverse y rotation - note that this is only used in doing collision to calculate our view // distance, the actual rotation itself is just below this else {} block - xRot += 180.0f; + yRot += 180.0f; } double xd = -Mth::sin(yRot / 180 * PI) * Mth::cos(xRot / 180 * PI) * cameraDist; double zd = Mth::cos(yRot / 180 * PI) * Mth::cos(xRot / 180 * PI) * cameraDist; double yd = -Mth::sin(xRot / 180 * PI) * cameraDist; + if (localplayer->ThirdPersonView() == 2) + { + yd = Mth::sin(xRot / 180 * PI) * cameraDist; + } + for (int i = 0; i < 8; i++) { float xo = (float)((i & 1) * 2 - 1); @@ -538,16 +541,7 @@ void GameRenderer::moveCameraToPlayer(float a) } } - if ( localplayer->ThirdPersonView() == 2) - { - glRotatef(180, 0, 1, 0); - } - - glRotatef(playerXRot - xRot, 1, 0, 0); - glRotatef(playerYRot - yRot, 0, 1, 0); glTranslatef(0, 0, (float) -cameraDist); - glRotatef(yRot - playerYRot, 0, 1, 0); - glRotatef(xRot - playerXRot, 1, 0, 0); } } else @@ -557,8 +551,21 @@ void GameRenderer::moveCameraToPlayer(float a) if (!mc->options->fixedCamera) { - glRotatef(player->xRotO + (player->xRot - player->xRotO) * a, 1, 0, 0); - glRotatef(player->yRotO + (player->yRot - player->yRotO) * a + 180, 0, 1, 0); + float pitch = player->xRotO + (player->xRot - player->xRotO) * a; + if (localplayer->ThirdPersonView() == 2) + { + pitch = -pitch; + } + + glRotatef(pitch, 1, 0, 0); + if (localplayer->ThirdPersonView() == 2) + { + glRotatef(player->yRotO + (player->yRot - player->yRotO) * a, 0, 1, 0); + } + else + { + glRotatef(player->yRotO + (player->yRot - player->yRotO) * a + 180, 0, 1, 0); + } } glTranslatef(0, heightOffset, 0); From 575cc4ce6ea59ef2acee6f81181bc80f5ed4b7d9 Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Tue, 3 Mar 2026 22:12:59 +0800 Subject: [PATCH 12/32] fix: fix horse texture rendering --- Minecraft.Client/Textures.cpp | 101 +++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Textures.cpp b/Minecraft.Client/Textures.cpp index ab9db75d9..b4817ee32 100644 --- a/Minecraft.Client/Textures.cpp +++ b/Minecraft.Client/Textures.cpp @@ -424,12 +424,109 @@ void Textures::bindTextureLayers(ResourceLocation *resource) { assert(resource->isPreloaded()); + // Hack: 4JLibs on Windows does not currently reproduce Minecraft's layered horse texture path reliably. + // Merge the layers on the CPU and bind the cached result as a normal single texture instead. + wstring cacheKey = L"%layered%"; int layers = resource->getTextureCount(); - for( int i = 0; i < layers; i++ ) { - RenderManager.TextureBind(loadTexture(resource->getTexture(i))); + cacheKey += std::to_wstring(resource->getTexture(i)); + cacheKey += L"/"; } + + int id = -1; + bool inMap = ( idMap.find(cacheKey) != idMap.end() ); + if( inMap ) + { + id = idMap[cacheKey]; + } + else + { + // Cache by layer signature so the merge cost is only paid once per horse texture combination. + intArray mergedPixels; + int mergedWidth = 0; + int mergedHeight = 0; + bool hasMergedPixels = false; + + for( int i = 0; i < layers; i++ ) + { + TEXTURE_NAME textureName = resource->getTexture(i); + if( textureName == (_TEXTURE_NAME)-1 ) + { + continue; + } + + wstring resourceName = wstring(preLoaded[textureName]) + L".png"; + BufferedImage *image = readImage(textureName, resourceName); + if( image == NULL ) + { + continue; + } + + int width = image->getWidth(); + int height = image->getHeight(); + intArray layerPixels = loadTexturePixels(image); + delete image; + + if( !hasMergedPixels ) + { + mergedWidth = width; + mergedHeight = height; + mergedPixels = intArray(width * height); + memcpy(mergedPixels.data, layerPixels.data, width * height * sizeof(int)); + hasMergedPixels = true; + } + else if( width == mergedWidth && height == mergedHeight ) + { + for( int p = 0; p < width * height; p++ ) + { + int dst = mergedPixels[p]; + int src = layerPixels[p]; + + float srcAlpha = ((src >> 24) & 0xff) / 255.0f; + if( srcAlpha <= 0.0f ) + { + continue; + } + + float dstAlpha = ((dst >> 24) & 0xff) / 255.0f; + float outAlpha = srcAlpha + dstAlpha * (1.0f - srcAlpha); + if( outAlpha <= 0.0f ) + { + mergedPixels[p] = 0; + continue; + } + + float srcFactor = srcAlpha / outAlpha; + float dstFactor = (dstAlpha * (1.0f - srcAlpha)) / outAlpha; + + int outA = (int)(outAlpha * 255.0f + 0.5f); + int outR = (int)((((src >> 16) & 0xff) * srcFactor) + (((dst >> 16) & 0xff) * dstFactor) + 0.5f); + int outG = (int)((((src >> 8) & 0xff) * srcFactor) + (((dst >> 8) & 0xff) * dstFactor) + 0.5f); + int outB = (int)(((src & 0xff) * srcFactor) + ((dst & 0xff) * dstFactor) + 0.5f); + mergedPixels[p] = (outA << 24) | (outR << 16) | (outG << 8) | outB; + } + } + + delete[] layerPixels.data; + } + + if( hasMergedPixels ) + { + BufferedImage *mergedImage = new BufferedImage(mergedWidth, mergedHeight, BufferedImage::TYPE_INT_ARGB); + memcpy(mergedImage->getData(), mergedPixels.data, mergedWidth * mergedHeight * sizeof(int)); + delete[] mergedPixels.data; + id = getTexture(mergedImage, C4JRender::TEXTURE_FORMAT_RxGyBzAw, false); + } + else + { + id = 0; + } + + idMap[cacheKey] = id; + } + + RenderManager.TextureBind(id); } void Textures::bind(int id) From 8ecfc525471720012f36a0016d88a4f0f4cfaa1d Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Wed, 4 Mar 2026 15:33:52 +0800 Subject: [PATCH 13/32] feat: add support for username, IP, and port configuration via launch arguments --- .../Windows64/Windows64_Minecraft.cpp | 59 +++++++++++-------- README.md | 14 ++++- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 9d9537c58..3d5eec00f 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -766,33 +766,44 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, //g_iScreenHeight = 544; } - // Default username will be "Windows" - strncpy_s(g_Win64Username, sizeof(g_Win64Username), "Windows", _TRUNCATE); + char cmdLineA[1024]; + strncpy_s(cmdLineA, sizeof(cmdLineA), lpCmdLine, _TRUNCATE); - char exePath[MAX_PATH] = {}; - GetModuleFileNameA(NULL, exePath, MAX_PATH); - char* lastSlash = strrchr(exePath, '\\'); - if (lastSlash) *(lastSlash + 1) = '\0'; - - char filePath[MAX_PATH] = {}; - _snprintf_s(filePath, sizeof(filePath), _TRUNCATE, "%susername.txt", exePath); - - FILE* f = nullptr; - if (fopen_s(&f, filePath, "r") == 0 && f) + char *nameArg = strstr(cmdLineA, "-name "); + if (nameArg) { - char buf[128] = {}; - if (fgets(buf, sizeof(buf), f)) - { - int len = (int)strlen(buf); - while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r' || buf[len - 1] == ' ')) - buf[--len] = '\0'; + 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); + } - if (len > 0) - { - strncpy_s(g_Win64Username, sizeof(g_Win64Username), buf, _TRUNCATE); - } - } - fclose(f); + char *ipArg = strstr(cmdLineA, "-ip "); + if (ipArg) + { + ipArg += 4; + while (*ipArg == ' ') ipArg++; + char ipBuf[256]; + int n = 0; + while (ipArg[n] && ipArg[n] != ' ' && n < 255) { ipBuf[n] = ipArg[n]; n++; } + ipBuf[n] = 0; + strncpy_s(g_Win64MultiplayerIP, 256, ipBuf, _TRUNCATE); + g_Win64MultiplayerJoin = true; + } + + char *portArg = strstr(cmdLineA, "-port "); + if (portArg) + { + portArg += 6; + while (*portArg == ' ') portArg++; + char portBuf[16]; + int n = 0; + while (portArg[n] && portArg[n] != ' ' && n < 15) { portBuf[n] = portArg[n]; n++; } + portBuf[n] = 0; + g_Win64MultiplayerPort = atoi(portBuf); } } diff --git a/README.md b/README.md index 6e9f18aed..e6583e768 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,22 @@ Basic LAN multiplayer is available on the Windows build - Other players on the same LAN can discover the session from the in-game Join Game menu - Game connections use TCP port `25565` by default - LAN discovery uses UDP port `25566` -- You can override your in-game username at launch with `username.txt` This feature is based on [LCEMP](https://github.com/LCEMP/LCEMP/) +### Launch Arguments + +| Argument | Description | +|--------------------|----------------------------------------------------------------------------------------------------------------| +| `-name ` | Sets your in-game username | +| `-ip

` | Manually connect to an IP if LAN advertising does not work or if the server cannot be discovered automatically | +| `-port ` | Override the default port if it was changed in the source | + +Example: +``` +Minecraft.Client.exe -name Steve -ip 192.168.0.25 -port 25565 +``` + ## Controls (Keyboard & Mouse) - **Movement**: `W` `A` `S` `D` From d112090fde200c545a70ec5dc33fe91cca0f26ec Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Wed, 4 Mar 2026 16:18:47 +0800 Subject: [PATCH 14/32] feat: headless server --- CMakeLists.txt | 1 + .../Network/PlatformNetworkManagerStub.cpp | 19 +- Minecraft.Client/Minecraft.Client.vcxproj | 5 +- Minecraft.Client/MinecraftServer.cpp | 470 ++++++++++++- Minecraft.Client/MinecraftServer.h | 1 + Minecraft.Client/Settings.cpp | 80 ++- Minecraft.Client/Settings.h | 6 +- .../Windows64/Network/WinsockNetLayer.cpp | 21 +- .../Windows64/Network/WinsockNetLayer.h | 5 +- .../Windows64/Windows64_Minecraft.cpp | 639 +++++++++++------- Minecraft.World/Minecraft.World.vcxproj | 5 +- README.md | 12 +- 12 files changed, 973 insertions(+), 291 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 74851754d..2d83c5c8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ function(configure_msvc_target target) $<$>,$>:/W3> $<$,$>:/W0> $<$:/MP> + $<$:/FS> $<$:/EHsc> $<$,$>:/GL /O2 /Oi /GT /GF> ) diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index 10d1a6a53..c2466fe39 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -362,12 +362,23 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame, #ifdef _WINDOWS64 int port = WIN64_NET_DEFAULT_PORT; + const char* bindIp = NULL; + if (g_Win64DedicatedServer) + { + if (g_Win64DedicatedServerPort > 0) + port = g_Win64DedicatedServerPort; + if (g_Win64DedicatedServerBindIP[0] != 0) + bindIp = g_Win64DedicatedServerBindIP; + } if (!WinsockNetLayer::IsActive()) - WinsockNetLayer::HostGame(port); + WinsockNetLayer::HostGame(port, bindIp); - 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); + if (WinsockNetLayer::IsActive()) + { + 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); + } #endif //#endif } diff --git a/Minecraft.Client/Minecraft.Client.vcxproj b/Minecraft.Client/Minecraft.Client.vcxproj index 549156369..d7c49acfc 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj +++ b/Minecraft.Client/Minecraft.Client.vcxproj @@ -1545,6 +1545,7 @@ if not exist "$(TargetDir)\savedata" mkdir "$(TargetDir)\savedata" true Default false + /FS %(AdditionalOptions) true @@ -1773,7 +1774,7 @@ xcopy /q /y /i /s /e $(ProjectDir)DurangoMedia\CU $(LayoutDir)Image\Loose\CUtrue true true - /Ob3 + /FS /Ob3 %(AdditionalOptions) true @@ -48677,4 +48678,4 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU - \ No newline at end of file + diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 4206a3995..974c74051 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -27,6 +27,11 @@ #include "..\Minecraft.World\Pos.h" #include "..\Minecraft.World\System.h" #include "..\Minecraft.World\StringHelpers.h" +#include "..\Minecraft.World\net.minecraft.world.entity.item.h" +#include "..\Minecraft.World\net.minecraft.world.item.h" +#include "..\Minecraft.World\net.minecraft.world.item.enchantment.h" +#include "..\Minecraft.World\net.minecraft.world.damagesource.h" +#include #ifdef SPLIT_SAVES #include "..\Minecraft.World\ConsoleSaveFileSplit.h" #endif @@ -78,6 +83,438 @@ bool MinecraftServer::s_slowQueuePacketSent = false; unordered_map MinecraftServer::ironTimers; +static void PrintConsoleLine(const wchar_t *prefix, const wstring &message) +{ + wprintf(L"%ls%ls\n", prefix, message.c_str()); + fflush(stdout); +} + +static bool TryParseIntValue(const wstring &text, int &value) +{ + std::wistringstream stream(text); + stream >> value; + return !stream.fail() && stream.eof(); +} + +static vector SplitConsoleCommand(const wstring &command) +{ + vector tokens; + std::wistringstream stream(command); + wstring token; + while (stream >> token) + { + tokens.push_back(token); + } + return tokens; +} + +static wstring JoinConsoleCommandTokens(const vector &tokens, size_t startIndex) +{ + wstring joined; + for (size_t i = startIndex; i < tokens.size(); ++i) + { + if (!joined.empty()) joined += L" "; + joined += tokens[i]; + } + return joined; +} + +static shared_ptr FindPlayerByName(PlayerList *playerList, const wstring &name) +{ + if (playerList == NULL) return nullptr; + + for (size_t i = 0; i < playerList->players.size(); ++i) + { + shared_ptr player = playerList->players[i]; + if (player != NULL && equalsIgnoreCase(player->getName(), name)) + { + return player; + } + } + + return nullptr; +} + +static void SetAllLevelTimes(MinecraftServer *server, int value) +{ + for (unsigned int i = 0; i < server->levels.length; ++i) + { + if (server->levels[i] != NULL) + { + server->levels[i]->setDayTime(value); + } + } +} + +static bool ExecuteConsoleCommand(MinecraftServer *server, const wstring &rawCommand) +{ + if (server == NULL) + return false; + + wstring command = trimString(rawCommand); + if (command.empty()) + return true; + + if (command[0] == L'/') + { + command = trimString(command.substr(1)); + } + + vector tokens = SplitConsoleCommand(command); + if (tokens.empty()) + return true; + + const wstring action = toLower(tokens[0]); + PlayerList *playerList = server->getPlayers(); + + if (action == L"help" || action == L"?") + { + server->info(L"Commands: help, stop, list, say , save-all, time , weather [seconds], tp , give [amount] [aux], enchant [level], kill "); + return true; + } + + if (action == L"stop") + { + server->info(L"Stopping server..."); + MinecraftServer::HaltServer(); + return true; + } + + if (action == L"list") + { + wstring playerNames = (playerList != NULL) ? playerList->getPlayerNames() : L""; + if (playerNames.empty()) playerNames = L"(none)"; + server->info(L"Players (" + _toString((playerList != NULL) ? playerList->getPlayerCount() : 0) + L"): " + playerNames); + return true; + } + + if (action == L"say") + { + if (tokens.size() < 2) + { + server->warn(L"Usage: say "); + return false; + } + + wstring message = L"[Server] " + JoinConsoleCommandTokens(tokens, 1); + if (playerList != NULL) + { + playerList->broadcastAll(shared_ptr(new ChatPacket(message))); + } + server->info(message); + return true; + } + + if (action == L"save-all") + { + if (playerList != NULL) + { + playerList->saveAll(NULL, false); + } + server->info(L"World saved."); + return true; + } + + if (action == L"time") + { + if (tokens.size() < 2) + { + server->warn(L"Usage: time set | time add "); + return false; + } + + if (toLower(tokens[1]) == L"add") + { + if (tokens.size() < 3) + { + server->warn(L"Usage: time add "); + return false; + } + + int delta = 0; + if (!TryParseIntValue(tokens[2], delta)) + { + server->warn(L"Invalid tick value: " + tokens[2]); + return false; + } + + for (unsigned int i = 0; i < server->levels.length; ++i) + { + if (server->levels[i] != NULL) + { + server->levels[i]->setDayTime(server->levels[i]->getDayTime() + delta); + } + } + + server->info(L"Added " + _toString(delta) + L" ticks."); + return true; + } + + wstring timeValue = toLower(tokens[1]); + if (timeValue == L"set") + { + if (tokens.size() < 3) + { + server->warn(L"Usage: time set "); + return false; + } + timeValue = toLower(tokens[2]); + } + + int targetTime = 0; + if (timeValue == L"day") + { + targetTime = 0; + } + else if (timeValue == L"night") + { + targetTime = 12500; + } + else if (!TryParseIntValue(timeValue, targetTime)) + { + server->warn(L"Invalid time value: " + timeValue); + return false; + } + + SetAllLevelTimes(server, targetTime); + server->info(L"Time set to " + _toString(targetTime) + L"."); + return true; + } + + if (action == L"weather") + { + if (tokens.size() < 2) + { + server->warn(L"Usage: weather [seconds]"); + return false; + } + + int durationSeconds = 600; + if (tokens.size() >= 3 && !TryParseIntValue(tokens[2], durationSeconds)) + { + server->warn(L"Invalid duration: " + tokens[2]); + return false; + } + + if (server->levels[0] == NULL) + { + server->warn(L"The overworld is not loaded."); + return false; + } + + LevelData *levelData = server->levels[0]->getLevelData(); + int duration = durationSeconds * SharedConstants::TICKS_PER_SECOND; + levelData->setRainTime(duration); + levelData->setThunderTime(duration); + + wstring weather = toLower(tokens[1]); + if (weather == L"clear") + { + levelData->setRaining(false); + levelData->setThundering(false); + } + else if (weather == L"rain") + { + levelData->setRaining(true); + levelData->setThundering(false); + } + else if (weather == L"thunder") + { + levelData->setRaining(true); + levelData->setThundering(true); + } + else + { + server->warn(L"Usage: weather [seconds]"); + return false; + } + + server->info(L"Weather set to " + weather + L"."); + return true; + } + + if (action == L"tp" || action == L"teleport") + { + if (tokens.size() < 3) + { + server->warn(L"Usage: tp "); + return false; + } + + shared_ptr subject = FindPlayerByName(playerList, tokens[1]); + shared_ptr destination = FindPlayerByName(playerList, tokens[2]); + if (subject == NULL) + { + server->warn(L"Unknown player: " + tokens[1]); + return false; + } + if (destination == NULL) + { + server->warn(L"Unknown player: " + tokens[2]); + return false; + } + if (subject->level->dimension->id != destination->level->dimension->id || !subject->isAlive()) + { + server->warn(L"Teleport failed because the players are not in the same dimension or the source player is dead."); + return false; + } + + subject->ride(nullptr); + subject->connection->teleport(destination->x, destination->y, destination->z, destination->yRot, destination->xRot); + server->info(L"Teleported " + subject->getName() + L" to " + destination->getName() + L"."); + return true; + } + + if (action == L"give") + { + if (tokens.size() < 3) + { + server->warn(L"Usage: give [amount] [aux]"); + return false; + } + + shared_ptr player = FindPlayerByName(playerList, tokens[1]); + if (player == NULL) + { + server->warn(L"Unknown player: " + tokens[1]); + return false; + } + + int itemId = 0; + int amount = 1; + int aux = 0; + if (!TryParseIntValue(tokens[2], itemId)) + { + server->warn(L"Invalid item id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !TryParseIntValue(tokens[3], amount)) + { + server->warn(L"Invalid amount: " + tokens[3]); + return false; + } + if (tokens.size() >= 5 && !TryParseIntValue(tokens[4], aux)) + { + server->warn(L"Invalid aux value: " + tokens[4]); + return false; + } + if (itemId <= 0 || Item::items[itemId] == NULL) + { + server->warn(L"Unknown item id: " + _toString(itemId)); + return false; + } + if (amount <= 0) + { + server->warn(L"Amount must be positive."); + return false; + } + + shared_ptr itemInstance(new ItemInstance(itemId, amount, aux)); + shared_ptr drop = player->drop(itemInstance); + if (drop != NULL) + { + drop->throwTime = 0; + } + server->info(L"Gave item " + _toString(itemId) + L" x" + _toString(amount) + L" to " + player->getName() + L"."); + return true; + } + + if (action == L"enchant") + { + if (tokens.size() < 3) + { + server->warn(L"Usage: enchant [level]"); + return false; + } + + shared_ptr player = FindPlayerByName(playerList, tokens[1]); + if (player == NULL) + { + server->warn(L"Unknown player: " + tokens[1]); + return false; + } + + int enchantmentId = 0; + int enchantmentLevel = 1; + if (!TryParseIntValue(tokens[2], enchantmentId)) + { + server->warn(L"Invalid enchantment id: " + tokens[2]); + return false; + } + if (tokens.size() >= 4 && !TryParseIntValue(tokens[3], enchantmentLevel)) + { + server->warn(L"Invalid enchantment level: " + tokens[3]); + return false; + } + + shared_ptr selectedItem = player->getSelectedItem(); + if (selectedItem == NULL) + { + server->warn(L"The player is not holding an item."); + return false; + } + + Enchantment *enchantment = Enchantment::enchantments[enchantmentId]; + if (enchantment == NULL) + { + server->warn(L"Unknown enchantment id: " + _toString(enchantmentId)); + return false; + } + if (!enchantment->canEnchant(selectedItem)) + { + server->warn(L"That enchantment cannot be applied to the selected item."); + return false; + } + + if (enchantmentLevel < enchantment->getMinLevel()) enchantmentLevel = enchantment->getMinLevel(); + if (enchantmentLevel > enchantment->getMaxLevel()) enchantmentLevel = enchantment->getMaxLevel(); + + if (selectedItem->hasTag()) + { + ListTag *enchantmentTags = selectedItem->getEnchantmentTags(); + if (enchantmentTags != NULL) + { + for (int i = 0; i < enchantmentTags->size(); i++) + { + int type = enchantmentTags->get(i)->getShort((wchar_t *)ItemInstance::TAG_ENCH_ID); + if (Enchantment::enchantments[type] != NULL && !Enchantment::enchantments[type]->isCompatibleWith(enchantment)) + { + server->warn(L"That enchantment conflicts with an existing enchantment on the selected item."); + return false; + } + } + } + } + + selectedItem->enchant(enchantment, enchantmentLevel); + server->info(L"Enchanted " + player->getName() + L"'s held item with " + _toString(enchantmentId) + L" " + _toString(enchantmentLevel) + L"."); + return true; + } + + if (action == L"kill") + { + if (tokens.size() < 2) + { + server->warn(L"Usage: kill "); + return false; + } + + shared_ptr player = FindPlayerByName(playerList, tokens[1]); + if (player == NULL) + { + server->warn(L"Unknown player: " + tokens[1]); + return false; + } + + player->hurt(DamageSource::outOfWorld, 3.4e38f); + server->info(L"Killed " + player->getName() + L"."); + return true; + } + + server->warn(L"Unknown command: " + command); + return false; +} + MinecraftServer::MinecraftServer() { // 4J - added initialisers @@ -107,12 +544,14 @@ MinecraftServer::MinecraftServer() forceGameType = false; commandDispatcher = new ServerCommandDispatcher(); + InitializeCriticalSection(&m_consoleInputCS); DispenserBootstrap::bootStrap(); } MinecraftServer::~MinecraftServer() { + DeleteCriticalSection(&m_consoleInputCS); } bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DWORD initSettings, bool findSeed) @@ -150,6 +589,15 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW #endif settings = new Settings(new File(L"server.properties")); + app.SetGameHostOption(eGameHostOption_Difficulty, settings->getInt(L"difficulty", app.GetGameHostOption(eGameHostOption_Difficulty))); + app.SetGameHostOption(eGameHostOption_GameType, settings->getInt(L"gamemode", app.GetGameHostOption(eGameHostOption_GameType))); + app.SetGameHostOption(eGameHostOption_Structures, settings->getBoolean(L"generate-structures", app.GetGameHostOption(eGameHostOption_Structures) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_BonusChest, settings->getBoolean(L"bonus-chest", app.GetGameHostOption(eGameHostOption_BonusChest) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_PvP, settings->getBoolean(L"pvp", app.GetGameHostOption(eGameHostOption_PvP) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_TrustPlayers, settings->getBoolean(L"trust-players", app.GetGameHostOption(eGameHostOption_TrustPlayers) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_FireSpreads, settings->getBoolean(L"fire-spreads", app.GetGameHostOption(eGameHostOption_FireSpreads) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_TNT, settings->getBoolean(L"tnt", app.GetGameHostOption(eGameHostOption_TNT) > 0) ? 1 : 0); + app.DebugPrintf("\n*** SERVER SETTINGS ***\n"); app.DebugPrintf("ServerSettings: host-friends-only is %s\n",(app.GetGameHostOption(eGameHostOption_FriendsOfFriends)>0)?"on":"off"); app.DebugPrintf("ServerSettings: game-type is %s\n",(app.GetGameHostOption(eGameHostOption_GameType)==0)?"Survival Mode":"Creative Mode"); @@ -169,11 +617,11 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW setAnimals(settings->getBoolean(L"spawn-animals", true)); setNpcsEnabled(settings->getBoolean(L"spawn-npcs", true)); - setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false); // settings->getBoolean(L"pvp", true); + setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false); // 4J Stu - We should never have hacked clients flying when they shouldn't be like the PC version, so enable flying always // Fix for #46612 - TU5: Code: Multiplayer: A client can be banned for flying when accidentaly being blown by dynamite - setFlightAllowed(true); //settings->getBoolean(L"allow-flight", false); + setFlightAllowed(settings->getBoolean(L"allow-flight", true)); // 4J Stu - Enabling flight to stop it kicking us when we use it #ifdef _DEBUG_MENUS_ENABLED @@ -1707,17 +2155,23 @@ void MinecraftServer::tick() void MinecraftServer::handleConsoleInput(const wstring& msg, ConsoleInputSource *source) { + EnterCriticalSection(&m_consoleInputCS); consoleInput.push_back(new ConsoleInput(msg, source)); + LeaveCriticalSection(&m_consoleInputCS); } void MinecraftServer::handleConsoleInputs() { - while (consoleInput.size() > 0) + vector pendingInputs; + EnterCriticalSection(&m_consoleInputCS); + pendingInputs.swap(consoleInput); + LeaveCriticalSection(&m_consoleInputCS); + + for (size_t i = 0; i < pendingInputs.size(); ++i) { - AUTO_VAR(it, consoleInput.begin()); - ConsoleInput *input = *it; - consoleInput.erase(it); - // commands->handleCommand(input); // 4J - removed - TODO - do we want equivalent of console commands? + ConsoleInput *input = pendingInputs[i]; + ExecuteConsoleCommand(this, input->msg); + delete input; } } @@ -1750,10 +2204,12 @@ File *MinecraftServer::getFile(const wstring& name) void MinecraftServer::info(const wstring& string) { + PrintConsoleLine(L"[INFO] ", string); } void MinecraftServer::warn(const wstring& string) { + PrintConsoleLine(L"[WARN] ", string); } wstring MinecraftServer::getConsoleName() diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index 5f33fa853..ec0218e01 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -103,6 +103,7 @@ private: // vector tickables = new ArrayList(); // 4J - removed CommandDispatcher *commandDispatcher; vector consoleInput; // 4J - was synchronizedList - TODO - investigate + CRITICAL_SECTION m_consoleInputCS; public: bool onlineMode; bool animals; diff --git a/Minecraft.Client/Settings.cpp b/Minecraft.Client/Settings.cpp index 89946773b..4223b0016 100644 --- a/Minecraft.Client/Settings.cpp +++ b/Minecraft.Client/Settings.cpp @@ -1,18 +1,90 @@ #include "stdafx.h" #include "Settings.h" +#include "..\Minecraft.World\File.h" #include "..\Minecraft.World\StringHelpers.h" +#include + +static wstring ParsePropertyText(const string &text) +{ + return trimString(convStringToWstring(text)); +} + +static bool TryParseBoolean(const wstring &text, bool defaultValue) +{ + wstring lowered = toLower(trimString(text)); + if (lowered == L"true" || lowered == L"1" || lowered == L"yes" || lowered == L"on") + return true; + if (lowered == L"false" || lowered == L"0" || lowered == L"no" || lowered == L"off") + return false; + return defaultValue; +} -// 4J - TODO - serialise/deserialise from file Settings::Settings(File *file) { + if (file != NULL) + { + filePath = file->getPath(); + } + + if (filePath.empty()) + return; + + std::ifstream stream(wstringtofilename(filePath), std::ios::in | std::ios::binary); + if (!stream.is_open()) + return; + + string line; + while (std::getline(stream, line)) + { + if (!line.empty() && line[line.size() - 1] == '\r') + line.erase(line.size() - 1); + + if (line.size() >= 3 && + (unsigned char)line[0] == 0xEF && + (unsigned char)line[1] == 0xBB && + (unsigned char)line[2] == 0xBF) + { + line.erase(0, 3); + } + + size_t commentPos = line.find_first_of("#;"); + if (commentPos != string::npos && line.find_first_not_of(" \t") == commentPos) + continue; + + size_t separatorPos = line.find('='); + if (separatorPos == string::npos) + continue; + + wstring key = ParsePropertyText(line.substr(0, separatorPos)); + if (key.empty()) + continue; + + wstring value = ParsePropertyText(line.substr(separatorPos + 1)); + properties[key] = value; + } } void Settings::generateNewProperties() { + saveProperties(); } void Settings::saveProperties() { + if (filePath.empty()) + return; + + std::ofstream stream(wstringtofilename(filePath), std::ios::out | std::ios::binary | std::ios::trunc); + if (!stream.is_open()) + return; + + stream << "# MinecraftConsoles dedicated server properties\r\n"; + for (unordered_map::const_iterator it = properties.begin(); it != properties.end(); ++it) + { + string key = string(wstringtochararray(it->first)); + string value = string(wstringtochararray(it->second)); + stream << key << "=" << value << "\r\n"; + } } wstring Settings::getString(const wstring& key, const wstring& defaultValue) @@ -39,17 +111,17 @@ bool Settings::getBoolean(const wstring& key, bool defaultValue) { if(properties.find(key) == properties.end()) { - properties[key] = _toString(defaultValue); + properties[key] = defaultValue ? L"true" : L"false"; saveProperties(); } MemSect(35); - bool retval = _fromString(properties[key]); + bool retval = TryParseBoolean(properties[key], defaultValue); MemSect(0); return retval; } void Settings::setBooleanAndSave(const wstring& key, bool value) { - properties[key] = _toString(value); + properties[key] = value ? L"true" : L"false"; saveProperties(); } \ No newline at end of file diff --git a/Minecraft.Client/Settings.h b/Minecraft.Client/Settings.h index b6a2c0181..4a3c130be 100644 --- a/Minecraft.Client/Settings.h +++ b/Minecraft.Client/Settings.h @@ -7,8 +7,8 @@ class Settings // public static Logger logger = Logger.getLogger("Minecraft"); // private Properties properties = new Properties(); private: - unordered_map properties; // 4J - TODO was Properties type, will need to implement something we can serialise/deserialise too - //File *file; + unordered_map properties; + wstring filePath; public: Settings(File *file); @@ -18,4 +18,4 @@ public: int getInt(const wstring& key, int defaultValue); bool getBoolean(const wstring& key, bool defaultValue); void setBooleanAndSave(const wstring& key, bool value); -}; +}; \ No newline at end of file diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index d3ea1c3aa..ca1d62af7 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -51,6 +51,9 @@ bool g_Win64MultiplayerHost = false; bool g_Win64MultiplayerJoin = false; int g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; char g_Win64MultiplayerIP[256] = "127.0.0.1"; +bool g_Win64DedicatedServer = false; +int g_Win64DedicatedServerPort = WIN64_NET_DEFAULT_PORT; +char g_Win64DedicatedServerBindIP[256] = ""; bool WinsockNetLayer::Initialize() { @@ -139,7 +142,7 @@ void WinsockNetLayer::Shutdown() } } -bool WinsockNetLayer::HostGame(int port) +bool WinsockNetLayer::HostGame(int port, const char* bindIp) { if (!s_initialized && !Initialize()) return false; @@ -159,15 +162,19 @@ bool WinsockNetLayer::HostGame(int port) hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; - hints.ai_flags = AI_PASSIVE; + hints.ai_flags = (bindIp == NULL || bindIp[0] == 0) ? AI_PASSIVE : 0; char portStr[16]; sprintf_s(portStr, "%d", port); - int iResult = getaddrinfo(NULL, portStr, &hints, &result); + const char* resolvedBindIp = (bindIp != NULL && bindIp[0] != 0) ? bindIp : NULL; + int iResult = getaddrinfo(resolvedBindIp, portStr, &hints, &result); if (iResult != 0) { - app.DebugPrintf("getaddrinfo failed: %d\n", iResult); + app.DebugPrintf("getaddrinfo failed for %s:%d - %d\n", + resolvedBindIp != NULL ? resolvedBindIp : "*", + port, + iResult); return false; } @@ -206,7 +213,9 @@ 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\n", + resolvedBindIp != NULL ? resolvedBindIp : "*", + port); return true; } @@ -908,4 +917,4 @@ DWORD WINAPI WinsockNetLayer::DiscoveryThreadProc(LPVOID param) return 0; } -#endif \ No newline at end of file +#endif diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index 029dd0a73..fd1280f7d 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -65,7 +65,7 @@ public: static bool Initialize(); static void Shutdown(); - static bool HostGame(int port); + static bool HostGame(int port, const char* bindIp = NULL); static bool JoinGame(const char* ip, int port); static bool SendToSmallId(BYTE targetSmallId, const void* data, int dataSize); @@ -147,5 +147,8 @@ extern bool g_Win64MultiplayerHost; extern bool g_Win64MultiplayerJoin; extern int g_Win64MultiplayerPort; extern char g_Win64MultiplayerIP[256]; +extern bool g_Win64DedicatedServer; +extern int g_Win64DedicatedServerPort; +extern char g_Win64DedicatedServerBindIP[256]; #endif diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 3d5eec00f..ed6781a31 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -4,7 +4,9 @@ #include "stdafx.h" #include +#include #include +#include #include "GameConfig\Minecraft.spa.h" #include "..\MinecraftServer.h" #include "..\LocalPlayer.h" @@ -34,6 +36,7 @@ #include "Sentient\SentientManager.h" #include "..\..\Minecraft.World\IntCache.h" #include "..\Textures.h" +#include "..\Settings.h" #include "Resource.h" #include "..\..\Minecraft.World\compression.h" #include "..\..\Minecraft.World\OldChunkStorage.h" @@ -95,6 +98,148 @@ wchar_t g_Win64UsernameW[17] = { 0 }; static bool g_isFullscreen = false; static WINDOWPLACEMENT g_wpPrev = { sizeof(g_wpPrev) }; +struct Win64LaunchOptions +{ + int screenMode; + bool serverMode; +}; + +static void CopyWideArgToAnsi(LPCWSTR source, char* dest, size_t destSize) +{ + if (destSize == 0) + return; + + dest[0] = 0; + if (source == NULL) + return; + + WideCharToMultiByte(CP_ACP, 0, source, -1, dest, (int)destSize, NULL, NULL); + dest[destSize - 1] = 0; +} + +static void ApplyScreenMode(int screenMode) +{ + switch (screenMode) + { + case 1: + g_iScreenWidth = 1280; + g_iScreenHeight = 720; + break; + case 2: + g_iScreenWidth = 640; + g_iScreenHeight = 480; + break; + case 3: + g_iScreenWidth = 720; + g_iScreenHeight = 408; + break; + default: + break; + } +} + +static Win64LaunchOptions ParseLaunchOptions() +{ + Win64LaunchOptions options = {}; + options.screenMode = 0; + options.serverMode = false; + + g_Win64MultiplayerJoin = false; + g_Win64MultiplayerPort = WIN64_NET_DEFAULT_PORT; + g_Win64DedicatedServer = false; + g_Win64DedicatedServerPort = WIN64_NET_DEFAULT_PORT; + g_Win64DedicatedServerBindIP[0] = 0; + + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (argv == NULL) + return options; + + if (argc > 1 && lstrlenW(argv[1]) == 1) + { + if (argv[1][0] >= L'1' && argv[1][0] <= L'3') + options.screenMode = argv[1][0] - L'0'; + } + + for (int i = 1; i < argc; ++i) + { + if (_wcsicmp(argv[i], L"-server") == 0) + { + options.serverMode = true; + break; + } + } + + g_Win64DedicatedServer = options.serverMode; + + for (int i = 1; i < argc; ++i) + { + if (_wcsicmp(argv[i], L"-name") == 0 && (i + 1) < argc) + { + CopyWideArgToAnsi(argv[++i], g_Win64Username, sizeof(g_Win64Username)); + } + else if (_wcsicmp(argv[i], L"-ip") == 0 && (i + 1) < argc) + { + char ipBuf[256]; + CopyWideArgToAnsi(argv[++i], ipBuf, sizeof(ipBuf)); + if (options.serverMode) + { + strncpy_s(g_Win64DedicatedServerBindIP, sizeof(g_Win64DedicatedServerBindIP), ipBuf, _TRUNCATE); + } + else + { + strncpy_s(g_Win64MultiplayerIP, sizeof(g_Win64MultiplayerIP), ipBuf, _TRUNCATE); + g_Win64MultiplayerJoin = true; + } + } + else if (_wcsicmp(argv[i], L"-port") == 0 && (i + 1) < argc) + { + wchar_t* endPtr = NULL; + long port = wcstol(argv[++i], &endPtr, 10); + if (endPtr != argv[i] && *endPtr == 0 && port > 0 && port <= 65535) + { + if (options.serverMode) + g_Win64DedicatedServerPort = (int)port; + else + g_Win64MultiplayerPort = (int)port; + } + } + } + + LocalFree(argv); + return options; +} + +static BOOL WINAPI HeadlessServerCtrlHandler(DWORD ctrlType) +{ + switch (ctrlType) + { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_SHUTDOWN_EVENT: + app.m_bShutdown = true; + MinecraftServer::HaltServer(); + return TRUE; + default: + return FALSE; + } +} + +static void SetupHeadlessServerConsole() +{ + if (AllocConsole()) + { + FILE* stream = NULL; + freopen_s(&stream, "CONIN$", "r", stdin); + freopen_s(&stream, "CONOUT$", "w", stdout); + freopen_s(&stream, "CONOUT$", "w", stderr); + SetConsoleTitleA("Minecraft Server"); + } + + SetConsoleCtrlHandler(HeadlessServerCtrlHandler, TRUE); +} + void DefineActions(void) { // The app needs to define the actions required, and the possible mappings for these @@ -722,6 +867,225 @@ void CleanupDevice() if( g_pd3dDevice ) g_pd3dDevice->Release(); } +static Minecraft* InitialiseMinecraftRuntime() +{ + app.loadMediaArchive(); + + RenderManager.Initialise(g_pd3dDevice, g_pSwapChain); + + app.loadStringTable(); + ui.init(g_pd3dDevice, g_pImmediateContext, g_pRenderTargetView, g_pDepthStencilView, g_iScreenWidth, g_iScreenHeight); + + InputManager.Initialise(1, 3, MINECRAFT_ACTION_MAX, ACTION_MAX_MENU); + KMInput.Init(g_hWnd); + DefineActions(); + InputManager.SetJoypadMapVal(0, 0); + InputManager.SetKeyRepeatRate(0.3f, 0.2f); + + ProfileManager.Initialise(TITLEID_MINECRAFT, + app.m_dwOfferID, + PROFILE_VERSION_10, + NUM_PROFILE_VALUES, + NUM_PROFILE_SETTINGS, + dwProfileSettingsA, + app.GAME_DEFINED_PROFILE_DATA_BYTES * XUSER_MAX_COUNT, + &app.uiGameDefinedDataChangedBitmask + ); + ProfileManager.SetDefaultOptionsCallback(&CConsoleMinecraftApp::DefaultOptionsCallback, (LPVOID)&app); + + g_NetworkManager.Initialise(); + + for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) + { + IQNet::m_player[i].m_smallId = (BYTE)i; + IQNet::m_player[i].m_isRemote = false; + IQNet::m_player[i].m_isHostPlayer = (i == 0); + swprintf_s(IQNet::m_player[i].m_gamertag, 32, L"Player%d", i); + } + wcscpy_s(IQNet::m_player[0].m_gamertag, 32, g_Win64UsernameW); + + WinsockNetLayer::Initialize(); + + ProfileManager.SetDebugFullOverride(true); + + Tesselator::CreateNewThreadStorage(1024 * 1024); + AABB::CreateNewThreadStorage(); + Vec3::CreateNewThreadStorage(); + IntCache::CreateNewThreadStorage(); + Compression::CreateNewThreadStorage(); + OldChunkStorage::CreateNewThreadStorage(); + Level::enableLightingCache(); + Tile::CreateNewThreadStorage(); + + Minecraft::main(); + Minecraft* pMinecraft = Minecraft::GetInstance(); + if (pMinecraft == NULL) + return NULL; + + app.InitGameSettings(); + app.InitialiseTips(); + + pMinecraft->options->set(Options::Option::MUSIC, 1.0f); + pMinecraft->options->set(Options::Option::SOUND, 1.0f); + + return pMinecraft; +} + +static int HeadlessServerConsoleThreadProc(void* lpParameter) +{ + UNREFERENCED_PARAMETER(lpParameter); + + std::string line; + while (!app.m_bShutdown) + { + if (!std::getline(std::cin, line)) + { + if (std::cin.eof()) + { + break; + } + + std::cin.clear(); + Sleep(50); + continue; + } + + wstring command = trimString(convStringToWstring(line)); + if (command.empty()) + continue; + + MinecraftServer* server = MinecraftServer::getInstance(); + if (server != NULL) + { + server->handleConsoleInput(command, server); + } + } + + return 0; +} + +static int RunHeadlessServer() +{ + SetupHeadlessServerConsole(); + + Settings serverSettings(new File(L"server.properties")); + wstring configuredBindIp = serverSettings.getString(L"server-ip", L""); + + const char* bindIp = "*"; + if (g_Win64DedicatedServerBindIP[0] != 0) + { + bindIp = g_Win64DedicatedServerBindIP; + } + else if (!configuredBindIp.empty()) + { + bindIp = wstringtochararray(configuredBindIp); + } + + const int port = g_Win64DedicatedServerPort > 0 ? g_Win64DedicatedServerPort : serverSettings.getInt(L"server-port", WIN64_NET_DEFAULT_PORT); + + printf("Starting headless server on %s:%d\n", bindIp, port); + fflush(stdout); + + Minecraft* pMinecraft = InitialiseMinecraftRuntime(); + if (pMinecraft == NULL) + { + fprintf(stderr, "Failed to initialise the Minecraft runtime.\n"); + return 1; + } + + app.SetGameHostOption(eGameHostOption_Difficulty, serverSettings.getInt(L"difficulty", 1)); + app.SetGameHostOption(eGameHostOption_Gamertags, 1); + app.SetGameHostOption(eGameHostOption_GameType, serverSettings.getInt(L"gamemode", 0)); + app.SetGameHostOption(eGameHostOption_LevelType, 0); + app.SetGameHostOption(eGameHostOption_Structures, serverSettings.getBoolean(L"generate-structures", true) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_BonusChest, serverSettings.getBoolean(L"bonus-chest", false) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_PvP, serverSettings.getBoolean(L"pvp", true) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_TrustPlayers, serverSettings.getBoolean(L"trust-players", true) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_FireSpreads, serverSettings.getBoolean(L"fire-spreads", true) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_TNT, serverSettings.getBoolean(L"tnt", true) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_HostCanFly, 1); + app.SetGameHostOption(eGameHostOption_HostCanChangeHunger, 1); + app.SetGameHostOption(eGameHostOption_HostCanBeInvisible, 1); + app.SetGameHostOption(eGameHostOption_MobGriefing, 1); + app.SetGameHostOption(eGameHostOption_KeepInventory, 0); + app.SetGameHostOption(eGameHostOption_DoMobSpawning, 1); + app.SetGameHostOption(eGameHostOption_DoMobLoot, 1); + app.SetGameHostOption(eGameHostOption_DoTileDrops, 1); + app.SetGameHostOption(eGameHostOption_NaturalRegeneration, 1); + app.SetGameHostOption(eGameHostOption_DoDaylightCycle, 1); + + MinecraftServer::resetFlags(); + g_NetworkManager.HostGame(0, false, true, MINECRAFT_NET_MAX_PLAYERS, 0); + + if (!WinsockNetLayer::IsActive()) + { + fprintf(stderr, "Failed to bind the server socket on %s:%d.\n", bindIp, port); + return 1; + } + + g_NetworkManager.FakeLocalPlayerJoined(); + + NetworkGameInitData* param = new NetworkGameInitData(); + param->seed = 0; + param->settings = app.GetGameHostOption(eGameHostOption_All); + + g_NetworkManager.ServerStoppedCreate(true); + g_NetworkManager.ServerReadyCreate(true); + + C4JThread* thread = new C4JThread(&CGameNetworkManager::ServerThreadProc, param, "Server", 256 * 1024); + thread->SetProcessor(CPU_CORE_SERVER); + thread->Run(); + + g_NetworkManager.ServerReadyWait(); + g_NetworkManager.ServerReadyDestroy(); + + if (MinecraftServer::serverHalted()) + { + fprintf(stderr, "The server halted during startup.\n"); + g_NetworkManager.LeaveGame(false); + return 1; + } + + app.SetGameStarted(true); + g_NetworkManager.DoWork(); + + printf("Server ready on %s:%d\n", bindIp, port); + printf("Type 'help' for server commands.\n"); + fflush(stdout); + + C4JThread* consoleThread = new C4JThread(&HeadlessServerConsoleThreadProc, NULL, "Server console", 128 * 1024); + consoleThread->Run(); + + MSG msg = { 0 }; + while (WM_QUIT != msg.message && !app.m_bShutdown && !MinecraftServer::serverHalted()) + { + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + continue; + } + + app.UpdateTime(); + ProfileManager.Tick(); + StorageManager.Tick(); + RenderManager.Tick(); + ui.tick(); + g_NetworkManager.DoWork(); + app.HandleXuiActions(); + + Sleep(10); + } + + printf("Stopping server...\n"); + fflush(stdout); + + app.m_bShutdown = true; + MinecraftServer::HaltServer(); + g_NetworkManager.LeaveGame(false); + return 0; +} + int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, @@ -742,70 +1106,8 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, SetProcessDPIAware(); g_iScreenWidth = GetSystemMetrics(SM_CXSCREEN); g_iScreenHeight = GetSystemMetrics(SM_CYSCREEN); - - if(lpCmdLine) - { - if(lpCmdLine[0] == '1') - { - g_iScreenWidth = 1280; - g_iScreenHeight = 720; - } - else if(lpCmdLine[0] == '2') - { - g_iScreenWidth = 640; - g_iScreenHeight = 480; - } - else if(lpCmdLine[0] == '3') - { - // Vita - g_iScreenWidth = 720; - g_iScreenHeight = 408; - - // Vita native - //g_iScreenWidth = 960; - //g_iScreenHeight = 544; - } - - char cmdLineA[1024]; - strncpy_s(cmdLineA, sizeof(cmdLineA), lpCmdLine, _TRUNCATE); - - char *nameArg = strstr(cmdLineA, "-name "); - if (nameArg) - { - 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); - } - - char *ipArg = strstr(cmdLineA, "-ip "); - if (ipArg) - { - ipArg += 4; - while (*ipArg == ' ') ipArg++; - char ipBuf[256]; - int n = 0; - while (ipArg[n] && ipArg[n] != ' ' && n < 255) { ipBuf[n] = ipArg[n]; n++; } - ipBuf[n] = 0; - strncpy_s(g_Win64MultiplayerIP, 256, ipBuf, _TRUNCATE); - g_Win64MultiplayerJoin = true; - } - - char *portArg = strstr(cmdLineA, "-port "); - if (portArg) - { - portArg += 6; - while (*portArg == ' ') portArg++; - char portBuf[16]; - int n = 0; - while (portArg[n] && portArg[n] != ' ' && n < 15) { portBuf[n] = portArg[n]; n++; } - portBuf[n] = 0; - g_Win64MultiplayerPort = atoi(portBuf); - } - } + Win64LaunchOptions launchOptions = ParseLaunchOptions(); + ApplyScreenMode(launchOptions.screenMode); if (g_Win64Username[0] == 0) { @@ -821,7 +1123,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, MyRegisterClass(hInstance); // Perform application initialization: - if (!InitInstance (hInstance, nCmdShow)) + if (!InitInstance (hInstance, launchOptions.serverMode ? SW_HIDE : nCmdShow)) { return FALSE; } @@ -834,6 +1136,13 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, return 0; } + if (launchOptions.serverMode) + { + int serverResult = RunHeadlessServer(); + CleanupDevice(); + return serverResult; + } + #if 0 // Main message loop MSG msg = {0}; @@ -886,202 +1195,12 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } #endif - app.loadMediaArchive(); - - RenderManager.Initialise(g_pd3dDevice, g_pSwapChain); - - app.loadStringTable(); - ui.init(g_pd3dDevice,g_pImmediateContext,g_pRenderTargetView,g_pDepthStencilView,g_iScreenWidth,g_iScreenHeight); - - //////////////// - // Initialise // - //////////////// - - // Set the number of possible joypad layouts that the user can switch between, and the number of actions - InputManager.Initialise(1,3,MINECRAFT_ACTION_MAX, ACTION_MAX_MENU); - - // Initialize keyboard/mouse input - KMInput.Init(g_hWnd); - - // Set the default joypad action mappings for Minecraft - DefineActions(); - InputManager.SetJoypadMapVal(0,0); - InputManager.SetKeyRepeatRate(0.3f,0.2f); - - // Initialise the profile manager with the game Title ID, Offer ID, a profile version number, and the number of profile values and settings - ProfileManager.Initialise(TITLEID_MINECRAFT, - app.m_dwOfferID, - PROFILE_VERSION_10, - NUM_PROFILE_VALUES, - NUM_PROFILE_SETTINGS, - dwProfileSettingsA, - app.GAME_DEFINED_PROFILE_DATA_BYTES*XUSER_MAX_COUNT, - &app.uiGameDefinedDataChangedBitmask - ); -#if 0 - // register the awards - ProfileManager.RegisterAward(eAward_TakingInventory, ACHIEVEMENT_01, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_GettingWood, ACHIEVEMENT_02, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_Benchmarking, ACHIEVEMENT_03, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_TimeToMine, ACHIEVEMENT_04, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_HotTopic, ACHIEVEMENT_05, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_AquireHardware, ACHIEVEMENT_06, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_TimeToFarm, ACHIEVEMENT_07, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_BakeBread, ACHIEVEMENT_08, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_TheLie, ACHIEVEMENT_09, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_GettingAnUpgrade, ACHIEVEMENT_10, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_DeliciousFish, ACHIEVEMENT_11, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_OnARail, ACHIEVEMENT_12, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_TimeToStrike, ACHIEVEMENT_13, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_MonsterHunter, ACHIEVEMENT_14, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_CowTipper, ACHIEVEMENT_15, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_WhenPigsFly, ACHIEVEMENT_16, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_LeaderOfThePack, ACHIEVEMENT_17, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_MOARTools, ACHIEVEMENT_18, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_DispenseWithThis, ACHIEVEMENT_19, eAwardType_Achievement); - ProfileManager.RegisterAward(eAward_InToTheNether, ACHIEVEMENT_20, eAwardType_Achievement); - - ProfileManager.RegisterAward(eAward_mine100Blocks, GAMER_PICTURE_GAMERPIC1, eAwardType_GamerPic,false,app.GetStringTable(),IDS_AWARD_TITLE,IDS_AWARD_GAMERPIC1,IDS_CONFIRM_OK); - ProfileManager.RegisterAward(eAward_kill10Creepers, GAMER_PICTURE_GAMERPIC2, eAwardType_GamerPic,false,app.GetStringTable(),IDS_AWARD_TITLE,IDS_AWARD_GAMERPIC2,IDS_CONFIRM_OK); - - ProfileManager.RegisterAward(eAward_eatPorkChop, AVATARASSETAWARD_PORKCHOP_TSHIRT, eAwardType_AvatarItem,false,app.GetStringTable(),IDS_AWARD_TITLE,IDS_AWARD_AVATAR1,IDS_CONFIRM_OK); - ProfileManager.RegisterAward(eAward_play100Days, AVATARASSETAWARD_WATCH, eAwardType_AvatarItem,false,app.GetStringTable(),IDS_AWARD_TITLE,IDS_AWARD_AVATAR2,IDS_CONFIRM_OK); - ProfileManager.RegisterAward(eAward_arrowKillCreeper, AVATARASSETAWARD_CAP, eAwardType_AvatarItem,false,app.GetStringTable(),IDS_AWARD_TITLE,IDS_AWARD_AVATAR3,IDS_CONFIRM_OK); - - ProfileManager.RegisterAward(eAward_socialPost, 0, eAwardType_Theme,false,app.GetStringTable(),IDS_AWARD_TITLE,IDS_AWARD_THEME,IDS_CONFIRM_OK,THEME_NAME,THEME_FILESIZE); - - // Rich Presence init - number of presences, number of contexts - ProfileManager.RichPresenceInit(4,1); - ProfileManager.RegisterRichPresenceContext(CONTEXT_GAME_STATE); - - // initialise the storage manager with a default save display name, a Minimum save size, and a callback for displaying the saving message - StorageManager.Init(app.GetString(IDS_DEFAULT_SAVENAME),"savegame.dat",FIFTY_ONE_MB,&CConsoleMinecraftApp::DisplaySavingMessage,(LPVOID)&app); - // Set up the global title storage path - StorageManager.StoreTMSPathName(); - - // set a function to be called when there's a sign in change, so we can exit a level if the primary player signs out - ProfileManager.SetSignInChangeCallback(&CConsoleMinecraftApp::SignInChangeCallback,(LPVOID)&app); - - // set a function to be called when the ethernet is disconnected, so we can back out if required - ProfileManager.SetNotificationsCallback(&CConsoleMinecraftApp::NotificationsCallback,(LPVOID)&app); - -#endif - // Set a callback for the default player options to be set - when there is no profile data for the player - ProfileManager.SetDefaultOptionsCallback(&CConsoleMinecraftApp::DefaultOptionsCallback,(LPVOID)&app); -#if 0 - // Set a callback to deal with old profile versions needing updated to new versions - ProfileManager.SetOldProfileVersionCallback(&CConsoleMinecraftApp::OldProfileVersionCallback,(LPVOID)&app); - - // Set a callback for when there is a read error on profile data - ProfileManager.SetProfileReadErrorCallback(&CConsoleMinecraftApp::ProfileReadErrorCallback,(LPVOID)&app); - -#endif - // QNet needs to be setup after profile manager, as we do not want its Notify listener to handle - // 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(); - - for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) + Minecraft *pMinecraft = InitialiseMinecraftRuntime(); + if (pMinecraft == NULL) { - IQNet::m_player[i].m_smallId = (BYTE)i; - IQNet::m_player[i].m_isRemote = false; - IQNet::m_player[i].m_isHostPlayer = (i == 0); - swprintf_s(IQNet::m_player[i].m_gamertag, 32, L"Player%d", i); + CleanupDevice(); + return 1; } - extern wchar_t g_Win64UsernameW[17]; - wcscpy_s(IQNet::m_player[0].m_gamertag, 32, g_Win64UsernameW); - - WinsockNetLayer::Initialize(); - - // 4J-PB moved further down - //app.InitGameSettings(); - - // debug switch to trial version - ProfileManager.SetDebugFullOverride(true); - -#if 0 - //ProfileManager.AddDLC(2); - StorageManager.SetDLCPackageRoot("DLCDrive"); - StorageManager.RegisterMarketplaceCountsCallback(&CConsoleMinecraftApp::MarketplaceCountsCallback,(LPVOID)&app); - // Kinect ! - - if(XNuiGetHardwareStatus()!=0) - { - // If the Kinect Sensor is not physically connected, this function returns 0. - NuiInitialize(NUI_INITIALIZE_FLAG_USES_HIGH_QUALITY_COLOR | NUI_INITIALIZE_FLAG_USES_DEPTH | - NUI_INITIALIZE_FLAG_EXTRAPOLATE_FLOOR_PLANE | NUI_INITIALIZE_FLAG_USES_FITNESS | NUI_INITIALIZE_FLAG_NUI_GUIDE_DISABLED | NUI_INITIALIZE_FLAG_SUPPRESS_AUTOMATIC_UI,NUI_INITIALIZE_DEFAULT_HARDWARE_THREAD ); - } - - // Sentient ! - hr = TelemetryManager->Init(); - -#endif - // Initialise TLS for tesselator, for this main thread - Tesselator::CreateNewThreadStorage(1024*1024); - // Initialise TLS for AABB and Vec3 pools, for this main thread - AABB::CreateNewThreadStorage(); - Vec3::CreateNewThreadStorage(); - IntCache::CreateNewThreadStorage(); - Compression::CreateNewThreadStorage(); - OldChunkStorage::CreateNewThreadStorage(); - Level::enableLightingCache(); - Tile::CreateNewThreadStorage(); - - Minecraft::main(); - Minecraft *pMinecraft=Minecraft::GetInstance(); - - app.InitGameSettings(); - -#if 0 - //bool bDisplayPauseMenu=false; - - // set the default gamma level - float fVal=50.0f*327.68f; - RenderManager.UpdateGamma((unsigned short)fVal); - - // load any skins - //app.AddSkinsToMemoryTextureFiles(); - - // set the achievement text for a trial achievement, now we have the string table loaded - ProfileManager.SetTrialTextStringTable(app.GetStringTable(),IDS_CONFIRM_OK, IDS_CONFIRM_CANCEL); - ProfileManager.SetTrialAwardText(eAwardType_Achievement,IDS_UNLOCK_TITLE,IDS_UNLOCK_ACHIEVEMENT_TEXT); - ProfileManager.SetTrialAwardText(eAwardType_GamerPic,IDS_UNLOCK_TITLE,IDS_UNLOCK_GAMERPIC_TEXT); - ProfileManager.SetTrialAwardText(eAwardType_AvatarItem,IDS_UNLOCK_TITLE,IDS_UNLOCK_AVATAR_TEXT); - ProfileManager.SetTrialAwardText(eAwardType_Theme,IDS_UNLOCK_TITLE,IDS_UNLOCK_THEME_TEXT); - ProfileManager.SetUpsellCallback(&app.UpsellReturnedCallback,&app); - - // Set up a debug character press sequence -#ifndef _FINAL_BUILD - app.SetDebugSequence("LRLRYYY"); -#endif - - // Initialise the social networking manager. - CSocialManager::Instance()->Initialise(); - - // Update the base scene quick selects now that the minecraft class exists - //CXuiSceneBase::UpdateScreenSettings(0); -#endif - app.InitialiseTips(); -#if 0 - - DWORD initData=0; - -#ifndef _FINAL_BUILD -#ifndef _DEBUG -#pragma message(__LOC__"Need to define the _FINAL_BUILD before submission") -#endif -#endif - - // Set the default sound levels - pMinecraft->options->set(Options::Option::MUSIC,1.0f); - pMinecraft->options->set(Options::Option::SOUND,1.0f); - - app.NavigateToScene(XUSER_INDEX_ANY,eUIScene_Intro,&initData); -#endif - - // Set the default sound levels - pMinecraft->options->set(Options::Option::MUSIC,1.0f); - pMinecraft->options->set(Options::Option::SOUND,1.0f); //app.TemporaryCreateGameStart(); @@ -1637,4 +1756,4 @@ void MemPixStuff() PIXAddNamedCounter(((float)allSectsTotal)/(4096.0f),"MemSect total pages"); } -#endif \ No newline at end of file +#endif diff --git a/Minecraft.World/Minecraft.World.vcxproj b/Minecraft.World/Minecraft.World.vcxproj index 58880529c..7ab7c4ce5 100644 --- a/Minecraft.World/Minecraft.World.vcxproj +++ b/Minecraft.World/Minecraft.World.vcxproj @@ -1257,6 +1257,7 @@ false true Default + /FS %(AdditionalOptions) true @@ -1355,7 +1356,7 @@ true true true - /Ob3 + /FS /Ob3 %(AdditionalOptions) true @@ -4957,4 +4958,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index e6583e768..a84ee508c 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,22 @@ This feature is based on [LCEMP](https://github.com/LCEMP/LCEMP/) | Argument | Description | |--------------------|----------------------------------------------------------------------------------------------------------------| | `-name ` | Sets your in-game username | -| `-ip
` | Manually connect to an IP if LAN advertising does not work or if the server cannot be discovered automatically | -| `-port ` | Override the default port if it was changed in the source | +| `-server` | Launches a headless server instead of the client | +| `-ip
` | Client mode: manually connect to an IP. Server mode: override the bind IP from `server.properties` | +| `-port ` | Client mode: override the join port. Server mode: override the listen port from `server.properties` | Example: ``` Minecraft.Client.exe -name Steve -ip 192.168.0.25 -port 25565 ``` +Headless server example: +``` +Minecraft.Client.exe -server -ip 0.0.0.0 -port 25565 +``` + +The headless server also reads and writes `server.properties` in the working directory. If `-ip` / `-port` are omitted in `-server` mode, it falls back to `server-ip` / `server-port` from that file. Dedicated-server host options such as `trust-players`, `pvp`, `fire-spreads`, `tnt`, `difficulty`, `gamemode`, `spawn-animals`, and `spawn-npcs` are persisted there as well. + ## Controls (Keyboard & Mouse) - **Movement**: `W` `A` `S` `D` From 5d9f6f6347c3bf21a0049f33401ec2a1c0cd6cbf Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Wed, 4 Mar 2026 17:47:16 +0800 Subject: [PATCH 15/32] docs: mark V-Sync note as WIP in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a84ee508c..c14b59132 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This project contains the source code of Minecraft Legacy Console Edition v1.6.0 - Fixed compilation and execution in both Debug and Release mode on Windows using Visual Studio 2022 - Added support for keyboard and mouse input - Added fullscreen mode support (toggle using F11) -- Disabled V-Sync for better performance +- (WIP) Disabled V-Sync for better performance - Added a high-resolution timer path on Windows for smoother high-FPS gameplay timing - Device's screen resolution will be used as the game resolution instead of using a fixed resolution (1920x1080) - LAN Multiplayer & Discovery From ef9b6fd500dfabd9463267b0dd9e29577eea8a2b Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Wed, 4 Mar 2026 17:48:19 +0800 Subject: [PATCH 16/32] docs: fix table formatting in README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c14b59132..23cfc2530 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ This feature is based on [LCEMP](https://github.com/LCEMP/LCEMP/) ### Launch Arguments -| Argument | Description | -|--------------------|----------------------------------------------------------------------------------------------------------------| -| `-name ` | Sets your in-game username | -| `-server` | Launches a headless server instead of the client | -| `-ip
` | Client mode: manually connect to an IP. Server mode: override the bind IP from `server.properties` | -| `-port ` | Client mode: override the join port. Server mode: override the listen port from `server.properties` | +| Argument | Description | +|--------------------|-----------------------------------------------------------------------------------------------------| +| `-name ` | Sets your in-game username | +| `-server` | Launches a headless server instead of the client | +| `-ip
` | Client mode: manually connect to an IP. Server mode: override the bind IP from `server.properties` | +| `-port ` | Client mode: override the join port. Server mode: override the listen port from `server.properties` | Example: ``` From 0993e628abc392821cb9049f4730cbb8f3d70522 Mon Sep 17 00:00:00 2001 From: Loki Rautio Date: Wed, 4 Mar 2026 05:44:16 -0600 Subject: [PATCH 17/32] Disable automatic Windows account username This could reveal someone's private information on a livestream or video. We need a long-term username implementation --- Minecraft.Client/Windows64/Windows64_Minecraft.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index ed6781a31..c36ceee5c 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1112,8 +1112,9 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, if (g_Win64Username[0] == 0) { DWORD sz = 17; - if (!GetUserNameA(g_Win64Username, &sz)) - strncpy_s(g_Win64Username, 17, "Player", _TRUNCATE); + //if (!GetUserNameA(g_Win64Username, &sz)) + // todo: SET USERNAMES PROPERLY + strncpy_s(g_Win64Username, 17, "Player", _TRUNCATE); g_Win64Username[16] = 0; } From ea17b152b70122d8c58783fd9a67f45ecd8330cf Mon Sep 17 00:00:00 2001 From: Loki Rautio Date: Wed, 4 Mar 2026 05:57:56 -0600 Subject: [PATCH 18/32] Restore username.txt loading without conflict Still allows for -name launch argument, but restores old expected behavior --- .../Windows64/Windows64_Minecraft.cpp | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index c36ceee5c..ec89feb18 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1106,16 +1106,48 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, SetProcessDPIAware(); g_iScreenWidth = GetSystemMetrics(SM_CXSCREEN); g_iScreenHeight = GetSystemMetrics(SM_CYSCREEN); + + // Load username from username.txt + char exePath[MAX_PATH] = {}; + GetModuleFileNameA(NULL, exePath, MAX_PATH); + char *lastSlash = strrchr(exePath, '\\'); + if (lastSlash) + { + *(lastSlash + 1) = '\0'; + } + + char filePath[MAX_PATH] = {}; + _snprintf_s(filePath, sizeof(filePath), _TRUNCATE, "%susername.txt", exePath); + + FILE *f = nullptr; + if (fopen_s(&f, filePath, "r") == 0 && f) + { + char buf[128] = {}; + if (fgets(buf, sizeof(buf), f)) + { + int len = (int)strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r' || buf[len - 1] == ' ')) + { + buf[--len] = '\0'; + } + + if (len > 0) + { + strncpy_s(g_Win64Username, sizeof(g_Win64Username), buf, _TRUNCATE); + } + } + fclose(f); + } + + // Load stuff from launch options, including username Win64LaunchOptions launchOptions = ParseLaunchOptions(); ApplyScreenMode(launchOptions.screenMode); + // If no username, let's fall back if (g_Win64Username[0] == 0) { - DWORD sz = 17; - //if (!GetUserNameA(g_Win64Username, &sz)) - // todo: SET USERNAMES PROPERLY - strncpy_s(g_Win64Username, 17, "Player", _TRUNCATE); - g_Win64Username[16] = 0; + // Default username will be "Player" + strncpy_s(g_Win64Username, sizeof(g_Win64Username), "Player", _TRUNCATE); } MultiByteToWideChar(CP_ACP, 0, g_Win64Username, -1, g_Win64UsernameW, 17); From 0e9d8629f1211e25f650f62e5c58aebd3caea031 Mon Sep 17 00:00:00 2001 From: Loki Rautio Date: Wed, 4 Mar 2026 05:59:48 -0600 Subject: [PATCH 19/32] Update README with info about username.txt --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 23cfc2530..7578f159f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This project contains the source code of Minecraft Legacy Console Edition v1.6.0 - Added a high-resolution timer path on Windows for smoother high-FPS gameplay timing - Device's screen resolution will be used as the game resolution instead of using a fixed resolution (1920x1080) - LAN Multiplayer & Discovery +- Added persistent username system via "username.txt" ## Multiplayer From 52d9bcc9a9fbccd995ab59847c005f98751ebe0a Mon Sep 17 00:00:00 2001 From: Loki Rautio Date: Wed, 4 Mar 2026 06:14:06 -0600 Subject: [PATCH 20/32] Enable Stained Glass in Creative Menu --- Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp index 973020db2..bda2228e3 100644 --- a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp @@ -210,7 +210,6 @@ void IUIScene_CreativeMenu::staticCtor() ITEM_AUX(Tile::woolCarpet_Id,13) // Green ITEM_AUX(Tile::woolCarpet_Id,12) // Brown -#if 0 ITEM_AUX(Tile::stained_glass_Id,14) // Red ITEM_AUX(Tile::stained_glass_Id,1) // Orange ITEM_AUX(Tile::stained_glass_Id,4) // Yellow @@ -244,7 +243,6 @@ void IUIScene_CreativeMenu::staticCtor() ITEM_AUX(Tile::stained_glass_pane_Id,15) // Black ITEM_AUX(Tile::stained_glass_pane_Id,13) // Green ITEM_AUX(Tile::stained_glass_pane_Id,12) // Brown -#endif #ifndef _CONTENT_PACKAGE DEF(eCreativeInventory_ArtToolsDecorations) From 2d430798a56b00f321ef92a416797d6ce069d7b0 Mon Sep 17 00:00:00 2001 From: Marlian <84173858+MCbabel@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:47:43 +0100 Subject: [PATCH 21/32] Fix creative inventory crash with Art Tools debug option (#399) Fix vector out-of-bounds crash when scrolling the potions tab in the creative inventory with Art Tools debug enabled. - Fix getPageCount() returning total rows instead of scrollable pages in Art Tools mode - Fix off-by-one boundary check in populateMenu() for both static and debug group loops (< should be <=) Fixes #386 --- Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp index bda2228e3..0099cea67 100644 --- a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp @@ -880,7 +880,7 @@ void IUIScene_CreativeMenu::TabSpec::populateMenu(AbstractContainerMenu *menu, i for(; currentGroup < m_staticGroupsCount; ++currentGroup) { int size = categoryGroups[m_staticGroupsA[currentGroup]].size(); - if( currentIndex + size < startIndex) + if( currentIndex + size <= startIndex) { currentIndex += size; continue; @@ -930,7 +930,7 @@ void IUIScene_CreativeMenu::TabSpec::populateMenu(AbstractContainerMenu *menu, i for(; currentGroup < m_debugGroupsCount; ++currentGroup) { int size = categoryGroups[m_debugGroupsA[currentGroup]].size(); - if( currentIndex + size < startIndex) + if( currentIndex + size <= startIndex) { currentIndex += size; continue; @@ -971,7 +971,9 @@ unsigned int IUIScene_CreativeMenu::TabSpec::getPageCount() #ifndef _CONTENT_PACKAGE if(app.DebugArtToolsOn()) { - return (int)ceil((float)(m_staticItems + m_debugItems) / m_staticPerPage); + int totalItems = m_staticItems + m_debugItems; + const int totalRows = (totalItems + columns - 1) / columns; + return std::max(1, totalRows - rows + 1); } else #endif From ca5fde56fed613a8f45767868636e2321b03d3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davi=20Eler=20Magalh=C3=A3es?= Date: Wed, 4 Mar 2026 12:07:37 -0300 Subject: [PATCH 22/32] Fix: Sorted the item list in the debug overlay (#340) * Fix: Sorted the item list in the debug overlay * revert show all files to false * Revert ShowAllFiles by removing it * removed extra line * Adressed PR review changes * Replaced push_back with emplace_back * Removed redundant emplace_back --- .../Common/UI/UIScene_DebugOverlay.cpp | 38 ++++++++++++++----- .../Minecraft.Client.vcxproj.user | 2 +- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene_DebugOverlay.cpp b/Minecraft.Client/Common/UI/UIScene_DebugOverlay.cpp index 8e45c3246..6514e6ece 100644 --- a/Minecraft.Client/Common/UI/UIScene_DebugOverlay.cpp +++ b/Minecraft.Client/Common/UI/UIScene_DebugOverlay.cpp @@ -39,17 +39,37 @@ UIScene_DebugOverlay::UIScene_DebugOverlay(int iPad, void *initData, UILayer *pa m_buttonSetNight.init(L"Set Night", eControl_SetNight); m_buttonListItems.init(eControl_Items); + + // Sort items alphabetically + std::vector> sortedItems; + for (size_t i = 0; i < Item::items.length; ++i) + { + if (Item::items[i] != NULL) + { + sortedItems.emplace_back(std::wstring(app.GetString(Item::items[i]->getDescriptionId())), i); + } + } + for (size_t i = 1; i < sortedItems.size(); ++i) + { + auto key = sortedItems[i]; + int j = i - 1; + while (j >= 0 && sortedItems[j].first > key.first) + { + sortedItems[j + 1] = sortedItems[j]; + --j; + } + sortedItems[j + 1] = key; + } + + // Populate the list in sorted order int listId = 0; - for(unsigned int i = 0; i < Item::items.length; ++i) - { - if(Item::items[i] != NULL) - { - m_itemIds.push_back(i); - m_buttonListItems.addItem(app.GetString(Item::items[i]->getDescriptionId()), listId); - ++listId; - } - } + for (const auto& entry : sortedItems) + { + m_itemIds.push_back(entry.second); + m_buttonListItems.addItem(entry.first.c_str(), listId); + ++listId; + } m_buttonListEnchantments.init(eControl_Enchantments); diff --git a/Minecraft.Client/Minecraft.Client.vcxproj.user b/Minecraft.Client/Minecraft.Client.vcxproj.user index 09a7ebc4a..24ca62f80 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj.user +++ b/Minecraft.Client/Minecraft.Client.vcxproj.user @@ -19,4 +19,4 @@ $(SolutionDir)$(Platform)\$(Configuration)\ WindowsLocalDebugger - \ No newline at end of file + From 2be856a2d447ab758e02bfeaaa9cac5e1114dbfc Mon Sep 17 00:00:00 2001 From: ModMaker101 <119018978+ModMaker101@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:43:29 -0500 Subject: [PATCH 23/32] Fix Chunk destructor segfault using smart pointers #112 (#414) --- Minecraft.Client/Chunk.cpp | 20 +++++++++----------- Minecraft.Client/Chunk.h | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Minecraft.Client/Chunk.cpp b/Minecraft.Client/Chunk.cpp index d039227bf..63cd05017 100644 --- a/Minecraft.Client/Chunk.cpp +++ b/Minecraft.Client/Chunk.cpp @@ -52,7 +52,7 @@ Chunk::Chunk(Level *level, LevelRenderer::rteMap &globalRenderableTileEntities, : globalRenderableTileEntities( &globalRenderableTileEntities ), globalRenderableTileEntities_cs(&globalRenderableTileEntities_cs) { clipChunk->visible = false; - bb = NULL; + bb = nullptr; id = 0; this->level = level; @@ -101,15 +101,15 @@ void Chunk::setPos(int x, int y, int z) float g = 6.0f; // 4J - changed to just set the value rather than make a new one, if we've already created storage - if( bb == NULL ) + if( !bb ) { - bb = AABB::newPermanent(-g, -g, -g, XZSIZE+g, SIZE+g, XZSIZE+g); + bb = shared_ptr(AABB::newPermanent(-g, -g, -g, XZSIZE+g, SIZE+g, XZSIZE+g)); } - else - { + else + { // 4J MGH - bounds are relative to the position now, so the AABB will be setup already, either above, or from the tesselator bounds. // bb->set(-g, -g, -g, SIZE+g, SIZE+g, SIZE+g); - } + } clipChunk->aabb[0] = bb->x0 + x; clipChunk->aabb[1] = bb->y0 + y; clipChunk->aabb[2] = bb->z0 + z; @@ -154,6 +154,7 @@ void Chunk::translateToPos() Chunk::Chunk() { + bb = nullptr; } void Chunk::makeCopyForRebuild(Chunk *source) @@ -998,7 +999,7 @@ int Chunk::getList(int layer) void Chunk::cull(Culler *culler) { - clipChunk->visible = culler->isVisible(bb); + clipChunk->visible = culler->isVisible(bb.get()); } void Chunk::renderBB() @@ -1027,10 +1028,7 @@ void Chunk::clearDirty() #endif } -Chunk::~Chunk() -{ - delete bb; -} +Chunk::~Chunk() = default; bool Chunk::emptyFlagSet(int layer) { diff --git a/Minecraft.Client/Chunk.h b/Minecraft.Client/Chunk.h index f7947156f..e0ae016ef 100644 --- a/Minecraft.Client/Chunk.h +++ b/Minecraft.Client/Chunk.h @@ -46,11 +46,11 @@ public: int xRender, yRender, zRender; int xRenderOffs, yRenderOffs, zRenderOffs; - int xm, ym, zm; - AABB *bb; + int xm, ym, zm; + shared_ptr bb; ClipChunk *clipChunk; - int id; + int id; //public: // vector > renderableTileEntities; // 4J - removed From cea1084978bbf525c983fba67dc44f3718f667e4 Mon Sep 17 00:00:00 2001 From: Alezito2008 <92759854+Alezito2008@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:28:20 -0300 Subject: [PATCH 24/32] Fix duplicated stained glass when art tools enabled (#426) --- .../Common/UI/IUIScene_CreativeMenu.cpp | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp index 0099cea67..544fedc0f 100644 --- a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp @@ -263,40 +263,6 @@ void IUIScene_CreativeMenu::staticCtor() BuildFirework(list, FireworksItem::TYPE_CREEPER, DyePowderItem::BLUE, 1, true, false); BuildFirework(list, FireworksItem::TYPE_STAR, DyePowderItem::YELLOW, 1, false, false); BuildFirework(list, FireworksItem::TYPE_BIG, DyePowderItem::WHITE, 1, true, true); - - ITEM_AUX(Tile::stained_glass_Id,14) // Red - ITEM_AUX(Tile::stained_glass_Id,1) // Orange - ITEM_AUX(Tile::stained_glass_Id,4) // Yellow - ITEM_AUX(Tile::stained_glass_Id,5) // Lime - ITEM_AUX(Tile::stained_glass_Id,3) // Light Blue - ITEM_AUX(Tile::stained_glass_Id,9) // Cyan - ITEM_AUX(Tile::stained_glass_Id,11) // Blue - ITEM_AUX(Tile::stained_glass_Id,10) // Purple - ITEM_AUX(Tile::stained_glass_Id,2) // Magenta - ITEM_AUX(Tile::stained_glass_Id,6) // Pink - ITEM_AUX(Tile::stained_glass_Id,0) // White - ITEM_AUX(Tile::stained_glass_Id,8) // Light Gray - ITEM_AUX(Tile::stained_glass_Id,7) // Gray - ITEM_AUX(Tile::stained_glass_Id,15) // Black - ITEM_AUX(Tile::stained_glass_Id,13) // Green - ITEM_AUX(Tile::stained_glass_Id,12) // Brown - - ITEM_AUX(Tile::stained_glass_pane_Id,14) // Red - ITEM_AUX(Tile::stained_glass_pane_Id,1) // Orange - ITEM_AUX(Tile::stained_glass_pane_Id,4) // Yellow - ITEM_AUX(Tile::stained_glass_pane_Id,5) // Lime - ITEM_AUX(Tile::stained_glass_pane_Id,3) // Light Blue - ITEM_AUX(Tile::stained_glass_pane_Id,9) // Cyan - ITEM_AUX(Tile::stained_glass_pane_Id,11) // Blue - ITEM_AUX(Tile::stained_glass_pane_Id,10) // Purple - ITEM_AUX(Tile::stained_glass_pane_Id,2) // Magenta - ITEM_AUX(Tile::stained_glass_pane_Id,6) // Pink - ITEM_AUX(Tile::stained_glass_pane_Id,0) // White - ITEM_AUX(Tile::stained_glass_pane_Id,8) // Light Gray - ITEM_AUX(Tile::stained_glass_pane_Id,7) // Gray - ITEM_AUX(Tile::stained_glass_pane_Id,15) // Black - ITEM_AUX(Tile::stained_glass_pane_Id,13) // Green - ITEM_AUX(Tile::stained_glass_pane_Id,12) // Brown } #endif From 464cf91f4c88b9df7921e0e730ac638b356c7138 Mon Sep 17 00:00:00 2001 From: ModMaker101 <119018978+ModMaker101@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:28:37 -0500 Subject: [PATCH 25/32] Prevent door sounds from playing twice #392 (#425) --- Minecraft.Client/MultiPlayerGameMode.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Minecraft.Client/MultiPlayerGameMode.cpp b/Minecraft.Client/MultiPlayerGameMode.cpp index e7611b37c..9470a49af 100644 --- a/Minecraft.Client/MultiPlayerGameMode.cpp +++ b/Minecraft.Client/MultiPlayerGameMode.cpp @@ -370,7 +370,9 @@ bool MultiPlayerGameMode::useItemOn(shared_ptr player, Level *level, sha // are meant to be directly caused by this. If we don't do this, then the sounds never happen as the tile's use method is only called on the // server, and that won't allow any sounds that are directly made, or broadcast back level events to us that would make the sound, since we are // the source of the event. - if( ( t > 0 ) && ( !bTestUseOnly ) && player->isAllowedToUse(Tile::tiles[t]) ) + // --------------------------------------------------------------------------------- + // Only call soundOnly version if we didn't already call the tile's use method above + if( !didSomething && ( t > 0 ) && ( !bTestUseOnly ) && player->isAllowedToUse(Tile::tiles[t]) ) { Tile::tiles[t]->use(level, x, y, z, player, face, clickX, clickY, clickZ, true); } From 206c6800f29ac608eef00775ee6a52c12f6f86d6 Mon Sep 17 00:00:00 2001 From: ModMaker101 <119018978+ModMaker101@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:41:59 -0500 Subject: [PATCH 26/32] Fix incorrect distance comparison return value (#432) --- Minecraft.World/NearestAttackableTargetGoal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Minecraft.World/NearestAttackableTargetGoal.cpp b/Minecraft.World/NearestAttackableTargetGoal.cpp index 1dbf587ed..7ddd88950 100644 --- a/Minecraft.World/NearestAttackableTargetGoal.cpp +++ b/Minecraft.World/NearestAttackableTargetGoal.cpp @@ -34,7 +34,7 @@ bool NearestAttackableTargetGoal::DistComp::operator() (shared_ptr e1, s double distSqr2 = source->distanceToSqr(e2); if (distSqr1 < distSqr2) return true; if (distSqr1 > distSqr2) return false; - return true; + return false; } NearestAttackableTargetGoal::NearestAttackableTargetGoal(PathfinderMob *mob, const type_info& targetType, int randomInterval, bool mustSee, bool mustReach /*= false*/, EntitySelector *entitySelector /* =NULL */) From ac03b88a907bb49f5159f08de07398f3fce32991 Mon Sep 17 00:00:00 2001 From: Alezito2008 <92759854+Alezito2008@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:42:32 -0300 Subject: [PATCH 27/32] Fix:Prevent horse spawner crash (#433) --- Minecraft.World/EntityHorse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Minecraft.World/EntityHorse.cpp b/Minecraft.World/EntityHorse.cpp index 2d67d21bd..d72500873 100644 --- a/Minecraft.World/EntityHorse.cpp +++ b/Minecraft.World/EntityHorse.cpp @@ -479,7 +479,7 @@ void EntityHorse::createInventory() void EntityHorse::updateEquipment() { - if (!level->isClientSide) + if (level && !level->isClientSide) { setSaddled(inventory->getItem(INV_SLOT_SADDLE) != NULL); if (canWearArmor()) From 1dc8a005ed111463c22c17b487e5ec8a3e2d30f3 Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Wed, 4 Mar 2026 21:19:40 +0800 Subject: [PATCH 28/32] refactor: refactor KBM input code --- .../Common/Tutorial/ChoiceTask.cpp | 4 +- Minecraft.Client/Common/Tutorial/InfoTask.cpp | 4 +- .../UI/IUIScene_AbstractContainerMenu.cpp | 42 +- .../UI/IUIScene_AbstractContainerMenu.h | 12 + Minecraft.Client/Common/UI/UIControl.cpp | 2 - Minecraft.Client/Common/UI/UIControl.h | 2 +- Minecraft.Client/Common/UI/UIController.cpp | 254 +++++++++- Minecraft.Client/Common/UI/UIController.h | 4 + Minecraft.Client/Common/UI/UIGroup.h | 2 - Minecraft.Client/Common/UI/UIScene.h | 4 +- .../UI/UIScene_AbstractContainerMenu.cpp | 166 ++----- .../Common/UI/UIScene_AbstractContainerMenu.h | 8 +- .../Common/UI/UIScene_LoadOrJoinMenu.cpp | 8 + .../UI/UIScene_SettingsGraphicsMenu.cpp | 38 +- Minecraft.Client/Input.cpp | 147 +++--- Minecraft.Client/Input.h | 9 +- Minecraft.Client/LocalPlayer.cpp | 33 +- Minecraft.Client/Minecraft.cpp | 148 +++--- Minecraft.Client/Screen.cpp | 8 +- .../Windows64/KeyboardMouseInput.cpp | 454 +++++++++++------- .../Windows64/KeyboardMouseInput.h | 170 ++++--- .../Windows64/Windows64_Minecraft.cpp | 220 +++++---- Minecraft.Client/stubs.cpp | 103 +++- Minecraft.Client/stubs.h | 33 +- README.md | 1 - 25 files changed, 1187 insertions(+), 689 deletions(-) diff --git a/Minecraft.Client/Common/Tutorial/ChoiceTask.cpp b/Minecraft.Client/Common/Tutorial/ChoiceTask.cpp index f42b3ee02..1ea34ace6 100644 --- a/Minecraft.Client/Common/Tutorial/ChoiceTask.cpp +++ b/Minecraft.Client/Common/Tutorial/ChoiceTask.cpp @@ -58,7 +58,7 @@ bool ChoiceTask::isCompleted() #ifdef _WINDOWS64 if (!m_bConfirmMappingComplete && (InputManager.GetValue(xboxPad, m_iConfirmMapping) > 0 - || KMInput.IsKeyDown(VK_RETURN))) + || g_KBMInput.IsKeyDown(VK_RETURN))) #else if (!m_bConfirmMappingComplete && InputManager.GetValue(xboxPad, m_iConfirmMapping) > 0) @@ -70,7 +70,7 @@ bool ChoiceTask::isCompleted() #ifdef _WINDOWS64 if (!m_bCancelMappingComplete && (InputManager.GetValue(xboxPad, m_iCancelMapping) > 0 - || KMInput.IsKeyDown('B'))) + || g_KBMInput.IsKeyDown('B'))) #else if (!m_bCancelMappingComplete && InputManager.GetValue(xboxPad, m_iCancelMapping) > 0) diff --git a/Minecraft.Client/Common/Tutorial/InfoTask.cpp b/Minecraft.Client/Common/Tutorial/InfoTask.cpp index 43a10357c..748093e5e 100644 --- a/Minecraft.Client/Common/Tutorial/InfoTask.cpp +++ b/Minecraft.Client/Common/Tutorial/InfoTask.cpp @@ -7,7 +7,7 @@ #include "TutorialConstraints.h" #include "InfoTask.h" #include "..\..\..\Minecraft.World\Material.h" -#include "..\..\KeyboardMouseInput.h" +#include "..\..\Windows64\KeyboardMouseInput.h" InfoTask::InfoTask(Tutorial *tutorial, int descriptionId, int promptId /*= -1*/, bool requiresUserInput /*= false*/, int iMapping /*= 0*/, ETelemetryChallenges telemetryEvent /*= eTelemetryTutorial_NoEvent*/) @@ -67,7 +67,7 @@ bool InfoTask::isCompleted() if(!current) { #ifdef _WINDOWS64 - if (InputManager.GetValue(pMinecraft->player->GetXboxPad(), (*it).first) > 0 || KMInput.IsKeyDown(VK_SPACE)) + if (InputManager.GetValue(pMinecraft->player->GetXboxPad(), (*it).first) > 0 || g_KBMInput.IsKeyDown(VK_SPACE)) #else if( InputManager.GetValue(pMinecraft->player->GetXboxPad(), (*it).first) > 0) #endif diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp index 667431b2b..882584211 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.cpp @@ -13,6 +13,12 @@ #include #endif +#ifdef _WINDOWS64 +#include "..\..\Windows64\KeyboardMouseInput.h" + +SavedInventoryCursorPos g_savedInventoryCursorPos = { 0.0f, 0.0f, false }; +#endif + IUIScene_AbstractContainerMenu::IUIScene_AbstractContainerMenu() { m_menu = NULL; @@ -474,6 +480,34 @@ void IUIScene_AbstractContainerMenu::onMouseTick() } #endif +#ifdef _WINDOWS64 + if (!g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) + { + int deltaX = g_KBMInput.GetMouseDeltaX(); + int deltaY = g_KBMInput.GetMouseDeltaY(); + + extern HWND g_hWnd; + RECT rc; + GetClientRect(g_hWnd, &rc); + int winW = rc.right - rc.left; + int winH = rc.bottom - rc.top; + + if (winW > 0 && winH > 0) + { + float scaleX = (float)getMovieWidth() / (float)winW; + float scaleY = (float)getMovieHeight() / (float)winH; + + vPointerPos.x += (float)deltaX * scaleX; + vPointerPos.y += (float)deltaY * scaleY; + } + + if (deltaX != 0 || deltaY != 0) + { + bStickInput = true; + } + } +#endif + // Determine which slot the pointer is currently over. ESceneSection eSectionUnderPointer = eSectionNone; int iNewSlotX = -1; @@ -694,7 +728,11 @@ 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! - if(!m_bPointerDrivenByMouse && CanHaveFocus(eSectionUnderPointer)) +#ifdef _WINDOWS64 + if((g_KBMInput.IsMouseGrabbed() || !g_KBMInput.IsKBMActive()) && CanHaveFocus(eSectionUnderPointer)) +#else + if(CanHaveFocus(eSectionUnderPointer)) +#endif { vPointerPos.x = vSnapPos.x; vPointerPos.y = vSnapPos.y; @@ -1329,7 +1367,7 @@ bool IUIScene_AbstractContainerMenu::handleKeyDown(int iPad, int iAction, bool b // Standard left click buttonNum = 0; - if (KMInput.IsKeyDown(VK_SHIFT)) + if (g_KBMInput.IsKeyDown(VK_LSHIFT)) { { validKeyPress = TRUE; diff --git a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h index fe79bf190..4877cfce8 100644 --- a/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h +++ b/Minecraft.Client/Common/UI/IUIScene_AbstractContainerMenu.h @@ -1,5 +1,15 @@ #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 @@ -265,4 +275,6 @@ protected: public: virtual int getPad() = 0; + virtual int getMovieWidth() = 0; + virtual int getMovieHeight() = 0; }; diff --git a/Minecraft.Client/Common/UI/UIControl.cpp b/Minecraft.Client/Common/UI/UIControl.cpp index ec2e13d8e..be267ada6 100644 --- a/Minecraft.Client/Common/UI/UIControl.cpp +++ b/Minecraft.Client/Common/UI/UIControl.cpp @@ -42,7 +42,6 @@ bool UIControl::setupControl(UIScene *scene, IggyValuePath *parent, const string return res; } -#ifdef __PSVITA__ void UIControl::UpdateControl() { F64 fx, fy, fwidth, fheight; @@ -55,7 +54,6 @@ void UIControl::UpdateControl() m_width = (S32)Math::round(fwidth); m_height = (S32)Math::round(fheight); } -#endif // __PSVITA__ void UIControl::ReInit() { diff --git a/Minecraft.Client/Common/UI/UIControl.h b/Minecraft.Client/Common/UI/UIControl.h index e37f04de5..29770df28 100644 --- a/Minecraft.Client/Common/UI/UIControl.h +++ b/Minecraft.Client/Common/UI/UIControl.h @@ -61,8 +61,8 @@ public: UIControl(); virtual bool setupControl(UIScene *scene, IggyValuePath *parent, const string &controlName); -#ifdef __PSVITA__ void UpdateControl(); +#ifdef __PSVITA__ void setHidden(bool bHidden) {m_bHidden=bHidden;} bool getHidden(void) {return m_bHidden;} #endif diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index 6ac2f9ba3..01ab49ba4 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -2,6 +2,7 @@ #include "UIController.h" #include "UI.h" #include "UIScene.h" +#include "UIControl_Slider.h" #include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\LocalPlayer.h" #include "..\..\DLCTexturePack.h" @@ -11,6 +12,9 @@ #include "..\..\EnderDragonRenderer.h" #include "..\..\MultiPlayerLocalPlayer.h" #include "UIFontData.h" +#ifdef _WINDOWS64 +#include "..\..\Windows64\KeyboardMouseInput.h" +#endif #ifdef __PSVITA__ #include #endif @@ -52,6 +56,21 @@ bool UIController::ms_bReloadSkinCSInitialised = false; DWORD UIController::m_dwTrialTimerLimitSecs=DYNAMIC_CONFIG_DEFAULT_TRIAL_TIME; +#ifdef _WINDOWS64 +static UIControl_Slider *FindSliderById(UIScene *pScene, int sliderId) +{ + vector *controls = pScene->GetControls(); + if (!controls) return NULL; + for (size_t i = 0; i < controls->size(); ++i) + { + UIControl *ctrl = (*controls)[i]; + if (ctrl && ctrl->getControlType() == UIControl::eSlider && ctrl->getId() == sliderId) + return (UIControl_Slider *)ctrl; + } + return NULL; +} +#endif + static void RADLINK WarningCallback(void *user_callback_data, Iggy *player, IggyResult code, const char *message) { //enum IggyResult{ IGGY_RESULT_SUCCESS = 0, IGGY_RESULT_Warning_None = 0, @@ -216,6 +235,10 @@ UIController::UIController() m_currentRenderViewport = C4JRender::VIEWPORT_TYPE_FULLSCREEN; m_bCustomRenderPosition = false; m_winUserIndex = 0; + m_mouseDraggingSliderScene = eUIScene_COUNT; + m_mouseDraggingSliderId = -1; + m_lastHoverMouseX = -1; + m_lastHoverMouseY = -1; m_accumulatedTicks = 0; m_lastUiSfx = 0; @@ -761,6 +784,168 @@ void UIController::tickInput() else #endif { +#ifdef _WINDOWS64 + if (!g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) + { + 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(); + int rawMouseX = g_KBMInput.GetMouseX(); + int rawMouseY = g_KBMInput.GetMouseY(); + F32 mouseX = (F32)rawMouseX; + F32 mouseY = (F32)rawMouseY; + + 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); + } + } + + // Only update hover focus when the mouse has actually moved, + // so that mouse-wheel scrolling can change list selection + // without the hover immediately snapping focus back. + bool mouseMoved = (rawMouseX != m_lastHoverMouseX || rawMouseY != m_lastHoverMouseY); + m_lastHoverMouseX = rawMouseX; + m_lastHoverMouseY = rawMouseY; + + if (mouseMoved) + { + IggyFocusHandle currentFocus = IGGY_FOCUS_NULL; + IggyFocusableObject focusables[64]; + S32 numFocusables = 0; + IggyPlayerGetFocusableObjects(movie, ¤tFocus, focusables, 64, &numFocusables); + + if (numFocusables > 0 && numFocusables <= 64) + { + 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 != currentFocus) + { + IggyPlayerSetFocusRS(movie, hitObject, 0); + } + } + } + + // Convert mouse to scene/movie coordinates for slider hit testing + F32 sceneMouseX = mouseX; + F32 sceneMouseY = mouseY; + { + S32 displayWidth = 0, displayHeight = 0; + pScene->GetParentLayer()->getRenderDimensions(displayWidth, displayHeight); + if (displayWidth > 0 && displayHeight > 0) + { + sceneMouseX = mouseX * ((F32)pScene->getRenderWidth() / (F32)displayWidth); + sceneMouseY = mouseY * ((F32)pScene->getRenderHeight() / (F32)displayHeight); + } + } + + // Get main panel offset (controls are positioned relative to it) + S32 panelOffsetX = 0, panelOffsetY = 0; + UIControl *pMainPanel = pScene->GetMainPanel(); + if (pMainPanel) + { + pMainPanel->UpdateControl(); + panelOffsetX = pMainPanel->getXPos(); + panelOffsetY = pMainPanel->getYPos(); + } + + bool leftPressed = g_KBMInput.IsMouseButtonPressed(KeyboardMouseInput::MOUSE_LEFT); + bool leftDown = leftPressed || g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT); + + if (m_mouseDraggingSliderScene != eUIScene_COUNT && m_mouseDraggingSliderScene != pScene->getSceneType()) + { + m_mouseDraggingSliderScene = eUIScene_COUNT; + m_mouseDraggingSliderId = -1; + } + + if (leftPressed) + { + 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()) + continue; + + UIControl_Slider *pSlider = (UIControl_Slider *)ctrl; + pSlider->UpdateControl(); + S32 cx = pSlider->getXPos() + panelOffsetX; + S32 cy = pSlider->getYPos() + panelOffsetY; + S32 cw = pSlider->GetRealWidth(); + S32 ch = pSlider->getHeight(); + if (cw <= 0 || ch <= 0) + continue; + + if (sceneMouseX >= cx && sceneMouseX <= cx + cw && sceneMouseY >= cy && sceneMouseY <= cy + ch) + { + m_mouseDraggingSliderScene = pScene->getSceneType(); + m_mouseDraggingSliderId = pSlider->getId(); + break; + } + } + } + } + + if (leftDown && m_mouseDraggingSliderScene == pScene->getSceneType() && m_mouseDraggingSliderId >= 0) + { + UIControl_Slider *pSlider = FindSliderById(pScene, m_mouseDraggingSliderId); + if (pSlider && pSlider->getVisible()) + { + pSlider->UpdateControl(); + S32 sliderX = pSlider->getXPos() + panelOffsetX; + S32 sliderWidth = pSlider->GetRealWidth(); + if (sliderWidth > 0) + { + float fNewSliderPos = (sceneMouseX - (float)sliderX) / (float)sliderWidth; + if (fNewSliderPos < 0.0f) fNewSliderPos = 0.0f; + if (fNewSliderPos > 1.0f) fNewSliderPos = 1.0f; + pSlider->SetSliderTouchPos(fNewSliderPos); + } + } + else + { + m_mouseDraggingSliderScene = eUIScene_COUNT; + m_mouseDraggingSliderId = -1; + } + } + else if (!leftDown) + { + m_mouseDraggingSliderScene = eUIScene_COUNT; + m_mouseDraggingSliderId = -1; + } + } + } +#endif handleInput(); ++m_accumulatedTicks; } @@ -995,28 +1180,59 @@ void UIController::handleKeyPress(unsigned int iPad, unsigned int key) released = InputManager.ButtonReleased(iPad,key); // Toggle #ifdef _WINDOWS64 - // Keyboard menu input for player 0 if (iPad == 0) { - bool kbDown = false, kbPressed = false, kbReleased = false; - switch(key) + int vk = 0; + switch (key) { - case ACTION_MENU_UP: kbDown = KMInput.IsKeyDown(VK_UP); kbPressed = KMInput.IsKeyPressed(VK_UP); kbReleased = KMInput.IsKeyReleased(VK_UP); break; - case ACTION_MENU_DOWN: kbDown = KMInput.IsKeyDown(VK_DOWN); kbPressed = KMInput.IsKeyPressed(VK_DOWN); kbReleased = KMInput.IsKeyReleased(VK_DOWN); break; - case ACTION_MENU_LEFT: kbDown = KMInput.IsKeyDown(VK_LEFT); kbPressed = KMInput.IsKeyPressed(VK_LEFT); kbReleased = KMInput.IsKeyReleased(VK_LEFT); break; - case ACTION_MENU_RIGHT: kbDown = KMInput.IsKeyDown(VK_RIGHT); kbPressed = KMInput.IsKeyPressed(VK_RIGHT); kbReleased = KMInput.IsKeyReleased(VK_RIGHT); break; - case ACTION_MENU_OK: kbDown = KMInput.IsKeyDown(VK_RETURN); kbPressed = KMInput.IsKeyPressed(VK_RETURN); kbReleased = KMInput.IsKeyReleased(VK_RETURN); break; - case ACTION_MENU_A: kbDown = KMInput.IsKeyDown(VK_RETURN); kbPressed = KMInput.IsKeyPressed(VK_RETURN); kbReleased = KMInput.IsKeyReleased(VK_RETURN); break; - case ACTION_MENU_CANCEL: kbDown = KMInput.IsKeyDown(VK_ESCAPE); kbPressed = KMInput.IsKeyPressed(VK_ESCAPE); kbReleased = KMInput.IsKeyReleased(VK_ESCAPE); break; - case ACTION_MENU_B: kbDown = KMInput.IsKeyDown(VK_ESCAPE); kbPressed = KMInput.IsKeyPressed(VK_ESCAPE); kbReleased = KMInput.IsKeyReleased(VK_ESCAPE); break; - case ACTION_MENU_PAUSEMENU: kbDown = KMInput.IsKeyDown(VK_ESCAPE); kbPressed = KMInput.IsKeyPressed(VK_ESCAPE); kbReleased = KMInput.IsKeyReleased(VK_ESCAPE); break; - case ACTION_MENU_LEFT_SCROLL: kbDown = KMInput.IsKeyDown('Q'); kbPressed = KMInput.IsKeyPressed('Q'); kbReleased = KMInput.IsKeyReleased('Q'); break; - case ACTION_MENU_RIGHT_SCROLL: kbDown = KMInput.IsKeyDown('E'); kbPressed = KMInput.IsKeyPressed('E'); kbReleased = KMInput.IsKeyReleased('E'); break; - case ACTION_MENU_QUICK_MOVE: kbDown = KMInput.IsKeyDown(VK_SHIFT); kbPressed = KMInput.IsKeyPressed(VK_SHIFT); kbReleased = KMInput.IsKeyReleased(VK_SHIFT); break; + case ACTION_MENU_OK: case ACTION_MENU_A: vk = VK_RETURN; break; + case ACTION_MENU_CANCEL: case ACTION_MENU_B: vk = VK_ESCAPE; break; + case ACTION_MENU_UP: vk = VK_UP; break; + case ACTION_MENU_DOWN: vk = VK_DOWN; break; + case ACTION_MENU_LEFT: vk = VK_LEFT; break; + case ACTION_MENU_RIGHT: vk = VK_RIGHT; break; + case ACTION_MENU_X: vk = 'R'; break; + case ACTION_MENU_Y: vk = VK_TAB; break; + case ACTION_MENU_LEFT_SCROLL: vk = 'Q'; break; + case ACTION_MENU_RIGHT_SCROLL: vk = 'E'; break; + case ACTION_MENU_PAGEUP: vk = VK_PRIOR; break; + case ACTION_MENU_PAGEDOWN: vk = VK_NEXT; break; + } + if (vk != 0) + { + if (g_KBMInput.IsKeyPressed(vk)) { pressed = true; down = true; } + if (g_KBMInput.IsKeyReleased(vk)) { released = true; down = false; } + if (!pressed && !released && g_KBMInput.IsKeyDown(vk)) { down = true; } + } + + if ((key == ACTION_MENU_OK || key == ACTION_MENU_A) && !g_KBMInput.IsMouseGrabbed()) + { + if (m_mouseDraggingSliderId < 0) + { + if (g_KBMInput.IsMouseButtonPressed(KeyboardMouseInput::MOUSE_LEFT)) { pressed = true; down = true; } + if (g_KBMInput.IsMouseButtonReleased(KeyboardMouseInput::MOUSE_LEFT)) { released = true; down = false; } + if (!pressed && !released && g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT)) { down = true; } + } + } + + // Scroll wheel for list scrolling — only consume the wheel value when the + // action key actually matches, so the other direction isn't lost. + if (!g_KBMInput.IsMouseGrabbed() && (key == ACTION_MENU_OTHER_STICK_UP || key == ACTION_MENU_OTHER_STICK_DOWN)) + { + int wheel = g_KBMInput.PeekMouseWheel(); + if (key == ACTION_MENU_OTHER_STICK_UP && wheel > 0) + { + g_KBMInput.ConsumeMouseWheel(); + pressed = true; + down = true; + } + else if (key == ACTION_MENU_OTHER_STICK_DOWN && wheel < 0) + { + g_KBMInput.ConsumeMouseWheel(); + pressed = true; + down = true; + } } - pressed = pressed || kbPressed; - released = released || kbReleased; - down = down || kbDown; } #endif @@ -3138,4 +3354,4 @@ void UIController::SendTouchInput(unsigned int iPad, unsigned int key, bool bPre } -#endif \ No newline at end of file +#endif diff --git a/Minecraft.Client/Common/UI/UIController.h b/Minecraft.Client/Common/UI/UIController.h index 49c780320..373d67b2c 100644 --- a/Minecraft.Client/Common/UI/UIController.h +++ b/Minecraft.Client/Common/UI/UIController.h @@ -158,6 +158,10 @@ private: vector m_queuedMessageBoxData; unsigned int m_winUserIndex; + EUIScene m_mouseDraggingSliderScene; + int m_mouseDraggingSliderId; + int m_lastHoverMouseX; + int m_lastHoverMouseY; //bool m_bSysUIShowing; bool m_bSystemUIShowing; C4JThread *m_reloadSkinThread; diff --git a/Minecraft.Client/Common/UI/UIGroup.h b/Minecraft.Client/Common/UI/UIGroup.h index 0ffee0ca2..28369f276 100644 --- a/Minecraft.Client/Common/UI/UIGroup.h +++ b/Minecraft.Client/Common/UI/UIGroup.h @@ -37,9 +37,7 @@ private: public: UIGroup(EUIGroup group, int iPad); -#ifdef __PSVITA__ EUIGroup GetGroup() {return m_group;} -#endif UIComponent_Tooltips *getTooltips() { return m_tooltips; } UIComponent_TutorialPopup *getTutorialPopup() { return m_tutorialPopup; } UIScene_HUD *getHUD() { return m_hud; } diff --git a/Minecraft.Client/Common/UI/UIScene.h b/Minecraft.Client/Common/UI/UIScene.h index 8c20aaae3..b4008fa01 100644 --- a/Minecraft.Client/Common/UI/UIScene.h +++ b/Minecraft.Client/Common/UI/UIScene.h @@ -107,8 +107,10 @@ public: int getRenderHeight() { return m_renderHeight; } #ifdef __PSVITA__ - UILayer *GetParentLayer() {return m_parentLayer;} EUIGroup GetParentLayerGroup() {return m_parentLayer->m_parentGroup->GetGroup();} +#endif +#if defined(__PSVITA__) || defined(_WINDOWS64) + UILayer *GetParentLayer() {return m_parentLayer;} 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 bb56c7809..6b196c1b8 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.cpp @@ -28,17 +28,6 @@ UIScene_AbstractContainerMenu::UIScene_AbstractContainerMenu(int iPad, UILayer * ui.OverrideSFX(m_iPad,ACTION_MENU_DOWN,true); m_bIgnoreInput=false; -#ifdef _WINDOWS64 - m_bMouseDragSlider=false; - m_bHasMousePosition = false; - m_lastMouseX = 0; - m_lastMouseY = 0; - - for (int btn = 0; btn < 3; btn++) - { - KMInput.ConsumeMousePress(btn); - } -#endif } UIScene_AbstractContainerMenu::~UIScene_AbstractContainerMenu() @@ -49,6 +38,16 @@ 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.SetScreenCursorHidden(false); + g_KBMInput.SetCursorHiddenForUI(false); +#endif + Minecraft *pMinecraft = Minecraft::GetInstance(); if( pMinecraft->localgameModes[m_iPad] != NULL ) { @@ -84,6 +83,10 @@ void UIScene_AbstractContainerMenu::InitDataAssociations(int iPad, AbstractConta void UIScene_AbstractContainerMenu::PlatformInitialize(int iPad, int startIndex) { +#ifdef _WINDOWS64 + g_KBMInput.SetScreenCursorHidden(true); + g_KBMInput.SetCursorHiddenForUI(true); +#endif m_labelInventory.init( app.GetString(IDS_INVENTORY) ); @@ -168,6 +171,19 @@ 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); @@ -190,139 +206,15 @@ void UIScene_AbstractContainerMenu::tick() { UIScene::tick(); -#ifdef _WINDOWS64 - bool mouseActive = (m_iPad == 0 && !KMInput.IsCaptured()); - bool drivePointerFromMouse = false; - float rawMouseMovieX = 0, rawMouseMovieY = 0; - int scrollDelta = 0; - // Map Windows mouse position to the virtual pointer in movie coordinates - if (mouseActive) - { - RECT clientRect; - GetClientRect(KMInput.GetHWnd(), &clientRect); - int clientWidth = clientRect.right; - int clientHeight = clientRect.bottom; - if (clientWidth > 0 && clientHeight > 0) - { - int mouseX = KMInput.GetMouseX(); - int mouseY = KMInput.GetMouseY(); - bool mouseMoved = !m_bHasMousePosition || mouseX != m_lastMouseX || mouseY != m_lastMouseY; - - m_bHasMousePosition = true; - m_lastMouseX = mouseX; - m_lastMouseY = mouseY; - scrollDelta = KMInput.ConsumeScrollDelta(); - - // Convert mouse position to movie coordinates using the movie/client ratio - float mx = (float)mouseX * ((float)m_movieWidth / (float)clientWidth) - (float)m_controlMainPanel.getXPos(); - float my = (float)mouseY * ((float)m_movieHeight / (float)clientHeight) - (float)m_controlMainPanel.getYPos(); - - rawMouseMovieX = mx; - rawMouseMovieY = my; - - // Once the mouse has taken over the container cursor, keep following the OS cursor - // until explicit controller input takes ownership back. - drivePointerFromMouse = m_bPointerDrivenByMouse || mouseMoved || KMInput.IsMouseDown(0) || KMInput.IsMouseDown(1) || KMInput.IsMouseDown(2) || scrollDelta != 0; - if (drivePointerFromMouse) - { - m_bPointerDrivenByMouse = true; - m_eCurrTapState = eTapStateNoInput; - m_pointerPos.x = mx; - m_pointerPos.y = my; - } - } - } -#endif - onMouseTick(); -#ifdef _WINDOWS64 - // Dispatch mouse clicks AFTER onMouseTick() has updated m_eCurrSection from the new pointer position - if (mouseActive) - { - if (KMInput.ConsumeMousePress(0)) - { - if (m_eCurrSection == eSectionInventoryCreativeSlider) - { - // Scrollbar click: use raw mouse position (onMouseTick may have snapped m_pointerPos) - m_bMouseDragSlider = true; - m_pointerPos.x = rawMouseMovieX; - m_pointerPos.y = rawMouseMovieY; - handleOtherClicked(m_iPad, eSectionInventoryCreativeSlider, 0, false); - } - else - { - handleKeyDown(m_iPad, ACTION_MENU_A, false); - } - } - else if (m_bMouseDragSlider && KMInput.IsMouseDown(0)) - { - // Continue scrollbar drag: update scroll position from current mouse Y - m_pointerPos.x = rawMouseMovieX; - m_pointerPos.y = rawMouseMovieY; - handleOtherClicked(m_iPad, eSectionInventoryCreativeSlider, 0, false); - } - - if (!KMInput.IsMouseDown(0)) - m_bMouseDragSlider = false; - - if (KMInput.ConsumeMousePress(1)) - { - handleKeyDown(m_iPad, ACTION_MENU_X, false); - } - if (KMInput.ConsumeMousePress(2)) - { - handleKeyDown(m_iPad, ACTION_MENU_Y, false); - } - - // Mouse scroll wheel for page scrolling - if (scrollDelta > 0) - { - handleKeyDown(m_iPad, ACTION_MENU_OTHER_STICK_UP, false); - } - else if (scrollDelta < 0) - { - handleKeyDown(m_iPad, ACTION_MENU_OTHER_STICK_DOWN, false); - } - - // ESC to close — must be last since it may destroy this scene - if (KMInput.ConsumeKeyPress(VK_ESCAPE)) - { - handleKeyDown(m_iPad, ACTION_MENU_B, false); - return; - } - } -#endif - IggyEvent mouseEvent; S32 width, height; m_parentLayer->getRenderDimensions(width, height); -#ifdef _WINDOWS64 - S32 x, y; - if (mouseActive && m_bPointerDrivenByMouse) - { - // Send raw mouse position directly as Iggy event to avoid coordinate round-trip errors - // Scale mouse client coords to the Iggy display space (which was set to getRenderDimensions()) - RECT clientRect; - GetClientRect(KMInput.GetHWnd(), &clientRect); - float mouseMovieX = (float)KMInput.GetMouseX() * ((float)m_movieWidth / (float)clientRect.right); - float mouseMovieY = (float)KMInput.GetMouseY() * ((float)m_movieHeight / (float)clientRect.bottom); - float mouseLocalX = mouseMovieX - (float)m_controlMainPanel.getXPos(); - float mouseLocalY = mouseMovieY - (float)m_controlMainPanel.getYPos(); + S32 x = (S32)(m_pointerPos.x * ((float)width / m_movieWidth)); + S32 y = (S32)(m_pointerPos.y * ((float)height / m_movieHeight)); - x = (S32)(mouseLocalX * ((float)width / m_movieWidth)); - y = (S32)(mouseLocalY * ((float)height / m_movieHeight)); - } - else - { - x = (S32)(m_pointerPos.x * ((float)width / m_movieWidth)); - y = (S32)(m_pointerPos.y * ((float)height / m_movieHeight)); - } -#else - S32 x = m_pointerPos.x*((float)width/m_movieWidth); - S32 y = m_pointerPos.y*((float)height/m_movieHeight); -#endif IggyMakeEventMouseMove( &mouseEvent, x, y); // 4J Stu - This seems to be broken on Durango, so do it ourself diff --git a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h index 812c5b41c..605f5dbdf 100644 --- a/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_AbstractContainerMenu.h @@ -10,12 +10,6 @@ class UIScene_AbstractContainerMenu : public UIScene, public virtual IUIScene_Ab private: ESceneSection m_focusSection; bool m_bIgnoreInput; -#ifdef _WINDOWS64 - bool m_bMouseDragSlider; - bool m_bHasMousePosition; - int m_lastMouseX; - int m_lastMouseY; -#endif protected: UIControl m_controlMainPanel; @@ -42,6 +36,8 @@ 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_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index a5cb9aa25..d3cbe4c30 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -1317,6 +1317,14 @@ void UIScene_LoadOrJoinMenu::handleInput(int iPad, int key, bool repeat, bool pr sendInputToMovie(key, repeat, pressed, released); handled = true; break; + case ACTION_MENU_OTHER_STICK_UP: + sendInputToMovie(ACTION_MENU_UP, repeat, pressed, released); + handled = true; + break; + case ACTION_MENU_OTHER_STICK_DOWN: + sendInputToMovie(ACTION_MENU_DOWN, repeat, pressed, released); + handled = true; + break; } } diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp index 9bf3b983b..0a76a5e5d 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp @@ -4,6 +4,33 @@ #include "..\..\Minecraft.h" #include "..\..\GameRenderer.h" +namespace +{ + const int FOV_MIN = 70; + const int FOV_MAX = 110; + const int FOV_SLIDER_MAX = 100; + + int clampFov(int value) + { + if (value < FOV_MIN) return FOV_MIN; + if (value > FOV_MAX) return FOV_MAX; + return value; + } + + int fovToSliderValue(float fov) + { + int clampedFov = clampFov((int)(fov + 0.5f)); + return ((clampedFov - FOV_MIN) * FOV_SLIDER_MAX) / (FOV_MAX - FOV_MIN); + } + + int sliderValueToFov(int sliderValue) + { + if (sliderValue < 0) sliderValue = 0; + if (sliderValue > FOV_SLIDER_MAX) sliderValue = FOV_SLIDER_MAX; + return FOV_MIN + ((sliderValue * (FOV_MAX - FOV_MIN)) / FOV_SLIDER_MAX); + } +} + UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { // Setup all the Iggy references we need for this scene @@ -22,8 +49,9 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD swprintf( (WCHAR *)TempString, 256, L"%ls: %d%%", app.GetString( IDS_SLIDER_GAMMA ),app.GetGameSettings(m_iPad,eGameSetting_Gamma)); m_sliderGamma.init(TempString,eControl_Gamma,0,100,app.GetGameSettings(m_iPad,eGameSetting_Gamma)); - swprintf((WCHAR*)TempString, 256, L"FOV: %d%%", (int)pMinecraft->gameRenderer->GetFovVal()); - m_sliderFOV.init(TempString, eControl_FOV, 70, 110, (int)pMinecraft->gameRenderer->GetFovVal()); + int initialFov = clampFov((int)(pMinecraft->gameRenderer->GetFovVal() + 0.5f)); + swprintf((WCHAR*)TempString, 256, L"FOV: %d", initialFov); + m_sliderFOV.init(TempString, eControl_FOV, 0, FOV_SLIDER_MAX, fovToSliderValue((float)initialFov)); swprintf( (WCHAR *)TempString, 256, L"%ls: %d%%", app.GetString( IDS_SLIDER_INTERFACEOPACITY ),app.GetGameSettings(m_iPad,eGameSetting_InterfaceOpacity)); m_sliderInterfaceOpacity.init(TempString,eControl_InterfaceOpacity,0,100,app.GetGameSettings(m_iPad,eGameSetting_InterfaceOpacity)); @@ -150,10 +178,12 @@ void UIScene_SettingsGraphicsMenu::handleSliderMove(F64 sliderId, F64 currentVal case eControl_FOV: { + m_sliderFOV.handleSliderMove(value); Minecraft* pMinecraft = Minecraft::GetInstance(); - pMinecraft->gameRenderer->SetFovVal((float)currentValue); + int fovValue = sliderValueToFov(value); + pMinecraft->gameRenderer->SetFovVal((float)fovValue); WCHAR TempString[256]; - swprintf((WCHAR*)TempString, 256, L"FOV: %d%%", (int)currentValue); + swprintf((WCHAR*)TempString, 256, L"FOV: %d", fovValue); m_sliderFOV.setLabel(TempString); } break; diff --git a/Minecraft.Client/Input.cpp b/Minecraft.Client/Input.cpp index 1639d09b8..5e41da299 100644 --- a/Minecraft.Client/Input.cpp +++ b/Minecraft.Client/Input.cpp @@ -7,20 +7,21 @@ #include "Input.h" #include "..\Minecraft.Client\LocalPlayer.h" #include "Options.h" +#ifdef _WINDOWS64 +#include "Windows64\KeyboardMouseInput.h" +#endif Input::Input() { xa = 0; ya = 0; - sprintForward = 0; wasJumping = false; jumping = false; sneaking = false; - usingKeyboardMovement = false; + sprinting = false; lReset = false; rReset = false; - m_gamepadSneaking = false; } void Input::tick(LocalPlayer *player) @@ -32,43 +33,43 @@ void Input::tick(LocalPlayer *player) Minecraft *pMinecraft=Minecraft::GetInstance(); int iPad=player->GetXboxPad(); + float controllerXA = 0.0f; + float controllerYA = 0.0f; + // 4J-PB minecraft movement seems to be the wrong way round, so invert x! if( pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LEFT) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_RIGHT) ) - xa = -InputManager.GetJoypadStick_LX(iPad); - else - xa = 0.0f; + controllerXA = -InputManager.GetJoypadStick_LX(iPad); if( pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_FORWARD) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_BACKWARD) ) - ya = InputManager.GetJoypadStick_LY(iPad); - else - ya = 0.0f; - sprintForward = ya; - usingKeyboardMovement = false; + controllerYA = InputManager.GetJoypadStick_LY(iPad); + float kbXA = 0.0f; + float kbYA = 0.0f; #ifdef _WINDOWS64 - // WASD movement (combine with gamepad) - if (iPad == 0 && KMInput.IsCaptured()) + if (iPad == 0 && g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) { - float kbX = 0.0f, kbY = 0.0f; - if (KMInput.IsKeyDown('W')) { kbY += 1.0f; sprintForward += 1.0f; usingKeyboardMovement = true; } - if (KMInput.IsKeyDown('S')) { kbY -= 1.0f; sprintForward -= 1.0f; usingKeyboardMovement = true; } - if (KMInput.IsKeyDown('A')) { kbX += 1.0f; usingKeyboardMovement = true; } // inverted like gamepad - if (KMInput.IsKeyDown('D')) { kbX -= 1.0f; usingKeyboardMovement = true; } - // Normalize diagonal - if (kbX != 0.0f && kbY != 0.0f) { kbX *= 0.707f; kbY *= 0.707f; } - if (pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LEFT) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_RIGHT)) - xa = max(min(xa + kbX, 1.0f), -1.0f); - if (pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_FORWARD) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_BACKWARD)) - ya = max(min(ya + kbY, 1.0f), -1.0f); + if( pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LEFT) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_RIGHT) ) + kbXA = g_KBMInput.GetMoveX(); + if( pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_FORWARD) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_BACKWARD) ) + kbYA = g_KBMInput.GetMoveY(); } #endif - sprintForward = max(min(sprintForward, 1.0f), -1.0f); + + if (kbXA != 0.0f || kbYA != 0.0f) + { + xa = kbXA; + ya = kbYA; + } + else + { + xa = controllerXA; + ya = controllerYA; + } #ifndef _CONTENT_PACKAGE if (app.GetFreezePlayers()) { xa = ya = 0.0f; - sprintForward = 0.0f; player->abilities.flying = true; } #endif @@ -80,7 +81,6 @@ void Input::tick(LocalPlayer *player) lReset = true; } xa = ya = 0.0f; - sprintForward = 0.0f; } // 4J: In flying mode, don't actually toggle sneaking (unless we're riding in which case we need to sneak to dismount) @@ -88,15 +88,46 @@ void Input::tick(LocalPlayer *player) { if((player->ullButtonsPressed&(1LL<localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_SNEAK_TOGGLE)) { - m_gamepadSneaking=!m_gamepadSneaking; + sneaking=!sneaking; } } - sneaking = m_gamepadSneaking; #ifdef _WINDOWS64 - // Keyboard hold-to-sneak (overrides gamepad toggle) - if (iPad == 0 && KMInput.IsCaptured() && KMInput.IsKeyDown(VK_SHIFT) && !player->abilities.flying) - sneaking = true; + if (iPad == 0 && g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) + { + // 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; + } + } + else if (iPad == 0) + { + sprinting = false; + } #endif if(sneaking) @@ -109,6 +140,7 @@ void Input::tick(LocalPlayer *player) float tx = 0.0f; float ty = 0.0f; + if( pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LOOK_LEFT) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LOOK_RIGHT) ) tx = InputManager.GetJoypadStick_RX(iPad)*(((float)app.GetGameSettings(iPad,eGameSetting_Sensitivity_InGame))/100.0f); // apply sensitivity to look if( pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LOOK_UP) || pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_LOOK_DOWN) ) @@ -132,47 +164,52 @@ void Input::tick(LocalPlayer *player) } tx = ty = 0.0f; } - player->interpolateTurn(tx * abs(tx) * turnSpeed, ty * abs(ty) * turnSpeed); + + float turnX = tx * abs(tx) * turnSpeed; + float turnY = ty * abs(ty) * turnSpeed; #ifdef _WINDOWS64 - // Mouse look is now handled per-frame in Minecraft::applyFrameMouseLook() - // to eliminate the 20Hz tick delay. Only flush any remaining delta here - // as a safety measure. - if (iPad == 0 && KMInput.IsCaptured()) + if (iPad == 0 && g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive()) { - float rawDx, rawDy; - KMInput.ConsumeMouseDelta(rawDx, rawDy); - // Delta should normally be 0 since applyFrameMouseLook() already consumed it - if (rawDx != 0.0f || rawDy != 0.0f) + float mouseSensitivity = ((float)app.GetGameSettings(iPad,eGameSetting_Sensitivity_InGame)) / 100.0f; + float mouseLookScale = 5.0f; + float mx = g_KBMInput.GetLookX(mouseSensitivity * mouseLookScale); + float my = g_KBMInput.GetLookY(mouseSensitivity * mouseLookScale); + + if ( app.GetGameSettings(iPad,eGameSetting_ControlInvertLook) ) { - float mouseSensitivity = ((float)app.GetGameSettings(iPad, eGameSetting_Sensitivity_InGame)) / 100.0f; - float mdx = rawDx * mouseSensitivity; - float mdy = -rawDy * mouseSensitivity; - if (app.GetGameSettings(iPad, eGameSetting_ControlInvertLook)) - mdy = -mdy; - player->interpolateTurn(mdx, mdy); + my = -my; } + + turnX += mx; + turnY += my; } #endif + player->interpolateTurn(turnX, turnY); + //jumping = controller.isButtonPressed(0); - unsigned int jump = InputManager.GetValue(iPad, MINECRAFT_ACTION_JUMP); - if( jump > 0 && pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_JUMP) ) + bool kbJump = false; +#ifdef _WINDOWS64 + kbJump = (iPad == 0) && g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsKBMActive() && g_KBMInput.IsKeyDown(KeyboardMouseInput::KEY_JUMP); +#endif + if( (jump > 0 || kbJump) && pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_JUMP) ) jumping = true; else jumping = false; -#ifdef _WINDOWS64 - // Keyboard jump (Space) - if (iPad == 0 && KMInput.IsCaptured() && KMInput.IsKeyDown(VK_SPACE) && pMinecraft->localgameModes[iPad]->isInputAllowed(MINECRAFT_ACTION_JUMP)) - jumping = true; -#endif - #ifndef _CONTENT_PACKAGE if (app.GetFreezePlayers()) jumping = false; #endif +#ifdef _WINDOWS64 + if (iPad == 0 && g_KBMInput.IsKeyPressed(VK_ESCAPE) && g_KBMInput.IsMouseGrabbed()) + { + g_KBMInput.SetMouseGrabbed(false); + } +#endif + //OutputDebugString("INPUT: End input tick\n"); } diff --git a/Minecraft.Client/Input.h b/Minecraft.Client/Input.h index d8dedd57e..c7e1eec47 100644 --- a/Minecraft.Client/Input.h +++ b/Minecraft.Client/Input.h @@ -6,20 +6,17 @@ class Input public: float xa; float ya; - float sprintForward; bool wasJumping; bool jumping; bool sneaking; - bool usingKeyboardMovement; - - Input(); // 4J - added + bool sprinting; + + Input(); virtual void tick(LocalPlayer *player); private: - bool lReset; bool rReset; - bool m_gamepadSneaking; }; diff --git a/Minecraft.Client/LocalPlayer.cpp b/Minecraft.Client/LocalPlayer.cpp index bb25105f7..77306621f 100644 --- a/Minecraft.Client/LocalPlayer.cpp +++ b/Minecraft.Client/LocalPlayer.cpp @@ -251,13 +251,10 @@ void LocalPlayer::aiStep() if (changingDimensionDelay > 0) changingDimensionDelay--; bool wasJumping = input->jumping; float runTreshold = 0.8f; - float sprintForward = input->sprintForward; - - bool wasRunning = sprintForward >= runTreshold; + bool wasRunning = input->ya >= runTreshold; //input->tick( dynamic_pointer_cast( shared_from_this() ) ); // 4J-PB - make it a localplayer input->tick( this ); - sprintForward = input->sprintForward; if (isUsingItem() && !isRiding()) { input->xa *= 0.2f; @@ -281,25 +278,9 @@ void LocalPlayer::aiStep() // world with low food, then reload it in creative. if(abilities.mayfly || isAllowedToFly() ) enoughFoodToSprint = true; - bool forwardEnoughToTriggerSprint = sprintForward >= runTreshold; - bool forwardReturnedToDeadzone = sprintForward == 0.0f; - bool forwardEnoughToContinueSprint = sprintForward >= runTreshold; - -#ifdef _WINDOWS64 - if (GetXboxPad() == 0 && input->usingKeyboardMovement) - { - forwardEnoughToContinueSprint = sprintForward > 0.0f; - } -#endif - -#ifdef _WINDOWS64 - // Keyboard sprint: Ctrl held while moving forward - if (GetXboxPad() == 0 && input->usingKeyboardMovement && KMInput.IsKeyDown(VK_CONTROL) && sprintForward > 0.0f && - enoughFoodToSprint && !isUsingItem() && !hasEffect(MobEffect::blindness) && onGround) - { - if (!isSprinting()) setSprinting(true); - } -#endif + bool forwardEnoughToTriggerSprint = input->ya >= runTreshold; + bool forwardReturnedToDeadzone = input->ya == 0.0f; + bool forwardEnoughToContinueSprint = input->ya >= runTreshold; // 4J - altered this slightly to make sure that the joypad returns to below returnTreshold in between registering two movements up to runThreshold if (onGround && !isSprinting() && enoughFoodToSprint && !isUsingItem() && !hasEffect(MobEffect::blindness)) @@ -327,6 +308,12 @@ 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() && (!forwardEnoughToContinueSprint || !enoughFoodToSprint || isSneaking() || isUsingItem())) diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index bd75a61a9..19ee79bdb 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -502,6 +502,13 @@ void Minecraft::setScreen(Screen *screen) this->screen->removed(); } +#ifdef _WINDOWS64 + if (screen != NULL && g_KBMInput.IsMouseGrabbed()) + { + g_KBMInput.SetMouseGrabbed(false); + } +#endif + //4J Gordon: Do not force a stats save here /*if (dynamic_cast(screen)!=NULL) { @@ -1184,11 +1191,11 @@ void Minecraft::applyFrameMouseLook() int iPad = localplayers[i]->GetXboxPad(); if (iPad != 0) continue; // Mouse only applies to pad 0 - if (!KMInput.IsCaptured()) continue; + if (!g_KBMInput.IsMouseGrabbed()) continue; if (localgameModes[iPad] == NULL) continue; float rawDx, rawDy; - KMInput.ConsumeMouseDelta(rawDx, rawDy); + g_KBMInput.ConsumeMouseDelta(rawDx, rawDy); if (rawDx == 0.0f && rawDy == 0.0f) continue; float mouseSensitivity = ((float)app.GetGameSettings(iPad, eGameSetting_Sensitivity_InGame)) / 100.0f; @@ -1450,14 +1457,54 @@ void Minecraft::run_middle() // Keyboard/mouse button presses for player 0 if (i == 0) { - if (KMInput.ConsumeKeyPress(VK_ESCAPE)) localplayers[i]->ullButtonsPressed |= 1LL<ullButtonsPressed |= 1LL<ullButtonsPressed |= 1LL<ullButtonsPressed |= 1LL<ullButtonsPressed |= 1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<inventory) + localplayers[i]->inventory->selected = slot; + } + } + } + + // Utility keys always work regardless of KBM active state + if(g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_PAUSE) && !ui.IsTutorialVisible(i)) + { + localplayers[i]->ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<abilities.flying && KMInput.IsKeyDown(VK_SHIFT) && !ui.GetMenuDisplayed(i)) - localplayers[i]->ullButtonsPressed |= 1LL<abilities.flying && !ui.GetMenuDisplayed(i)) + localplayers[i]->ullButtonsPressed|=1LL< 0 && gameMode->isInputAllowed(MINECRAFT_ACTION_LEFT_SCROLL)) wheel += 1; - else if (kbWheel < 0 && gameMode->isInputAllowed(MINECRAFT_ACTION_RIGHT_SCROLL)) wheel -= 1; - - // 1-9 keys for direct hotbar selection - if (gameMode->isInputAllowed(MINECRAFT_ACTION_LEFT_SCROLL)) - { - for (int k = '1'; k <= '9'; k++) - { - if (KMInput.ConsumeKeyPress(k)) - { - player->inventory->selected = k - '1'; - app.SetOpacityTimer(iPad); - break; - } - } - } + wheel = g_KBMInput.GetMouseWheel(); } #endif if (wheel != 0) @@ -3485,33 +3528,20 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) player->handleMouseClick(0); player->lastClickTick[0] = ticks; } -#ifdef _WINDOWS64 - else if (iPad == 0 && KMInput.IsCaptured() && KMInput.ConsumeMousePress(0)) - { - player->handleMouseClick(0); - player->lastClickTick[0] = ticks; - } -#endif - if (InputManager.ButtonDown(iPad, MINECRAFT_ACTION_ACTION) && ticks - player->lastClickTick[0] >= timer->ticksPerSecond / 4) +#ifdef _WINDOWS64 + bool actionHeld = InputManager.ButtonDown(iPad, MINECRAFT_ACTION_ACTION) || (iPad == 0 && g_KBMInput.IsKBMActive() && g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT)); +#else + bool actionHeld = InputManager.ButtonDown(iPad, MINECRAFT_ACTION_ACTION); +#endif + if (actionHeld && ticks - player->lastClickTick[0] >= timer->ticksPerSecond / 4) { //printf("MINECRAFT_ACTION_ACTION ButtonDown"); player->handleMouseClick(0); player->lastClickTick[0] = ticks; } -#ifdef _WINDOWS64 - else if (iPad == 0 && KMInput.IsCaptured() && KMInput.IsMouseDown(0) && ticks - player->lastClickTick[0] >= timer->ticksPerSecond / 4) - { - player->handleMouseClick(0); - player->lastClickTick[0] = ticks; - } -#endif - if(InputManager.ButtonDown(iPad, MINECRAFT_ACTION_ACTION) -#ifdef _WINDOWS64 - || (iPad == 0 && KMInput.IsCaptured() && KMInput.IsMouseDown(0)) -#endif - ) + if(actionHeld) { player->handleMouseDown(0, true ); } @@ -3530,25 +3560,21 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) lastClickTick = ticks; } */ +#ifdef _WINDOWS64 + bool useHeld = InputManager.ButtonDown(iPad, MINECRAFT_ACTION_USE) || (iPad == 0 && g_KBMInput.IsKBMActive() && g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_RIGHT)); +#else + bool useHeld = InputManager.ButtonDown(iPad, MINECRAFT_ACTION_USE); +#endif if( player->isUsingItem() ) { - if(!InputManager.ButtonDown(iPad, MINECRAFT_ACTION_USE) -#ifdef _WINDOWS64 - && !(iPad == 0 && KMInput.IsCaptured() && KMInput.IsMouseDown(1)) -#endif - ) gameMode->releaseUsingItem(player); + if(!useHeld) gameMode->releaseUsingItem(player); } else if( gameMode->isInputAllowed(MINECRAFT_ACTION_USE) ) { -#ifdef _WINDOWS64 - bool useButtonDown = InputManager.ButtonDown(iPad, MINECRAFT_ACTION_USE) || (iPad == 0 && KMInput.IsCaptured() && KMInput.IsMouseDown(1)); -#else - bool useButtonDown = InputManager.ButtonDown(iPad, MINECRAFT_ACTION_USE); -#endif if( player->abilities.instabuild ) { // 4J - attempt to handle click in special creative mode fashion if possible (used for placing blocks at regular intervals) - bool didClick = player->creativeModeHandleMouseClick(1, useButtonDown ); + bool didClick = player->creativeModeHandleMouseClick(1, useHeld ); // If this handler has put us in lastClick_oldRepeat mode then it is because we aren't placing blocks - behave largely as the code used to if( player->lastClickState == LocalPlayer::lastClick_oldRepeat ) { @@ -3560,7 +3586,7 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) else { // Otherwise just the original game code for handling autorepeat - if (useButtonDown && ticks - player->lastClickTick[1] >= timer->ticksPerSecond / 4) + if (useHeld && ticks - player->lastClickTick[1] >= timer->ticksPerSecond / 4) { player->handleMouseClick(1); player->lastClickTick[1] = ticks; @@ -3576,7 +3602,7 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) bool firstClick = ( player->lastClickTick[1] == 0 ); bool autoRepeat = ticks - player->lastClickTick[1] >= timer->ticksPerSecond / 4; if ( player->isRiding() || player->isSprinting() || player->isSleeping() ) autoRepeat = false; - if (useButtonDown ) + if (useHeld ) { // If the player has just exited a bed, then delay the time before a repeat key is allowed without releasing if(player->isSleeping() ) player->lastClickTick[1] = ticks + (timer->ticksPerSecond * 2); diff --git a/Minecraft.Client/Screen.cpp b/Minecraft.Client/Screen.cpp index 6a402b04c..e69b73736 100644 --- a/Minecraft.Client/Screen.cpp +++ b/Minecraft.Client/Screen.cpp @@ -110,13 +110,13 @@ void Screen::updateEvents() // Poll mouse button state and dispatch click/release events for (int btn = 0; btn < 3; btn++) { - if (KMInput.ConsumeMousePress(btn)) + if (g_KBMInput.IsMouseButtonPressed(btn)) { int xm = Mouse::getX() * width / minecraft->width; int ym = height - Mouse::getY() * height / minecraft->height - 1; mouseClicked(xm, ym, btn); } - if (KMInput.ConsumeMouseRelease(btn)) + if (g_KBMInput.IsMouseButtonReleased(btn)) { int xm = Mouse::getX() * width / minecraft->width; int ym = height - Mouse::getY() * height / minecraft->height - 1; @@ -127,7 +127,7 @@ void Screen::updateEvents() // Poll keyboard events for (int vk = 0; vk < 256; vk++) { - if (KMInput.ConsumeKeyPress(vk)) + if (g_KBMInput.IsKeyPressed(vk)) { // Map Windows virtual key to the Keyboard constants used by Screen::keyPressed int mappedKey = -1; @@ -144,7 +144,7 @@ void Screen::updateEvents() else if (vk >= 'A' && vk <= 'Z') { ch = (wchar_t)(vk - 'A' + L'a'); - if (KMInput.IsKeyDown(VK_SHIFT)) ch = (wchar_t)vk; + if (g_KBMInput.IsKeyDown(VK_LSHIFT) || g_KBMInput.IsKeyDown(VK_RSHIFT)) ch = (wchar_t)vk; } else if (vk >= '0' && vk <= '9') ch = (wchar_t)vk; else if (vk == VK_SPACE) ch = L' '; diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.cpp b/Minecraft.Client/Windows64/KeyboardMouseInput.cpp index df57db442..7ab99a719 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.cpp +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.cpp @@ -3,118 +3,158 @@ #ifdef _WINDOWS64 #include "KeyboardMouseInput.h" +#include -KeyboardMouseInput KMInput; +KeyboardMouseInput g_KBMInput; -KeyboardMouseInput::KeyboardMouseInput() - : m_mouseDeltaXAccum(0.0f) - , m_mouseDeltaYAccum(0.0f) - , m_scrollDeltaAccum(0) - , m_captured(false) - , m_hWnd(NULL) - , m_initialized(false) - , m_mouseX(0) - , m_mouseY(0) +extern HWND g_hWnd; + +// Forward declaration +static void ClipCursorToWindow(HWND hWnd); + +void KeyboardMouseInput::Init() { - memset(m_keyState, 0, sizeof(m_keyState)); - memset(m_keyStatePrev, 0, sizeof(m_keyStatePrev)); - memset(m_mouseButtons, 0, sizeof(m_mouseButtons)); - memset(m_mouseButtonsPrev, 0, sizeof(m_mouseButtonsPrev)); + memset(m_keyDown, 0, sizeof(m_keyDown)); + memset(m_keyDownPrev, 0, sizeof(m_keyDownPrev)); memset(m_keyPressedAccum, 0, sizeof(m_keyPressedAccum)); - memset(m_mousePressedAccum, 0, sizeof(m_mousePressedAccum)); - memset(m_mouseReleasedAccum, 0, sizeof(m_mouseReleasedAccum)); -} + memset(m_keyReleasedAccum, 0, sizeof(m_keyReleasedAccum)); + memset(m_keyPressed, 0, sizeof(m_keyPressed)); + memset(m_keyReleased, 0, sizeof(m_keyReleased)); + memset(m_mouseButtonDown, 0, sizeof(m_mouseButtonDown)); + memset(m_mouseButtonDownPrev, 0, sizeof(m_mouseButtonDownPrev)); + memset(m_mouseBtnPressedAccum, 0, sizeof(m_mouseBtnPressedAccum)); + memset(m_mouseBtnReleasedAccum, 0, sizeof(m_mouseBtnReleasedAccum)); + memset(m_mouseBtnPressed, 0, sizeof(m_mouseBtnPressed)); + memset(m_mouseBtnReleased, 0, sizeof(m_mouseBtnReleased)); + m_mouseX = 0; + m_mouseY = 0; + m_mouseDeltaX = 0; + m_mouseDeltaY = 0; + m_mouseDeltaAccumX = 0; + m_mouseDeltaAccumY = 0; + m_mouseWheelAccum = 0; + m_mouseGrabbed = false; + m_cursorHiddenForUI = false; + m_windowFocused = true; + m_hasInput = false; + m_kbmActive = true; + m_screenWantsCursorHidden = false; -KeyboardMouseInput::~KeyboardMouseInput() -{ - if (m_captured) - { - SetCapture(false); - } -} - -void KeyboardMouseInput::Init(HWND hWnd) -{ - m_hWnd = hWnd; - m_initialized = true; - - // Register for raw mouse input RAWINPUTDEVICE rid; - rid.usUsagePage = HID_USAGE_PAGE_GENERIC; - rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.usUsagePage = 0x01; // HID_USAGE_PAGE_GENERIC + rid.usUsage = 0x02; // HID_USAGE_GENERIC_MOUSE rid.dwFlags = 0; - rid.hwndTarget = hWnd; + rid.hwndTarget = g_hWnd; RegisterRawInputDevices(&rid, 1, sizeof(rid)); } +void KeyboardMouseInput::ClearAllState() +{ + memset(m_keyDown, 0, sizeof(m_keyDown)); + memset(m_keyDownPrev, 0, sizeof(m_keyDownPrev)); + memset(m_keyPressedAccum, 0, sizeof(m_keyPressedAccum)); + memset(m_keyReleasedAccum, 0, sizeof(m_keyReleasedAccum)); + memset(m_keyPressed, 0, sizeof(m_keyPressed)); + memset(m_keyReleased, 0, sizeof(m_keyReleased)); + memset(m_mouseButtonDown, 0, sizeof(m_mouseButtonDown)); + memset(m_mouseButtonDownPrev, 0, sizeof(m_mouseButtonDownPrev)); + memset(m_mouseBtnPressedAccum, 0, sizeof(m_mouseBtnPressedAccum)); + memset(m_mouseBtnReleasedAccum, 0, sizeof(m_mouseBtnReleasedAccum)); + memset(m_mouseBtnPressed, 0, sizeof(m_mouseBtnPressed)); + memset(m_mouseBtnReleased, 0, sizeof(m_mouseBtnReleased)); + m_mouseDeltaX = 0; + m_mouseDeltaY = 0; + m_mouseDeltaAccumX = 0; + m_mouseDeltaAccumY = 0; + m_mouseWheelAccum = 0; +} + void KeyboardMouseInput::Tick() { - // Keep cursor pinned to center while captured - if (m_captured) - CenterCursor(); -} + memcpy(m_keyDownPrev, m_keyDown, sizeof(m_keyDown)); + memcpy(m_mouseButtonDownPrev, m_mouseButtonDown, sizeof(m_mouseButtonDown)); -void KeyboardMouseInput::EndFrame() -{ - // Advance previous state for next frame's edge detection. - // Must be called AFTER all per-frame consumers have read IsKeyPressed/Released etc. - memcpy(m_keyStatePrev, m_keyState, sizeof(m_keyState)); - memcpy(m_mouseButtonsPrev, m_mouseButtons, sizeof(m_mouseButtons)); -} + memcpy(m_keyPressed, m_keyPressedAccum, sizeof(m_keyPressedAccum)); + memcpy(m_keyReleased, m_keyReleasedAccum, sizeof(m_keyReleasedAccum)); + memset(m_keyPressedAccum, 0, sizeof(m_keyPressedAccum)); + memset(m_keyReleasedAccum, 0, sizeof(m_keyReleasedAccum)); -void KeyboardMouseInput::OnKeyDown(WPARAM vk) -{ - if (vk < 256) + memcpy(m_mouseBtnPressed, m_mouseBtnPressedAccum, sizeof(m_mouseBtnPressedAccum)); + memcpy(m_mouseBtnReleased, m_mouseBtnReleasedAccum, sizeof(m_mouseBtnReleasedAccum)); + memset(m_mouseBtnPressedAccum, 0, sizeof(m_mouseBtnPressedAccum)); + memset(m_mouseBtnReleasedAccum, 0, sizeof(m_mouseBtnReleasedAccum)); + + m_mouseDeltaX = m_mouseDeltaAccumX; + m_mouseDeltaY = m_mouseDeltaAccumY; + m_mouseDeltaAccumX = 0; + m_mouseDeltaAccumY = 0; + + m_hasInput = (m_mouseDeltaX != 0 || m_mouseDeltaY != 0 || m_mouseWheelAccum != 0); + if (!m_hasInput) { - if (!m_keyState[vk]) m_keyPressedAccum[vk] = true; - m_keyState[vk] = true; - } -} - -void KeyboardMouseInput::OnKeyUp(WPARAM vk) -{ - if (vk < 256) - { - m_keyState[vk] = false; - } -} - -void KeyboardMouseInput::OnRawMouseInput(LPARAM lParam) -{ - if (!m_captured) return; - - UINT dwSize = 0; - GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER)); - - BYTE* lpb = (BYTE*)alloca(dwSize); - if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) - return; - - RAWINPUT* raw = (RAWINPUT*)lpb; - if (raw->header.dwType == RIM_TYPEMOUSE) - { - if (raw->data.mouse.usFlags == MOUSE_MOVE_RELATIVE) + for (int i = 0; i < MAX_KEYS; i++) { - m_mouseDeltaXAccum += (float)raw->data.mouse.lLastX; - m_mouseDeltaYAccum += (float)raw->data.mouse.lLastY; + if (m_keyDown[i]) { m_hasInput = true; break; } } } -} - -void KeyboardMouseInput::OnMouseButton(int button, bool down) -{ - if (ui.IsPauseMenuDisplayed(ProfileManager.GetPrimaryPad())) { return; } - if (button >= 0 && button < 3) + if (!m_hasInput) { - if (down && !m_mouseButtons[button]) m_mousePressedAccum[button] = true; - if (!down && m_mouseButtons[button]) m_mouseReleasedAccum[button] = true; - m_mouseButtons[button] = down; + for (int i = 0; i < MAX_MOUSE_BUTTONS; i++) + { + if (m_mouseButtonDown[i]) { m_hasInput = true; break; } + } + } + + if ((m_mouseGrabbed || m_cursorHiddenForUI) && 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); } } -void KeyboardMouseInput::OnMouseWheel(int delta) +void KeyboardMouseInput::OnKeyDown(int vkCode) { - m_scrollDeltaAccum += delta; + if (vkCode >= 0 && vkCode < MAX_KEYS) + { + if (!m_keyDown[vkCode]) + m_keyPressedAccum[vkCode] = true; + m_keyDown[vkCode] = true; + } +} + +void KeyboardMouseInput::OnKeyUp(int vkCode) +{ + if (vkCode >= 0 && vkCode < MAX_KEYS) + { + if (m_keyDown[vkCode]) + m_keyReleasedAccum[vkCode] = true; + m_keyDown[vkCode] = false; + } +} + +void KeyboardMouseInput::OnMouseButtonDown(int button) +{ + if (button >= 0 && button < MAX_MOUSE_BUTTONS) + { + if (!m_mouseButtonDown[button]) + m_mouseBtnPressedAccum[button] = true; + m_mouseButtonDown[button] = true; + } +} + +void KeyboardMouseInput::OnMouseButtonUp(int button) +{ + if (button >= 0 && button < MAX_MOUSE_BUTTONS) + { + if (m_mouseButtonDown[button]) + m_mouseBtnReleasedAccum[button] = true; + m_mouseButtonDown[button] = false; + } } void KeyboardMouseInput::OnMouseMove(int x, int y) @@ -123,139 +163,193 @@ void KeyboardMouseInput::OnMouseMove(int x, int y) m_mouseY = y; } -int KeyboardMouseInput::GetMouseX() const { return m_mouseX; } -int KeyboardMouseInput::GetMouseY() const { return m_mouseY; } -HWND KeyboardMouseInput::GetHWnd() const { return m_hWnd; } - -void KeyboardMouseInput::ClearAllState() +void KeyboardMouseInput::OnMouseWheel(int delta) { - memset(m_keyState, 0, sizeof(m_keyState)); - memset(m_mouseButtons, 0, sizeof(m_mouseButtons)); - memset(m_keyPressedAccum, 0, sizeof(m_keyPressedAccum)); - memset(m_mousePressedAccum, 0, sizeof(m_mousePressedAccum)); - memset(m_mouseReleasedAccum, 0, sizeof(m_mouseReleasedAccum)); - m_mouseDeltaXAccum = 0.0f; - m_mouseDeltaYAccum = 0.0f; - m_scrollDeltaAccum = 0; + // Normalize from raw Windows delta (multiples of WHEEL_DELTA=120) to discrete notch counts + m_mouseWheelAccum += delta / WHEEL_DELTA; } -// Per-frame key queries -bool KeyboardMouseInput::IsKeyDown(int vk) const +int KeyboardMouseInput::GetMouseWheel() { - if (vk < 0 || vk >= 256) return false; - return m_keyState[vk]; + int val = m_mouseWheelAccum; + m_mouseWheelAccum = 0; + return val; } -bool KeyboardMouseInput::IsKeyPressed(int vk) const +void KeyboardMouseInput::OnRawMouseDelta(int dx, int dy) { - if (vk < 0 || vk >= 256) return false; - return m_keyState[vk] && !m_keyStatePrev[vk]; + m_mouseDeltaAccumX += dx; + m_mouseDeltaAccumY += dy; } -bool KeyboardMouseInput::IsKeyReleased(int vk) const +bool KeyboardMouseInput::IsKeyDown(int vkCode) const { - if (vk < 0 || vk >= 256) return false; - return !m_keyState[vk] && m_keyStatePrev[vk]; + if (vkCode >= 0 && vkCode < MAX_KEYS) + return m_keyDown[vkCode]; + return false; } -// Per-frame mouse button queries -bool KeyboardMouseInput::IsMouseDown(int btn) const +bool KeyboardMouseInput::IsKeyPressed(int vkCode) const { - if (btn < 0 || btn >= 3) return false; - return m_mouseButtons[btn]; + if (vkCode >= 0 && vkCode < MAX_KEYS) + return m_keyPressed[vkCode]; + return false; } -bool KeyboardMouseInput::IsMousePressed(int btn) const +bool KeyboardMouseInput::IsKeyReleased(int vkCode) const { - if (btn < 0 || btn >= 3) return false; - return m_mouseButtons[btn] && !m_mouseButtonsPrev[btn]; + if (vkCode >= 0 && vkCode < MAX_KEYS) + return m_keyReleased[vkCode]; + return false; } -bool KeyboardMouseInput::IsMouseReleased(int btn) const +bool KeyboardMouseInput::IsMouseButtonDown(int button) const { - if (btn < 0 || btn >= 3) return false; - return !m_mouseButtons[btn] && m_mouseButtonsPrev[btn]; + if (button >= 0 && button < MAX_MOUSE_BUTTONS) + return m_mouseButtonDown[button]; + return false; } -// Game-tick consume methods -bool KeyboardMouseInput::ConsumeKeyPress(int vk) +bool KeyboardMouseInput::IsMouseButtonPressed(int button) const { - if (vk < 0 || vk >= 256) return false; - bool pressed = m_keyPressedAccum[vk]; - m_keyPressedAccum[vk] = false; - return pressed; + if (button >= 0 && button < MAX_MOUSE_BUTTONS) + return m_mouseBtnPressed[button]; + return false; } -bool KeyboardMouseInput::ConsumeMousePress(int btn) +bool KeyboardMouseInput::IsMouseButtonReleased(int button) const { - if (btn < 0 || btn >= 3) return false; - bool pressed = m_mousePressedAccum[btn]; - m_mousePressedAccum[btn] = false; - return pressed; -} - -bool KeyboardMouseInput::ConsumeMouseRelease(int btn) -{ - if (btn < 0 || btn >= 3) return false; - bool released = m_mouseReleasedAccum[btn]; - m_mouseReleasedAccum[btn] = false; - return released; + if (button >= 0 && button < MAX_MOUSE_BUTTONS) + return m_mouseBtnReleased[button]; + return false; } void KeyboardMouseInput::ConsumeMouseDelta(float &dx, float &dy) { - dx = m_mouseDeltaXAccum; - dy = m_mouseDeltaYAccum; - m_mouseDeltaXAccum = 0.0f; - m_mouseDeltaYAccum = 0.0f; + dx = (float)m_mouseDeltaAccumX; + dy = (float)m_mouseDeltaAccumY; + m_mouseDeltaAccumX = 0; + m_mouseDeltaAccumY = 0; } -int KeyboardMouseInput::ConsumeScrollDelta() +void KeyboardMouseInput::SetMouseGrabbed(bool grabbed) { - int delta = m_scrollDeltaAccum; - m_scrollDeltaAccum = 0; - return delta; -} + if (m_mouseGrabbed == grabbed) + return; -// Mouse capture -void KeyboardMouseInput::SetCapture(bool capture) -{ - if (capture == m_captured) return; - m_captured = capture; - - if (capture) + m_mouseGrabbed = grabbed; + if (grabbed && g_hWnd) { - ShowCursor(FALSE); - RECT rect; - GetClientRect(m_hWnd, &rect); - POINT topLeft = { rect.left, rect.top }; - POINT bottomRight = { rect.right, rect.bottom }; - ClientToScreen(m_hWnd, &topLeft); - ClientToScreen(m_hWnd, &bottomRight); - RECT screenRect = { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y }; - ClipCursor(&screenRect); - CenterCursor(); + while (ShowCursor(FALSE) >= 0) {} + ClipCursorToWindow(g_hWnd); - // Flush accumulated deltas so the snap-to-center doesn't cause a jump - m_mouseDeltaXAccum = 0.0f; - m_mouseDeltaYAccum = 0.0f; + 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 + else if (!grabbed && !m_cursorHiddenForUI && g_hWnd) { - ShowCursor(TRUE); + while (ShowCursor(TRUE) < 0) {} ClipCursor(NULL); } } -bool KeyboardMouseInput::IsCaptured() const { return m_captured; } - -void KeyboardMouseInput::CenterCursor() +void KeyboardMouseInput::SetCursorHiddenForUI(bool hidden) { - RECT rect; - GetClientRect(m_hWnd, &rect); - POINT center = { (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2 }; - ClientToScreen(m_hWnd, ¢er); - SetCursorPos(center.x, center.y); + 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) +{ + if (!hWnd) return; + RECT rc; + GetClientRect(hWnd, &rc); + POINT topLeft = { rc.left, rc.top }; + POINT bottomRight = { rc.right, rc.bottom }; + ClientToScreen(hWnd, &topLeft); + ClientToScreen(hWnd, &bottomRight); + RECT clipRect = { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y }; + ClipCursor(&clipRect); +} + +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); + } + } + else + { + while (ShowCursor(TRUE) < 0) {} + ClipCursor(NULL); + } +} + +float KeyboardMouseInput::GetMoveX() const +{ + float x = 0.0f; + if (m_keyDown[KEY_LEFT]) x += 1.0f; + if (m_keyDown[KEY_RIGHT]) x -= 1.0f; + return x; +} + +float KeyboardMouseInput::GetMoveY() const +{ + float y = 0.0f; + if (m_keyDown[KEY_FORWARD]) y += 1.0f; + if (m_keyDown[KEY_BACKWARD]) y -= 1.0f; + return y; +} + +float KeyboardMouseInput::GetLookX(float sensitivity) const +{ + return (float)m_mouseDeltaX * sensitivity; +} + +float KeyboardMouseInput::GetLookY(float sensitivity) const +{ + return (float)(-m_mouseDeltaY) * sensitivity; } #endif // _WINDOWS64 diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.h b/Minecraft.Client/Windows64/KeyboardMouseInput.h index a09843f90..f098ccabd 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.h +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.h @@ -4,88 +4,130 @@ #include -// HID usage page and usage for raw input registration -#ifndef HID_USAGE_PAGE_GENERIC -#define HID_USAGE_PAGE_GENERIC ((USHORT)0x01) -#endif -#ifndef HID_USAGE_GENERIC_MOUSE -#define HID_USAGE_GENERIC_MOUSE ((USHORT)0x02) -#endif - class KeyboardMouseInput { public: - KeyboardMouseInput(); - ~KeyboardMouseInput(); + static const int MAX_KEYS = 256; - void Init(HWND hWnd); + static const int MOUSE_LEFT = 0; + static const int MOUSE_RIGHT = 1; + static const int MOUSE_MIDDLE = 2; + static const int MAX_MOUSE_BUTTONS = 3; + + static const int KEY_FORWARD = 'W'; + static const int KEY_BACKWARD = 'S'; + static const int KEY_LEFT = 'A'; + 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 = 'C'; + static const int KEY_CRAFTING_ALT = 'R'; + static const int KEY_CONFIRM = VK_RETURN; + static const int KEY_CANCEL = VK_ESCAPE; + static const int KEY_PAUSE = VK_ESCAPE; + static const int KEY_THIRD_PERSON = VK_F5; + static const int KEY_DEBUG_INFO = VK_F3; + + void Init(); void Tick(); - void EndFrame(); - - // Called from WndProc - void OnKeyDown(WPARAM vk); - void OnKeyUp(WPARAM vk); - void OnRawMouseInput(LPARAM lParam); - void OnMouseButton(int button, bool down); - void OnMouseWheel(int delta); void ClearAllState(); - // Per-frame edge detection (for UI / per-frame logic like Alt toggle) - bool IsKeyDown(int vk) const; - bool IsKeyPressed(int vk) const; - bool IsKeyReleased(int vk) const; - bool IsMouseDown(int btn) const; - bool IsMousePressed(int btn) const; - bool IsMouseReleased(int btn) const; - - // Game-tick consume methods: accumulate across frames, clear on read. - // Use these from code that runs at game tick rate (20Hz). - bool ConsumeKeyPress(int vk); - bool ConsumeMousePress(int btn); - bool ConsumeMouseRelease(int btn); - void ConsumeMouseDelta(float &dx, float &dy); - int ConsumeScrollDelta(); - - // Absolute cursor position (client-area coordinates, for GUI when not captured) + void OnKeyDown(int vkCode); + void OnKeyUp(int vkCode); + void OnMouseButtonDown(int button); + void OnMouseButtonUp(int button); void OnMouseMove(int x, int y); - int GetMouseX() const; - int GetMouseY() const; - HWND GetHWnd() const; + void OnMouseWheel(int delta); + void OnRawMouseDelta(int dx, int dy); - // Mouse capture for FPS look - void SetCapture(bool capture); - bool IsCaptured() const; + bool IsKeyDown(int vkCode) const; + bool IsKeyPressed(int vkCode) const; + bool IsKeyReleased(int vkCode) const; + + bool IsMouseButtonDown(int button) const; + bool IsMouseButtonPressed(int button) const; + bool IsMouseButtonReleased(int button) const; + + int GetMouseX() const { return m_mouseX; } + int GetMouseY() const { return m_mouseY; } + + int GetMouseDeltaX() const { return m_mouseDeltaX; } + int GetMouseDeltaY() const { return m_mouseDeltaY; } + + int GetMouseWheel(); + int PeekMouseWheel() const { return m_mouseWheelAccum; } + void ConsumeMouseWheel() { m_mouseWheelAccum = 0; } + + // Per-frame delta consumption for low-latency mouse look. + // Reads and clears the raw accumulators (not the per-tick snapshot). + void ConsumeMouseDelta(float &dx, float &dy); + + 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; } + + bool HasAnyInput() const { return m_hasInput; } + + void SetKBMActive(bool active) { m_kbmActive = active; } + bool IsKBMActive() const { return m_kbmActive; } + + void SetScreenCursorHidden(bool hidden) { m_screenWantsCursorHidden = hidden; } + bool IsScreenCursorHidden() const { return m_screenWantsCursorHidden; } + + float GetMoveX() const; + float GetMoveY() const; + + float GetLookX(float sensitivity) const; + float GetLookY(float sensitivity) const; private: - void CenterCursor(); + bool m_keyDown[MAX_KEYS]; + bool m_keyDownPrev[MAX_KEYS]; - // Per-frame double-buffered state (for IsKeyPressed/Released per-frame edge detection) - bool m_keyState[256]; - bool m_keyStatePrev[256]; - bool m_mouseButtons[3]; - bool m_mouseButtonsPrev[3]; + bool m_keyPressedAccum[MAX_KEYS]; + bool m_keyReleasedAccum[MAX_KEYS]; + bool m_keyPressed[MAX_KEYS]; + bool m_keyReleased[MAX_KEYS]; - // Sticky press accumulators (persist until consumed by game tick) - bool m_keyPressedAccum[256]; - bool m_mousePressedAccum[3]; - bool m_mouseReleasedAccum[3]; + bool m_mouseButtonDown[MAX_MOUSE_BUTTONS]; + bool m_mouseButtonDownPrev[MAX_MOUSE_BUTTONS]; - // Mouse delta accumulators (persist until consumed by game tick) - float m_mouseDeltaXAccum; - float m_mouseDeltaYAccum; + bool m_mouseBtnPressedAccum[MAX_MOUSE_BUTTONS]; + bool m_mouseBtnReleasedAccum[MAX_MOUSE_BUTTONS]; + bool m_mouseBtnPressed[MAX_MOUSE_BUTTONS]; + bool m_mouseBtnReleased[MAX_MOUSE_BUTTONS]; - // Scroll accumulator (persists until consumed by game tick) - int m_scrollDeltaAccum; - - bool m_captured; - HWND m_hWnd; - bool m_initialized; - - // Absolute cursor position in client coordinates int m_mouseX; int m_mouseY; + + int m_mouseDeltaX; + int m_mouseDeltaY; + int m_mouseDeltaAccumX; + int m_mouseDeltaAccumY; + + int m_mouseWheelAccum; + + bool m_mouseGrabbed; + + bool m_cursorHiddenForUI; + + bool m_windowFocused; + + bool m_hasInput; + + bool m_kbmActive; + + bool m_screenWantsCursorHidden; }; -extern KeyboardMouseInput KMInput; +extern KeyboardMouseInput g_KBMInput; #endif // _WINDOWS64 diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index ec89feb18..0f83fae57 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -501,82 +501,90 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) PostQuitMessage(0); break; - // Keyboard/Mouse input handling - case WM_KEYDOWN: - if (!(lParam & 0x40000000)) // ignore auto-repeat - KMInput.OnKeyDown(wParam); - break; - case WM_KEYUP: - KMInput.OnKeyUp(wParam); - break; - case WM_SYSKEYDOWN: - if (wParam == VK_MENU) // Alt key - { - if (!(lParam & 0x40000000)) - KMInput.OnKeyDown(wParam); - return 0; // prevent default Alt behavior - } - return DefWindowProc(hWnd, message, wParam, lParam); - case WM_SYSKEYUP: - if (wParam == VK_MENU) - { - KMInput.OnKeyUp(wParam); - return 0; - } - return DefWindowProc(hWnd, message, wParam, lParam); - case WM_INPUT: - KMInput.OnRawMouseInput(lParam); - break; - case WM_LBUTTONDOWN: - KMInput.OnMouseButton(0, true); - break; - case WM_LBUTTONUP: - KMInput.OnMouseButton(0, false); - break; - case WM_RBUTTONDOWN: - KMInput.OnMouseButton(1, true); - break; - case WM_RBUTTONUP: - KMInput.OnMouseButton(1, false); - break; - case WM_MBUTTONDOWN: - KMInput.OnMouseButton(2, true); - break; - case WM_MBUTTONUP: - KMInput.OnMouseButton(2, false); - break; - case WM_MOUSEWHEEL: - KMInput.OnMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); - break; - case WM_MOUSEMOVE: - KMInput.OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); - break; - case WM_ACTIVATE: - if (LOWORD(wParam) == WA_INACTIVE) - KMInput.SetCapture(false); - break; - case WM_SETFOCUS: - { - // Re-capture when window receives focus (e.g., after clicking on it) - Minecraft *pMinecraft = Minecraft::GetInstance(); - bool shouldCapture = pMinecraft && app.GetGameStarted() && !ui.GetMenuDisplayed(0) && pMinecraft->screen == NULL; - if (shouldCapture) - KMInput.SetCapture(true); - } - break; case WM_KILLFOCUS: - KMInput.SetCapture(false); - KMInput.ClearAllState(); + g_KBMInput.ClearAllState(); + g_KBMInput.SetWindowFocused(false); + if (g_KBMInput.IsMouseGrabbed()) + g_KBMInput.SetMouseGrabbed(false); break; - case WM_SETCURSOR: - // Hide the OS cursor when an Iggy/Flash menu is displayed (it has its own Flash cursor) - if (LOWORD(lParam) == HTCLIENT && !KMInput.IsCaptured() && ui.GetMenuDisplayed(0)) + case WM_SETFOCUS: + g_KBMInput.SetWindowFocused(true); + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + { + int vk = (int)wParam; + if (lParam & 0x40000000) break; // ignore auto-repeat + if (vk == VK_SHIFT) + vk = (MapVirtualKey((lParam >> 16) & 0xFF, MAPVK_VSC_TO_VK_EX) == VK_RSHIFT) ? VK_RSHIFT : VK_LSHIFT; + else if (vk == VK_CONTROL) + vk = (lParam & (1 << 24)) ? VK_RCONTROL : VK_LCONTROL; + else if (vk == VK_MENU) + vk = (lParam & (1 << 24)) ? VK_RMENU : VK_LMENU; + g_KBMInput.OnKeyDown(vk); + break; + } + case WM_KEYUP: + case WM_SYSKEYUP: + { + int vk = (int)wParam; + if (vk == VK_SHIFT) + vk = (MapVirtualKey((lParam >> 16) & 0xFF, MAPVK_VSC_TO_VK_EX) == VK_RSHIFT) ? VK_RSHIFT : VK_LSHIFT; + else if (vk == VK_CONTROL) + vk = (lParam & (1 << 24)) ? VK_RCONTROL : VK_LCONTROL; + else if (vk == VK_MENU) + vk = (lParam & (1 << 24)) ? VK_RMENU : VK_LMENU; + g_KBMInput.OnKeyUp(vk); + break; + } + + case WM_LBUTTONDOWN: + g_KBMInput.OnMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT); + break; + case WM_LBUTTONUP: + g_KBMInput.OnMouseButtonUp(KeyboardMouseInput::MOUSE_LEFT); + break; + case WM_RBUTTONDOWN: + g_KBMInput.OnMouseButtonDown(KeyboardMouseInput::MOUSE_RIGHT); + break; + case WM_RBUTTONUP: + g_KBMInput.OnMouseButtonUp(KeyboardMouseInput::MOUSE_RIGHT); + break; + case WM_MBUTTONDOWN: + g_KBMInput.OnMouseButtonDown(KeyboardMouseInput::MOUSE_MIDDLE); + break; + case WM_MBUTTONUP: + g_KBMInput.OnMouseButtonUp(KeyboardMouseInput::MOUSE_MIDDLE); + break; + + case WM_MOUSEMOVE: + g_KBMInput.OnMouseMove(LOWORD(lParam), HIWORD(lParam)); + break; + + case WM_MOUSEWHEEL: + g_KBMInput.OnMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); + break; + + case WM_INPUT: { - SetCursor(NULL); - return TRUE; + UINT dwSize = 0; + GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER)); + if (dwSize > 0 && dwSize <= 256) + { + BYTE rawBuffer[256]; + if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, rawBuffer, &dwSize, sizeof(RAWINPUTHEADER)) == dwSize) + { + RAWINPUT* raw = (RAWINPUT*)rawBuffer; + if (raw->header.dwType == RIM_TYPEMOUSE) + { + g_KBMInput.OnRawMouseDelta(raw->data.mouse.lLastX, raw->data.mouse.lLastY); + } + } + } } - return DefWindowProc(hWnd, message, wParam, lParam); + break; default: return DefWindowProc(hWnd, message, wParam, lParam); } @@ -852,6 +860,9 @@ void ToggleFullscreen() SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); } g_isFullscreen = !g_isFullscreen; + + if (g_KBMInput.IsWindowFocused()) + g_KBMInput.SetWindowFocused(true); } //-------------------------------------------------------------------------------------- @@ -877,7 +888,7 @@ static Minecraft* InitialiseMinecraftRuntime() ui.init(g_pd3dDevice, g_pImmediateContext, g_pRenderTargetView, g_pDepthStencilView, g_iScreenWidth, g_iScreenHeight); InputManager.Initialise(1, 3, MINECRAFT_ACTION_MAX, ACTION_MAX_MENU); - KMInput.Init(g_hWnd); + g_KBMInput.Init(); DefineActions(); InputManager.SetJoypadMapVal(0, 0); InputManager.SetKeyRepeatRate(0.3f, 0.2f); @@ -1263,12 +1274,16 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, MSG msg = {0}; while( WM_QUIT != msg.message && !app.m_bShutdown) { - if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) + g_KBMInput.Tick(); + + while( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); - continue; + if (msg.message == WM_QUIT) break; } + if (msg.message == WM_QUIT) break; + RenderManager.StartFrame(); #if 0 if(pMinecraft->soundEngine->isStreamingWavebankReady() && @@ -1290,7 +1305,34 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, app.UpdateTime(); PIXBeginNamedEvent(0,"Input manager tick"); InputManager.Tick(); - KMInput.Tick(); + + // Detect KBM vs controller input mode + if (InputManager.IsPadConnected(0)) + { + bool controllerUsed = InputManager.ButtonPressed(0) || + InputManager.GetJoypadStick_LX(0, false) != 0.0f || + InputManager.GetJoypadStick_LY(0, false) != 0.0f || + InputManager.GetJoypadStick_RX(0, false) != 0.0f || + InputManager.GetJoypadStick_RY(0, false) != 0.0f; + + if (controllerUsed) + g_KBMInput.SetKBMActive(false); + else if (g_KBMInput.HasAnyInput()) + g_KBMInput.SetKBMActive(true); + } + else + { + g_KBMInput.SetKBMActive(true); + } + + if (!g_KBMInput.IsMouseGrabbed()) + { + if (!g_KBMInput.IsKBMActive()) + g_KBMInput.SetCursorHiddenForUI(true); + else if (!g_KBMInput.IsScreenCursorHidden()) + g_KBMInput.SetCursorHiddenForUI(false); + } + PIXEndNamedEvent(); PIXBeginNamedEvent(0,"Profile manager tick"); // ProfileManager.Tick(); @@ -1420,29 +1462,29 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, ui.CheckMenuDisplayed(); - // Update mouse capture: capture when in-game and no menu is open + // Update mouse grab: grab when in-game and no menu is open { static bool altToggleSuppressCapture = false; bool shouldCapture = app.GetGameStarted() && !ui.GetMenuDisplayed(0) && pMinecraft->screen == NULL; // Left Alt key toggles capture on/off for debugging - if (KMInput.IsKeyPressed(VK_MENU)) + if (g_KBMInput.IsKeyPressed(VK_LMENU) || g_KBMInput.IsKeyPressed(VK_RMENU)) { - if (KMInput.IsCaptured()) { KMInput.SetCapture(false); altToggleSuppressCapture = true; } - else if (shouldCapture) { KMInput.SetCapture(true); altToggleSuppressCapture = false; } + if (g_KBMInput.IsMouseGrabbed()) { g_KBMInput.SetMouseGrabbed(false); altToggleSuppressCapture = true; } + else if (shouldCapture) { g_KBMInput.SetMouseGrabbed(true); altToggleSuppressCapture = false; } } else if (!shouldCapture) { - if (KMInput.IsCaptured()) KMInput.SetCapture(false); + if (g_KBMInput.IsMouseGrabbed()) g_KBMInput.SetMouseGrabbed(false); altToggleSuppressCapture = false; } - else if (shouldCapture && !KMInput.IsCaptured() && GetFocus() == g_hWnd && !altToggleSuppressCapture) + else if (shouldCapture && !g_KBMInput.IsMouseGrabbed() && GetFocus() == g_hWnd && !altToggleSuppressCapture) { - KMInput.SetCapture(true); + g_KBMInput.SetMouseGrabbed(true); } } // F1 toggles the HUD - if (KMInput.IsKeyPressed(VK_F1)) + if (g_KBMInput.IsKeyPressed(VK_F1)) { int primaryPad = ProfileManager.GetPrimaryPad(); unsigned char displayHud = app.GetGameSettings(primaryPad, eGameSetting_DisplayHUD); @@ -1451,7 +1493,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } // F3 toggles onscreen debug info - if (KMInput.IsKeyPressed(VK_F3)) + if (g_KBMInput.IsKeyPressed(VK_F3)) { if (Minecraft* pMinecraft = Minecraft::GetInstance()) { @@ -1464,7 +1506,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, #ifdef _DEBUG_MENUS_ENABLED // F4 Open debug overlay - if (KMInput.IsKeyPressed(VK_F4)) + if (g_KBMInput.IsKeyPressed(VK_F4)) { if (Minecraft *pMinecraft = Minecraft::GetInstance()) { @@ -1477,7 +1519,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } // F6 Open debug console - if (KMInput.IsKeyPressed(VK_F6)) + if (g_KBMInput.IsKeyPressed(VK_F6)) { static bool s_debugConsole = false; s_debugConsole = !s_debugConsole; @@ -1486,13 +1528,13 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, #endif // F11 Toggle fullscreen - if (KMInput.IsKeyPressed(VK_F11)) + if (g_KBMInput.IsKeyPressed(VK_F11)) { ToggleFullscreen(); } // TAB opens game info menu. - Vvis :3 - Updated by detectiveren - if (KMInput.IsKeyPressed(VK_TAB) && !ui.GetMenuDisplayed(0)) + if (g_KBMInput.IsKeyPressed(VK_TAB) && !ui.GetMenuDisplayed(0)) { if (Minecraft* pMinecraft = Minecraft::GetInstance()) { @@ -1593,8 +1635,6 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // Fix for #7318 - Title crashes after short soak in the leaderboards menu // A memory leak was caused because the icon renderer kept creating new Vec3's because the pool wasn't reset Vec3::resetPool(); - - KMInput.EndFrame(); } // Free resources, unregister custom classes, and exit. diff --git a/Minecraft.Client/stubs.cpp b/Minecraft.Client/stubs.cpp index 16215ba2f..af65eb75e 100644 --- a/Minecraft.Client/stubs.cpp +++ b/Minecraft.Client/stubs.cpp @@ -3,41 +3,98 @@ #ifdef _WINDOWS64 #include "Windows64\KeyboardMouseInput.h" +static const int s_keyToVK[] = { + 'A', // KEY_A = 0 + 'B', // KEY_B = 1 + 'C', // KEY_C = 2 + 'D', // KEY_D = 3 + 'E', // KEY_E = 4 + 'F', // KEY_F = 5 + 'G', // KEY_G = 6 + 'H', // KEY_H = 7 + 'I', // KEY_I = 8 + 'J', // KEY_J = 9 + 'K', // KEY_K = 10 + 'L', // KEY_L = 11 + 'M', // KEY_M = 12 + 'N', // KEY_N = 13 + 'O', // KEY_O = 14 + 'P', // KEY_P = 15 + 'Q', // KEY_Q = 16 + 'R', // KEY_R = 17 + 'S', // KEY_S = 18 + 'T', // KEY_T = 19 + 'U', // KEY_U = 20 + 'V', // KEY_V = 21 + 'W', // KEY_W = 22 + 'X', // KEY_X = 23 + 'Y', // KEY_Y = 24 + 'Z', // KEY_Z = 25 + VK_SPACE, // KEY_SPACE = 26 + VK_LSHIFT, // KEY_LSHIFT = 27 + VK_ESCAPE, // KEY_ESCAPE = 28 + VK_BACK, // KEY_BACK = 29 + VK_RETURN, // KEY_RETURN = 30 + VK_RSHIFT, // KEY_RSHIFT = 31 + VK_UP, // KEY_UP = 32 + VK_DOWN, // KEY_DOWN = 33 + VK_TAB, // KEY_TAB = 34 + '1', // KEY_1 = 35 + '2', // KEY_2 = 36 + '3', // KEY_3 = 37 + '4', // KEY_4 = 38 + '5', // KEY_5 = 39 + '6', // KEY_6 = 40 + '7', // KEY_7 = 41 + '8', // KEY_8 = 42 + '9', // KEY_9 = 43 + VK_F1, // KEY_F1 = 44 + VK_F3, // KEY_F3 = 45 + VK_F4, // KEY_F4 = 46 + VK_F5, // KEY_F5 = 47 + VK_F6, // KEY_F6 = 48 + VK_F8, // KEY_F8 = 49 + VK_F9, // KEY_F9 = 50 + VK_F11, // KEY_F11 = 51 + VK_ADD, // KEY_ADD = 52 + VK_SUBTRACT,// KEY_SUBTRACT = 53 + VK_LEFT, // KEY_LEFT = 54 + VK_RIGHT, // KEY_RIGHT = 55 +}; +static const int s_keyToVKCount = sizeof(s_keyToVK) / sizeof(s_keyToVK[0]); + +int Keyboard::toVK(int keyConst) +{ + if (keyConst >= 0 && keyConst < s_keyToVKCount) + return s_keyToVK[keyConst]; + return 0; +} + +bool Keyboard::isKeyDown(int keyCode) +{ + int vk = toVK(keyCode); + if (vk > 0) + return g_KBMInput.IsKeyDown(vk); + return false; +} + int Mouse::getX() { - return KMInput.GetMouseX(); + return g_KBMInput.GetMouseX(); } int Mouse::getY() { // Return Y in bottom-up coordinates (OpenGL convention, matching original Java LWJGL Mouse) + extern HWND g_hWnd; RECT rect; - GetClientRect(KMInput.GetHWnd(), &rect); - return (rect.bottom - 1) - KMInput.GetMouseY(); + GetClientRect(g_hWnd, &rect); + return (rect.bottom - 1) - g_KBMInput.GetMouseY(); } bool Mouse::isButtonDown(int button) { - return KMInput.IsMouseDown(button); -} - -bool Keyboard::isKeyDown(int key) -{ - // Map Keyboard constants to Windows virtual key codes - if (key == Keyboard::KEY_LSHIFT) return KMInput.IsKeyDown(VK_LSHIFT); - if (key == Keyboard::KEY_RSHIFT) return KMInput.IsKeyDown(VK_RSHIFT); - if (key == Keyboard::KEY_ESCAPE) return KMInput.IsKeyDown(VK_ESCAPE); - if (key == Keyboard::KEY_RETURN) return KMInput.IsKeyDown(VK_RETURN); - if (key == Keyboard::KEY_BACK) return KMInput.IsKeyDown(VK_BACK); - if (key == Keyboard::KEY_SPACE) return KMInput.IsKeyDown(VK_SPACE); - if (key == Keyboard::KEY_TAB) return KMInput.IsKeyDown(VK_TAB); - if (key == Keyboard::KEY_UP) return KMInput.IsKeyDown(VK_UP); - if (key == Keyboard::KEY_DOWN) return KMInput.IsKeyDown(VK_DOWN); - if (key == Keyboard::KEY_LEFT) return KMInput.IsKeyDown(VK_LEFT); - if (key == Keyboard::KEY_RIGHT) return KMInput.IsKeyDown(VK_RIGHT); - if (key >= Keyboard::KEY_A && key <= Keyboard::KEY_Z) - return KMInput.IsKeyDown('A' + (key - Keyboard::KEY_A)); - return false; + return g_KBMInput.IsMouseButtonDown(button); } #endif diff --git a/Minecraft.Client/stubs.h b/Minecraft.Client/stubs.h index cc7888672..f4ae056f2 100644 --- a/Minecraft.Client/stubs.h +++ b/Minecraft.Client/stubs.h @@ -187,12 +187,13 @@ public: static void create() {} static void destroy() {} #ifdef _WINDOWS64 - static bool isKeyDown(int key); + static bool isKeyDown(int keyCode); #else - static bool isKeyDown(int) {return false;} + static bool isKeyDown(int) { return false; } #endif static wstring getKeyName(int) { return L"KEYNAME"; } static void enableRepeatEvents(bool) {} + static const int KEY_A = 0; static const int KEY_B = 1; static const int KEY_C = 2; @@ -228,8 +229,32 @@ public: static const int KEY_UP = 32; static const int KEY_DOWN = 33; static const int KEY_TAB = 34; - static const int KEY_LEFT = 35; - static const int KEY_RIGHT = 36; + static const int KEY_1 = 35; + static const int KEY_2 = 36; + static const int KEY_3 = 37; + static const int KEY_4 = 38; + static const int KEY_5 = 39; + static const int KEY_6 = 40; + static const int KEY_7 = 41; + static const int KEY_8 = 42; + static const int KEY_9 = 43; + static const int KEY_F1 = 44; + static const int KEY_F3 = 45; + static const int KEY_F4 = 46; + static const int KEY_F5 = 47; + static const int KEY_F6 = 48; + static const int KEY_F8 = 49; + static const int KEY_F9 = 50; + static const int KEY_F11 = 51; + static const int KEY_ADD = 52; + static const int KEY_SUBTRACT = 53; + static const int KEY_LEFT = 54; + static const int KEY_RIGHT = 55; + +#ifdef _WINDOWS64 + // Map LWJGL-style key constant to Windows VK code + static int toVK(int keyConst); +#endif }; class Mouse diff --git a/README.md b/README.md index 7578f159f..960deff38 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ The headless server also reads and writes `server.properties` in the working dir - **Toggle View (FPS/TPS)**: `F5` - **Fullscreen**: `F11` - **Pause Menu**: `Esc` -- **Toggle Mouse Capture**: `Left Alt` (for debugging) - **Attack / Destroy**: `Left Click` - **Use / Place**: `Right Click` - **Select Item**: `Mouse Wheel` or keys `1` to `9` From 870d3e4b6854215ff4c0e2aa449e3835e1e37327 Mon Sep 17 00:00:00 2001 From: ModMaker101 <119018978+ModMaker101@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:24:42 -0500 Subject: [PATCH 29/32] Fix mob nametag position to match correct height offset #422 (#440) --- Minecraft.Client/LivingEntityRenderer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/LivingEntityRenderer.cpp b/Minecraft.Client/LivingEntityRenderer.cpp index 89d656142..757948a37 100644 --- a/Minecraft.Client/LivingEntityRenderer.cpp +++ b/Minecraft.Client/LivingEntityRenderer.cpp @@ -487,11 +487,11 @@ void LivingEntityRenderer::renderNameTag(shared_ptr mob, const wst Font *font = getFont(); - float size = 1.60f; - float s = 1 / 60.0f * size; + constexpr float size = 1.60f; + constexpr float s = 1 / 60.0f * size; glPushMatrix(); - glTranslatef((float) x + 0, (float) y + 2.3f, (float) z); + glTranslatef(static_cast(x) + 0, static_cast(y) + mob->bbHeight + 0.5f, static_cast(z)); glNormal3f(0, 1, 0); glRotatef(-this->entityRenderDispatcher->playerRotY, 0, 1, 0); From ef66f6736dc0150c680afba4992b67970dbab992 Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Thu, 5 Mar 2026 01:23:29 +0800 Subject: [PATCH 30/32] fix: ignore dedicated server properties in normal world startup --- Minecraft.Client/MinecraftServer.cpp | 65 ++++++++++++++++++++-------- Minecraft.Client/PlayerList.cpp | 4 +- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index a81bce592..af9e226bc 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -31,6 +31,9 @@ #include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\net.minecraft.world.item.enchantment.h" #include "..\Minecraft.World\net.minecraft.world.damagesource.h" +#ifdef _WINDOWS64 +#include "Windows64\Network\WinsockNetLayer.h" +#endif #include #ifdef SPLIT_SAVES #include "..\Minecraft.World\ConsoleSaveFileSplit.h" @@ -83,6 +86,30 @@ bool MinecraftServer::s_slowQueuePacketSent = false; unordered_map MinecraftServer::ironTimers; +static bool ShouldUseDedicatedServerProperties() +{ +#ifdef _WINDOWS64 + return g_Win64DedicatedServer; +#else + return false; +#endif +} + +static int GetDedicatedServerInt(Settings *settings, const wchar_t *key, int defaultValue) +{ + return (ShouldUseDedicatedServerProperties() && settings != NULL) ? settings->getInt(key, defaultValue) : defaultValue; +} + +static bool GetDedicatedServerBool(Settings *settings, const wchar_t *key, bool defaultValue) +{ + return (ShouldUseDedicatedServerProperties() && settings != NULL) ? settings->getBoolean(key, defaultValue) : defaultValue; +} + +static wstring GetDedicatedServerString(Settings *settings, const wchar_t *key, const wstring &defaultValue) +{ + return (ShouldUseDedicatedServerProperties() && settings != NULL) ? settings->getString(key, defaultValue) : defaultValue; +} + static void PrintConsoleLine(const wchar_t *prefix, const wstring &message) { wprintf(L"%ls%ls\n", prefix, message.c_str()); @@ -589,14 +616,14 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW #endif settings = new Settings(new File(L"server.properties")); - app.SetGameHostOption(eGameHostOption_Difficulty, settings->getInt(L"difficulty", app.GetGameHostOption(eGameHostOption_Difficulty))); - app.SetGameHostOption(eGameHostOption_GameType, settings->getInt(L"gamemode", app.GetGameHostOption(eGameHostOption_GameType))); - app.SetGameHostOption(eGameHostOption_Structures, settings->getBoolean(L"generate-structures", app.GetGameHostOption(eGameHostOption_Structures) > 0) ? 1 : 0); - app.SetGameHostOption(eGameHostOption_BonusChest, settings->getBoolean(L"bonus-chest", app.GetGameHostOption(eGameHostOption_BonusChest) > 0) ? 1 : 0); - app.SetGameHostOption(eGameHostOption_PvP, settings->getBoolean(L"pvp", app.GetGameHostOption(eGameHostOption_PvP) > 0) ? 1 : 0); - app.SetGameHostOption(eGameHostOption_TrustPlayers, settings->getBoolean(L"trust-players", app.GetGameHostOption(eGameHostOption_TrustPlayers) > 0) ? 1 : 0); - app.SetGameHostOption(eGameHostOption_FireSpreads, settings->getBoolean(L"fire-spreads", app.GetGameHostOption(eGameHostOption_FireSpreads) > 0) ? 1 : 0); - app.SetGameHostOption(eGameHostOption_TNT, settings->getBoolean(L"tnt", app.GetGameHostOption(eGameHostOption_TNT) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_Difficulty, GetDedicatedServerInt(settings, L"difficulty", app.GetGameHostOption(eGameHostOption_Difficulty))); + app.SetGameHostOption(eGameHostOption_GameType, GetDedicatedServerInt(settings, L"gamemode", app.GetGameHostOption(eGameHostOption_GameType))); + app.SetGameHostOption(eGameHostOption_Structures, GetDedicatedServerBool(settings, L"generate-structures", app.GetGameHostOption(eGameHostOption_Structures) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_BonusChest, GetDedicatedServerBool(settings, L"bonus-chest", app.GetGameHostOption(eGameHostOption_BonusChest) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_PvP, GetDedicatedServerBool(settings, L"pvp", app.GetGameHostOption(eGameHostOption_PvP) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_TrustPlayers, GetDedicatedServerBool(settings, L"trust-players", app.GetGameHostOption(eGameHostOption_TrustPlayers) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_FireSpreads, GetDedicatedServerBool(settings, L"fire-spreads", app.GetGameHostOption(eGameHostOption_FireSpreads) > 0) ? 1 : 0); + app.SetGameHostOption(eGameHostOption_TNT, GetDedicatedServerBool(settings, L"tnt", app.GetGameHostOption(eGameHostOption_TNT) > 0) ? 1 : 0); app.DebugPrintf("\n*** SERVER SETTINGS ***\n"); app.DebugPrintf("ServerSettings: host-friends-only is %s\n",(app.GetGameHostOption(eGameHostOption_FriendsOfFriends)>0)?"on":"off"); @@ -615,13 +642,13 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW //motd = settings->getString(L"motd", L"A Minecraft Server"); //motd.replace('�', '$'); - setAnimals(settings->getBoolean(L"spawn-animals", true)); - setNpcsEnabled(settings->getBoolean(L"spawn-npcs", true)); + setAnimals(GetDedicatedServerBool(settings, L"spawn-animals", true)); + setNpcsEnabled(GetDedicatedServerBool(settings, L"spawn-npcs", true)); setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false); // 4J Stu - We should never have hacked clients flying when they shouldn't be like the PC version, so enable flying always // Fix for #46612 - TU5: Code: Multiplayer: A client can be banned for flying when accidentaly being blown by dynamite - setFlightAllowed(settings->getBoolean(L"allow-flight", true)); + setFlightAllowed(GetDedicatedServerBool(settings, L"allow-flight", true)); // 4J Stu - Enabling flight to stop it kicking us when we use it #ifdef _DEBUG_MENUS_ENABLED @@ -667,7 +694,7 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW __int64 levelNanoTime = System::nanoTime(); - wstring levelName = (initData && !initData->levelName.empty()) ? initData->levelName : settings->getString(L"level-name", L"world"); + wstring levelName = (initData && !initData->levelName.empty()) ? initData->levelName : GetDedicatedServerString(settings, L"level-name", L"world"); wstring levelTypeString; bool gameRuleUseFlatWorld = false; @@ -677,11 +704,11 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW } if(gameRuleUseFlatWorld || app.GetGameHostOption(eGameHostOption_LevelType)>0) { - levelTypeString = settings->getString(L"level-type", L"flat"); + levelTypeString = GetDedicatedServerString(settings, L"level-type", L"flat"); } else { - levelTypeString = settings->getString(L"level-type",L"default"); + levelTypeString = GetDedicatedServerString(settings, L"level-type",L"default"); } LevelType *pLevelType = LevelType::getLevelType(levelTypeString); @@ -702,7 +729,7 @@ bool MinecraftServer::initServer(__int64 seed, NetworkGameInitData *initData, DW #endif } - setMaxBuildHeight(settings->getInt(L"max-build-height", Level::maxBuildHeight)); + setMaxBuildHeight(GetDedicatedServerInt(settings, L"max-build-height", Level::maxBuildHeight)); setMaxBuildHeight(((getMaxBuildHeight() + 8) / 16) * 16); setMaxBuildHeight(Mth::clamp(getMaxBuildHeight(), 64, Level::maxBuildHeight)); //settings->setProperty(L"max-build-height", maxBuildHeight); @@ -851,7 +878,7 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring // 4J TODO - free levels here if there are already some? levels = ServerLevelArray(3); - int gameTypeId = settings->getInt(L"gamemode", app.GetGameHostOption(eGameHostOption_GameType));//LevelSettings::GAMETYPE_SURVIVAL); + int gameTypeId = GetDedicatedServerInt(settings, L"gamemode", app.GetGameHostOption(eGameHostOption_GameType));//LevelSettings::GAMETYPE_SURVIVAL); GameType *gameType = LevelSettings::validateGameType(gameTypeId); app.DebugPrintf("Default game type: %d\n" , gameTypeId); @@ -950,7 +977,7 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring #if DEBUG_SERVER_DONT_SPAWN_MOBS levels[i]->setSpawnSettings(false, false); #else - levels[i]->setSpawnSettings(settings->getBoolean(L"spawn-monsters", true), animals); + levels[i]->setSpawnSettings(GetDedicatedServerBool(settings, L"spawn-monsters", true), animals); #endif levels[i]->getLevelData()->setGameType(gameType); @@ -1038,7 +1065,7 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring for (int i = 0; i < levels.length ; i++) { // logger.info("Preparing start region for level " + i); - if (i == 0 || settings->getBoolean(L"allow-nether", true)) + if (i == 0 || GetDedicatedServerBool(settings, L"allow-nether", true)) { ServerLevel *level = levels[i]; if(levelChunksNeedConverted) @@ -1780,7 +1807,7 @@ void MinecraftServer::run(__int64 seed, void *lpParameter) MinecraftServer::setTimeOfDayAtEndOfTick = false; for (unsigned int i = 0; i < levels.length; i++) { - if (i == 0 || settings->getBoolean(L"allow-nether", true)) + if (i == 0 || GetDedicatedServerBool(settings, L"allow-nether", true)) { ServerLevel *level = levels[i]; level->setDayTime( MinecraftServer::setTimeOfDay ); diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 3cf47a1cf..0de0c36be 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -53,14 +53,12 @@ PlayerList::PlayerList(MinecraftServer *server) //int viewDistance = server->settings->getInt(L"view-distance", 10); - maxPlayers = server->settings->getInt(L"max-players", 20); - doWhiteList = false; - #ifdef _WINDOWS64 maxPlayers = MINECRAFT_NET_MAX_PLAYERS; #else maxPlayers = server->settings->getInt(L"max-players", 20); #endif + doWhiteList = false; InitializeCriticalSection(&m_kickPlayersCS); InitializeCriticalSection(&m_closePlayersCS); } From 7b35df871444842976e3b8389825f39a4267a270 Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Thu, 5 Mar 2026 01:38:34 +0800 Subject: [PATCH 31/32] Fix controller paging regression in creative menu Preserve smooth row-by-row scrolling for mouse wheel input, but restore full-page movement for controller/menu scroll actions in the creative inventory. Commit 3093ca3 changed page indexing to support smooth scrolling, which caused ACTION_MENU_OTHER_STICK_UP/DOWN to advance by one row instead of one page. Track whether the scroll action originated from the mouse wheel and only use single-row steps in that case. Fixes #253 --- .../Common/UI/IUIScene_CreativeMenu.cpp | 22 +++++++++++++++++-- .../Windows64/KeyboardMouseInput.cpp | 5 +++++ .../Windows64/KeyboardMouseInput.h | 4 +++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp index 544fedc0f..04852e97d 100644 --- a/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_CreativeMenu.cpp @@ -1098,7 +1098,15 @@ void IUIScene_CreativeMenu::handleAdditionalKeyPress(int iAction) } break; case ACTION_MENU_OTHER_STICK_DOWN: - ++m_tabPage[m_curTab]; + { + int pageStep = TabSpec::rows; +#ifdef _WINDOWS64 + if (g_KBMInput.WasMouseWheelConsumed()) + { + pageStep = 1; + } +#endif + m_tabPage[m_curTab] += pageStep; if(m_tabPage[m_curTab] >= specs[m_curTab]->getPageCount()) { m_tabPage[m_curTab] = specs[m_curTab]->getPageCount() - 1; @@ -1107,9 +1115,18 @@ void IUIScene_CreativeMenu::handleAdditionalKeyPress(int iAction) { switchTab(m_curTab); } + } break; case ACTION_MENU_OTHER_STICK_UP: - --m_tabPage[m_curTab]; + { + int pageStep = TabSpec::rows; +#ifdef _WINDOWS64 + if (g_KBMInput.WasMouseWheelConsumed()) + { + pageStep = 1; + } +#endif + m_tabPage[m_curTab] -= pageStep; if(m_tabPage[m_curTab] < 0) { m_tabPage[m_curTab] = 0; @@ -1118,6 +1135,7 @@ void IUIScene_CreativeMenu::handleAdditionalKeyPress(int iAction) { switchTab(m_curTab); } + } break; } } diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.cpp b/Minecraft.Client/Windows64/KeyboardMouseInput.cpp index 7ab99a719..95c3f4fc2 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.cpp +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.cpp @@ -33,6 +33,7 @@ void KeyboardMouseInput::Init() m_mouseDeltaAccumX = 0; m_mouseDeltaAccumY = 0; m_mouseWheelAccum = 0; + m_mouseWheelConsumed = false; m_mouseGrabbed = false; m_cursorHiddenForUI = false; m_windowFocused = true; @@ -67,6 +68,7 @@ void KeyboardMouseInput::ClearAllState() m_mouseDeltaAccumX = 0; m_mouseDeltaAccumY = 0; m_mouseWheelAccum = 0; + m_mouseWheelConsumed = false; } void KeyboardMouseInput::Tick() @@ -88,6 +90,7 @@ void KeyboardMouseInput::Tick() m_mouseDeltaY = m_mouseDeltaAccumY; m_mouseDeltaAccumX = 0; m_mouseDeltaAccumY = 0; + m_mouseWheelConsumed = false; m_hasInput = (m_mouseDeltaX != 0 || m_mouseDeltaY != 0 || m_mouseWheelAccum != 0); if (!m_hasInput) @@ -172,6 +175,8 @@ void KeyboardMouseInput::OnMouseWheel(int delta) int KeyboardMouseInput::GetMouseWheel() { int val = m_mouseWheelAccum; + if (val != 0) + m_mouseWheelConsumed = true; m_mouseWheelAccum = 0; return val; } diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.h b/Minecraft.Client/Windows64/KeyboardMouseInput.h index f098ccabd..050670550 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.h +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.h @@ -59,7 +59,8 @@ public: int GetMouseWheel(); int PeekMouseWheel() const { return m_mouseWheelAccum; } - void ConsumeMouseWheel() { m_mouseWheelAccum = 0; } + void ConsumeMouseWheel() { if (m_mouseWheelAccum != 0) m_mouseWheelConsumed = true; m_mouseWheelAccum = 0; } + bool WasMouseWheelConsumed() const { return m_mouseWheelConsumed; } // Per-frame delta consumption for low-latency mouse look. // Reads and clears the raw accumulators (not the per-tick snapshot). @@ -114,6 +115,7 @@ private: int m_mouseDeltaAccumY; int m_mouseWheelAccum; + bool m_mouseWheelConsumed; bool m_mouseGrabbed; From 4108ed142b82443c9c50f440b931d7589d22eeda Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Thu, 5 Mar 2026 01:49:38 +0800 Subject: [PATCH 32/32] fix: restore KBM sprint on either Ctrl key --- Minecraft.Client/Input.cpp | 2 +- .../Windows64/KeyboardMouseInput.cpp | 24 +++++++++++++++++++ .../Windows64/KeyboardMouseInput.h | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Input.cpp b/Minecraft.Client/Input.cpp index 5e41da299..4d931d3e6 100644 --- a/Minecraft.Client/Input.cpp +++ b/Minecraft.Client/Input.cpp @@ -104,7 +104,7 @@ void Input::tick(LocalPlayer *player) } } - // Left Ctrl + forward = sprint (hold to sprint) + // Ctrl + forward = sprint (hold to sprint) if (!player->abilities.flying) { bool ctrlHeld = g_KBMInput.IsKeyDown(KeyboardMouseInput::KEY_SPRINT); diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.cpp b/Minecraft.Client/Windows64/KeyboardMouseInput.cpp index 95c3f4fc2..6206db87e 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.cpp +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.cpp @@ -12,6 +12,21 @@ extern HWND g_hWnd; // Forward declaration static void ClipCursorToWindow(HWND hWnd); +static bool IsModifierKeyDown(const bool* keyState, int vkCode) +{ + switch (vkCode) + { + case VK_SHIFT: + return keyState[VK_LSHIFT] || keyState[VK_RSHIFT]; + case VK_CONTROL: + return keyState[VK_LCONTROL] || keyState[VK_RCONTROL]; + case VK_MENU: + return keyState[VK_LMENU] || keyState[VK_RMENU]; + default: + return false; + } +} + void KeyboardMouseInput::Init() { memset(m_keyDown, 0, sizeof(m_keyDown)); @@ -189,6 +204,9 @@ void KeyboardMouseInput::OnRawMouseDelta(int dx, int dy) bool KeyboardMouseInput::IsKeyDown(int vkCode) const { + if (vkCode == VK_SHIFT || vkCode == VK_CONTROL || vkCode == VK_MENU) + return IsModifierKeyDown(m_keyDown, vkCode); + if (vkCode >= 0 && vkCode < MAX_KEYS) return m_keyDown[vkCode]; return false; @@ -196,6 +214,9 @@ bool KeyboardMouseInput::IsKeyDown(int vkCode) const bool KeyboardMouseInput::IsKeyPressed(int vkCode) const { + if (vkCode == VK_SHIFT || vkCode == VK_CONTROL || vkCode == VK_MENU) + return IsModifierKeyDown(m_keyPressed, vkCode); + if (vkCode >= 0 && vkCode < MAX_KEYS) return m_keyPressed[vkCode]; return false; @@ -203,6 +224,9 @@ bool KeyboardMouseInput::IsKeyPressed(int vkCode) const bool KeyboardMouseInput::IsKeyReleased(int vkCode) const { + if (vkCode == VK_SHIFT || vkCode == VK_CONTROL || vkCode == VK_MENU) + return IsModifierKeyDown(m_keyReleased, vkCode); + if (vkCode >= 0 && vkCode < MAX_KEYS) return m_keyReleased[vkCode]; return false; diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.h b/Minecraft.Client/Windows64/KeyboardMouseInput.h index 050670550..89a028b10 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.h +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.h @@ -20,7 +20,7 @@ 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_SPRINT = VK_CONTROL; static const int KEY_INVENTORY = 'E'; static const int KEY_DROP = 'Q'; static const int KEY_CRAFTING = 'C';