diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 1e038de6..72f5437c 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -206,6 +206,8 @@ CMinecraftApp::CMinecraftApp() m_dwRequiredTexturePackID=0; m_bResetNether=false; + m_seedOverride = 0; + m_hasSeedOverride = false; #ifdef _XBOX // m_bTransferSavesToXboxOne=false; diff --git a/Minecraft.Client/Common/Consoles_App.h b/Minecraft.Client/Common/Consoles_App.h index 9512894b..d750cbf6 100644 --- a/Minecraft.Client/Common/Consoles_App.h +++ b/Minecraft.Client/Common/Consoles_App.h @@ -706,6 +706,8 @@ private: bool m_bGameNewWorldSizeUseMoat; unsigned int m_GameNewHellScale; #endif + int64_t m_seedOverride; + bool m_hasSeedOverride; unsigned int FromBigEndian(unsigned int uiValue); public: @@ -723,6 +725,10 @@ public: void SetGameNewHellScale(unsigned int newScale) { m_GameNewHellScale = newScale; } unsigned int GetGameNewHellScale() { return m_GameNewHellScale; } #endif + void SetSeedOverride(int64_t seed) { m_seedOverride = seed; m_hasSeedOverride = true; } + bool HasSeedOverride() { return m_hasSeedOverride; } + int64_t GetSeedOverride() { return m_seedOverride; } + void SetResetNether(bool bResetNether) {m_bResetNether=bResetNether;} bool GetResetNether() {return m_bResetNether;} bool CanRecordStatsAndAchievements(); diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 73654da1..022833d0 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -735,10 +735,11 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW if( findSeed ) { + int worldSizeChunks = (initData && initData->xzSize > 0) ? (int)initData->xzSize : 54; #ifdef __PSVITA__ - seed = BiomeSource::findSeed(pLevelType, &running); + seed = BiomeSource::findSeed(pLevelType, &running, worldSizeChunks); #else - seed = BiomeSource::findSeed(pLevelType); + seed = BiomeSource::findSeed(pLevelType, worldSizeChunks); #endif } diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index 9f3a787c..010dfd9f 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -824,6 +824,20 @@ ServerPropertiesConfig LoadServerPropertiesConfig() config.maxPlayers = ReadNormalizedIntProperty(&merged, "max-players", kDefaultMaxPlayers, 1, kMaxDedicatedPlayers, &shouldWrite); config.seed = 0; config.hasSeed = ReadNormalizedOptionalInt64Property(&merged, "level-seed", &config.seed, &shouldWrite); + config.overrideSeed = 0; + config.hasOverrideSeed = false; + { + auto it = merged.find("override-seed"); + if (it != merged.end() && !TrimAscii(it->second).empty()) + { + __int64 parsed = 0; + if (TryParseInt64(TrimAscii(it->second), &parsed)) + { + config.overrideSeed = parsed; + config.hasOverrideSeed = true; + } + } + } config.logLevel = ReadNormalizedLogLevelProperty(&merged, "log-level", eServerLogLevel_Info, &shouldWrite); config.autosaveIntervalSeconds = ReadNormalizedIntProperty(&merged, "autosave-interval", kDefaultAutosaveIntervalSeconds, 5, 3600, &shouldWrite); diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 6592cda4..c41f0b79 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -31,6 +31,9 @@ namespace ServerRuntime bool hasSeed; /** `level-seed` */ __int64 seed; + /** `override-seed` replaces the seed for biome generation on existing worlds */ + bool hasOverrideSeed; + __int64 overrideSeed; /** `log-level` */ EServerLogLevel logLevel; /** `autosave-interval` (seconds) */ diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 69d69c40..38a4c28e 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -28,6 +28,8 @@ #include "../../Minecraft.World/TilePos.h" #include "../../Minecraft.World/compression.h" #include "../../Minecraft.World/OldChunkStorage.h" +#include "../../Minecraft.World/BiomeSource.h" +#include "../../Minecraft.World/LevelType.h" #include "../../Minecraft.World/net.minecraft.world.level.tile.h" #include "../../Minecraft.World/Random.h" @@ -545,6 +547,12 @@ int main(int argc, char **argv) app.SetGameNewHellScale(serverProperties.worldHellScale); #endif + if (serverProperties.hasOverrideSeed) + { + LogInfof("startup", "Seed override active: %lld", serverProperties.overrideSeed); + app.SetSeedOverride(serverProperties.overrideSeed); + } + StorageManager.SetSaveDisabled(serverProperties.disableSaving); // Read world name and fixed save-id from server.properties // Delegate load-vs-create decision to WorldManager @@ -584,9 +592,17 @@ int main(int argc, char **argv) { param->seed = config.seed; } + else if (worldBootstrap.saveData == nullptr) + { + // Only run seed validation when creating a brand-new world. + // Existing worlds already have their seed in level.dat. + LogInfof("startup", "Finding seed with biome diversity for %d-chunk world...", config.worldSizeChunks); + param->seed = BiomeSource::findSeed(LevelType::lvl_normal, config.worldSizeChunks); + LogInfof("startup", "Selected seed: %lld", param->seed); + } else { - param->seed = (new Random())->nextLong(); + param->seed = (new Random())->nextLong(); // placeholder; level.dat seed takes priority } #ifdef _LARGE_WORLDS param->xzSize = (unsigned int)config.worldSizeChunks; diff --git a/Minecraft.World/BiomeSource.cpp b/Minecraft.World/BiomeSource.cpp index 89422677..55b989e8 100644 --- a/Minecraft.World/BiomeSource.cpp +++ b/Minecraft.World/BiomeSource.cpp @@ -411,9 +411,9 @@ void BiomeSource::update() // 4J added - find a seed for this biomesource that matches certain criteria #ifdef __PSVITA__ -int64_t BiomeSource::findSeed(LevelType *generator, bool* pServerRunning) // MGH - added pRunning, so we can early out of this on Vita as it can take up to 60 secs +int64_t BiomeSource::findSeed(LevelType *generator, bool* pServerRunning, int worldSizeChunks) #else -int64_t BiomeSource::findSeed(LevelType *generator) +int64_t BiomeSource::findSeed(LevelType *generator, int worldSizeChunks) #endif { @@ -440,8 +440,8 @@ int64_t BiomeSource::findSeed(LevelType *generator) // Raw biome data has one result per 4x4 group of tiles. // Removing a border of 8 from each side since we'll be doing special things at the edge to turn our world into an island, and so don't want to count things // in the edge region in case they later get removed - static const int biomeWidth = ( 54 * 4 ) - 16; // Should be even so we can offset evenly - static const int biomeOffset = -( biomeWidth / 2 ); + const int biomeWidth = ( worldSizeChunks * 4 ) - 16; // Should be even so we can offset evenly + const int biomeOffset = -( biomeWidth / 2 ); // Storage for our biome indices intArray indices = intArray( biomeWidth * biomeWidth ); diff --git a/Minecraft.World/BiomeSource.h b/Minecraft.World/BiomeSource.h index a1e8a50b..2a2a65ed 100644 --- a/Minecraft.World/BiomeSource.h +++ b/Minecraft.World/BiomeSource.h @@ -36,9 +36,9 @@ private: static void getFracs(intArray indices, float *fracs); // 4J added public: #ifdef __PSVITA__ - static int64_t findSeed(LevelType *generator, bool* pServerRunning); // MGH - added pRunning, so we can early out of this on Vita as it can take up to 60 secs // 4J added + static int64_t findSeed(LevelType *generator, bool* pServerRunning, int worldSizeChunks = 54); #else - static int64_t findSeed(LevelType *generator); // 4J added + static int64_t findSeed(LevelType *generator, int worldSizeChunks = 54); #endif ~BiomeSource(); diff --git a/Minecraft.World/LevelData.cpp b/Minecraft.World/LevelData.cpp index 5fffe1b9..0edd2d7e 100644 --- a/Minecraft.World/LevelData.cpp +++ b/Minecraft.World/LevelData.cpp @@ -13,6 +13,15 @@ LevelData::LevelData() LevelData::LevelData(CompoundTag *tag) { seed = tag->getLong(L"RandomSeed"); + + // Allow server.properties to override the world seed for biome generation + // on existing worlds (e.g. to fix monotonous biomes after world expansion). + if (app.HasSeedOverride()) + { + app.DebugPrintf("Overriding world seed: %lld -> %lld\n", seed, app.GetSeedOverride()); + seed = app.GetSeedOverride(); + } + m_pGenerator = LevelType::lvl_normal; if (tag->contains(L"generatorName")) { diff --git a/README.md b/README.md index 89ca47f7..59e43619 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ This project is based on source code of Minecraft Legacy Console Edition v1.6.05 ## Latest: +Dedicated server biome diversity fix: +- The dedicated server previously used a completely random seed with no biome diversity checks — unlike the client which validates seeds to guarantee varied biomes. This could result in server worlds with large regions dominated by only one or two biome types (e.g. all taiga/snowy) +- On top of that, the client's seed validation was hardcoded to only check a 54-chunk (Classic) area, so even validated seeds had no diversity guarantee beyond that — making the problem especially noticeable on Large worlds or worlds expanded from Classic to Large +- New server worlds now validate seeds for biome diversity, and the validation scales to the full target world size +- Added `override-seed` in server.properties to fix existing worlds without deleting them — set it to any seed number and newly generated chunks will use it instead of the original + Server list and connection improvements: - Server edits and deletions now apply immediately without needing to restart the game - Connecting to an offline/unreachable server no longer freezes the game indefinitely