diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 325e949b..5d9559ae 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -342,7 +342,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; @@ -411,7 +411,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; @@ -2870,7 +2870,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 0a2fd159..bd0ef11b 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -8084,6 +8084,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 f0d44319..187a38c1 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -854,6 +854,210 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) // font.draw(str, x + 1, y, 0xffffff); // } +#ifndef _FINAL_BUILD + MemSect(31); + + // temporarily render overlay at all times so version is more obvious in bug reports + // we can turn this off once things stabilize + if (true)// minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr) + { + const int debugLeft = 1; + const int debugTop = 1; + const float maxContentWidth = 1200.f; + const float maxContentHeight = 420.f; + float scale = static_cast(screenWidth - debugLeft - 8) / maxContentWidth; + float scaleV = static_cast(screenHeight - debugTop - 80) / maxContentHeight; + if (scaleV < scale) scale = scaleV; + if (scale > 1.f) scale = 1.f; + if (scale < 0.5f) scale = 0.5f; + glPushMatrix(); + glTranslatef(static_cast(debugLeft), static_cast(debugTop), 0.f); + glScalef(scale, scale, 1.f); + glTranslatef(static_cast(-debugLeft), static_cast(-debugTop), 0.f); + + vector lines; + + // Only add version/branch lines if the watermark toggle is enabled + if (ClientConstants::SHOW_VERSION_WATERMARK) { + lines.push_back(ClientConstants::VERSION_STRING); + lines.push_back(ClientConstants::BRANCH_STRING); + } + if (minecraft->options->renderDebug && minecraft->player != nullptr && minecraft->level != nullptr) + { + lines.push_back(minecraft->fpsString); + lines.push_back(L"E: " + std::to_wstring(minecraft->level->getAllEntities().size())); // Could maybe use entity::shouldRender to work out how many are rendered but thats like expensive + // TODO Add server information with packet counts - once multiplayer is more stable + int renderDistance = app.GetGameSettings(iPad, eGameSetting_RenderDistance); + // Calculate the chunk sections using 16 * (2n + 1)^2 + lines.push_back(L"C: " + std::to_wstring(16 * (2 * renderDistance + 1) * (2 * renderDistance + 1)) + L" D: " + std::to_wstring(renderDistance)); + lines.push_back(minecraft->gatherStats4()); // Chunk Cache + + // Dimension + wstring dimension = L"unknown"; + switch (minecraft->player->dimension) + { + case -1: + dimension = L"minecraft:the_nether"; + break; + case 0: + dimension = L"minecraft:overworld"; + break; + case 1: + dimension = L"minecraft:the_end"; + break; + } + lines.push_back(dimension); + + lines.push_back(L""); // Spacer + + // Players block pos + int xBlockPos = Mth::floor(minecraft->player->x); + int yBlockPos = Mth::floor(minecraft->player->y); + int zBlockPos = Mth::floor(minecraft->player->z); + + // Chunk player is in + int xChunkPos = xBlockPos >> 4; + int yChunkPos = yBlockPos >> 4; + int zChunkPos = zBlockPos >> 4; + + // Players offset within the chunk + int xChunkOffset = xBlockPos & 15; + int yChunkOffset = yBlockPos & 15; + int zChunkOffset = zBlockPos & 15; + + // Format the position like java with limited decumal places + WCHAR posString[44]; // Allows upto 7 digit positions (+-9_999_999) + swprintf(posString, 44, L"%.3f / %.5f / %.3f", minecraft->player->x, minecraft->player->y, minecraft->player->z); + + lines.push_back(L"XYZ: " + std::wstring(posString)); + lines.push_back(L"Block: " + std::to_wstring(static_cast(xBlockPos)) + L" " + std::to_wstring(static_cast(yBlockPos)) + L" " + std::to_wstring(static_cast(zBlockPos))); + lines.push_back(L"Chunk: " + std::to_wstring(xChunkOffset) + L" " + std::to_wstring(yChunkOffset) + L" " + std::to_wstring(zChunkOffset) + L" in " + std::to_wstring(xChunkPos) + L" " + std::to_wstring(yChunkPos) + L" " + std::to_wstring(zChunkPos)); + + // Wrap the yRot to 360 then adjust to (-180 to 180) range to match java + float yRotDisplay = fmod(minecraft->player->yRot, 360.0f); + if (yRotDisplay > 180.0f) + { + yRotDisplay -= 360.0f; + } + if (yRotDisplay < -180.0f) + { + yRotDisplay += 360.0f; + } + // Generate the angle string in the format "yRot / xRot" with one decimal place, similar to java edition + WCHAR angleString[16]; + swprintf(angleString, 16, L"%.1f / %.1f", yRotDisplay, minecraft->player->xRot); + + // Work out the named direction + int direction = Mth::floor(minecraft->player->yRot * 4.0f / 360.0f + 0.5) & 0x3; + wstring cardinalDirection; + switch (direction) + { + case 0: + cardinalDirection = L"south"; + break; + case 1: + cardinalDirection = L"west"; + break; + case 2: + cardinalDirection = L"north"; + break; + case 3: + cardinalDirection = L"east"; + break; + } + + lines.push_back(L"Facing: " + cardinalDirection + L" (" + angleString + L")"); + + // We have to limit y to 256 as we don't get any information past that + if (minecraft->level != NULL && minecraft->level->hasChunkAt(xBlockPos, fmod(yBlockPos, 256), zBlockPos)) + { + LevelChunk *chunkAt = minecraft->level->getChunkAt(xBlockPos, zBlockPos); + if (chunkAt != NULL) + { + int skyLight = chunkAt->getBrightness(LightLayer::Sky, xChunkOffset, yChunkOffset, zChunkOffset); + int blockLight = chunkAt->getBrightness(LightLayer::Block, xChunkOffset, yChunkOffset, zChunkOffset); + int maxLight = fmax(skyLight, blockLight); + lines.push_back(L"Light: " + std::to_wstring(maxLight) + L" (" + std::to_wstring(skyLight) + L" sky, " + std::to_wstring(blockLight) + L" block)"); + + lines.push_back(L"CH S: " + std::to_wstring(chunkAt->getHeightmap(xChunkOffset, zChunkOffset))); + + Biome *biome = chunkAt->getBiome(xChunkOffset, zChunkOffset, minecraft->level->getBiomeSource()); + lines.push_back(L"Biome: " + biome->m_name + L" (" + std::to_wstring(biome->id) + L")"); + + lines.push_back(L"Difficulty: " + std::to_wstring(minecraft->level->difficulty) + L" (Day " + std::to_wstring(minecraft->level->getGameTime() / Level::TICKS_PER_DAY) + L")"); + } + } + + // This is all LCE only stuff, it was never on java + lines.push_back(L""); // Spacer + lines.push_back(L"Seed: " + std::to_wstring(minecraft->level->getLevelData()->getSeed())); + lines.push_back(minecraft->gatherStats1()); // Time to autosave + lines.push_back(minecraft->gatherStats2()); // Empty currently - CPlatformNetworkManagerStub::GatherStats() + lines.push_back(minecraft->gatherStats3()); // RTT + } + +#ifdef _DEBUG // Only show terrain features in debug builds not release + // TERRAIN FEATURES + if (minecraft->level->dimension->id == 0) + { + wstring wfeature[eTerrainFeature_Count]; + + wfeature[eTerrainFeature_Stronghold] = L"Stronghold: "; + wfeature[eTerrainFeature_Mineshaft] = L"Mineshaft: "; + wfeature[eTerrainFeature_Village] = L"Village: "; + wfeature[eTerrainFeature_Ravine] = L"Ravine: "; + + float maxW = static_cast(screenWidth - debugLeft - 8) / scale; + float maxWForContent = maxW - static_cast(font->width(L"...")); + bool truncated[eTerrainFeature_Count] = {}; + + for (size_t i = 0; i < app.m_vTerrainFeatures.size(); i++) + { + FEATURE_DATA *pFeatureData = app.m_vTerrainFeatures[i]; + int type = pFeatureData->eTerrainFeature; + if (type < eTerrainFeature_Stronghold || type > eTerrainFeature_Ravine) + { + continue; + } + if (truncated[type]) + { + continue; + } + + wstring itemInfo = L"[" + std::to_wstring(pFeatureData->x * 16) + L", " + std::to_wstring(pFeatureData->z * 16) + L"] "; + if (font->width(wfeature[type] + itemInfo) <= maxWForContent) + { + wfeature[type] += itemInfo; + } + else + { + wfeature[type] += L"..."; + truncated[type] = true; + } + } + + lines.push_back(L""); // Add a spacer line + for (int i = eTerrainFeature_Stronghold; i <= static_cast(eTerrainFeature_Ravine); i++) + { + lines.push_back(wfeature[i]); + } + lines.push_back(L""); + } +#endif + + // Loop through the lines and draw them all on screen + int yPos = debugTop; + for (const auto &line : lines) + { + drawString(font, line, debugLeft, yPos, 0xffffff); + yPos += 10; + } + + glPopMatrix(); + } + MemSect(0); +#endif + lastTickA = a; // 4J Stu - This is now displayed in a xui scene #if 0 diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index a545eff8..cbd35535 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; @@ -887,6 +888,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); @@ -1642,7 +1652,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 1ed5db9d..7313471d 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -265,6 +265,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: @@ -278,6 +280,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 0284bc6a..fe7a9afa 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; @@ -130,8 +131,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; } @@ -140,5 +141,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 ba82ec6a..73d783d8 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -67,6 +67,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); } @@ -80,6 +81,7 @@ PlayerList::~PlayerList() player->gameMode = nullptr; } + DeleteCriticalSection(&m_banCS); DeleteCriticalSection(&m_kickPlayersCS); DeleteCriticalSection(&m_closePlayersCS); } @@ -271,7 +273,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)); @@ -702,6 +705,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 @@ -739,7 +749,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) ); @@ -856,7 +866,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; @@ -1670,6 +1680,7 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) bool banned = false; + EnterCriticalSection(&m_banCS); for(PlayerUID it : m_bannedXuids) { if( ProfileManager.AreXUIDSEqual( xuid, it ) ) @@ -1678,6 +1689,7 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) break; } } + LeaveCriticalSection(&m_banCS); #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) if (!banned && g_Win64DedicatedServer) @@ -1689,6 +1701,34 @@ bool PlayerList::isXuidBanned(PlayerUID xuid) 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);