From b28c0026af9178baa7e409681fd88fce8b50abe0 Mon Sep 17 00:00:00 2001 From: Revela Date: Fri, 13 Mar 2026 06:56:46 -0500 Subject: [PATCH] detailed summary of every changed file: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Minecraft.Client/ClientConnection.cpp Purpose: Propagate hardcore flag through network level creation - handleLogin() (2 sites): Changed MultiPlayerLevel constructor calls from hardcoded false for the hardcore parameter to packet->m_isHardcore, so the client-side level correctly knows it's hardcore when joining a server. - handleRespawn(): Same change - when creating a new dimension level on respawn, uses packet->m_isHardcore instead of querying minecraft->level->getLevelData()->isHardcore() (which could be stale/wrong). --- Minecraft.Client/Common/App_Defines.h Purpose: Define bitmask for hardcore host option - Added GAME_HOST_OPTION_BITMASK_HARDCORE (0x40000000) - a new bit in the host options bitfield to store whether the game is hardcore. --- Minecraft.Client/Common/App_enums.h Purpose: Add hardcore enum value - Added eGameHostOption_Hardcore to the eGameHostOption enum so code can get/set the hardcore flag via SetGameHostOption/GetGameHostOption. --- Minecraft.Client/Common/Consoles_App.cpp Purpose: Implement hardcore get/set in host options bitfield - SetGameHostOption(): Added case eGameHostOption_Hardcore - sets or clears the GAME_HOST_OPTION_BITMASK_HARDCORE bit. - GetGameHostOption(): Added case eGameHostOption_Hardcore - returns 1 if the hardcore bit is set, 0 otherwise. --- Minecraft.Client/Common/Consoles_App.h Purpose: Store save folder name for hardcore world deletion - Added SetCurrentSaveFolderName() and GetCurrentSaveFolderName() public methods. - Added wstring m_currentSaveFolderName private member - stores the save folder name so the hardcore death handler can find and delete the world. --- Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp Purpose: Delete hardcore world's save data on exit - Added Win64_DeleteSaveDirectory() - a recursive directory deletion helper (Windows64 only). - In _ExitWorld(): Before the server is torn down, captures whether this is a hardcore death exit (getDeleteWorldOnExit()). Tries 3 sources for the save folder name: app storage, StorageManager, and MinecraftServer. - After the server fully stops, if shouldDeleteHardcoreWorld is true, deletes the entire Windows64\GameHDD\ directory. --- Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp Purpose: Hardcore difficulty slider in Create World menu - Added file-scope s_bHardcore flag to track when the slider is at position 4 (Hardcore). - Constructor: Extended difficulty slider range from 0-3 to 0-4, resets s_bHardcore to false. - handleSliderMove(): When slider value >= 4, sets s_bHardcore = true, stores actual difficulty as 3 (Hard), and displays "Hardcore" label. Otherwise behaves normally. - CreateGame(): Clears the save folder name (new world), and sets eGameHostOption_Hardcore based on s_bHardcore. - Minor: Changed RequestErrorMessage to RequestAlertMessage for a content restriction dialog. --- Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp Purpose: Hardcore death screen behavior (Iggy UI) - Constructor: Checks if current level is hardcore. If so, shows IDS_HARDCORE_DEATH_MESSAGE on the respawn button and hides it. Otherwise shows normal "Respawn" button. - handlePress() - Respawn: Added safeguard - if hardcore, blocks respawn entirely. - handlePress() - Exit Game: If hardcore and host, skips save dialog, disables save-on-exit, enables delete-world-on-exit, and triggers immediate world exit. --- Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp Purpose: Show "Difficulty: Hardcore" in Load World menu + persist hardcore through game launch - Static array: Expanded m_iDifficultyTitleSettingA from 4 to 5 entries, added IDS_GAMEMODE_HARDCORE at index 4. - Constructor: Initializes m_bHardcore = false. In Windows64 block: sets up thumbnail name from save details, and reads isHardcore from params->saveDetails. If hardcore, immediately initializes the difficulty slider to show "Hardcore" locked at position 4. - tick(): When host options are read (bHostOptionsRead block), also reads the hardcore flag and re-initializes the slider if needed (for console path). - handleSliderMove(): If m_bHardcore, locks the slider at position 4 (prevents changing difficulty). - StartGameFromSave(): Stores the save folder name in app for later hardcore deletion. Sets eGameHostOption_Hardcore from m_bHardcore. --- Minecraft.Client/Common/UI/UIScene_LoadMenu.h Purpose: Declare hardcore member - Expanded m_iDifficultyTitleSettingA from [4] to [5]. - Added bool m_bHardcore private member. --- Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp Purpose: Read hardcore flag from level.dat when building the save list - ReadLevelNameFromSaveFile(): Added optional bool *outHardcore parameter. Inside the NBT Data compound tag parsing, if outHardcore is non-null, reads dataTag->getBoolean(L"hardcore"). - Save enumeration block (Windows64): Passes &saveHardcore to ReadLevelNameFromSaveFile and stores the result in m_saveDetails[i].isHardcore. --- Minecraft.Client/Common/UI/UIStructs.h Purpose: Add isHardcore to save list details struct - Added bool isHardcore field to _SaveListDetails struct. - Initialized to false in the constructor. --- Minecraft.Client/Common/XUI/XUI_Death.cpp Purpose: Hardcore death screen behavior (XUI/Xbox UI) - Mirror of the Iggy UIScene_DeathMenu.cpp changes but for the XUI rendering path. - OnInit(): Checks isHardcore(), hides respawn button and shows death message if true. - OnNotifyPressEx() - Exit Game: If hardcore and host, skips save, flags world for deletion, exits immediately. - OnNotifyPressEx() - Respawn: Safeguard to block respawn in hardcore. --- Minecraft.Client/Gui.cpp Purpose: Syntax fix - Fixed lines.push_back(L"" → lines.push_back(L"") - missing closing quote/paren in debug terrain feature display. --- Minecraft.Client/MinecraftServer.cpp Purpose: Server-side hardcore support - Constructor: Initializes m_deleteWorldOnExit = false. - loadLevel(): Captures the save folder name from StorageManager into m_saveFolderName for later use in hardcore world deletion. - isHardcore(): Changed from always returning false to returning app.GetGameHostOption(eGameHostOption_Hardcore) > 0 - this is the key change that makes the server actually report hardcore mode. --- Minecraft.Client/MinecraftServer.h Purpose: Declare hardcore-related members - Added bool m_deleteWorldOnExit and wstring m_saveFolderName private members. - Added setDeleteWorldOnExit(), getDeleteWorldOnExit(), and getSaveFolderName() public methods. --- Minecraft.Client/PlayerConnection.h Purpose: Thread-safety fix for kicked flag - Changed m_bWasKicked from bool to std::atomic (initialized with {false}). - Changed setWasKicked()/getWasKicked() to use .store()/.load() - fixes a race condition where the kicked flag is set on one thread and read on another. --- Minecraft.Client/PlayerList.cpp Purpose: Hardcore multiplayer - ban, respawn as Adventure, thread-safe bans - Constructor/Destructor: Added InitializeCriticalSection/DeleteCriticalSection for m_banCS. - placeNewPlayer(): Passes isHardcore() flag to the LoginPacket constructor so clients joining know it's hardcore. - respawn(): After respawn in hardcore, forces the player into Adventure mode (spectate-like: can look around but not interact). Sends GameEventPacket to sync client. - respawn() and toggleDimension() (2 sites): Pass isHardcore() to RespawnPacket constructor. - isXuidBanned(): Wrapped m_bannedXuids iteration with EnterCriticalSection/LeaveCriticalSection for thread safety. - banXuid() (new): Thread-safe method to add a player's XUID to the ban list - used when a player dies in hardcore multiplayer. --- Minecraft.Client/PlayerList.h Purpose: Declare ban-related additions - Added CRITICAL_SECTION m_banCS to protect m_bannedXuids. - Added void banXuid(PlayerUID xuid) public method. --- Minecraft.Client/SelectWorldScreen.cpp Purpose: Show [Hardcore] badge in Java-style world list - In renderItem(): If levelSummary->isHardcore(), appends [Hardcore] to the world name display. --- Minecraft.Client/ServerPlayer.cpp Purpose: Hardcore death behavior on server - die(): If the level is hardcore, switches the dead player to Adventure mode (so they can't interact if somehow respawned). - Minor: Two comment lines changed // → /// (no functional change). --- Minecraft.Client/Windows64Media/strings.h Purpose: String IDs for hardcore UI text - Added 8 new string IDs (2286-2293): IDS_GAMEMODE_HARDCORE, IDS_HARDCORE, IDS_HARDCORE_TOOLTIP, IDS_HARDCORE_WARNING_TITLE, IDS_HARDCORE_WARNING_TEXT, IDS_HARDCORE_DEATH_MESSAGE, IDS_LABEL_HARDCORE, IDS_GAMEOPTION_HARDCORE. --- Minecraft.World/ConsoleSaveFileOriginal.cpp Purpose: Capture save folder name after first save (for new worlds) - SaveSaveDataCallback() (Windows64 only): After a successful save, if the app doesn't yet know the save folder name, attempts to capture it via StorageManager or by scanning Windows64\GameHDD\ for the newest folder. This handles the case where a newly-created hardcore world hasn't been saved yet when the folder name is needed. --- Minecraft.World/DisconnectPacket.h Purpose: Hardcore disconnect reason - Added eDisconnect_HardcoreDeath to the disconnect reason enum - used when kicking a player who died in hardcore multiplayer. --- Minecraft.World/LoginPacket.cpp & LoginPacket.h Purpose: Serialize hardcore flag in login packet - Added bool m_isHardcore member, initialized to false in both constructors. - Server→Client constructor now accepts bool isHardcore = false parameter. - read(): Reads m_isHardcore from the stream. - write(): Writes m_isHardcore to the stream. - getEstimatedSize(): Added sizeof(bool) for the new field. --- Minecraft.World/RespawnPacket.cpp & RespawnPacket.h Purpose: Serialize hardcore flag in respawn packet - Added bool m_isHardcore member, initialized to false. - Constructor now accepts bool isHardcore = false parameter. - read()/write(): Serialize m_isHardcore via readBoolean()/writeBoolean(). - getEstimatedSize(): Changed from 13 to 14 bytes to account for the new boolean. --- .claude/settings.local.json | 19 +++++ Minecraft.Client/ClientConnection.cpp | 6 +- Minecraft.Client/Common/App_Defines.h | 1 + Minecraft.Client/Common/App_enums.h | 1 + Minecraft.Client/Common/Consoles_App.cpp | 13 ++++ Minecraft.Client/Common/Consoles_App.h | 5 ++ .../Common/UI/IUIScene_PauseMenu.cpp | 72 +++++++++++++++++++ .../Common/UI/UIScene_CreateWorldMenu.cpp | 24 +++++-- .../Common/UI/UIScene_DeathMenu.cpp | 50 +++++++++++-- .../Common/UI/UIScene_LoadMenu.cpp | 66 +++++++++++++++-- Minecraft.Client/Common/UI/UIScene_LoadMenu.h | 3 +- .../Common/UI/UIScene_LoadOrJoinMenu.cpp | 10 ++- Minecraft.Client/Common/UI/UIStructs.h | 3 + Minecraft.Client/Common/XUI/XUI_Death.cpp | 38 +++++++++- Minecraft.Client/Gui.cpp | 2 +- Minecraft.Client/MinecraftServer.cpp | 12 +++- Minecraft.Client/MinecraftServer.h | 5 ++ Minecraft.Client/PlayerConnection.h | 7 +- Minecraft.Client/PlayerList.cpp | 46 +++++++++++- Minecraft.Client/PlayerList.h | 2 + Minecraft.Client/SelectWorldScreen.cpp | 6 ++ Minecraft.Client/ServerPlayer.cpp | 10 ++- Minecraft.Client/Windows64Media/strings.h | 8 +++ Minecraft.World/ConsoleSaveFileOriginal.cpp | 45 ++++++++++++ Minecraft.World/DisconnectPacket.h | 2 + Minecraft.World/LoginPacket.cpp | 9 ++- Minecraft.World/LoginPacket.h | 3 +- Minecraft.World/RespawnPacket.cpp | 8 ++- Minecraft.World/RespawnPacket.h | 3 +- 29 files changed, 439 insertions(+), 40 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..2df5e3e3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(find . -name \"*LevelData*\" -o -name \"*level.dat*\" 2>/dev/null | head -20)", + "Bash(grep -n \"isHardcore\\\\|m_isHardcore\" Minecraft.World/LoginPacket.* Minecraft.Client/PlayerList.cpp)", + "Bash(find \"C:\\\\Users\\\\rexma\\\\Documents\\\\Minecraft\\\\itsRevela\" -name \"*.resx\" -path \"*/Common/Media/*\" ! -path \"*/x64/*\" ! -path \"*/de-DE/*\" ! -path \"*/es-ES/*\" ! -path \"*/fr-FR/*\" ! -path \"*/it-IT/*\" ! -path \"*/ja-JP/*\" ! -path \"*/ko-KR/*\" ! -path \"*/pt-BR/*\" ! -path \"*/pt-PT/*\" ! -path \"*/zh-CHT/*\" -type f 2>/dev/null | head -20)", + "Bash(find /c/Users/rexma/Documents/Minecraft/itsRevela -name \"*.cpp\" -type f | xargs grep -l \"SetSaveUniqueFilename\" | grep -v \"Stubs\\\\|4JLibs\")", + "Bash(find /c/Users/rexma/Documents/Minecraft/itsRevela/Minecraft.Client/Windows64 -name \"*[Ss]torage*\" -type f 2>/dev/null | head -20)", + "Bash(find \"C:\\\\Users\\\\rexma\\\\Documents\\\\Minecraft\\\\itsRevela\" -name \"*.cpp\" -o -name \"*.h\" | xargs grep -l \"GameHDD\" 2>/dev/null)", + "Bash(find \"C:\\\\Users\\\\rexma\\\\Documents\\\\Minecraft\\\\itsRevela\\\\Minecraft.Client\\\\Windows64\" -name \"*.cpp\" -type f 2>/dev/null | head -20)", + "Bash(find \"C:\\\\Users\\\\rexma\\\\Documents\\\\Minecraft\\\\itsRevela\" -name \"*Storage*.cpp\" -o -name \"*Storage*.h\" 2>/dev/null | grep -i windows)", + "Bash(find \"C:\\\\Users\\\\rexma\\\\Documents\\\\Minecraft\\\\itsRevela\\\\Minecraft.Client\\\\Windows64\" -name \"*.cpp\" | xargs grep -l \"SaveSaveData\\\\|SetSaveUniqueFilename\" 2>/dev/null)", + "Bash(find /c/Users/rexma/Documents/Minecraft/itsRevela -type f -name \"*.h\" | xargs grep -l \"StorageManager\\\\|ResetSaveData\\\\|ReturnSavesInfo\\\\|GetSavesInfo\" | head -10)", + "Bash(grep -rn \"SetSaveUniqueFilename\\\\\\(\" Minecraft.Client --include=\"*.cpp\" --include=\"*.h\" 2>/dev/null | grep -v \"^[^:]*Extrax64Stubs.cpp\" | grep -v \"^[^:]*\\\\.h-\")", + "Bash(find /c/Users/rexma/Documents/Minecraft/itsRevela -name \"*.h\" | xargs grep -l \"SetCurrentSaveFolderName\\\\|GetCurrentSaveFolderName\" 2>/dev/null)", + "Bash(find \"C:\\\\Users\\\\rexma\\\\Documents\\\\Minecraft\\\\itsRevela\\\\Minecraft.Client\" -name \"*.h\" -o -name \"*.cpp\" 2>/dev/null | xargs grep -l \"LaunchMoreOptionsMenuParams\" | head -5)" + ] + } +} diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index ba40b43e..fa87f6ce 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -294,7 +294,7 @@ void ClientConnection::handleLogin(shared_ptr packet) Level *dimensionLevel = minecraft->getLevel( packet->dimension ); if( dimensionLevel == nullptr ) { - level = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, false, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty); + level = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, packet->m_isHardcore, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty); // 4J Stu - We want to share the SavedDataStorage between levels int otherDimensionId = packet->dimension == 0 ? -1 : 0; @@ -363,7 +363,7 @@ void ClientConnection::handleLogin(shared_ptr packet) activeLevel = minecraft->getLevel(otherDimensionId); } - MultiPlayerLevel *dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, false, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty); + MultiPlayerLevel *dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->seed, GameType::byId(packet->gameType), false, packet->m_isHardcore, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty); dimensionLevel->savedDataStorage = activeLevel->savedDataStorage; @@ -2798,7 +2798,7 @@ void ClientConnection::handleRespawn(shared_ptr packet) MultiPlayerLevel *dimensionLevel = (MultiPlayerLevel *)minecraft->getLevel( packet->dimension ); if( dimensionLevel == nullptr ) { - dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->mapSeed, packet->playerGameType, false, minecraft->level->getLevelData()->isHardcore(), packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty); + dimensionLevel = new MultiPlayerLevel(this, new LevelSettings(packet->mapSeed, packet->playerGameType, false, packet->m_isHardcore, packet->m_newSeaLevel, packet->m_pLevelType, packet->m_xzSize, packet->m_hellScale), packet->dimension, packet->difficulty); // 4J Stu - We want to shared the savedDataStorage between both levels //if( dimensionLevel->savedDataStorage != nullptr ) diff --git a/Minecraft.Client/Common/App_Defines.h b/Minecraft.Client/Common/App_Defines.h index 7e96896c..332e1e73 100644 --- a/Minecraft.Client/Common/App_Defines.h +++ b/Minecraft.Client/Common/App_Defines.h @@ -45,6 +45,7 @@ #define GAME_HOST_OPTION_BITMASK_DOTILEDROPS 0x08000000 #define GAME_HOST_OPTION_BITMASK_NATURALREGEN 0x10000000 #define GAME_HOST_OPTION_BITMASK_DODAYLIGHTCYCLE 0x20000000 +#define GAME_HOST_OPTION_BITMASK_HARDCORE 0x40000000 // 4J Added - for hardcore mode #define GAME_HOST_OPTION_BITMASK_ALL 0xFFFFFFFF #define GAME_HOST_OPTION_BITMASK_WORLDSIZE_BITSHIFT 20 diff --git a/Minecraft.Client/Common/App_enums.h b/Minecraft.Client/Common/App_enums.h index 15a17978..696374ec 100644 --- a/Minecraft.Client/Common/App_enums.h +++ b/Minecraft.Client/Common/App_enums.h @@ -648,6 +648,7 @@ enum eGameHostOption eGameHostOption_DoTileDrops, eGameHostOption_NaturalRegeneration, eGameHostOption_DoDaylightCycle, + eGameHostOption_Hardcore, // 4J Added - for hardcore mode }; // 4J-PB - If any new DLC items are added to the TMSFiles, this array needs updated diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index c3a623d5..47b8ea19 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -8063,6 +8063,16 @@ void CMinecraftApp::SetGameHostOption(unsigned int &uiHostSettings, eGameHostOpt uiHostSettings&=~GAME_HOST_OPTION_BITMASK_WORLDSIZE; uiHostSettings|=(GAME_HOST_OPTION_BITMASK_WORLDSIZE & (uiVal<getDeleteWorldOnExit()) + { + shouldDeleteHardcoreWorld = true; + // Try 1: Use the save folder name stored by UIScene_LoadMenu::StartGameFromSave (works for existing saves) + hardcoreSaveFolderName = app.GetCurrentSaveFolderName(); + if (!hardcoreSaveFolderName.empty()) + { + app.DebugPrintf("Hardcore mode: save folder from app = '%ls'\n", hardcoreSaveFolderName.c_str()); + } + // Try 2: StorageManager (may work for new saves after first autosave) + if (hardcoreSaveFolderName.empty()) + { + char szSaveFolder[MAX_SAVEFILENAME_LENGTH] = {}; + StorageManager.GetSaveUniqueFilename(szSaveFolder); + if (szSaveFolder[0] != '\0') + { + wchar_t wSaveFolder[MAX_SAVEFILENAME_LENGTH] = {}; + mbstowcs(wSaveFolder, szSaveFolder, MAX_SAVEFILENAME_LENGTH - 1); + hardcoreSaveFolderName = wSaveFolder; + app.DebugPrintf("Hardcore mode: save folder from StorageManager = '%s'\n", szSaveFolder); + } + } + // Try 3: Stored during loadLevel + if (hardcoreSaveFolderName.empty()) + { + hardcoreSaveFolderName = MinecraftServer::getInstance()->getSaveFolderName(); + app.DebugPrintf("Hardcore mode: save folder from server = '%ls'\n", hardcoreSaveFolderName.c_str()); + } + MinecraftServer::getInstance()->setDeleteWorldOnExit(false); + } +#endif + int exitReasonStringId = pMinecraft->progressRenderer->getCurrentTitle(); int exitReasonTitleId = IDS_CONNECTION_LOST; @@ -625,6 +686,17 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter) { Sleep(1); } + // 4J Added: Hardcore mode — delete world save data now that the server is fully stopped +#ifdef _WINDOWS64 + if (shouldDeleteHardcoreWorld && !hardcoreSaveFolderName.empty()) + { + wchar_t wFolderPath[MAX_PATH] = {}; + swprintf_s(wFolderPath, MAX_PATH, L"Windows64\\GameHDD\\%s", hardcoreSaveFolderName.c_str()); + app.DebugPrintf("Hardcore mode: Deleting world save folder '%ls'\n", wFolderPath); + Win64_DeleteSaveDirectory(wFolderPath); + } +#endif + pMinecraft->setLevel(nullptr,exitReasonStringId,nullptr,saveStats); TelemetryManager->Flush(); diff --git a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp index ef72ec16..8eb74739 100644 --- a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp @@ -26,6 +26,8 @@ #define GAME_CREATE_ONLINE_TIMER_ID 0 #define GAME_CREATE_ONLINE_TIMER_TIME 100 +static bool s_bHardcore = false; // 4J Added: tracks when difficulty slider is at Hardcore position (file-scope to avoid header layout changes) + int UIScene_CreateWorldMenu::m_iDifficultyTitleSettingA[4]= { IDS_DIFFICULTY_TITLE_PEACEFUL, @@ -59,8 +61,9 @@ UIScene_CreateWorldMenu::UIScene_CreateWorldMenu(int iPad, void *initData, UILay WCHAR TempString[256]; swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[app.GetGameSettings(m_iPad,eGameSetting_Difficulty)])); - m_sliderDifficulty.init(TempString,eControl_Difficulty,0,3,app.GetGameSettings(m_iPad,eGameSetting_Difficulty)); + m_sliderDifficulty.init(TempString,eControl_Difficulty,0,4,app.GetGameSettings(m_iPad,eGameSetting_Difficulty)); + s_bHardcore = false; m_MoreOptionsParams.bGenerateOptions=TRUE; m_MoreOptionsParams.bStructures=TRUE; m_MoreOptionsParams.bFlatWorld=FALSE; @@ -654,8 +657,13 @@ void UIScene_CreateWorldMenu::handleSliderMove(F64 sliderId, F64 currentValue) case eControl_Difficulty: m_sliderDifficulty.handleSliderMove(value); - app.SetGameSettings(m_iPad,eGameSetting_Difficulty,value); - swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value])); + // 4J Added: Difficulty value 4 = Hardcore (store actual difficulty as Hard, track hardcore separately) + s_bHardcore = (value >= 4); + app.SetGameSettings(m_iPad, eGameSetting_Difficulty, s_bHardcore ? 3 : value); + if (value >= 4) + swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ), L"Hardcore"); + else + swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value])); m_sliderDifficulty.setLabel(TempString); break; } @@ -823,7 +831,7 @@ void UIScene_CreateWorldMenu::checkStateAndStartGame() // 4J Stu - This is a bit messy and is due to the library incorrectly returning false for IsSignedInLive if the npAvailability isn't SCE_OK UINT uiIDA[1]; uiIDA[0]=IDS_OK; - ui.RequestErrorMessage(IDS_ONLINE_SERVICE_TITLE, IDS_CONTENT_RESTRICTION, uiIDA, 1, iPadNotSignedInLive); + ui.RequestAlertMessage(IDS_ONLINE_SERVICE_TITLE, IDS_CONTENT_RESTRICTION, uiIDA, 1, iPadNotSignedInLive); } else { @@ -1113,6 +1121,10 @@ void UIScene_CreateWorldMenu::CreateGame(UIScene_CreateWorldMenu* pClass, DWORD StorageManager.ResetSaveData(); // Make our next save default to the name of the level StorageManager.SetSaveTitle((wchar_t *)wWorldName.c_str()); +#ifdef _WINDOWS64 + // New world — save folder doesn't exist yet, clear for now (will be set after first autosave) + app.SetCurrentSaveFolderName(L""); +#endif wstring wSeed; if(!pClass->m_MoreOptionsParams.seed.empty() ) @@ -1179,7 +1191,9 @@ void UIScene_CreateWorldMenu::CreateGame(UIScene_CreateWorldMenu* pClass, DWORD Minecraft *pMinecraft = Minecraft::GetInstance(); pMinecraft->skins->selectTexturePackById(pClass->m_MoreOptionsParams.dwTexturePack); - app.SetGameHostOption(eGameHostOption_Difficulty,Minecraft::GetInstance()->options->difficulty); + // 4J Added: If hardcore was selected on difficulty slider, set difficulty to Hard and enable hardcore flag + app.SetGameHostOption(eGameHostOption_Difficulty, Minecraft::GetInstance()->options->difficulty); + app.SetGameHostOption(eGameHostOption_Hardcore, s_bHardcore ? 1 : 0); app.SetGameHostOption(eGameHostOption_FriendsOfFriends,pClass->m_MoreOptionsParams.bAllowFriendsOfFriends); app.SetGameHostOption(eGameHostOption_Gamertags,app.GetGameSettings(pClass->m_iPad,eGameSetting_GamertagsVisible)?1:0); diff --git a/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp b/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp index a4dbe8a8..76bc6e6c 100644 --- a/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_DeathMenu.cpp @@ -3,24 +3,43 @@ #include "UIScene_DeathMenu.h" #include "IUIScene_PauseMenu.h" #include "..\..\Minecraft.h" +#include "..\..\MultiPlayerLevel.h" #include "..\..\MultiplayerLocalPlayer.h" +#include "..\..\MinecraftServer.h" +#include "..\..\..\Minecraft.World\net.minecraft.world.level.storage.h" UIScene_DeathMenu::UIScene_DeathMenu(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { // Setup all the Iggy references we need for this scene initialiseMovie(); - m_buttonRespawn.init(app.GetString(IDS_RESPAWN),eControl_Respawn); m_buttonExitGame.init(app.GetString(IDS_EXIT_GAME),eControl_ExitGame); m_labelTitle.setLabel(app.GetString(IDS_YOU_DIED)); + // 4J Added: In hardcore mode, disable respawn and show hardcore death message + Minecraft *pMC = Minecraft::GetInstance(); + bool isHardcore = false; + if (pMC != nullptr && pMC->level != nullptr) + { + isHardcore = pMC->level->getLevelData()->isHardcore(); + } + + if (isHardcore) + { + m_buttonRespawn.init(app.GetString(IDS_HARDCORE_DEATH_MESSAGE), eControl_Respawn); + m_buttonRespawn.setVisible(false); + } + else + { + m_buttonRespawn.init(app.GetString(IDS_RESPAWN), eControl_Respawn); + } + m_bIgnoreInput = false; - Minecraft *pMinecraft = Minecraft::GetInstance(); - if(pMinecraft != nullptr && pMinecraft->localgameModes[iPad] != nullptr ) + if(pMC != nullptr && pMC->localgameModes[iPad] != nullptr ) { - TutorialMode *gameMode = static_cast(pMinecraft->localgameModes[iPad]); + TutorialMode *gameMode = static_cast(pMC->localgameModes[iPad]); // This just allows it to be shown gameMode->getTutorial()->showTutorialPopup(false); @@ -84,8 +103,16 @@ void UIScene_DeathMenu::handlePress(F64 controlId, F64 childId) switch(static_cast(controlId)) { case eControl_Respawn: - m_bIgnoreInput = true; - app.SetAction(m_iPad,eAppAction_Respawn); + { + // 4J Added: Safeguard - don't respawn in hardcore mode + Minecraft *pMC = Minecraft::GetInstance(); + if (pMC != nullptr && pMC->level != nullptr && pMC->level->getLevelData()->isHardcore()) + { + break; + } + m_bIgnoreInput = true; + app.SetAction(m_iPad,eAppAction_Respawn); + } #ifdef _DURANGO //InputManager.SetEnabledGtcButtons(_360_GTC_MENU|_360_GTC_PAUSE|_360_GTC_VIEW); #endif @@ -109,7 +136,16 @@ void UIScene_DeathMenu::handlePress(F64 controlId, F64 childId) playTime = static_cast(pMinecraft->localplayers[m_iPad]->getSessionTimer()); } TelemetryManager->RecordLevelExit(m_iPad, eSen_LevelExitStatus_Failed); - + + // 4J Added: Hardcore mode — skip save dialog, exit without saving, delete world + if (pMinecraft->level != nullptr && pMinecraft->level->getLevelData()->isHardcore() && g_NetworkManager.IsHost()) + { + MinecraftServer::getInstance()->setSaveOnExit(false); + MinecraftServer::getInstance()->setDeleteWorldOnExit(true); + app.SetAction(m_iPad, eAppAction_ExitWorld); + break; + } + #if defined (_XBOX_ONE) || defined(__ORBIS__) if(g_NetworkManager.IsHost() && StorageManager.GetSaveDisabled()) { diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp index d61a7902..b6d9ffe4 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp @@ -24,12 +24,13 @@ #define CHECKFORAVAILABLETEXTUREPACKS_TIMER_TIME 50 #endif -int UIScene_LoadMenu::m_iDifficultyTitleSettingA[4]= +int UIScene_LoadMenu::m_iDifficultyTitleSettingA[5]= { IDS_DIFFICULTY_TITLE_PEACEFUL, IDS_DIFFICULTY_TITLE_EASY, IDS_DIFFICULTY_TITLE_NORMAL, - IDS_DIFFICULTY_TITLE_HARD + IDS_DIFFICULTY_TITLE_HARD, + IDS_GAMEMODE_HARDCORE }; int UIScene_LoadMenu::LoadSaveDataThumbnailReturned(LPVOID lpParam,PBYTE pbThumbnail,DWORD dwThumbnailBytes) @@ -110,6 +111,7 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye m_bThumbnailGetFailed = false; m_seed = 0; m_bIsCorrupt = false; + m_bHardcore = false; m_bMultiplayerAllowed = ProfileManager.IsSignedInLive( m_iPad ) && ProfileManager.AllowedToPlayMultiplayer(m_iPad); // 4J-PB - read the settings for the online flag. We'll only save this setting if the user changed it. @@ -257,6 +259,30 @@ UIScene_LoadMenu::UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLaye m_levelName = wstring(wSaveName); m_labelGameName.init(m_levelName); } + if (params->saveDetails != nullptr) + { + // Set thumbnail name from save filename (needed for texture display in tick) + wchar_t wFilename[MAX_SAVEFILENAME_LENGTH]; + ZeroMemory(wFilename, sizeof(wFilename)); + mbstowcs(wFilename, params->saveDetails->UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1); + m_thumbnailName = wFilename; + + if (params->saveDetails->pbThumbnailData && params->saveDetails->dwThumbnailSize > 0) + { + m_pbThumbnailData = params->saveDetails->pbThumbnailData; + m_uiThumbnailSize = params->saveDetails->dwThumbnailSize; + m_bSaveThumbnailReady = true; + m_bRetrievingSaveThumbnail = false; + } + + m_bHardcore = params->saveDetails->isHardcore; + if (m_bHardcore) + { + WCHAR TempString[256]; + swprintf((WCHAR *)TempString, 256, L"%ls: %ls", app.GetString(IDS_SLIDER_DIFFICULTY), L"Hardcore"); + m_sliderDifficulty.init(TempString, eControl_Difficulty, 0, 4, 4); + } + } #endif } @@ -546,6 +572,14 @@ void UIScene_LoadMenu::tick() { m_MoreOptionsParams.bAllowFriendsOfFriends = TRUE; } + + m_bHardcore = app.GetGameHostOption(uiHostOptions, eGameHostOption_Hardcore) > 0; + if (m_bHardcore) + { + WCHAR TempString[256]; + swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ), L"Hardcore"); + m_sliderDifficulty.init(TempString, eControl_Difficulty, 0, 4, 4); + } } Minecraft *pMinecraft = Minecraft::GetInstance(); @@ -950,10 +984,15 @@ void UIScene_LoadMenu::handleSliderMove(F64 sliderId, F64 currentValue) switch(static_cast(sliderId)) { case eControl_Difficulty: + if (m_bHardcore) + { + m_sliderDifficulty.handleSliderMove(4); + break; + } m_sliderDifficulty.handleSliderMove(value); app.SetGameSettings(m_iPad,eGameSetting_Difficulty,value); - swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value])); + swprintf( (WCHAR *)TempString, 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[value])); m_sliderDifficulty.setLabel(TempString); break; } @@ -1167,7 +1206,7 @@ void UIScene_LoadMenu::LaunchGame(void) #if TO_BE_IMPLEMENTED if(eLoadStatus==C4JStorage::ELoadGame_DeviceRemoved) { - // disable saving + // disable saving StorageManager.SetSaveDisabled(true); StorageManager.SetSaveDeviceSelected(m_iPad,false); UINT uiIDA[1]; @@ -1580,6 +1619,24 @@ void UIScene_LoadMenu::StartGameFromSave(UIScene_LoadMenu* pClass, DWORD dwLocal PSAVE_DETAILS pSaveDetails=StorageManager.ReturnSavesInfo(); +#ifdef _WINDOWS64 + // 4J Added: Store save folder name for potential hardcore world deletion + app.DebugPrintf("StartGameFromSave: pSaveDetails=%p, levelGen=%p, saveInfoIndex=%d\n", pSaveDetails, pClass->m_levelGen, pClass->m_iSaveGameInfoIndex); + if (pSaveDetails != nullptr && pClass->m_levelGen == nullptr) + { + app.DebugPrintf("StartGameFromSave: UTF8SaveFilename='%s'\n", pSaveDetails->SaveInfoA[(int)pClass->m_iSaveGameInfoIndex].UTF8SaveFilename); + wchar_t wFolder[MAX_SAVEFILENAME_LENGTH] = {}; + mbstowcs(wFolder, pSaveDetails->SaveInfoA[(int)pClass->m_iSaveGameInfoIndex].UTF8SaveFilename, MAX_SAVEFILENAME_LENGTH - 1); + app.SetCurrentSaveFolderName(wFolder); + app.DebugPrintf("StartGameFromSave: stored folder name '%ls'\n", wFolder); + } + else + { + app.DebugPrintf("StartGameFromSave: no save details or is levelGen, clearing folder name\n"); + app.SetCurrentSaveFolderName(L""); + } +#endif + NetworkGameInitData *param = new NetworkGameInitData(); param->seed = pClass->m_seed; param->saveData = nullptr; @@ -1612,6 +1669,7 @@ void UIScene_LoadMenu::StartGameFromSave(UIScene_LoadMenu* pClass, DWORD dwLocal app.SetGameHostOption(eGameHostOption_DoTileDrops, pClass->m_MoreOptionsParams.bDoTileDrops); app.SetGameHostOption(eGameHostOption_NaturalRegeneration, pClass->m_MoreOptionsParams.bNaturalRegeneration); app.SetGameHostOption(eGameHostOption_DoDaylightCycle, pClass->m_MoreOptionsParams.bDoDaylightCycle); + app.SetGameHostOption(eGameHostOption_Hardcore, pClass->m_bHardcore ? 1 : 0); #ifdef _LARGE_WORLDS app.SetGameHostOption(eGameHostOption_WorldSize, pClass->m_MoreOptionsParams.worldSize+1 ); // 0 is GAME_HOST_OPTION_WORLDSIZE_UNKNOWN diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.h b/Minecraft.Client/Common/UI/UIScene_LoadMenu.h index 53d66d55..91a6adc0 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.h @@ -15,7 +15,7 @@ private: eControl_OnlineGame, }; - static int m_iDifficultyTitleSettingA[4]; + static int m_iDifficultyTitleSettingA[5]; UIControl m_controlMainPanel; UIControl_Label m_labelGameName, m_labelSeed, m_labelCreatedMode; @@ -71,6 +71,7 @@ private: wstring m_thumbnailName; bool m_bRebuildTouchBoxes; + bool m_bHardcore; public: UIScene_LoadMenu(int iPad, void *initData, UILayer *parentLayer); diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index d73148f5..88f042b6 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -29,7 +29,7 @@ #include "..\..\..\Minecraft.World\NbtIo.h" #include "..\..\..\Minecraft.World\compression.h" -static wstring ReadLevelNameFromSaveFile(const wstring& filePath) +static wstring ReadLevelNameFromSaveFile(const wstring& filePath, bool *outHardcore = nullptr) { // Check for a worldname.txt sidecar written by the rename feature first size_t slashPos = filePath.rfind(L'\\'); @@ -124,7 +124,11 @@ static wstring ReadLevelNameFromSaveFile(const wstring& filePath) { CompoundTag *dataTag = root->getCompound(L"Data"); if (dataTag != nullptr) + { result = dataTag->getString(L"LevelName"); + if (outHardcore) + *outHardcore = dataTag->getBoolean(L"hardcore"); + } delete root; } } @@ -775,7 +779,9 @@ void UIScene_LoadOrJoinMenu::tick() 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); + bool saveHardcore = false; + wstring levelName = ReadLevelNameFromSaveFile(filePath, &saveHardcore); + m_saveDetails[i].isHardcore = saveHardcore; if (!levelName.empty()) { m_buttonListSaves.addItem(levelName, wstring(L"")); diff --git a/Minecraft.Client/Common/UI/UIStructs.h b/Minecraft.Client/Common/UI/UIStructs.h index 99c3d7bd..e5902b95 100644 --- a/Minecraft.Client/Common/UI/UIStructs.h +++ b/Minecraft.Client/Common/UI/UIStructs.h @@ -247,11 +247,14 @@ typedef struct _SaveListDetails #endif #endif + bool isHardcore; + _SaveListDetails() { saveId = 0; pbThumbnailData = nullptr; dwThumbnailSize = 0; + isHardcore = false; #ifdef _DURANGO ZeroMemory(UTF16SaveName,sizeof(wchar_t)*128); ZeroMemory(UTF16SaveFilename,sizeof(wchar_t)*MAX_SAVEFILENAME_LENGTH); diff --git a/Minecraft.Client/Common/XUI/XUI_Death.cpp b/Minecraft.Client/Common/XUI/XUI_Death.cpp index 1788ceff..58feb216 100644 --- a/Minecraft.Client/Common/XUI/XUI_Death.cpp +++ b/Minecraft.Client/Common/XUI/XUI_Death.cpp @@ -11,6 +11,7 @@ #include "..\..\..\Minecraft.World\Entity.h" #include "..\..\..\Minecraft.Client\MultiplayerLocalPlayer.h" #include "..\..\..\Minecraft.World\Level.h" +#include "..\..\..\Minecraft.World\net.minecraft.world.level.storage.h" #include "..\..\..\Minecraft.World\ChunkSource.h" #include "..\..\..\Minecraft.Client\ProgressRenderer.h" #include "..\..\..\Minecraft.Client\GameRenderer.h" @@ -18,6 +19,7 @@ #include "..\..\..\Minecraft.World\Pos.h" #include "..\..\..\Minecraft.World\Dimension.h" #include "..\..\Minecraft.h" +#include "..\..\MinecraftServer.h" #include "..\..\Options.h" #include "..\..\LocalPlayer.h" #include "..\..\..\Minecraft.World\compression.h" @@ -37,9 +39,26 @@ HRESULT CScene_Death::OnInit( XUIMessageInit* pInitData, BOOL& bHandled ) } XuiControlSetText(m_Title,app.GetString(IDS_YOU_DIED)); - XuiControlSetText(m_Buttons[BUTTON_DEATH_RESPAWN],app.GetString(IDS_RESPAWN)); XuiControlSetText(m_Buttons[BUTTON_DEATH_EXITGAME],app.GetString(IDS_EXIT_GAME)); + // 4J Added: In hardcore mode, disable respawn and show hardcore death message + Minecraft *pMinecraft = Minecraft::GetInstance(); + bool isHardcore = false; + if (pMinecraft != nullptr && pMinecraft->level != nullptr) + { + isHardcore = pMinecraft->level->getLevelData()->isHardcore(); + } + + if (isHardcore) + { + XuiControlSetText(m_Buttons[BUTTON_DEATH_RESPAWN], app.GetString(IDS_HARDCORE_DEATH_MESSAGE)); + XuiElementSetShow(m_Buttons[BUTTON_DEATH_RESPAWN], FALSE); + } + else + { + XuiControlSetText(m_Buttons[BUTTON_DEATH_RESPAWN], app.GetString(IDS_RESPAWN)); + } + // Display the tooltips ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT); @@ -110,7 +129,16 @@ HRESULT CScene_Death::OnNotifyPressEx(HXUIOBJ hObjPressed, XUINotifyPress* pNoti playTime = static_cast(pMinecraft->localplayers[pNotifyPressData->UserIndex]->getSessionTimer()); } TelemetryManager->RecordLevelExit(pNotifyPressData->UserIndex, eSen_LevelExitStatus_Failed); - + + // 4J Added: Hardcore mode — skip save dialog, exit without saving, delete world + if (pMinecraft->level != nullptr && pMinecraft->level->getLevelData()->isHardcore() && g_NetworkManager.IsHost()) + { + MinecraftServer::getInstance()->setSaveOnExit(false); + MinecraftServer::getInstance()->setDeleteWorldOnExit(true); + app.SetAction(pNotifyPressData->UserIndex, eAppAction_ExitWorld); + break; + } + if(StorageManager.GetSaveDisabled()) { uiIDA[0]=IDS_CONFIRM_CANCEL; @@ -172,6 +200,12 @@ HRESULT CScene_Death::OnNotifyPressEx(HXUIOBJ hObjPressed, XUINotifyPress* pNoti break; case BUTTON_DEATH_RESPAWN: { + // 4J Added: Safeguard - don't respawn in hardcore mode + Minecraft *pMC = Minecraft::GetInstance(); + if (pMC != nullptr && pMC->level != nullptr && pMC->level->getLevelData()->isHardcore()) + { + break; + } m_bIgnoreInput = true; app.SetAction(pNotifyPressData->UserIndex,eAppAction_Respawn); } diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index d538dd4b..c35ea256 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -1031,7 +1031,7 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) } } - lines.push_back(L""; // Add a spacer line + lines.push_back(L""); // Add a spacer line for (int i = eTerrainFeature_Stronghold; i <= static_cast(eTerrainFeature_Ravine); i++) { lines.push_back(wfeature[i]); diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 2cf6930a..d74af72f 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -561,6 +561,7 @@ MinecraftServer::MinecraftServer() m_serverPausedEvent = new C4JThread::Event; m_saveOnExit = false; + m_deleteWorldOnExit = false; m_suspending = false; m_ugcPlayersVersion = 0; @@ -881,6 +882,15 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring // } ProgressRenderer *mcprogress = Minecraft::GetInstance()->progressRenderer; + // 4J Added - store save folder name for potential hardcore world deletion + { + char szSaveFolder[MAX_SAVEFILENAME_LENGTH] = {}; + StorageManager.GetSaveUniqueFilename(szSaveFolder); + wchar_t wSaveFolder[MAX_SAVEFILENAME_LENGTH] = {}; + mbstowcs(wSaveFolder, szSaveFolder, MAX_SAVEFILENAME_LENGTH - 1); + m_saveFolderName = wSaveFolder; + } + // 4J TODO - free levels here if there are already some? levels = ServerLevelArray(3); @@ -1636,7 +1646,7 @@ bool MinecraftServer::isNetherEnabled() bool MinecraftServer::isHardcore() { - return false; + return app.GetGameHostOption(eGameHostOption_Hardcore) > 0; } int MinecraftServer::getOperatorUserPermissionLevel() diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index a33888bc..d9b33e4a 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -262,6 +262,8 @@ private: private: // 4J Added bool m_saveOnExit; + bool m_deleteWorldOnExit; // 4J Added - for hardcore mode world deletion + wstring m_saveFolderName; // 4J Added - stored for hardcore world deletion bool m_suspending; public: @@ -275,6 +277,9 @@ public: void chunkPacketManagement_PostTick(); void setSaveOnExit(bool save) { m_saveOnExit = save; s_bSaveOnExitAnswered = true; } + void setDeleteWorldOnExit(bool del) { m_deleteWorldOnExit = del; } + bool getDeleteWorldOnExit() const { return m_deleteWorldOnExit; } + const wstring& getSaveFolderName() const { return m_saveFolderName; } void Suspend(); bool IsSuspending(); diff --git a/Minecraft.Client/PlayerConnection.h b/Minecraft.Client/PlayerConnection.h index ff6093a3..b71a375a 100644 --- a/Minecraft.Client/PlayerConnection.h +++ b/Minecraft.Client/PlayerConnection.h @@ -1,6 +1,7 @@ #include "ConsoleInputSource.h" #include "..\Minecraft.World\PacketListener.h" #include "..\Minecraft.World\JavaIntHash.h" +#include class MinecraftServer; class Connection; @@ -125,8 +126,8 @@ public: void setShowOnMaps(bool bVal); - void setWasKicked() { m_bWasKicked = true; } - bool getWasKicked() { return m_bWasKicked; } + void setWasKicked() { m_bWasKicked.store(true); } + bool getWasKicked() { return m_bWasKicked.load(); } // 4J Added bool hasClientTickedOnce() { return m_bHasClientTickedOnce; } @@ -135,5 +136,5 @@ private: bool m_bCloseOnTick; vector m_texturesRequested; - bool m_bWasKicked; + std::atomic m_bWasKicked{false}; }; \ No newline at end of file diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 645e3fb5..bff68c85 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -62,6 +62,7 @@ PlayerList::PlayerList(MinecraftServer *server) int rawMax = server->settings->getInt(L"max-players", 8); maxPlayers = static_cast(Mth::clamp(rawMax, 1, MINECRAFT_NET_MAX_PLAYERS)); doWhiteList = false; + InitializeCriticalSection(&m_banCS); InitializeCriticalSection(&m_kickPlayersCS); InitializeCriticalSection(&m_closePlayersCS); } @@ -75,6 +76,7 @@ PlayerList::~PlayerList() player->gameMode = nullptr; } + DeleteCriticalSection(&m_banCS); DeleteCriticalSection(&m_kickPlayersCS); DeleteCriticalSection(&m_closePlayersCS); } @@ -266,7 +268,8 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr static_cast(playerIndex), level->useNewSeaLevel(), player->getAllPlayerGamePrivileges(), level->getLevelData()->getXZSize(), - level->getLevelData()->getHellScale())); + level->getLevelData()->getHellScale(), + level->getLevelData()->isHardcore())); playerConnection->send(std::make_shared(spawnPos->x, spawnPos->y, spawnPos->z)); playerConnection->send(std::make_shared(&player->abilities)); playerConnection->send(std::make_shared(player->inventory->selected)); @@ -697,6 +700,13 @@ shared_ptr PlayerList::respawn(shared_ptr serverPlay // necessary) updatePlayerGameMode(player, serverPlayer, level); + // 4J Added: Hardcore mode — force Adventure mode on respawn + if (server->getLevel(0)->getLevelData()->isHardcore()) + { + player->gameMode->setGameModeForPlayer(GameType::ADVENTURE); + player->connection->send(std::make_shared(GameEventPacket::CHANGE_GAME_MODE, GameType::ADVENTURE->getId())); + } + if(serverPlayer->wonGame && targetDimension == oldDimension && serverPlayer->getHealth() > 0) { // If the player is still alive and respawning to the same dimension, they are just being added back from someone else viewing the Win screen @@ -734,7 +744,7 @@ shared_ptr PlayerList::respawn(shared_ptr serverPlay player->connection->send( std::make_shared( static_cast(player->dimension), player->level->getSeed(), player->level->getMaxBuildHeight(), player->gameMode->getGameModeForPlayer(), level->difficulty, level->getLevelData()->getGenerator(), - player->level->useNewSeaLevel(), player->entityId, level->getLevelData()->getXZSize(), level->getLevelData()->getHellScale() ) ); + player->level->useNewSeaLevel(), player->entityId, level->getLevelData()->getXZSize(), level->getLevelData()->getHellScale(), level->getLevelData()->isHardcore() ) ); player->connection->teleport(player->x, player->y, player->z, player->yRot, player->xRot); player->connection->send( std::make_shared( player->experienceProgress, player->totalExperience, player->experienceLevel) ); @@ -851,7 +861,7 @@ void PlayerList::toggleDimension(shared_ptr player, int targetDime player->connection->send(std::make_shared(static_cast(player->dimension), newLevel->getSeed(), newLevel->getMaxBuildHeight(), player->gameMode->getGameModeForPlayer(), newLevel->difficulty, newLevel->getLevelData()->getGenerator(), - newLevel->useNewSeaLevel(), player->entityId, newLevel->getLevelData()->getXZSize(), newLevel->getLevelData()->getHellScale())); + newLevel->useNewSeaLevel(), player->entityId, newLevel->getLevelData()->getXZSize(), newLevel->getLevelData()->getHellScale(), newLevel->getLevelData()->isHardcore())); oldLevel->removeEntityImmediately(player); player->removed = false; @@ -1665,6 +1675,7 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) bool banned = false; + EnterCriticalSection(&m_banCS); for(PlayerUID it : m_bannedXuids) { if( ProfileManager.AreXUIDSEqual( xuid, it ) ) @@ -1673,10 +1684,39 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) break; } } + LeaveCriticalSection(&m_banCS); return banned; } +void PlayerList::banXuid(PlayerUID xuid) +{ + // 4J Added - for hardcore mode ban-on-death + // Ban a player's XUID. Used when a player dies in a hardcore multiplayer world. + if(xuid == INVALID_XUID) return; + + EnterCriticalSection(&m_banCS); + + // Check if already banned + bool alreadyBanned = false; + for(PlayerUID it : m_bannedXuids) + { + if( ProfileManager.AreXUIDSEqual( xuid, it ) ) + { + alreadyBanned = true; + break; + } + } + + if(!alreadyBanned) + { + m_bannedXuids.push_back(xuid); + app.DebugPrintf("PlayerList::banXuid - Player XUID banned for hardcore death\n"); + } + + LeaveCriticalSection(&m_banCS); +} + // AP added for Vita so the range can be increased once the level starts void PlayerList::setViewDistance(int newViewDistance) { diff --git a/Minecraft.Client/PlayerList.h b/Minecraft.Client/PlayerList.h index a4ae9c5d..0892634a 100644 --- a/Minecraft.Client/PlayerList.h +++ b/Minecraft.Client/PlayerList.h @@ -31,6 +31,7 @@ private: // 4J Added vector m_bannedXuids; + CRITICAL_SECTION m_banCS; // 4J Added - protects m_bannedXuids for concurrent access deque m_smallIdsToKick; CRITICAL_SECTION m_kickPlayersCS; deque m_smallIdsToClose; @@ -135,6 +136,7 @@ public: void closePlayerConnectionBySmallId(BYTE networkSmallId); void queueSmallIdForRecycle(BYTE smallId); bool isXuidBanned(PlayerUID xuid); + void banXuid(PlayerUID xuid); // 4J Added - for hardcore mode ban-on-death // AP added for Vita so the range can be increased once the level starts void setViewDistance(int newViewDistance); }; diff --git a/Minecraft.Client/SelectWorldScreen.cpp b/Minecraft.Client/SelectWorldScreen.cpp index c6d417c5..213bf8df 100644 --- a/Minecraft.Client/SelectWorldScreen.cpp +++ b/Minecraft.Client/SelectWorldScreen.cpp @@ -303,6 +303,12 @@ void SelectWorldScreen::WorldSelectionList::renderItem(int i, int x, int y, int info = parent->conversionLang + L" " + info; } + // 4J Added: Show [Hardcore] badge for hardcore worlds + if (levelSummary->isHardcore()) + { + name = name + L" [Hardcore]"; + } + parent->drawString(parent->font, name, x + 2, y + 1, 0xffffff); parent->drawString(parent->font, id, x + 2, y + 12, 0x808080); parent->drawString(parent->font, info, x + 2, y + 12 + 10, 0x808080); diff --git a/Minecraft.Client/ServerPlayer.cpp b/Minecraft.Client/ServerPlayer.cpp index c7ff4fda..94de3906 100644 --- a/Minecraft.Client/ServerPlayer.cpp +++ b/Minecraft.Client/ServerPlayer.cpp @@ -564,6 +564,12 @@ void ServerPlayer::die(DamageSource *source) { server->getPlayers()->broadcastAll(getCombatTracker()->getDeathMessagePacket()); + // 4J Added: Hardcore mode — switch to Adventure mode on death (can look but not break/place blocks) + if (level->getLevelData()->isHardcore()) + { + setGameMode(GameType::ADVENTURE); + } + if (!level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY)) { inventory->dropAll(); @@ -1604,9 +1610,9 @@ bool ServerPlayer::hasPermission(EGameCommand command) // // // 4J - Don't need // //if (server.isSingleplayer() && server.getSingleplayerName().equals(name)) -// //{ +/// //{ // // server.setDifficulty(packet.getDifficulty()); -// //} +/// //} //} int ServerPlayer::getViewDistance() diff --git a/Minecraft.Client/Windows64Media/strings.h b/Minecraft.Client/Windows64Media/strings.h index 63e3a7f4..47331556 100644 --- a/Minecraft.Client/Windows64Media/strings.h +++ b/Minecraft.Client/Windows64Media/strings.h @@ -2289,3 +2289,11 @@ #define IDS_RICHPRESENCESTATE_BREWING 2283 #define IDS_RICHPRESENCESTATE_ANVIL 2284 #define IDS_RICHPRESENCESTATE_TRADING 2285 +#define IDS_GAMEMODE_HARDCORE 2286 +#define IDS_HARDCORE 2287 +#define IDS_HARDCORE_TOOLTIP 2288 +#define IDS_HARDCORE_WARNING_TITLE 2289 +#define IDS_HARDCORE_WARNING_TEXT 2290 +#define IDS_HARDCORE_DEATH_MESSAGE 2291 +#define IDS_LABEL_HARDCORE 2292 +#define IDS_GAMEOPTION_HARDCORE 2293 \ No newline at end of file diff --git a/Minecraft.World/ConsoleSaveFileOriginal.cpp b/Minecraft.World/ConsoleSaveFileOriginal.cpp index 8a7d1f9e..4207aba4 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.cpp +++ b/Minecraft.World/ConsoleSaveFileOriginal.cpp @@ -838,6 +838,51 @@ int ConsoleSaveFileOriginal::SaveSaveDataCallback(LPVOID lpParam,bool bRes) { ConsoleSaveFile *pClass=static_cast(lpParam); +#ifdef _WINDOWS64 + // 4J Added: After save completes, capture the save folder name for hardcore world deletion + if (bRes && app.GetCurrentSaveFolderName().empty()) + { + // Try 1: Ask the library for the folder name + char szSaveFolder[MAX_SAVEFILENAME_LENGTH] = {}; + if (StorageManager.GetSaveUniqueFilename(szSaveFolder) && szSaveFolder[0] != '\0') + { + wchar_t wFolder[MAX_SAVEFILENAME_LENGTH] = {}; + mbstowcs(wFolder, szSaveFolder, MAX_SAVEFILENAME_LENGTH - 1); + app.SetCurrentSaveFolderName(wFolder); + app.DebugPrintf("SaveSaveDataCallback: captured save folder '%s'\n", szSaveFolder); + } + // Try 2: Scan GameHDD for the newest folder — right after save, it's guaranteed to be ours + if (app.GetCurrentSaveFolderName().empty()) + { + WIN32_FIND_DATAW fd; + HANDLE hFind = FindFirstFileW(L"Windows64\\GameHDD\\*", &fd); + if (hFind != INVALID_HANDLE_VALUE) + { + FILETIME newestTime = {}; + wchar_t newestFolder[MAX_PATH] = {}; + do + { + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && + wcscmp(fd.cFileName, L".") != 0 && wcscmp(fd.cFileName, L"..") != 0) + { + if (CompareFileTime(&fd.ftLastWriteTime, &newestTime) > 0) + { + newestTime = fd.ftLastWriteTime; + wcscpy_s(newestFolder, MAX_PATH, fd.cFileName); + } + } + } while (FindNextFileW(hFind, &fd)); + FindClose(hFind); + if (newestFolder[0] != L'\0') + { + app.SetCurrentSaveFolderName(newestFolder); + app.DebugPrintf("SaveSaveDataCallback: captured save folder via scan '%ls'\n", newestFolder); + } + } + } + } +#endif + return 0; } diff --git a/Minecraft.World/DisconnectPacket.h b/Minecraft.World/DisconnectPacket.h index 3c96a429..b6648ddf 100644 --- a/Minecraft.World/DisconnectPacket.h +++ b/Minecraft.World/DisconnectPacket.h @@ -50,6 +50,7 @@ public: #ifdef _XBOX_ONE eDisconnect_ExitedGame, #endif + eDisconnect_HardcoreDeath, // 4J Added - for hardcore mode multiplayer ban-on-death }; // 4J Stu - The reason was a string, but we need to send a non-locale specific reason @@ -71,3 +72,4 @@ public: }; + diff --git a/Minecraft.World/LoginPacket.cpp b/Minecraft.World/LoginPacket.cpp index 79cdfbae..74b6d8da 100644 --- a/Minecraft.World/LoginPacket.cpp +++ b/Minecraft.World/LoginPacket.cpp @@ -34,6 +34,7 @@ LoginPacket::LoginPacket() m_uiGamePrivileges = 0; m_xzSize = LEVEL_MAX_WIDTH; m_hellScale = HELL_LEVEL_MAX_SCALE; + m_isHardcore = false; } // Client -> Server @@ -63,10 +64,11 @@ LoginPacket::LoginPacket(const wstring& userName, int clientVersion, PlayerUID o m_uiGamePrivileges = 0; m_xzSize = LEVEL_MAX_WIDTH; m_hellScale = HELL_LEVEL_MAX_SCALE; + m_isHardcore = false; } // Server -> Client -LoginPacket::LoginPacket(const wstring& userName, int clientVersion, LevelType *pLevelType, int64_t seed, int gameType, char dimension, BYTE mapHeight, BYTE maxPlayers, char difficulty, INT multiplayerInstanceId, BYTE playerIndex, bool newSeaLevel, unsigned int uiGamePrivileges, int xzSize, int hellScale) +LoginPacket::LoginPacket(const wstring& userName, int clientVersion, LevelType *pLevelType, int64_t seed, int gameType, char dimension, BYTE mapHeight, BYTE maxPlayers, char difficulty, INT multiplayerInstanceId, BYTE playerIndex, bool newSeaLevel, unsigned int uiGamePrivileges, int xzSize, int hellScale, bool isHardcore) { this->userName = userName; this->clientVersion = clientVersion; @@ -92,6 +94,7 @@ LoginPacket::LoginPacket(const wstring& userName, int clientVersion, LevelType * m_uiGamePrivileges = uiGamePrivileges; m_xzSize = xzSize; m_hellScale = hellScale; + m_isHardcore = isHardcore; } void LoginPacket::read(DataInputStream *dis) //throws IOException @@ -127,6 +130,7 @@ void LoginPacket::read(DataInputStream *dis) //throws IOException m_xzSize = dis->readShort(); m_hellScale = dis->read(); #endif + m_isHardcore = dis->readBoolean(); app.DebugPrintf("LoginPacket::read - Difficulty = %d\n",difficulty); } @@ -164,6 +168,7 @@ void LoginPacket::write(DataOutputStream *dos) //throws IOException dos->writeShort(m_xzSize); dos->write(m_hellScale); #endif + dos->writeBoolean(m_isHardcore); } void LoginPacket::handle(PacketListener *listener) @@ -179,5 +184,5 @@ int LoginPacket::getEstimatedSize() length = static_cast(m_pLevelType->getGeneratorName().length()); } - return static_cast(sizeof(int) + userName.length() + 4 + 6 + sizeof(int64_t) + sizeof(char) + sizeof(int) + (2 * sizeof(PlayerUID)) + 1 + sizeof(char) + sizeof(BYTE) + sizeof(bool) + sizeof(bool) + length + sizeof(unsigned int)); + return static_cast(sizeof(int) + userName.length() + 4 + 6 + sizeof(int64_t) + sizeof(char) + sizeof(int) + (2 * sizeof(PlayerUID)) + 1 + sizeof(char) + sizeof(BYTE) + sizeof(bool) + sizeof(bool) + length + sizeof(unsigned int) + sizeof(bool)); } diff --git a/Minecraft.World/LoginPacket.h b/Minecraft.World/LoginPacket.h index 02a62b60..e168ef79 100644 --- a/Minecraft.World/LoginPacket.h +++ b/Minecraft.World/LoginPacket.h @@ -24,6 +24,7 @@ public: unsigned int m_uiGamePrivileges; int m_xzSize; // 4J Added int m_hellScale; // 4J Added + bool m_isHardcore; // 4J Added - for hardcore mode // 1.8.2 int gameType; @@ -31,7 +32,7 @@ public: BYTE maxPlayers; LoginPacket(); - LoginPacket(const wstring& userName, int clientVersion, LevelType *pLevelType, int64_t seed, int gameType, char dimension, BYTE mapHeight, BYTE maxPlayers, char difficulty, INT m_multiplayerInstanceId, BYTE playerIndex, bool newSeaLevel, unsigned int uiGamePrivileges, int xzSize, int hellScale); // Server -> Client + LoginPacket(const wstring& userName, int clientVersion, LevelType *pLevelType, int64_t seed, int gameType, char dimension, BYTE mapHeight, BYTE maxPlayers, char difficulty, INT m_multiplayerInstanceId, BYTE playerIndex, bool newSeaLevel, unsigned int uiGamePrivileges, int xzSize, int hellScale, bool isHardcore = false); // Server -> Client LoginPacket(const wstring& userName, int clientVersion, PlayerUID offlineXuid, PlayerUID onlineXuid, bool friendsOnlyUGC, DWORD ugcPlayersVersion, DWORD skinId, DWORD capeId, bool isGuest); // Client -> Server virtual void read(DataInputStream *dis); diff --git a/Minecraft.World/RespawnPacket.cpp b/Minecraft.World/RespawnPacket.cpp index 6dbebfac..174dd720 100644 --- a/Minecraft.World/RespawnPacket.cpp +++ b/Minecraft.World/RespawnPacket.cpp @@ -17,9 +17,10 @@ RespawnPacket::RespawnPacket() m_newEntityId = 0; m_xzSize = LEVEL_MAX_WIDTH; m_hellScale = HELL_LEVEL_MAX_SCALE; + m_isHardcore = false; } -RespawnPacket::RespawnPacket(char dimension, int64_t mapSeed, int mapHeight, GameType *playerGameType, char difficulty, LevelType *pLevelType, bool newSeaLevel, int newEntityId, int xzSize, int hellScale) +RespawnPacket::RespawnPacket(char dimension, int64_t mapSeed, int mapHeight, GameType *playerGameType, char difficulty, LevelType *pLevelType, bool newSeaLevel, int newEntityId, int xzSize, int hellScale, bool isHardcore) { this->dimension = dimension; this->mapSeed = mapSeed; @@ -31,6 +32,7 @@ RespawnPacket::RespawnPacket(char dimension, int64_t mapSeed, int mapHeight, Gam this->m_newEntityId = newEntityId; m_xzSize = xzSize; m_hellScale = hellScale; + m_isHardcore = isHardcore; app.DebugPrintf("RespawnPacket - Difficulty = %d\n",difficulty); } @@ -59,6 +61,7 @@ void RespawnPacket::read(DataInputStream *dis) //throws IOException m_xzSize = dis->readShort(); m_hellScale = dis->read(); #endif + m_isHardcore = dis->readBoolean(); app.DebugPrintf("RespawnPacket::read - Difficulty = %d\n",difficulty); } @@ -84,6 +87,7 @@ void RespawnPacket::write(DataOutputStream *dos) //throws IOException dos->writeShort(m_xzSize); dos->write(m_hellScale); #endif + dos->writeBoolean(m_isHardcore); } int RespawnPacket::getEstimatedSize() @@ -93,5 +97,5 @@ int RespawnPacket::getEstimatedSize() { length = static_cast(m_pLevelType->getGeneratorName().length()); } - return 13+length; + return 14+length; } diff --git a/Minecraft.World/RespawnPacket.h b/Minecraft.World/RespawnPacket.h index dc815994..bcc72592 100644 --- a/Minecraft.World/RespawnPacket.h +++ b/Minecraft.World/RespawnPacket.h @@ -19,9 +19,10 @@ public: int m_newEntityId; int m_xzSize; // 4J Added int m_hellScale; // 4J Added + bool m_isHardcore; // 4J Added - for hardcore mode RespawnPacket(); - RespawnPacket(char dimension, int64_t mapSeed, int mapHeight, GameType *playerGameType, char difficulty, LevelType *pLevelType, bool newSeaLevel, int newEntityId, int xzSize, int hellScale); + RespawnPacket(char dimension, int64_t mapSeed, int mapHeight, GameType *playerGameType, char difficulty, LevelType *pLevelType, bool newSeaLevel, int newEntityId, int xzSize, int hellScale, bool isHardcore = false); virtual void handle(PacketListener *listener); virtual void read(DataInputStream *dis);