diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index a02210dba2..71a720f1d4 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -4,6 +4,7 @@ import com.badlogic.gdx.Application.ApplicationType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.Array; import io.anuke.mindustry.core.*; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.entities.TileEntity; @@ -14,13 +15,14 @@ import io.anuke.mindustry.entities.effect.Shield; import io.anuke.mindustry.entities.units.BaseUnit; import io.anuke.mindustry.game.Team; import io.anuke.mindustry.io.Version; +import io.anuke.mindustry.core.Platform; +import io.anuke.mindustry.net.EditLog; import io.anuke.ucore.entities.EffectEntity; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.Entity; 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{ @@ -86,6 +88,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 111edab356..dfd59f386a 100644 --- a/core/src/io/anuke/mindustry/core/NetClient.java +++ b/core/src/io/anuke/mindustry/core/NetClient.java @@ -3,6 +3,8 @@ package io.anuke.mindustry.core; import com.badlogic.gdx.graphics.Color; 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.Player; import io.anuke.mindustry.entities.SyncEntity; @@ -151,6 +153,10 @@ public class NetClient extends Module { state.wave = packet.wave; }); + Net.handleClient(BlockLogRequestPacket.class, packet -> { + currentEditLogs = packet.editlogs; + }); + Net.handleClient(PlacePacket.class, (packet) -> { Player placer = playerGroup.getByID(packet.playerid); diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index c648b847fd..6030f9d9cb 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -30,7 +30,6 @@ import io.anuke.ucore.util.Timer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; - import static io.anuke.mindustry.Vars.*; public class NetServer extends Module{ @@ -217,6 +216,7 @@ public class NetServer extends Module{ TraceInfo trace = admins.getTraceByID(getUUID(id)); + admins.logEdit(packet.x, packet.y, connections.get(id), block, packet.rotation, EditLog.EditAction.PLACE); trace.lastBlockPlaced = block; trace.totalBlocksPlaced ++; admins.getInfo(getUUID(id)).totalBlockPlaced ++; @@ -244,6 +244,7 @@ public class NetServer extends Module{ if(block != null) { TraceInfo trace = admins.getTraceByID(getUUID(id)); + admins.logEdit(packet.x, packet.y, connections.get(id), block, tile.getRotation(), EditLog.EditAction.BREAK); trace.lastBlockBroken = block; trace.totalBlocksBroken++; admins.getInfo(getUUID(id)).totalBlocksBroken ++; @@ -353,6 +354,24 @@ public class NetServer extends Module{ 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(){ diff --git a/core/src/io/anuke/mindustry/core/UI.java b/core/src/io/anuke/mindustry/core/UI.java index 37022cd046..434ce1f4ef 100644 --- a/core/src/io/anuke/mindustry/core/UI.java +++ b/core/src/io/anuke/mindustry/core/UI.java @@ -54,6 +54,7 @@ public class UI extends SceneModule{ public BansDialog bans; public AdminsDialog admins; public TraceDialog traces; + public RollbackDialog rollback; public ChangelogDialog changelog; public LocalPlayerDialog localplayers; @@ -169,6 +170,7 @@ public class UI extends SceneModule{ bans = new BansDialog(); admins = new AdminsDialog(); traces = new TraceDialog(); + rollback = new RollbackDialog(); maps = new MapsDialog(); localplayers = new LocalPlayerDialog(); diff --git a/core/src/io/anuke/mindustry/input/DefaultKeybinds.java b/core/src/io/anuke/mindustry/input/DefaultKeybinds.java index 0f3142e22a..bfbe7016af 100644 --- a/core/src/io/anuke/mindustry/input/DefaultKeybinds.java +++ b/core/src/io/anuke/mindustry/input/DefaultKeybinds.java @@ -40,7 +40,8 @@ public class DefaultKeybinds { "chat_history_prev", Input.UP, "chat_history_next", Input.DOWN, "chat_scroll", new Axis(Input.SCROLL), - "console", Input.GRAVE + "console", Input.GRAVE, + "block_logs", Input.I, ); KeyBinds.defaultSection(section, DeviceType.controller, diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index af4cbc32bb..6cda0f126d 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -151,7 +151,7 @@ public class DesktopInput extends InputHandler{ renderer.minimap().zoomBy(-(int)Inputs.getAxisTapped(section,"zoom_minimap")); rotation = Mathf.mod(rotation + (int)Inputs.getAxisTapped(section,"rotate"), 4); - + Tile cursor = tileAt(control.gdxInput().getX(), control.gdxInput().getY()); if(cursor != null){ @@ -165,6 +165,16 @@ public class DesktopInput extends InputHandler{ && cursor.block() == Blocks.air){ cursorType = drill; } + + if(recipe == null && !ui.hasMouse() && Inputs.keyDown("block_info") + && cursor.block().fullDescription != null){ + cursorType = hand; + if(Inputs.keyTap("select")){ + ui.hudfrag.blockfrag.showBlockInfo(cursor.block()); + Cursors.restoreCursor(); + cursorType = normal; + } + } } if(!ui.hasMouse()) { diff --git a/core/src/io/anuke/mindustry/net/Administration.java b/core/src/io/anuke/mindustry/net/Administration.java index c4ca970bf8..8b5090b92b 100644 --- a/core/src/io/anuke/mindustry/net/Administration.java +++ b/core/src/io/anuke/mindustry/net/Administration.java @@ -1,9 +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; @@ -13,6 +23,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(){ @@ -44,6 +57,68 @@ public class Administration { 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 0000000000..e9bbeec043 --- /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 f2a65bbb56..58165053b8 100644 --- a/core/src/io/anuke/mindustry/net/NetEvents.java +++ b/core/src/io/anuke/mindustry/net/NetEvents.java @@ -120,4 +120,20 @@ public class NetEvents { ui.traces.show(target, netServer.admins.getTraceByID(target.uuid)); } } + + 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 7afaee42e2..15b61a95ef 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -1,5 +1,6 @@ package io.anuke.mindustry.net; +import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Base64Coder; import com.badlogic.gdx.utils.TimeUtils; import com.badlogic.gdx.utils.reflect.ClassReflection; @@ -159,6 +160,60 @@ public class Packets { } } + 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 Player player; diff --git a/core/src/io/anuke/mindustry/net/Registrator.java b/core/src/io/anuke/mindustry/net/Registrator.java index 0997cdc84a..9b1b92ccaf 100644 --- a/core/src/io/anuke/mindustry/net/Registrator.java +++ b/core/src/io/anuke/mindustry/net/Registrator.java @@ -38,6 +38,8 @@ public class Registrator { AdministerRequestPacket.class, TracePacket.class, InvokePacket.class, + BlockLogRequestPacket.class, + RollbackRequestPacket.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 0000000000..751ddcd9fc --- /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/PlayerListFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java index 2cb96716f6..09d49afd0b 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 class PlayerListFragment implements Fragment{ 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 b846df3281..acca16539d 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -3,6 +3,7 @@ package io.anuke.mindustry.server; 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; @@ -16,6 +17,9 @@ import io.anuke.mindustry.net.Administration.PlayerInfo; 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.io.Map; +import io.anuke.mindustry.world.Build; import io.anuke.mindustry.world.Tile; import io.anuke.ucore.core.*; import io.anuke.ucore.modules.Module; @@ -715,6 +719,28 @@ public class ServerControl extends Module { 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(){