diff --git a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp index 6be67bed..a9ff02f5 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 1c07e540..bb399b97 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 e45fa09c..e245e3be 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 1d90da77..e3749dcb 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 ceb9554b..5e7ce794 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 e61001a3..ac99a415 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 139d99ac..7a11b5e1 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 dd9a7965..5be0c8fb 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