diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index e758434d576b..fd1153ce6ff2 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -70,6 +70,8 @@ text.server.bans=Bans text.server.bans.none=No banned players found! text.server.admins=Admins text.server.admins.none=No admins found! +text.server.rollback=Rollback +text.server.rollback.numberfield=Rollback Amount: text.server.add=Add Server text.server.delete=Are you sure you want to delete this server? text.server.hostname=Host: {0} @@ -209,6 +211,8 @@ placemode.touch.name=touch placemode.cursor.name=cursor text.blocks.extrainfo=[accent]extra block info: text.blocks.blockinfo=Block Info +text.blocks.editlogs=Edit Logs +text.block.editlogsnotfound=[red]There are no edit logs for this location. text.blocks.powercapacity=Power Capacity text.blocks.powershot=Power/shot text.blocks.powersecond=Power/second diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index 5451ed95cffe..66842fa7340d 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -4,6 +4,8 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntMap; import io.anuke.mindustry.core.*; import io.anuke.mindustry.entities.Bullet; import io.anuke.mindustry.entities.Player; @@ -11,6 +13,7 @@ import io.anuke.mindustry.entities.effect.Shield; import io.anuke.mindustry.entities.enemies.Enemy; import io.anuke.mindustry.core.Platform; +import io.anuke.mindustry.net.EditLog; import io.anuke.mindustry.net.ClientDebug; import io.anuke.mindustry.net.ServerDebug; import io.anuke.ucore.UCore; @@ -19,7 +22,6 @@ import io.anuke.ucore.entities.EntityGroup; import io.anuke.ucore.scene.ui.layout.Unit; import io.anuke.ucore.util.OS; - import java.util.Locale; public class Vars{ @@ -96,6 +98,8 @@ public class Vars{ //amount of drops that are left when breaking a block public static final float breakDropAmount = 0.5f; + public static Array currentEditLogs = new Array<>(); + //only if smoothCamera public static boolean snapCamera = true; diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index d2da6b091d98..85fc3b3ebcb6 100644 --- a/core/src/io/anuke/mindustry/core/NetClient.java +++ b/core/src/io/anuke/mindustry/core/NetClient.java @@ -4,6 +4,7 @@ import com.badlogic.gdx.utils.IntMap; import com.badlogic.gdx.utils.IntSet; import com.badlogic.gdx.utils.TimeUtils; +import io.anuke.mindustry.Vars; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.entities.Bullet; import io.anuke.mindustry.entities.BulletType; @@ -166,6 +167,10 @@ public NetClient(){ ui.hudfrag.updateItems(); }); + + Net.handleClient(BlockLogRequestPacket.class, packet -> { + currentEditLogs = packet.editlogs; + }); Net.handleClient(PlacePacket.class, (packet) -> { Placement.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, Timers.get("placeblocksound", 10)); diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index 8723bf1c552e..0559a441c6e8 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -25,7 +25,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; - import static io.anuke.mindustry.Vars.*; public class NetServer extends Module{ @@ -44,7 +43,10 @@ public class NetServer extends Module{ public NetServer(){ - Events.on(GameOverEvent.class, () -> weapons.clear()); + Events.on(GameOverEvent.class, () -> { + weapons.clear(); + admins.getEditLogs().clear(); + }); Net.handleServer(Connect.class, (id, connect) -> { if(admins.isIPBanned(connect.addressTCP)){ @@ -211,6 +213,7 @@ public NetServer(){ Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false); + admins.logEdit(packet.x, packet.y, connections.get(id), block, packet.rotation, EditLog.EditAction.PLACE); admins.getTrace(Net.getConnection(id).address).lastBlockPlaced = block; admins.getTrace(Net.getConnection(id).address).totalBlocksPlaced ++; admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlockPlaced ++; @@ -235,6 +238,7 @@ public NetServer(){ Block block = Placement.breakBlock(packet.x, packet.y, true, false); if(block != null) { + admins.logEdit(packet.x, packet.y, connections.get(id), block, tile.getRotation(), EditLog.EditAction.BREAK); admins.getTrace(Net.getConnection(id).address).lastBlockBroken = block; admins.getTrace(Net.getConnection(id).address).totalBlocksBroken++; admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlocksBroken ++; @@ -312,7 +316,7 @@ public NetServer(){ packet.id = connections.get(id).id; Net.sendExcept(id, packet, SendMode.tcp); }); - + Net.handleServer(AdministerRequestPacket.class, (id, packet) -> { Player player = connections.get(id); @@ -345,6 +349,24 @@ public NetServer(){ Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name); } }); + + Net.handleServer(BlockLogRequestPacket.class, (id, packet) -> { + packet.editlogs = admins.getEditLogs().get(packet.x + packet.y * world.width(), new Array<>()); + Net.sendTo(id, packet, SendMode.udp); + }); + + Net.handleServer(RollbackRequestPacket.class, (id, packet) -> { + Player player = connections.get(id); + + if(!player.isAdmin){ + Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform a rollback without proper security access.", + player.name, Net.getConnection(player.clientid).address); + return; + } + + admins.rollbackWorld(packet.rollbackTimes); + Log.info("&lc{0} has rolled back the world {1} times.", player.name, packet.rollbackTimes); + }); } public void update(){ @@ -474,7 +496,7 @@ void sync(){ packet.wave = state.wave; packet.time = Timers.time(); packet.timestamp = TimeUtils.millis(); - + Net.send(packet, SendMode.udp); } } diff --git a/core/src/io/anuke/mindustry/core/Renderer.java b/core/src/io/anuke/mindustry/core/Renderer.java index dbd1d3cb2039..214b5785c0ad 100644 --- a/core/src/io/anuke/mindustry/core/Renderer.java +++ b/core/src/io/anuke/mindustry/core/Renderer.java @@ -489,6 +489,12 @@ void drawOverlay(){ Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize); Draw.color(); } + + if(Inputs.keyDown("block_logs")){ + Draw.color(Colors.get("accent")); + Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize); + Draw.color(); + } if(target.entity != null) { int bot = 0, top = 0; diff --git a/core/src/io/anuke/mindustry/core/UI.java b/core/src/io/anuke/mindustry/core/UI.java index e4775c63b13d..6d992ea49d45 100644 --- a/core/src/io/anuke/mindustry/core/UI.java +++ b/core/src/io/anuke/mindustry/core/UI.java @@ -48,6 +48,7 @@ public class UI extends SceneModule{ public BansDialog bans; public AdminsDialog admins; public TraceDialog traces; + public RollbackDialog rollback; public ChangelogDialog changelog; public final MenuFragment menufrag = new MenuFragment(); @@ -159,6 +160,7 @@ public void init(){ bans = new BansDialog(); admins = new AdminsDialog(); traces = new TraceDialog(); + rollback = new RollbackDialog(); build.begin(scene); diff --git a/core/src/io/anuke/mindustry/input/DefaultKeybinds.java b/core/src/io/anuke/mindustry/input/DefaultKeybinds.java index f17fcc437cc1..d35b60e229c2 100644 --- a/core/src/io/anuke/mindustry/input/DefaultKeybinds.java +++ b/core/src/io/anuke/mindustry/input/DefaultKeybinds.java @@ -25,6 +25,7 @@ public static void load(){ "rotate", new Axis(Input.SCROLL), "toggle_menus", Input.C, "block_info", Input.CONTROL_LEFT, + "block_logs", Input.I, "player_list", Input.TAB, "chat", Input.ENTER, "chat_history_prev", Input.UP, diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index 1c7a3f68f35f..7564fae8aade 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -107,6 +107,17 @@ public void update(){ Cursors.restoreCursor(); } } + + if(recipe == null && !ui.hasMouse() && Inputs.keyDown("block_logs")) { + showCursor = true; + if(Inputs.keyTap("select")){ + NetEvents.handleBlockLogRequest(getBlockX(), getBlockY()); + Timers.runTask(20f, () -> { + ui.hudfrag.blockfrag.showBlockLogs(getBlockX(), getBlockY()); + Cursors.restoreCursor(); + }); + } + } if(target != null && target.block().isConfigurable(target)){ showCursor = true; diff --git a/core/src/io/anuke/mindustry/net/Administration.java b/core/src/io/anuke/mindustry/net/Administration.java index ee2443deb7c2..cd412e9c25a8 100644 --- a/core/src/io/anuke/mindustry/net/Administration.java +++ b/core/src/io/anuke/mindustry/net/Administration.java @@ -1,10 +1,19 @@ package io.anuke.mindustry.net; import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntMap; import com.badlogic.gdx.utils.Json; import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.TimeUtils; +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Placement; +import io.anuke.mindustry.world.blocks.types.BlockPart; +import io.anuke.mindustry.world.blocks.types.Floor; +import io.anuke.mindustry.world.blocks.types.Rock; +import io.anuke.mindustry.world.blocks.types.StaticBlock; import io.anuke.ucore.core.Settings; +import static io.anuke.mindustry.Vars.world; public class Administration { public static final int defaultMaxBrokenBlocks = 15; @@ -15,6 +24,9 @@ public class Administration { private ObjectMap playerInfo = new ObjectMap<>(); /**Maps UUIDs to trace infos. This is wiped when a player logs off.*/ private ObjectMap traceInfo = new ObjectMap<>(); + /**Maps packed coordinates to logs for that coordinate */ + private IntMap> editLogs = new IntMap<>(); + private Array bannedIPs = new Array<>(); public Administration(){ @@ -48,6 +60,68 @@ public void setAntiGriefParams(int maxBreak, int cooldown){ Settings.save(); } + public IntMap> getEditLogs() { + return editLogs; + } + + public void logEdit(int x, int y, Player player, Block block, int rotation, EditLog.EditAction action) { + if(block instanceof BlockPart || block instanceof Rock || block instanceof Floor || block instanceof StaticBlock) return; + if(editLogs.containsKey(x + y * world.width())) { + editLogs.get(x + y * world.width()).add(new EditLog(player.name, block, rotation, action)); + } + else { + Array logs = new Array<>(); + logs.add(new EditLog(player.name, block, rotation, action)); + editLogs.put(x + y * world.width(), logs); + } + } + + public void rollbackWorld(int rollbackTimes) { + for(IntMap.Entry> editLog : editLogs.entries()) { + int coords = editLog.key; + Array logs = editLog.value; + + for(int i = 0; i < rollbackTimes; i++) { + + EditLog log = logs.get(logs.size - 1); + + int x = coords % world.width(); + int y = coords / world.width(); + Block result = log.block; + int rotation = log.rotation; + + if(log.action == EditLog.EditAction.PLACE) { + Placement.breakBlock(x, y, false, false); + + Packets.BreakPacket packet = new Packets.BreakPacket(); + packet.x = (short) x; + packet.y = (short) y; + packet.playerid = 0; + + Net.send(packet, Net.SendMode.tcp); + } + else if(log.action == EditLog.EditAction.BREAK) { + Placement.placeBlock(x, y, result, rotation, false, false); + + Packets.PlacePacket packet = new Packets.PlacePacket(); + packet.x = (short) x; + packet.y = (short) y; + packet.rotation = (byte) rotation; + packet.playerid = 0; + packet.block = result.id; + + Net.send(packet, Net.SendMode.tcp); + } + + logs.removeIndex(logs.size - 1); + if(logs.size == 0) { + editLogs.remove(coords); + break; + } + } + } + } + public boolean validateBreak(String id, String ip){ if(!isAntiGrief() || isAdmin(id, ip)) return true; diff --git a/core/src/io/anuke/mindustry/net/EditLog.java b/core/src/io/anuke/mindustry/net/EditLog.java new file mode 100644 index 000000000000..e9bbeec04395 --- /dev/null +++ b/core/src/io/anuke/mindustry/net/EditLog.java @@ -0,0 +1,26 @@ +package io.anuke.mindustry.net; + +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.world.Block; + +public class EditLog { + public String playername; + public Block block; + public int rotation; + public EditAction action; + + EditLog(String playername, Block block, int rotation, EditAction action) { + this.playername = playername; + this.block = block; + this.rotation = rotation; + this.action = action; + } + + public String info() { + return String.format("Player: %s, Block: %s, Rotation: %s, Edit Action: %s", playername, block.name(), rotation, action.toString()); + } + + public enum EditAction { + PLACE, BREAK; + } +} diff --git a/core/src/io/anuke/mindustry/net/NetEvents.java b/core/src/io/anuke/mindustry/net/NetEvents.java index ccb59f36a15c..c9603e92fc13 100644 --- a/core/src/io/anuke/mindustry/net/NetEvents.java +++ b/core/src/io/anuke/mindustry/net/NetEvents.java @@ -177,4 +177,20 @@ public static void handleTraceRequest(Player target){ ui.traces.show(target, netServer.admins.getTrace(Net.getConnection(target.clientid).address)); } } + + public static void handleBlockLogRequest(int x, int y) { + BlockLogRequestPacket packet = new BlockLogRequestPacket(); + packet.x = x; + packet.y = y; + packet.editlogs = Vars.currentEditLogs; + + Net.send(packet, SendMode.udp); + } + + public static void handleRollbackRequest(int rollbackTimes) { + RollbackRequestPacket packet = new RollbackRequestPacket(); + packet.rollbackTimes = rollbackTimes; + + Net.send(packet, SendMode.udp); + } } diff --git a/core/src/io/anuke/mindustry/net/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index d7465cb7c913..bd0b8b9d3959 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -1,8 +1,11 @@ package io.anuke.mindustry.net; +import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Base64Coder; +import com.badlogic.gdx.utils.IntMap; import com.badlogic.gdx.utils.reflect.ClassReflection; import com.badlogic.gdx.utils.reflect.ReflectionException; +import io.anuke.mindustry.Vars; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.entities.SyncEntity; import io.anuke.mindustry.io.Version; @@ -12,7 +15,6 @@ import io.anuke.mindustry.world.Block; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.EntityGroup; - import java.nio.ByteBuffer; /**Class for storing all packets.*/ @@ -139,7 +141,61 @@ public void read(ByteBuffer buffer) { timestamp = buffer.getLong(); } } - + + public static class BlockLogRequestPacket implements Packet { + public int x; + public int y; + public Array editlogs; + + @Override + public void write(ByteBuffer buffer) { + buffer.putShort((short)x); + buffer.putShort((short)y); + buffer.putInt(editlogs.size); + for(EditLog value : editlogs) { + buffer.put((byte)value.playername.getBytes().length); + buffer.put(value.playername.getBytes()); + buffer.putInt(value.block.id); + buffer.put((byte) value.rotation); + buffer.put((byte) value.action.ordinal()); + } + } + + @Override + public void read(ByteBuffer buffer) { + x = buffer.getShort(); + y = buffer.getShort(); + editlogs = new Array<>(); + int arraySize = buffer.getInt(); + for(int a = 0; a < arraySize; a ++) { + byte length = buffer.get(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + String name = new String(bytes); + + int blockid = buffer.getInt(); + int rotation = buffer.get(); + int ordinal = buffer.get(); + + editlogs.add(new EditLog(name, Block.getByID(blockid), rotation, EditLog.EditAction.values()[ordinal])); + } + } + } + + public static class RollbackRequestPacket implements Packet { + public int rollbackTimes; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(rollbackTimes); + } + + @Override + public void read(ByteBuffer buffer) { + rollbackTimes = buffer.getInt(); + } + } + public static class PositionPacket implements Packet{ public byte[] data; diff --git a/core/src/io/anuke/mindustry/net/Registrator.java b/core/src/io/anuke/mindustry/net/Registrator.java index 8e1dfa829727..92d6eca4b879 100644 --- a/core/src/io/anuke/mindustry/net/Registrator.java +++ b/core/src/io/anuke/mindustry/net/Registrator.java @@ -17,6 +17,8 @@ public class Registrator { PlacePacket.class, BreakPacket.class, StateSyncPacket.class, + BlockLogRequestPacket.class, + RollbackRequestPacket.class, BlockSyncPacket.class, BulletPacket.class, EnemyDeathPacket.class, @@ -44,7 +46,7 @@ public class Registrator { NetErrorPacket.class, PlayerAdminPacket.class, AdministerRequestPacket.class, - TracePacket.class, + TracePacket.class }; private static ObjectIntMap> ids = new ObjectIntMap<>(); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/RollbackDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/RollbackDialog.java new file mode 100644 index 000000000000..751ddcd9fc34 --- /dev/null +++ b/core/src/io/anuke/mindustry/ui/dialogs/RollbackDialog.java @@ -0,0 +1,40 @@ +package io.anuke.mindustry.ui.dialogs; + +import io.anuke.mindustry.net.NetEvents; +import io.anuke.ucore.scene.ui.Label; +import io.anuke.ucore.scene.ui.TextField; +import io.anuke.ucore.scene.ui.layout.Table; +import io.anuke.ucore.util.Strings; +import static io.anuke.mindustry.Vars.*; + +public class RollbackDialog extends FloatingDialog { + + public RollbackDialog(){ + super("$text.server.rollback"); + + setup(); + shown(this::setup); + } + + private void setup(){ + content().clear(); + buttons().clear(); + + if(gwt) return; + + content().row(); + content().add("$text.server.rollback.numberfield"); + + TextField field = content().addField("", t->{}).size(200f, 48f).get(); + field.setTextFieldFilter((f, c) -> field.getText().length() < 4); + + content().row(); + buttons().defaults().size(200f, 50f).left().pad(2f); + buttons().addButton("$text.cancel", this::hide); + + buttons().addButton("$text.ok", () -> { + NetEvents.handleRollbackRequest(Integer.valueOf(field.getText())); + hide(); + }).disabled(b -> field.getText().isEmpty() || !Strings.canParsePostiveInt(field.getText())); + } +} diff --git a/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java b/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java index fa1a178024ee..67cb96cd38da 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java @@ -7,6 +7,7 @@ import com.badlogic.gdx.utils.Array; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.input.InputHandler; +import io.anuke.mindustry.net.EditLog; import io.anuke.mindustry.resource.*; import io.anuke.mindustry.ui.dialogs.FloatingDialog; import io.anuke.mindustry.world.Block; @@ -24,7 +25,6 @@ import io.anuke.ucore.util.Bundles; import io.anuke.ucore.util.Mathf; import io.anuke.ucore.util.Strings; - import static io.anuke.mindustry.Vars.*; public class BlocksFragment implements Fragment{ @@ -344,7 +344,45 @@ public void showBlockInfo(Block block){ d.show(); } - + + public void showBlockLogs(int x, int y){ + boolean wasPaused = state.is(State.paused); + state.set(State.paused); + + FloatingDialog d = new FloatingDialog("$text.blocks.editlogs"); + Table table = new Table(); + table.defaults().pad(1f); + ScrollPane pane = new ScrollPane(table, "clear"); + pane.setFadeScrollBars(false); + Table top = new Table(); + top.left(); + top.add("[accent]Edit logs for: "+ x + ", " + y); + table.add(top).fill().left(); + table.row(); + + d.content().add(pane).grow(); + + if(currentEditLogs == null || currentEditLogs.size == 0) { + table.add("$text.block.editlogsnotfound").left(); + table.row(); + } + else { + for(int i = 0; i < currentEditLogs.size; i++) { + EditLog log = currentEditLogs.get(i); + table.add("[gold]" + (i + 1) + ". [white]" + log.info()).left(); + table.row(); + } + } + + d.buttons().addButton("$text.ok", () -> { + if(!wasPaused) + state.set(State.playing); + d.hide(); + }).size(110, 50).pad(10f); + + d.show(); + } + public void updateItems(){ itemtable.clear(); diff --git a/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java index d98aed3a78af..dce4a9b89dca 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java @@ -59,6 +59,10 @@ public void build(){ new button("$text.server.admins", () -> { ui.admins.show(); }).padTop(-12).padBottom(-12).padRight(-12).fillY().cell.disabled(b -> Net.client()); + + new button("$text.server.rollback", () -> { + ui.rollback.show(); + }).padTop(-12).padBottom(-12).padRight(-12).fillY().cell.disabled(b -> !player.isAdmin); }}.pad(10f).growX().end(); }}.end(); diff --git a/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index 55ac99e2b02d..c71c26120fdf 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.ApplicationLogger; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.IntMap; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.game.Difficulty; @@ -15,7 +16,9 @@ import io.anuke.mindustry.net.Packets.ChatPacket; import io.anuke.mindustry.net.Packets.KickReason; import io.anuke.mindustry.ui.fragments.DebugFragment; +import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Map; +import io.anuke.mindustry.world.Placement; import io.anuke.mindustry.world.Tile; import io.anuke.ucore.core.*; import io.anuke.ucore.modules.Module; @@ -32,8 +35,6 @@ import static io.anuke.mindustry.Vars.*; import static io.anuke.ucore.util.Log.*; -; - public class ServerControl extends Module { private final CommandHandler handler = new CommandHandler(""); private ShuffleMode mode; @@ -727,6 +728,28 @@ private void registerCommands(){ info("Nobody with that name could be found."); } }); + + handler.register("rollback", "", "Rollback the block edits in the world", arg -> { + if(!state.is(State.playing)) { + err("Open the server first."); + return; + } + + if(!Strings.canParsePostiveInt(arg[0])) { + err("Please input a valid, positive, number of times to rollback"); + return; + } + + int rollbackTimes = Integer.valueOf(arg[0]); + IntMap> editLogs = netServer.admins.getEditLogs(); + if(editLogs.size == 0){ + err("Nothing to rollback!"); + return; + } + + netServer.admins.rollbackWorld(rollbackTimes); + info("Rollback done!"); + }); } private void readCommands(){