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);