diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index e55b3502e7..86e5af0127 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -209,6 +209,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 f7a3b756a2..eed8707e94 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.IntMap; import io.anuke.mindustry.core.*; import io.anuke.mindustry.entities.Bullet; import io.anuke.mindustry.entities.Player; @@ -11,6 +12,7 @@ import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.entities.effect.Shield; import io.anuke.mindustry.entities.enemies.Enemy; import io.anuke.mindustry.io.Platform; +import io.anuke.mindustry.net.EditLog; import io.anuke.mindustry.net.ClientDebug; import io.anuke.mindustry.net.ServerDebug; import io.anuke.ucore.UCore; @@ -18,7 +20,7 @@ import io.anuke.ucore.entities.EffectEntity; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.EntityGroup; import io.anuke.ucore.scene.ui.layout.Unit; - +import java.util.ArrayList; import java.util.Locale; public class Vars{ @@ -79,6 +81,8 @@ public class Vars{ public static boolean showUI = true; //whether to show block debug public static boolean showBlockDebug = false; + + public static IntMap> editLogs = new IntMap<>(); public static boolean headless = false; diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index 2df2ef14c4..c5fa2bbd34 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.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.Bullet; import io.anuke.mindustry.entities.BulletType; @@ -167,6 +168,10 @@ public class NetClient extends Module { ui.hudfrag.updateItems(); }); + + Net.handleClient(BlockLogSyncPacket.class, packet -> { + Vars.editLogs = 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 ce48cc0827..5532a0dbb7 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -26,7 +26,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{ @@ -34,6 +33,7 @@ public class NetServer extends Module{ private final static int timerEntitySync = 0; private final static int timerStateSync = 1; + private final static int timerBlockLogSync = 2; public final Administration admins = new Administration(); @@ -212,6 +212,7 @@ public class NetServer extends Module{ 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 ++; @@ -236,6 +237,7 @@ public class NetServer extends Module{ 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 ++; @@ -313,7 +315,7 @@ public class NetServer extends Module{ packet.id = connections.get(id).id; Net.sendExcept(id, packet, SendMode.tcp); }); - + Net.handleServer(AdministerRequestPacket.class, (id, packet) -> { Player player = connections.get(id); @@ -475,7 +477,14 @@ public class NetServer extends Module{ packet.wave = state.wave; packet.time = Timers.time(); packet.timestamp = TimeUtils.millis(); - + + Net.send(packet, SendMode.udp); + } + + if(timer.get(timerBlockLogSync, serverSyncTime)) { + BlockLogSyncPacket packet = new BlockLogSyncPacket(); + packet.editlogs = admins.getEditLogs(); + 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 dbd1d3cb20..214b5785c0 100644 --- a/core/src/io/anuke/mindustry/core/Renderer.java +++ b/core/src/io/anuke/mindustry/core/Renderer.java @@ -489,6 +489,12 @@ public class Renderer extends RendererModule{ 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/input/DefaultKeybinds.java b/core/src/io/anuke/mindustry/input/DefaultKeybinds.java index f17fcc437c..d35b60e229 100644 --- a/core/src/io/anuke/mindustry/input/DefaultKeybinds.java +++ b/core/src/io/anuke/mindustry/input/DefaultKeybinds.java @@ -25,6 +25,7 @@ public class DefaultKeybinds { "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 1c7a3f68f3..ef457f8b86 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -107,6 +107,16 @@ public class DesktopInput extends InputHandler{ Cursors.restoreCursor(); } } + + if(recipe == null && !ui.hasMouse() && Inputs.keyDown("block_logs")) { + showCursor = true; + if(Inputs.keyTap("select")){ + 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 ee2443deb7..2e59ed0bda 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.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 java.util.ArrayList; +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,22 @@ 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, block, rotation, action)); + } + else { + ArrayList logs = new ArrayList<>(); + logs.add(new EditLog(player, block, rotation, action)); + editLogs.put(x + y * world.width(), logs); + } + } + 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..036c1ee0b2 --- /dev/null +++ b/core/src/io/anuke/mindustry/net/EditLog.java @@ -0,0 +1,41 @@ +package io.anuke.mindustry.net; + +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.world.Block; +import static io.anuke.mindustry.Vars.playerGroup; + +public class EditLog { + + public Player player; + public Block block; + public int rotation; + public EditAction action; + + EditLog(Player player, Block block, int rotation, EditAction action){ + this.player = player; + this.block = block; + this.rotation = rotation; + this.action = action; + } + + public String info() { + return String.format("Player: %s, Block: %s, Rotation: %s, Edit Action: %s", player.name, block.name(), rotation, action.toString()); + } + + public static EditLog valueOf(String string) { + String[] parts = string.split(":"); + return new EditLog(playerGroup.getByID(Integer.valueOf(parts[0])), + Block.getByID(Integer.valueOf(parts[1])), + Integer.valueOf(parts[2]), + EditAction.valueOf(parts[3])); + } + + public String toString() { + return String.format("%s:%s:%s:%s", player.id, block.id, rotation, action.toString()); + } + + public enum EditAction{ + PLACE, + BREAK; + } +} diff --git a/core/src/io/anuke/mindustry/net/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index d7465cb7c9..98137941d9 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -1,6 +1,7 @@ package io.anuke.mindustry.net; 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.entities.Player; @@ -12,8 +13,8 @@ import io.anuke.mindustry.resource.Item; import io.anuke.mindustry.world.Block; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.EntityGroup; - import java.nio.ByteBuffer; +import java.util.ArrayList; /**Class for storing all packets.*/ public class Packets { @@ -140,6 +141,54 @@ public class Packets { } } + public static class BlockLogSyncPacket implements Packet { + public IntMap> editlogs; + + @Override + public void write(ByteBuffer buffer) { + int size = editlogs.size; + buffer.putInt(size); + for(IntMap.Entry> editlog :editlogs.entries()) { + + String key = String.valueOf(editlog.key); + buffer.put((byte) key.getBytes().length); + buffer.put(key.getBytes()); + + ArrayList values = editlog.value; + buffer.putInt(values.size()); + for(EditLog value : values) { + + String rawValue = value.toString(); + buffer.put((byte) rawValue.getBytes().length); + buffer.put(rawValue.getBytes()); + } + } + } + + @Override + public void read(ByteBuffer buffer) { + editlogs = new IntMap<>(); + int logssize = buffer.getInt(); + for(int i = 0; i < logssize; i ++){ + + byte length = buffer.get(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + Integer key = Integer.valueOf(new String(bytes)); + + ArrayList list = new ArrayList<>(); + int arraySize = buffer.getInt(); + for(int a = 0; a < arraySize; a ++) { + + byte[] arraybytes = new byte[buffer.get()]; + buffer.get(arraybytes); + list.add(EditLog.valueOf(new String(arraybytes))); + } + editlogs.put(key, list); + } + } + } + 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 8e1dfa8297..54c97743a0 100644 --- a/core/src/io/anuke/mindustry/net/Registrator.java +++ b/core/src/io/anuke/mindustry/net/Registrator.java @@ -17,6 +17,7 @@ public class Registrator { PlacePacket.class, BreakPacket.class, StateSyncPacket.class, + BlockLogSyncPacket.class, BlockSyncPacket.class, BulletPacket.class, EnemyDeathPacket.class, @@ -44,7 +45,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/fragments/BlocksFragment.java b/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java index fa1a178024..2690096267 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/BlocksFragment.java @@ -5,8 +5,10 @@ import com.badlogic.gdx.graphics.Colors; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.Interpolation; import com.badlogic.gdx.utils.Array; +import io.anuke.mindustry.Vars; 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 +26,7 @@ import io.anuke.ucore.scene.ui.layout.Table; import io.anuke.ucore.util.Bundles; import io.anuke.ucore.util.Mathf; import io.anuke.ucore.util.Strings; - +import java.util.ArrayList; import static io.anuke.mindustry.Vars.*; public class BlocksFragment implements Fragment{ @@ -344,7 +346,46 @@ public class BlocksFragment implements Fragment{ 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(); + + ArrayList logs = Vars.editLogs.get(x + y * world.width()); + if(logs == null || logs.isEmpty()) { + table.add("$text.block.editlogsnotfound").left(); + table.row(); + } + else { + for(int i = 0; i < logs.size(); i++) { + EditLog log = logs.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/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index 57a6cd25ac..216b03e843 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; @@ -15,7 +16,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.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; @@ -27,6 +30,8 @@ import io.anuke.ucore.util.Log; import io.anuke.ucore.util.Strings; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import java.util.Scanner; import static io.anuke.mindustry.Vars.*; @@ -723,6 +728,69 @@ 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(arg[0] == null) { + err("Please specify the amount of block edit cycles to rollback"); + return; + } + + int rollbackTimes = Integer.valueOf(arg[0]); + IntMap> editLogs = netServer.admins.getEditLogs(); + if(editLogs.size == 0){ + err("Nothing to rollback!"); + return; + } + + for(IntMap.Entry> editLog : editLogs.entries()) { + int coords = editLog.key; + ArrayList 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.remove(logs.size() - 1); + if(logs.isEmpty()) { + editLogs.remove(coords); + break; + } + } + } + info("Rollback done!"); + }); } private void readCommands(){