diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index 8d318c5313..90df752232 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -14,6 +14,7 @@ import io.anuke.mindustry.net.Packets.*; import io.anuke.mindustry.resource.*; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Placement; +import io.anuke.mindustry.world.Tile; import io.anuke.ucore.core.Events; import io.anuke.ucore.core.Timers; import io.anuke.ucore.entities.Entities; @@ -199,6 +200,14 @@ public class NetServer extends Module{ if(recipe == null) return; + Tile tile = world.tile(packet.x, packet.y); + if(tile.synthetic() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){ + if(Timers.get("break-message-" + id, 120)){ + sendMessageTo(id, "[scarlet]Anti-grief: you are replacing blocks too quickly. wait until replacing again."); + } + return; + } + state.inventory.removeItems(recipe.requirements); Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false); @@ -215,6 +224,15 @@ public class NetServer extends Module{ if(!Placement.validBreak(packet.x, packet.y)) return; + Tile tile = world.tile(packet.x, packet.y); + + if(tile.synthetic() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){ + if(Timers.get("break-message-" + id, 120)){ + sendMessageTo(id, "[scarlet]Anti-grief: you are breaking blocks too quickly. wait until breaking again."); + } + return; + } + Block block = Placement.breakBlock(packet.x, packet.y, true, false); if(block != null) { @@ -376,6 +394,12 @@ public class NetServer extends Module{ admins.save(); } + void sendMessageTo(int id, String message){ + ChatPacket packet = new ChatPacket(); + packet.text = message; + Net.sendTo(id, packet, SendMode.tcp); + } + void sync(){ if(timer.get(timerEntitySync, serverSyncTime)){ diff --git a/core/src/io/anuke/mindustry/net/Administration.java b/core/src/io/anuke/mindustry/net/Administration.java index 00355d78f9..7fec757dd3 100644 --- a/core/src/io/anuke/mindustry/net/Administration.java +++ b/core/src/io/anuke/mindustry/net/Administration.java @@ -3,9 +3,13 @@ package io.anuke.mindustry.net; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Json; import com.badlogic.gdx.utils.ObjectMap; +import com.badlogic.gdx.utils.TimeUtils; import io.anuke.ucore.core.Settings; public class Administration { + public static final int defaultMaxBrokenBlocks = 15; + public static final int defaultBreakCooldown = 1000*15; + private Json json = new Json(); /**All player info. Maps UUIDs to info. This persists throughout restarts.*/ private ObjectMap playerInfo = new ObjectMap<>(); @@ -14,12 +18,68 @@ public class Administration { private Array bannedIPs = new Array<>(); public Administration(){ - Settings.defaults("playerInfo", "{}"); - Settings.defaults("bannedIPs", "{}"); + Settings.defaultList( + "playerInfo", "{}", + "bannedIPs", "{}", + "antigrief", false, + "antigrief-max", defaultMaxBrokenBlocks, + "antigrief-cooldown", defaultBreakCooldown + ); load(); } + public boolean isAntiGrief(){ + return Settings.getBool("antigrief"); + } + + public void setAntiGrief(boolean antiGrief){ + Settings.putBool("antigrief", antiGrief); + Settings.save(); + } + + public void setAntiGriefParams(int maxBreak, int cooldown){ + Settings.putInt("antigrief-max", maxBreak); + Settings.putInt("antigrief-cooldown", cooldown); + Settings.save(); + } + + public boolean validateBreak(String id, String ip){ + if(!isAntiGrief() || isAdmin(id, ip)) return true; + + PlayerInfo info = getCreateInfo(id); + + if(info.lastBroken == null || info.lastBroken.length != Settings.getInt("antigrief-max")){ + info.lastBroken = new long[Settings.getInt("antigrief-max")]; + } + + long[] breaks = info.lastBroken; + + int shiftBy = 0; + for(int i = 0; i < breaks.length && breaks[i] != 0; i ++){ + if(TimeUtils.timeSinceMillis(breaks[i]) >= Settings.getInt("antigrief-cooldown")){ + shiftBy = i; + } + } + + for (int i = 0; i < breaks.length; i++) { + breaks[i] = (i + shiftBy >= breaks.length) ? 0 : breaks[i + shiftBy]; + } + + int remaining = 0; + for(int i = 0; i < breaks.length; i ++){ + if(breaks[i] == 0){ + remaining = breaks.length - i; + break; + } + } + + if(remaining == 0) return false; + + breaks[breaks.length - remaining] = TimeUtils.millis(); + return true; + } + /**Call when a player joins to update their information here.*/ public void updatePlayerJoined(String id, String ip, String name){ PlayerInfo info = getCreateInfo(id); @@ -248,6 +308,8 @@ public class Administration { public boolean banned, admin; public long lastKicked; //last kicked timestamp + public long[] lastBroken; + PlayerInfo(String id){ this.id = id; } diff --git a/core/src/io/anuke/mindustry/world/Tile.java b/core/src/io/anuke/mindustry/world/Tile.java index 058acb0842..528e9bcd9c 100644 --- a/core/src/io/anuke/mindustry/world/Tile.java +++ b/core/src/io/anuke/mindustry/world/Tile.java @@ -180,6 +180,11 @@ public class Tile{ Block floor = floor(); return isLinked() || !((floor.solid && (block == Blocks.air || block.solidifes)) || (block.solid && (!block.destructible && !block.update))); } + + public boolean synthetic(){ + Block block = block(); + return block.update || block.destructible; + } public boolean solid(){ Block block = block(); diff --git a/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index afb2e32963..57a6cd25ac 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -10,13 +10,10 @@ import io.anuke.mindustry.game.EventType.GameOverEvent; import io.anuke.mindustry.game.GameMode; import io.anuke.mindustry.io.SaveIO; import io.anuke.mindustry.io.Version; +import io.anuke.mindustry.net.*; import io.anuke.mindustry.net.Administration.PlayerInfo; -import io.anuke.mindustry.net.Net; -import io.anuke.mindustry.net.NetConnection; -import io.anuke.mindustry.net.NetEvents; import io.anuke.mindustry.net.Packets.ChatPacket; import io.anuke.mindustry.net.Packets.KickReason; -import io.anuke.mindustry.net.TraceInfo; import io.anuke.mindustry.ui.fragments.DebugFragment; import io.anuke.mindustry.world.Map; import io.anuke.mindustry.world.Tile; @@ -254,7 +251,7 @@ public class ServerControl extends Module { } }); - handler.register("friendlyfire", "", "Enable or disable friendly fire", arg -> { + handler.register("friendlyfire", "", "Enable or disable friendly fire.", arg -> { String s = arg[0]; if(s.equalsIgnoreCase("on")){ NetEvents.handleFriendlyFireChange(true); @@ -269,6 +266,35 @@ public class ServerControl extends Module { } }); + handler.register("antigrief", "[on/off] [max-break] [cooldown-in-ms]", "Enable or disable anti-grief.", arg -> { + if(arg.length == 0){ + info("Anti-grief is currently &lc{0}.", netServer.admins.isAntiGrief() ? "on" : "off"); + return; + } + + String s = arg[0]; + if(s.equalsIgnoreCase("on")){ + netServer.admins.setAntiGrief(true); + info("Anti-grief enabled."); + }else if(s.equalsIgnoreCase("off")){ + netServer.admins.setAntiGrief(false); + info("Anti-grief disabled."); + }else{ + err("Incorrect command usage."); + } + + if(arg.length >= 2) { + try { + int maxbreak = Integer.parseInt(arg[1]); + int cooldown = (arg.length >= 3 ? Integer.parseInt(arg[2]) : Administration.defaultBreakCooldown); + netServer.admins.setAntiGriefParams(maxbreak, cooldown); + info("Anti-grief parameters set."); + } catch (NumberFormatException e) { + err("Invalid number format."); + } + } + }); + handler.register("shuffle", "", "Set map shuffling.", arg -> { try{