From 63e590d783cc3302ec6a5b639dd831e930a557c6 Mon Sep 17 00:00:00 2001 From: dtentiion Date: Mon, 2 Mar 2026 21:13:11 +0000 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 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 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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