diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 8b1266d1..73654da1 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -1007,6 +1007,9 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring #endif levels[i]->getLevelData()->setGameType(gameType); + // Apply hardcore flag from host option to level data so loaded worlds respect server.properties + levels[i]->getLevelData()->setHardcore(isHardcore()); + if(app.getLevelGenerationOptions() != nullptr) { LevelGenerationOptions *mapOptions = app.getLevelGenerationOptions(); diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index 6319b660..054dcf71 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -25,6 +25,7 @@ #include "..\Minecraft.World\StringHelpers.h" #include "..\Minecraft.World\Socket.h" #include "..\Minecraft.World\Achievements.h" +#include "..\Minecraft.World\LevelData.h" #include "..\Minecraft.World\net.minecraft.h" #include "EntityTracker.h" #include "ServerConnection.h" @@ -142,6 +143,14 @@ void PlayerConnection::tick() { dropSpamTickCount--; } + + // Ensure server-side player tick runs even when no move packet was received this tick. + // Without this, environmental damage (drowning, fire, lava) is never applied to clients + // that don't send frequent move packets. + if (!didTick && player != nullptr) + { + player->doTick(false); + } } void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason) @@ -1109,22 +1118,12 @@ void PlayerConnection::handleClientCommand(shared_ptr packe { player = server->getPlayers()->respawn(player, player->m_enteredEndExitPortal?0:player->dimension, true); } - //else if (player.getLevel().getLevelData().isHardcore()) - //{ - // if (server.isSingleplayer() && player.name.equals(server.getSingleplayerName())) - // { - // player.connection.disconnect("You have died. Game over, man, it's game over!"); - // server.selfDestruct(); - // } - // else - // { - // BanEntry ban = new BanEntry(player.name); - // ban.setReason("Death in Hardcore"); - - // server.getPlayers().getBans().add(ban); - // player.connection.disconnect("You have died. Game over, man, it's game over!"); - // } - //} + else if (player->level->getLevelData()->isHardcore()) + { + // Hardcore mode — server rejects respawn. Ban and disconnect are already + // handled in ServerPlayer::die() via banPlayerForHardcoreDeath(). + return; + } else { if (player->getHealth() > 0) return; diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 73d783d8..8848ed16 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -39,6 +39,9 @@ #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\Access\Access.h" +#include "..\Minecraft.Server\Common\StringUtils.h" +#include "..\Minecraft.Server\ServerLogger.h" +#include "..\Minecraft.Server\ServerLogManager.h" extern bool g_Win64DedicatedServer; #endif @@ -1729,6 +1732,71 @@ void PlayerList::banXuid(PlayerUID xuid) LeaveCriticalSection(&m_banCS); } +void PlayerList::banPlayerForHardcoreDeath(ServerPlayer *player) +{ + if (player == nullptr) return; + + // Always apply the in-memory XUID ban (works for both client-hosted and dedicated) + banXuid(player->getOnlineXuid()); + +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (g_Win64DedicatedServer) + { + const std::string playerName = ServerRuntime::StringUtils::WideToUtf8(player->getName()); + + ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Hardcore Death"); + metadata.reason = "Died in hardcore mode"; + + // Ban online XUID + ServerRuntime::Access::AddPlayerBan(player->getOnlineXuid(), playerName, metadata); + + // Also ban offline XUID if it differs (follows CliCommandBan pattern) + PlayerUID offlineXuid = player->getXuid(); + if (offlineXuid != INVALID_XUID && offlineXuid != player->getOnlineXuid()) + { + ServerRuntime::Access::AddPlayerBan(offlineXuid, playerName, metadata); + } + + // Ban the player's IP address (uses same access path as CliCommandBanIp) + if (player->connection != nullptr && player->connection->connection != nullptr && player->connection->connection->getSocket() != nullptr) + { + const unsigned char smallId = player->connection->connection->getSocket()->getSmallId(); + std::string ip; + if (smallId != 0 && ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, &ip)) + { + ServerRuntime::Access::AddIpBan(ip, metadata); + ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID + IP %s) for dying in hardcore mode.", playerName.c_str(), ip.c_str()); + } + else + { + ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID only, IP not available) for dying in hardcore mode.", playerName.c_str()); + } + } + else + { + ServerRuntime::LogInfof("Hardcore", "Player %s banned (XUID only, no connection) for dying in hardcore mode.", playerName.c_str()); + } + + // Send ban reason then defer the actual close to the next tick, because this + // method runs mid-tick inside ServerPlayer::die(). A synchronous disconnect + // can invalidate the player/connection while the tick is still executing. + if (player->connection != nullptr) + { + player->connection->send(std::make_shared(DisconnectPacket::eDisconnect_Banned)); + player->connection->closeOnTick(); + } + + } + else +#endif + { + // Client-hosted: force-save so the host cannot circumvent death by quitting without saving. + // On dedicated server the autosave handles persistence, so skip the forced save to avoid + // the client getting stuck on a "host is saving" screen during disconnect. + app.SetXuiServerAction(ProfileManager.GetPrimaryPad(), eXuiServerAction_SaveGame); + } +} + // 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 0892634a..e310d19c 100644 --- a/Minecraft.Client/PlayerList.h +++ b/Minecraft.Client/PlayerList.h @@ -137,6 +137,7 @@ public: void queueSmallIdForRecycle(BYTE smallId); bool isXuidBanned(PlayerUID xuid); void banXuid(PlayerUID xuid); // 4J Added - for hardcore mode ban-on-death + void banPlayerForHardcoreDeath(ServerPlayer *player); // Persistent XUID + IP ban on hardcore death // AP added for Vita so the range can be increased once the level starts void setViewDistance(int newViewDistance); }; diff --git a/Minecraft.Client/ServerPlayer.cpp b/Minecraft.Client/ServerPlayer.cpp index dd92bdac..14f2391e 100644 --- a/Minecraft.Client/ServerPlayer.cpp +++ b/Minecraft.Client/ServerPlayer.cpp @@ -574,10 +574,10 @@ void ServerPlayer::die(DamageSource *source) { setGameMode(GameType::ADVENTURE); - // Ban this player's XUID and force-save so the host - // cannot circumvent the death by quitting without saving. - server->getPlayers()->banXuid(getOnlineXuid()); - app.SetXuiServerAction(ProfileManager.GetPrimaryPad(), eXuiServerAction_SaveGame); + // Ban this player's XUID and queue disconnect. + // The force-save is triggered inside banPlayerForHardcoreDeath after the + // disconnect is queued, so the client doesn't get stuck on a save screen. + server->getPlayers()->banPlayerForHardcoreDeath(this); } if (!level->getGameRules()->getBoolean(GameRules::RULE_KEEPINVENTORY)) diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index d6ba64e7..2b70f475 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -54,6 +54,7 @@ static const ServerPropertyDefault kServerPropertyDefaults[] = { "gamemode", "0" }, { "gamertags", "true" }, { "generate-structures", "true" }, + { "hardcore", "false" }, { "host-can-be-invisible", "true" }, { "host-can-change-hunger", "true" }, { "host-can-fly", "true" }, @@ -861,6 +862,7 @@ ServerPropertiesConfig LoadServerPropertiesConfig() config.doTileDrops = ReadNormalizedBoolProperty(&merged, "do-tile-drops", true, &shouldWrite); config.naturalRegeneration = ReadNormalizedBoolProperty(&merged, "natural-regeneration", true, &shouldWrite); config.doDaylightCycle = ReadNormalizedBoolProperty(&merged, "do-daylight-cycle", true, &shouldWrite); + config.hardcore = ReadNormalizedBoolProperty(&merged, "hardcore", false, &shouldWrite); config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite); config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite); diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 3bb5aca8..1e303399 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -73,6 +73,7 @@ namespace ServerRuntime bool doTileDrops; bool naturalRegeneration; bool doDaylightCycle; + bool hardcore; /** other MinecraftServer runtime settings */ int maxBuildHeight; diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index a8d5fc66..69d69c40 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -369,6 +369,13 @@ int main(int argc, char **argv) ServerPropertiesConfig serverProperties = LoadServerPropertiesConfig(); ApplyServerPropertiesToDedicatedConfig(serverProperties, &config); + // Hardcore mode forces Hard difficulty (matches vanilla Java behavior) + if (serverProperties.hardcore && serverProperties.difficulty != 3) + { + LogInfof("startup", "Hardcore mode enabled: forcing difficulty from %d to 3 (Hard).", serverProperties.difficulty); + serverProperties.difficulty = 3; + } + if (!ParseCommandLine(argc, argv, &config)) { PrintUsage(); @@ -529,6 +536,7 @@ int main(int argc, char **argv) app.SetGameHostOption(eGameHostOption_DoTileDrops, serverProperties.doTileDrops ? 1 : 0); app.SetGameHostOption(eGameHostOption_NaturalRegeneration, serverProperties.naturalRegeneration ? 1 : 0); app.SetGameHostOption(eGameHostOption_DoDaylightCycle, serverProperties.doDaylightCycle ? 1 : 0); + app.SetGameHostOption(eGameHostOption_Hardcore, serverProperties.hardcore ? 1 : 0); #ifdef _LARGE_WORLDS app.SetGameHostOption(eGameHostOption_WorldSize, serverProperties.worldSize); // Apply desired target size for loading existing worlds. diff --git a/Minecraft.World/DerivedLevelData.cpp b/Minecraft.World/DerivedLevelData.cpp index bc7bf03f..b7bd441c 100644 --- a/Minecraft.World/DerivedLevelData.cpp +++ b/Minecraft.World/DerivedLevelData.cpp @@ -180,6 +180,10 @@ bool DerivedLevelData::isHardcore() return wrapped->isHardcore(); } +void DerivedLevelData::setHardcore(bool hardcore) +{ +} + LevelType *DerivedLevelData::getGenerator() { return wrapped->getGenerator(); diff --git a/Minecraft.World/DerivedLevelData.h b/Minecraft.World/DerivedLevelData.h index 9b439053..306f12dd 100644 --- a/Minecraft.World/DerivedLevelData.h +++ b/Minecraft.World/DerivedLevelData.h @@ -53,6 +53,7 @@ public: bool isGenerateMapFeatures(); void setGameType(GameType *gameType); bool isHardcore(); + void setHardcore(bool hardcore); LevelType *getGenerator(); void setGenerator(LevelType *generator); bool getAllowCommands(); diff --git a/Minecraft.World/LevelData.cpp b/Minecraft.World/LevelData.cpp index fbc10ddb..5fffe1b9 100644 --- a/Minecraft.World/LevelData.cpp +++ b/Minecraft.World/LevelData.cpp @@ -671,6 +671,11 @@ bool LevelData::isHardcore() return hardcore; } +void LevelData::setHardcore(bool hardcore) +{ + this->hardcore = hardcore; +} + bool LevelData::getAllowCommands() { return allowCommands; diff --git a/Minecraft.World/LevelData.h b/Minecraft.World/LevelData.h index 9c8d08ed..2f17d827 100644 --- a/Minecraft.World/LevelData.h +++ b/Minecraft.World/LevelData.h @@ -141,6 +141,7 @@ public: virtual wstring getGeneratorOptions(); virtual void setGeneratorOptions(const wstring &options); virtual bool isHardcore(); + virtual void setHardcore(bool hardcore); virtual bool getAllowCommands(); virtual void setAllowCommands(bool allowCommands); virtual bool isInitialized(); diff --git a/Minecraft.World/LoginPacket.cpp b/Minecraft.World/LoginPacket.cpp index 37ab70a3..1fe74d3c 100644 --- a/Minecraft.World/LoginPacket.cpp +++ b/Minecraft.World/LoginPacket.cpp @@ -149,7 +149,7 @@ void LoginPacket::write(DataOutputStream *dos) //throws IOException writeUtf(m_pLevelType->getGeneratorName(), dos); } dos->writeLong(seed); - dos->writeInt(gameType | (m_isHardcore ? 0x8 : 0)); + dos->writeInt(m_isHardcore ? (gameType | 0x8) : gameType); dos->writeByte(dimension); dos->writeByte(mapHeight); dos->writeByte(maxPlayers); diff --git a/Minecraft.World/RespawnPacket.cpp b/Minecraft.World/RespawnPacket.cpp index 7e456af0..c4a19c8c 100644 --- a/Minecraft.World/RespawnPacket.cpp +++ b/Minecraft.World/RespawnPacket.cpp @@ -70,7 +70,7 @@ void RespawnPacket::read(DataInputStream *dis) //throws IOException void RespawnPacket::write(DataOutputStream *dos) //throws IOException { dos->writeByte(dimension); - dos->writeByte(playerGameType->getId() | (m_isHardcore ? 0x8 : 0)); + dos->writeByte(m_isHardcore ? (playerGameType->getId() | 0x8) : playerGameType->getId()); dos->writeShort(mapHeight); if (m_pLevelType == nullptr) {