diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 2cf6930ac..a545eff85 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -569,6 +569,7 @@ MinecraftServer::MinecraftServer() playerIdleTimeout = 0; m_postUpdateThread = nullptr; forceGameType = false; + m_spawnProtectionRadius = 0; commandDispatcher = new ServerCommandDispatcher(); InitializeCriticalSection(&m_consoleInputCS); @@ -615,6 +616,10 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW logger.info("Loading properties"); #endif settings = new Settings(new File(L"server.properties")); + // Dedicated-only: spawn-protection radius in blocks; 0 disables protection. + m_spawnProtectionRadius = GetDedicatedServerInt(settings, L"spawn-protection", 0); + if (m_spawnProtectionRadius < 0) m_spawnProtectionRadius = 0; + if (m_spawnProtectionRadius > 256) m_spawnProtectionRadius = 256; app.SetGameHostOption(eGameHostOption_Difficulty, GetDedicatedServerInt(settings, L"difficulty", app.GetGameHostOption(eGameHostOption_Difficulty))); app.SetGameHostOption(eGameHostOption_GameType, GetDedicatedServerInt(settings, L"gamemode", app.GetGameHostOption(eGameHostOption_GameType))); @@ -631,6 +636,7 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW app.DebugPrintf("ServerSettings: pvp is %s\n",(app.GetGameHostOption(eGameHostOption_PvP)>0)?"on":"off"); app.DebugPrintf("ServerSettings: fire spreads is %s\n",(app.GetGameHostOption(eGameHostOption_FireSpreads)>0)?"on":"off"); app.DebugPrintf("ServerSettings: tnt explodes is %s\n",(app.GetGameHostOption(eGameHostOption_TNT)>0)?"on":"off"); + app.DebugPrintf("ServerSettings: spawn protection radius is %d\n", m_spawnProtectionRadius); app.DebugPrintf("\n"); // TODO 4J Stu - Init a load of settings based on data passed as params @@ -1661,7 +1667,9 @@ Level *MinecraftServer::getCommandSenderWorld() int MinecraftServer::getSpawnProtectionRadius() { - return 16; + // Client-host mode must never apply dedicated-server spawn protection settings. + if (!ShouldUseDedicatedServerProperties()) return 0; + return m_spawnProtectionRadius; } bool MinecraftServer::isUnderSpawnProtection(Level *level, int x, int y, int z, shared_ptr player) diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index 18ed1f36a..1ed5db9d7 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -121,6 +121,7 @@ public: int maxBuildHeight; int playerIdleTimeout; bool forceGameType; + int m_spawnProtectionRadius; private: // 4J Added diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index 782c03247..6319b660e 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -38,6 +38,19 @@ #include "..\Minecraft.Server\ServerLogManager.h" #endif +namespace +{ + // Anti-cheat thresholds. Keep server-side checks authoritative even in host mode. + // Base max squared displacement allowed per move packet before speed flags trigger. + const double kMoveBaseAllowanceSq = 100.0; + // Extra squared displacement allowance derived from current server-side velocity. + const double kMoveVelocityAllowanceScale = 100.0; + // Max squared distance for interact/attack when the target is visible (normal reach). + const double kInteractReachSq = 6.0 * 6.0; + // Stricter max squared distance used when LOS is blocked to reduce wall-hit abuse. + const double kInteractBlockedReachSq = 3.0 * 3.0; +} + Random PlayerConnection::random; @@ -300,16 +313,19 @@ void PlayerConnection::handleMovePlayer(shared_ptr packet) double dist = xDist * xDist + yDist * yDist + zDist * zDist; - // 4J-PB - removing this one for now - /*if (dist > 100.0f) + // Anti-cheat: reject movement packets that exceed server-authoritative bounds. + double velocitySq = player->xd * player->xd + player->yd * player->yd + player->zd * player->zd; + double maxAllowedSq = kMoveBaseAllowanceSq + (velocitySq * kMoveVelocityAllowanceScale); + if (player->isAllowedToFly() || player->gameMode->isCreative()) { - // logger.warning(player->name + " moved too quickly!"); - disconnect(DisconnectPacket::eDisconnect_MovedTooQuickly); - // System.out.println("Moved too quickly at " + xt + ", " + yt + ", " + zt); - // teleport(player->x, player->y, player->z, player->yRot, player->xRot); - return; + // Creative / flight-allowed players can move farther legitimately per tick. + maxAllowedSq *= 1.5; + } + if (dist > maxAllowedSq) + { + disconnect(DisconnectPacket::eDisconnect_MovedTooQuickly); + return; } - */ float r = 1 / 16.0f; bool oldOk = level->getCubes(player, player->bb->copy()->shrink(r, r, r))->empty(); @@ -337,8 +353,8 @@ void PlayerConnection::handleMovePlayer(shared_ptr packet) xDist = xt - player->x; yDist = yt - player->y; - // 4J-PB - line below will always be true! - if (yDist > -0.5 || yDist < 0.5) + // Clamp tiny Y drift noise to reduce false positives. + if (yDist > -0.5 && yDist < 0.5) { yDist = 0; } @@ -459,7 +475,8 @@ void PlayerConnection::handlePlayerAction(shared_ptr packet) if (packet->action == PlayerActionPacket::START_DESTROY_BLOCK) { - if (true) player->gameMode->startDestroyBlock(x, y, z, packet->face); // 4J - condition was !server->isUnderSpawnProtection(level, x, y, z, player) (from Java 1.6.4) but putting back to old behaviour + // Anti-cheat: validate spawn protection on the server for mining start. + if (!server->isUnderSpawnProtection(level, x, y, z, player)) player->gameMode->startDestroyBlock(x, y, z, packet->face); else player->connection->send(std::make_shared(x, y, z, level)); } @@ -487,8 +504,6 @@ void PlayerConnection::handleUseItem(shared_ptr packet) int face = packet->getFace(); player->resetLastActionTime(); - // 4J Stu - We don't have ops, so just use the levels setting - bool canEditSpawn = level->canEditSpawn; // = level->dimension->id != 0 || server->players->isOp(player->name); if (packet->getFace() == 255) { if (item == nullptr) return; @@ -498,7 +513,8 @@ void PlayerConnection::handleUseItem(shared_ptr packet) { if (synched && player->distanceToSqr(x + 0.5, y + 0.5, z + 0.5) < 8 * 8) { - if (true) // 4J - condition was !server->isUnderSpawnProtection(level, x, y, z, player) (from java 1.6.4) but putting back to old behaviour + // Anti-cheat: block placement/use must pass server-side spawn protection. + if (!server->isUnderSpawnProtection(level, x, y, z, player)) { player->gameMode->useItemOn(player, level, item, x, y, z, face, packet->getClickX(), packet->getClickY(), packet->getClickZ()); } @@ -782,17 +798,16 @@ void PlayerConnection::handleInteract(shared_ptr packet) // 4J Stu - If the client says that we hit something, then agree with it. The canSee can fail here as it checks // a ray from head->head, but we may actually be looking at a different part of the entity that can be seen // even though the ray is blocked. - if (target != nullptr) // && player->canSee(target) && player->distanceToSqr(target) < 6 * 6) + if (target != nullptr) { - //boole canSee = player->canSee(target); - //double maxDist = 6 * 6; - //if (!canSee) - //{ - // maxDist = 3 * 3; - //} + // Anti-cheat: enforce reach and LOS on the server to reject forged hits. + bool canSeeTarget = player->canSee(target); + double maxDistSq = canSeeTarget ? kInteractReachSq : kInteractBlockedReachSq; + if (player->distanceToSqr(target) > maxDistSq) + { + return; + } - //if (player->distanceToSqr(target) < maxDist) - //{ if (packet->action == InteractPacket::INTERACT) { player->interact(target); @@ -807,7 +822,6 @@ void PlayerConnection::handleInteract(shared_ptr packet) } player->attack(target); } - //} } } diff --git a/Minecraft.Client/ServerPlayerGameMode.cpp b/Minecraft.Client/ServerPlayerGameMode.cpp index d2dcaaf68..041487f5d 100644 --- a/Minecraft.Client/ServerPlayerGameMode.cpp +++ b/Minecraft.Client/ServerPlayerGameMode.cpp @@ -176,31 +176,29 @@ void ServerPlayerGameMode::stopDestroyBlock(int x, int y, int z) { if (x == xDestroyBlock && y == yDestroyBlock && z == zDestroyBlock) { - // int ticksSpentDestroying = gameTicks - destroyProgressStart; - int t = level->getTile(x, y, z); if (t != 0) { Tile *tile = Tile::tiles[t]; - - // MGH - removed checking for the destroy progress here, it has already been checked on the client before it sent the packet. - // fixes issues with this failing to destroy because of packets bunching up - // float destroyProgress = tile->getDestroyProgress(player, player->level, x, y, z) * (ticksSpentDestroying + 1); - // if (destroyProgress >= .7f || bIgnoreDestroyProgress) + // Anti-cheat: re-check destroy progress on the server for STOP_DESTROY. + int ticksSpentDestroying = gameTicks - destroyProgressStart; + float destroyProgress = tile->getDestroyProgress(player, player->level, x, y, z) * (ticksSpentDestroying + 1); + if (destroyProgress >= 1.0f) { isDestroyingBlock = false; level->destroyTileProgress(player->entityId, x, y, z, -1); destroyBlock(x, y, z); } - // else if (!hasDelayedDestroy) - // { - // isDestroyingBlock = false; - // hasDelayedDestroy = true; - // delayedDestroyX = x; - // delayedDestroyY = y; - // delayedDestroyZ = z; - // delayedTickStart = destroyProgressStart; - // } + else if (!hasDelayedDestroy) + { + // Keep server-authoritative mining while allowing legit latency to finish via delayed tick progression. + isDestroyingBlock = false; + hasDelayedDestroy = true; + delayedDestroyX = x; + delayedDestroyY = y; + delayedDestroyZ = z; + delayedTickStart = destroyProgressStart; + } } } } @@ -393,4 +391,4 @@ void ServerPlayerGameMode::setGameRules(GameRulesInstance *rules) { if(m_gameRules != nullptr) delete m_gameRules; m_gameRules = rules; -} \ No newline at end of file +} diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index 33a7df37d..d6ba64e7e 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -63,6 +63,7 @@ static const ServerPropertyDefault kServerPropertyDefaults[] = { "level-seed", "" }, { "level-type", "default" }, { "world-size", "classic" }, + { "spawn-protection", "0" }, { "log-level", "info" }, { "max-build-height", "256" }, { "max-players", "16" }, @@ -834,6 +835,7 @@ ServerPropertiesConfig LoadServerPropertiesConfig() &config.worldHellScale, &shouldWrite); config.levelType = ReadNormalizedLevelTypeProperty(&merged, "level-type", &config.levelTypeFlat, &shouldWrite); + config.spawnProtectionRadius = ReadNormalizedIntProperty(&merged, "spawn-protection", 0, 0, 256, &shouldWrite); config.generateStructures = ReadNormalizedBoolProperty(&merged, "generate-structures", true, &shouldWrite); config.bonusChest = ReadNormalizedBoolProperty(&merged, "bonus-chest", false, &shouldWrite); config.pvp = ReadNormalizedBoolProperty(&merged, "pvp", true, &shouldWrite); diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 28261b5cd..3bb5aca80 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -46,6 +46,8 @@ namespace ServerRuntime /** Nether scale derived from `world-size` */ int worldHellScale; bool levelTypeFlat; + /** `spawn-protection` radius in blocks (0 disables protection) */ + int spawnProtectionRadius; bool generateStructures; bool bonusChest; bool pvp; diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 2b7929fd2..a8d5fc66b 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -409,6 +409,7 @@ int main(int argc, char **argv) accessShutdownGuard.Activate(); LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled"); LogInfof("startup", "Whitelist: %s", serverProperties.whiteListEnabled ? "enabled" : "disabled"); + LogInfof("startup", "Spawn protection radius: %d", serverProperties.spawnProtectionRadius); #ifdef _LARGE_WORLDS LogInfof( "startup",