From 854f4d49407e45d67dd5754afd21a7e59970ca5b Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Sun, 3 May 2020 21:06:38 +0100 Subject: Multiplayer support (#221) * First pass on multiplayer * Apply review suggestions * Dedicated Enigma server * Don't jump to references when other users do stuff * Better UI + translations * french translation * Apply review suggestions * Document the protocol * Fix most issues with scrolling. * Apply review suggestions * Fix zip hash issues + add a bit more logging * Optimize zip hash * Fix a couple of login bugs * Add message log and user list * Make Message an abstract class * Make status bar work, add chat box * Hide message log/users list when not connected * Fix status bar not resetting entirely * Run stop server task on server thread to prevent multithreading race conditions * Add c2s message to packet id list * Fix message scroll bar not scrolling to the end * Formatting * User list size -> ushort * Combine contains and remove check * Check removal before sending packet * Add password to login packet * Fix the GUI closing the rename text field when someone else renames something * Update fr_fr.json * oups * Make connection/server create dialogs not useless if it fails once * Refactor UI state updating * Fix imports * Fix Collab menu * Fix NPE when rename not allowed * Make the log file a configurable option * Don't use modified UTF * Update fr_fr.json * Bump version to 0.15.4 * Apparently I can't spell neither words nor semantic versions Co-authored-by: Yanis48 Co-authored-by: 2xsaiko --- .../enigma/network/DedicatedEnigmaServer.java | 164 ++++++++++++ .../java/cuchaz/enigma/network/EnigmaClient.java | 85 ++++++ .../java/cuchaz/enigma/network/EnigmaServer.java | 292 +++++++++++++++++++++ .../enigma/network/IntegratedEnigmaServer.java | 16 ++ .../cuchaz/enigma/network/ServerPacketHandler.java | 22 ++ .../enigma/network/packet/ChangeDocsC2SPacket.java | 59 +++++ .../enigma/network/packet/ChangeDocsS2CPacket.java | 44 ++++ .../network/packet/ConfirmChangeC2SPacket.java | 33 +++ .../enigma/network/packet/KickS2CPacket.java | 33 +++ .../enigma/network/packet/LoginC2SPacket.java | 75 ++++++ .../network/packet/MarkDeobfuscatedC2SPacket.java | 48 ++++ .../network/packet/MarkDeobfuscatedS2CPacket.java | 40 +++ .../enigma/network/packet/MessageC2SPacket.java | 39 +++ .../enigma/network/packet/MessageS2CPacket.java | 36 +++ .../java/cuchaz/enigma/network/packet/Packet.java | 15 ++ .../cuchaz/enigma/network/packet/PacketHelper.java | 135 ++++++++++ .../enigma/network/packet/PacketRegistry.java | 64 +++++ .../network/packet/RemoveMappingC2SPacket.java | 55 ++++ .../network/packet/RemoveMappingS2CPacket.java | 40 +++ .../enigma/network/packet/RenameC2SPacket.java | 64 +++++ .../enigma/network/packet/RenameS2CPacket.java | 48 ++++ .../network/packet/SyncMappingsS2CPacket.java | 88 +++++++ .../enigma/network/packet/UserListS2CPacket.java | 44 ++++ 23 files changed, 1539 insertions(+) create mode 100644 src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java create mode 100644 src/main/java/cuchaz/enigma/network/EnigmaClient.java create mode 100644 src/main/java/cuchaz/enigma/network/EnigmaServer.java create mode 100644 src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java create mode 100644 src/main/java/cuchaz/enigma/network/ServerPacketHandler.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/Packet.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/PacketHelper.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java create mode 100644 src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java (limited to 'src/main/java/cuchaz/enigma/network') diff --git a/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java b/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java new file mode 100644 index 0000000..2cfe823 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java @@ -0,0 +1,164 @@ +package cuchaz.enigma.network; + +import com.google.common.io.MoreFiles; +import cuchaz.enigma.*; +import cuchaz.enigma.throwables.MappingParseException; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.serde.MappingFormat; +import cuchaz.enigma.utils.Utils; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +public class DedicatedEnigmaServer extends EnigmaServer { + + private final EnigmaProfile profile; + private final MappingFormat mappingFormat; + private final Path mappingsFile; + private final PrintWriter log; + private BlockingQueue tasks = new LinkedBlockingDeque<>(); + + public DedicatedEnigmaServer( + byte[] jarChecksum, + char[] password, + EnigmaProfile profile, + MappingFormat mappingFormat, + Path mappingsFile, + PrintWriter log, + EntryRemapper mappings, + int port + ) { + super(jarChecksum, password, mappings, port); + this.profile = profile; + this.mappingFormat = mappingFormat; + this.mappingsFile = mappingsFile; + this.log = log; + } + + @Override + protected void runOnThread(Runnable task) { + tasks.add(task); + } + + @Override + public void log(String message) { + super.log(message); + log.println(message); + } + + public static void main(String[] args) { + OptionParser parser = new OptionParser(); + + OptionSpec jarOpt = parser.accepts("jar", "Jar file to open at startup") + .withRequiredArg() + .required() + .withValuesConvertedBy(Main.PathConverter.INSTANCE); + + OptionSpec mappingsOpt = parser.accepts("mappings", "Mappings file to open at startup") + .withRequiredArg() + .required() + .withValuesConvertedBy(Main.PathConverter.INSTANCE); + + OptionSpec profileOpt = parser.accepts("profile", "Profile json to apply at startup") + .withRequiredArg() + .withValuesConvertedBy(Main.PathConverter.INSTANCE); + + OptionSpec portOpt = parser.accepts("port", "Port to run the server on") + .withOptionalArg() + .ofType(Integer.class) + .defaultsTo(EnigmaServer.DEFAULT_PORT); + + OptionSpec passwordOpt = parser.accepts("password", "The password to join the server") + .withRequiredArg() + .defaultsTo(""); + + OptionSpec logFileOpt = parser.accepts("log", "The log file to write to") + .withRequiredArg() + .withValuesConvertedBy(Main.PathConverter.INSTANCE) + .defaultsTo(Paths.get("log.txt")); + + OptionSet parsedArgs = parser.parse(args); + Path jar = parsedArgs.valueOf(jarOpt); + Path mappingsFile = parsedArgs.valueOf(mappingsOpt); + Path profileFile = parsedArgs.valueOf(profileOpt); + int port = parsedArgs.valueOf(portOpt); + char[] password = parsedArgs.valueOf(passwordOpt).toCharArray(); + if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) { + System.err.println("Password too long, must be at most " + EnigmaServer.MAX_PASSWORD_LENGTH + " characters"); + System.exit(1); + } + Path logFile = parsedArgs.valueOf(logFileOpt); + + System.out.println("Starting Enigma server"); + DedicatedEnigmaServer server; + try { + byte[] checksum = Utils.zipSha1(parsedArgs.valueOf(jarOpt)); + + EnigmaProfile profile = EnigmaProfile.read(profileFile); + Enigma enigma = Enigma.builder().setProfile(profile).build(); + System.out.println("Indexing Jar..."); + EnigmaProject project = enigma.openJar(jar, ProgressListener.none()); + + MappingFormat mappingFormat = MappingFormat.ENIGMA_DIRECTORY; + EntryRemapper mappings; + if (!Files.exists(mappingsFile)) { + mappings = EntryRemapper.empty(project.getJarIndex()); + } else { + System.out.println("Reading mappings..."); + if (Files.isDirectory(mappingsFile)) { + mappingFormat = MappingFormat.ENIGMA_DIRECTORY; + } else if ("zip".equalsIgnoreCase(MoreFiles.getFileExtension(mappingsFile))) { + mappingFormat = MappingFormat.ENIGMA_ZIP; + } else { + mappingFormat = MappingFormat.ENIGMA_FILE; + } + mappings = EntryRemapper.mapped(project.getJarIndex(), mappingFormat.read(mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters())); + } + + PrintWriter log = new PrintWriter(Files.newBufferedWriter(logFile)); + + server = new DedicatedEnigmaServer(checksum, password, profile, mappingFormat, mappingsFile, log, mappings, port); + server.start(); + System.out.println("Server started"); + } catch (IOException | MappingParseException e) { + System.err.println("Error starting server!"); + e.printStackTrace(); + System.exit(1); + return; + } + + // noinspection RedundantSuppression + // noinspection Convert2MethodRef - javac 8 bug + Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> server.runOnThread(() -> server.saveMappings()), 0, 1, TimeUnit.MINUTES); + Runtime.getRuntime().addShutdownHook(new Thread(server::saveMappings)); + + while (true) { + try { + server.tasks.take().run(); + } catch (InterruptedException e) { + break; + } + } + } + + @Override + public synchronized void stop() { + super.stop(); + System.exit(0); + } + + private void saveMappings() { + mappingFormat.write(getMappings().getObfToDeobf(), getMappings().takeMappingDelta(), mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters()); + log.flush(); + } +} diff --git a/src/main/java/cuchaz/enigma/network/EnigmaClient.java b/src/main/java/cuchaz/enigma/network/EnigmaClient.java new file mode 100644 index 0000000..bfa53d7 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/EnigmaClient.java @@ -0,0 +1,85 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.network.packet.LoginC2SPacket; +import cuchaz.enigma.network.packet.Packet; +import cuchaz.enigma.network.packet.PacketRegistry; + +import javax.swing.SwingUtilities; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; + +public class EnigmaClient { + + private final GuiController controller; + + private final String ip; + private final int port; + private Socket socket; + private DataOutput output; + + public EnigmaClient(GuiController controller, String ip, int port) { + this.controller = controller; + this.ip = ip; + this.port = port; + } + + public void connect() throws IOException { + socket = new Socket(ip, port); + output = new DataOutputStream(socket.getOutputStream()); + Thread thread = new Thread(() -> { + try { + DataInput input = new DataInputStream(socket.getInputStream()); + while (true) { + int packetId; + try { + packetId = input.readUnsignedByte(); + } catch (EOFException | SocketException e) { + break; + } + Packet packet = PacketRegistry.createS2CPacket(packetId); + if (packet == null) { + throw new IOException("Received invalid packet id " + packetId); + } + packet.read(input); + SwingUtilities.invokeLater(() -> packet.handle(controller)); + } + } catch (IOException e) { + controller.disconnectIfConnected(e.toString()); + return; + } + controller.disconnectIfConnected("Disconnected"); + }); + thread.setName("Client I/O thread"); + thread.setDaemon(true); + thread.start(); + } + + public synchronized void disconnect() { + if (socket != null && !socket.isClosed()) { + try { + socket.close(); + } catch (IOException e1) { + System.err.println("Failed to close socket"); + e1.printStackTrace(); + } + } + } + + + public void sendPacket(Packet packet) { + try { + output.writeByte(PacketRegistry.getC2SId(packet)); + packet.write(output); + } catch (IOException e) { + controller.disconnectIfConnected(e.toString()); + } + } + +} diff --git a/src/main/java/cuchaz/enigma/network/EnigmaServer.java b/src/main/java/cuchaz/enigma/network/EnigmaServer.java new file mode 100644 index 0000000..b0e15a3 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/EnigmaServer.java @@ -0,0 +1,292 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.network.packet.KickS2CPacket; +import cuchaz.enigma.network.packet.MessageS2CPacket; +import cuchaz.enigma.network.packet.Packet; +import cuchaz.enigma.network.packet.PacketRegistry; +import cuchaz.enigma.network.packet.RemoveMappingS2CPacket; +import cuchaz.enigma.network.packet.RenameS2CPacket; +import cuchaz.enigma.network.packet.UserListS2CPacket; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.Message; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +public abstract class EnigmaServer { + + // https://discordapp.com/channels/507304429255393322/566418023372816394/700292322918793347 + public static final int DEFAULT_PORT = 34712; + public static final int PROTOCOL_VERSION = 0; + public static final String OWNER_USERNAME = "Owner"; + public static final int CHECKSUM_SIZE = 20; + public static final int MAX_PASSWORD_LENGTH = 255; // length is written as a byte in the login packet + + private final int port; + private ServerSocket socket; + private List clients = new CopyOnWriteArrayList<>(); + private Map usernames = new HashMap<>(); + private Set unapprovedClients = new HashSet<>(); + + private final byte[] jarChecksum; + private final char[] password; + + public static final int DUMMY_SYNC_ID = 0; + private final EntryRemapper mappings; + private Map, Integer> syncIds = new HashMap<>(); + private Map> inverseSyncIds = new HashMap<>(); + private Map> clientsNeedingConfirmation = new HashMap<>(); + private int nextSyncId = DUMMY_SYNC_ID + 1; + + private static int nextIoId = 0; + + public EnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) { + this.jarChecksum = jarChecksum; + this.password = password; + this.mappings = mappings; + this.port = port; + } + + public void start() throws IOException { + socket = new ServerSocket(port); + log("Server started on " + socket.getInetAddress() + ":" + port); + Thread thread = new Thread(() -> { + try { + while (!socket.isClosed()) { + acceptClient(); + } + } catch (SocketException e) { + System.out.println("Server closed"); + } catch (IOException e) { + e.printStackTrace(); + } + }); + thread.setName("Server client listener"); + thread.setDaemon(true); + thread.start(); + } + + private void acceptClient() throws IOException { + Socket client = socket.accept(); + clients.add(client); + Thread thread = new Thread(() -> { + try { + DataInput input = new DataInputStream(client.getInputStream()); + while (true) { + int packetId; + try { + packetId = input.readUnsignedByte(); + } catch (EOFException | SocketException e) { + break; + } + Packet packet = PacketRegistry.createC2SPacket(packetId); + if (packet == null) { + throw new IOException("Received invalid packet id " + packetId); + } + packet.read(input); + runOnThread(() -> packet.handle(new ServerPacketHandler(client, this))); + } + } catch (IOException e) { + kick(client, e.toString()); + e.printStackTrace(); + return; + } + kick(client, "disconnect.disconnected"); + }); + thread.setName("Server I/O thread #" + (nextIoId++)); + thread.setDaemon(true); + thread.start(); + } + + public void stop() { + runOnThread(() -> { + if (socket != null && !socket.isClosed()) { + for (Socket client : clients) { + kick(client, "disconnect.server_closed"); + } + try { + socket.close(); + } catch (IOException e) { + System.err.println("Failed to close server socket"); + e.printStackTrace(); + } + } + }); + } + + public void kick(Socket client, String reason) { + if (!clients.remove(client)) return; + + sendPacket(client, new KickS2CPacket(reason)); + + clientsNeedingConfirmation.values().removeIf(list -> { + list.remove(client); + return list.isEmpty(); + }); + String username = usernames.remove(client); + try { + client.close(); + } catch (IOException e) { + System.err.println("Failed to close server client socket"); + e.printStackTrace(); + } + + if (username != null) { + System.out.println("Kicked " + username + " because " + reason); + sendMessage(Message.disconnect(username)); + } + sendUsernamePacket(); + } + + public boolean isUsernameTaken(String username) { + return usernames.containsValue(username); + } + + public void setUsername(Socket client, String username) { + usernames.put(client, username); + sendUsernamePacket(); + } + + private void sendUsernamePacket() { + List usernames = new ArrayList<>(this.usernames.values()); + Collections.sort(usernames); + sendToAll(new UserListS2CPacket(usernames)); + } + + public String getUsername(Socket client) { + return usernames.get(client); + } + + public void sendPacket(Socket client, Packet packet) { + if (!client.isClosed()) { + int packetId = PacketRegistry.getS2CId(packet); + try { + DataOutput output = new DataOutputStream(client.getOutputStream()); + output.writeByte(packetId); + packet.write(output); + } catch (IOException e) { + if (!(packet instanceof KickS2CPacket)) { + kick(client, e.toString()); + e.printStackTrace(); + } + } + } + } + + public void sendToAll(Packet packet) { + for (Socket client : clients) { + sendPacket(client, packet); + } + } + + public void sendToAllExcept(Socket excluded, Packet packet) { + for (Socket client : clients) { + if (client != excluded) { + sendPacket(client, packet); + } + } + } + + public boolean canModifyEntry(Socket client, Entry entry) { + if (unapprovedClients.contains(client)) { + return false; + } + + Integer syncId = syncIds.get(entry); + if (syncId == null) { + return true; + } + Set clients = clientsNeedingConfirmation.get(syncId); + return clients == null || !clients.contains(client); + } + + public int lockEntry(Socket exception, Entry entry) { + int syncId = nextSyncId; + nextSyncId++; + // sync id is sent as an unsigned short, can't have more than 65536 + if (nextSyncId == 65536) { + nextSyncId = DUMMY_SYNC_ID + 1; + } + Integer oldSyncId = syncIds.get(entry); + if (oldSyncId != null) { + clientsNeedingConfirmation.remove(oldSyncId); + } + syncIds.put(entry, syncId); + inverseSyncIds.put(syncId, entry); + Set clients = new HashSet<>(this.clients); + clients.remove(exception); + clientsNeedingConfirmation.put(syncId, clients); + return syncId; + } + + public void confirmChange(Socket client, int syncId) { + if (usernames.containsKey(client)) { + unapprovedClients.remove(client); + } + + Set clients = clientsNeedingConfirmation.get(syncId); + if (clients != null) { + clients.remove(client); + if (clients.isEmpty()) { + clientsNeedingConfirmation.remove(syncId); + syncIds.remove(inverseSyncIds.remove(syncId)); + } + } + } + + public void sendCorrectMapping(Socket client, Entry entry, boolean refreshClassTree) { + EntryMapping oldMapping = mappings.getDeobfMapping(entry); + String oldName = oldMapping == null ? null : oldMapping.getTargetName(); + if (oldName == null) { + sendPacket(client, new RemoveMappingS2CPacket(DUMMY_SYNC_ID, entry)); + } else { + sendPacket(client, new RenameS2CPacket(0, entry, oldName, refreshClassTree)); + } + } + + protected abstract void runOnThread(Runnable task); + + public void log(String message) { + System.out.println(message); + } + + protected boolean isRunning() { + return !socket.isClosed(); + } + + public byte[] getJarChecksum() { + return jarChecksum; + } + + public char[] getPassword() { + return password; + } + + public EntryRemapper getMappings() { + return mappings; + } + + public void sendMessage(Message message) { + log(String.format("[MSG] %s", message.translate())); + sendToAll(new MessageS2CPacket(message)); + } + +} diff --git a/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java b/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java new file mode 100644 index 0000000..21c6825 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java @@ -0,0 +1,16 @@ +package cuchaz.enigma.network; + +import cuchaz.enigma.translation.mapping.EntryRemapper; + +import javax.swing.*; + +public class IntegratedEnigmaServer extends EnigmaServer { + public IntegratedEnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) { + super(jarChecksum, password, mappings, port); + } + + @Override + protected void runOnThread(Runnable task) { + SwingUtilities.invokeLater(task); + } +} diff --git a/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java b/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java new file mode 100644 index 0000000..8618553 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java @@ -0,0 +1,22 @@ +package cuchaz.enigma.network; + +import java.net.Socket; + +public class ServerPacketHandler { + + private final Socket client; + private final EnigmaServer server; + + public ServerPacketHandler(Socket client, EnigmaServer server) { + this.client = client; + this.server = server; + } + + public Socket getClient() { + return client; + } + + public EnigmaServer getServer() { + return server; + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java new file mode 100644 index 0000000..4d5d86f --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java @@ -0,0 +1,59 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.Message; +import cuchaz.enigma.utils.Utils; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class ChangeDocsC2SPacket implements Packet { + private Entry entry; + private String newDocs; + + ChangeDocsC2SPacket() { + } + + public ChangeDocsC2SPacket(Entry entry, String newDocs) { + this.entry = entry; + this.newDocs = newDocs; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + this.newDocs = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newDocs); + } + + @Override + public void handle(ServerPacketHandler handler) { + EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry); + + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + if (!valid) { + String oldDocs = mapping == null ? null : mapping.getJavadoc(); + handler.getServer().sendPacket(handler.getClient(), new ChangeDocsS2CPacket(EnigmaServer.DUMMY_SYNC_ID, entry, oldDocs == null ? "" : oldDocs)); + return; + } + + if (mapping == null) { + mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()); + } + handler.getServer().getMappings().mapFromObf(entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs)); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs)); + handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry)); + } + +} diff --git a/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java new file mode 100644 index 0000000..bf5b7cb --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java @@ -0,0 +1,44 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class ChangeDocsS2CPacket implements Packet { + private int syncId; + private Entry entry; + private String newDocs; + + ChangeDocsS2CPacket() { + } + + public ChangeDocsS2CPacket(int syncId, Entry entry, String newDocs) { + this.syncId = syncId; + this.entry = entry; + this.newDocs = newDocs; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + this.newDocs = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newDocs); + } + + @Override + public void handle(GuiController controller) { + controller.changeDocs(new EntryReference<>(entry, entry.getName()), newDocs, false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java new file mode 100644 index 0000000..78ef964 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class ConfirmChangeC2SPacket implements Packet { + private int syncId; + + ConfirmChangeC2SPacket() { + } + + public ConfirmChangeC2SPacket(int syncId) { + this.syncId = syncId; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + } + + @Override + public void handle(ServerPacketHandler handler) { + handler.getServer().confirmChange(handler.getClient(), syncId); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java new file mode 100644 index 0000000..bd007d3 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java @@ -0,0 +1,33 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.gui.GuiController; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class KickS2CPacket implements Packet { + private String reason; + + KickS2CPacket() { + } + + public KickS2CPacket(String reason) { + this.reason = reason; + } + + @Override + public void read(DataInput input) throws IOException { + this.reason = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeString(output, reason); + } + + @Override + public void handle(GuiController controller) { + controller.disconnectIfConnected(reason); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java new file mode 100644 index 0000000..722cbbf --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java @@ -0,0 +1,75 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.utils.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Arrays; + +public class LoginC2SPacket implements Packet { + private byte[] jarChecksum; + private char[] password; + private String username; + + LoginC2SPacket() { + } + + public LoginC2SPacket(byte[] jarChecksum, char[] password, String username) { + this.jarChecksum = jarChecksum; + this.password = password; + this.username = username; + } + + @Override + public void read(DataInput input) throws IOException { + if (input.readUnsignedShort() != EnigmaServer.PROTOCOL_VERSION) { + throw new IOException("Mismatching protocol"); + } + this.jarChecksum = new byte[EnigmaServer.CHECKSUM_SIZE]; + input.readFully(jarChecksum); + this.password = new char[input.readUnsignedByte()]; + for (int i = 0; i < password.length; i++) { + password[i] = input.readChar(); + } + this.username = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(EnigmaServer.PROTOCOL_VERSION); + output.write(jarChecksum); + output.writeByte(password.length); + for (char c : password) { + output.writeChar(c); + } + PacketHelper.writeString(output, username); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean usernameTaken = handler.getServer().isUsernameTaken(username); + handler.getServer().setUsername(handler.getClient(), username); + handler.getServer().log(username + " logged in with IP " + handler.getClient().getInetAddress().toString() + ":" + handler.getClient().getPort()); + + if (!Arrays.equals(password, handler.getServer().getPassword())) { + handler.getServer().kick(handler.getClient(), "disconnect.wrong_password"); + return; + } + + if (usernameTaken) { + handler.getServer().kick(handler.getClient(), "disconnect.username_taken"); + return; + } + + if (!Arrays.equals(jarChecksum, handler.getServer().getJarChecksum())) { + handler.getServer().kick(handler.getClient(), "disconnect.wrong_jar"); + return; + } + + handler.getServer().sendPacket(handler.getClient(), new SyncMappingsS2CPacket(handler.getServer().getMappings().getObfToDeobf())); + handler.getServer().sendMessage(Message.connect(username)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java new file mode 100644 index 0000000..98d20d9 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java @@ -0,0 +1,48 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class MarkDeobfuscatedC2SPacket implements Packet { + private Entry entry; + + MarkDeobfuscatedC2SPacket() { + } + + public MarkDeobfuscatedC2SPacket(Entry entry) { + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + if (!valid) { + handler.getServer().sendCorrectMapping(handler.getClient(), entry, true); + return; + } + + handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName())); + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated"); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry)); + handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry)); + + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java new file mode 100644 index 0000000..b7d6eda --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class MarkDeobfuscatedS2CPacket implements Packet { + private int syncId; + private Entry entry; + + MarkDeobfuscatedS2CPacket() { + } + + public MarkDeobfuscatedS2CPacket(int syncId, Entry entry) { + this.syncId = syncId; + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(GuiController controller) { + controller.markAsDeobfuscated(new EntryReference<>(entry, entry.getName()), false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java new file mode 100644 index 0000000..b8e0f14 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java @@ -0,0 +1,39 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.utils.Message; + +public class MessageC2SPacket implements Packet { + + private String message; + + MessageC2SPacket() { + } + + public MessageC2SPacket(String message) { + this.message = message; + } + + @Override + public void read(DataInput input) throws IOException { + message = PacketHelper.readString(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeString(output, message); + } + + @Override + public void handle(ServerPacketHandler handler) { + String message = this.message.trim(); + if (!message.isEmpty()) { + handler.getServer().sendMessage(Message.chat(handler.getServer().getUsername(handler.getClient()), message)); + } + } + +} diff --git a/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java new file mode 100644 index 0000000..edeaae0 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java @@ -0,0 +1,36 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.utils.Message; + +public class MessageS2CPacket implements Packet { + + private Message message; + + MessageS2CPacket() { + } + + public MessageS2CPacket(Message message) { + this.message = message; + } + + @Override + public void read(DataInput input) throws IOException { + message = Message.read(input); + } + + @Override + public void write(DataOutput output) throws IOException { + message.write(output); + } + + @Override + public void handle(GuiController handler) { + handler.addMessage(message); + } + +} diff --git a/src/main/java/cuchaz/enigma/network/packet/Packet.java b/src/main/java/cuchaz/enigma/network/packet/Packet.java new file mode 100644 index 0000000..2f16dfb --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/Packet.java @@ -0,0 +1,15 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public interface Packet { + + void read(DataInput input) throws IOException; + + void write(DataOutput output) throws IOException; + + void handle(H handler); + +} diff --git a/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java b/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java new file mode 100644 index 0000000..464606e --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java @@ -0,0 +1,135 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.translation.representation.MethodDescriptor; +import cuchaz.enigma.translation.representation.TypeDescriptor; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class PacketHelper { + + private static final int ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3; + private static final int MAX_STRING_LENGTH = 65535; + + public static Entry readEntry(DataInput input) throws IOException { + return readEntry(input, null, true); + } + + public static Entry readEntry(DataInput input, Entry parent, boolean includeParent) throws IOException { + int type = input.readUnsignedByte(); + + if (includeParent && input.readBoolean()) { + parent = readEntry(input, null, true); + } + + String name = readString(input); + + String javadocs = null; + if (input.readBoolean()) { + javadocs = readString(input); + } + + switch (type) { + case ENTRY_CLASS: { + if (parent != null && !(parent instanceof ClassEntry)) { + throw new IOException("Class requires class parent"); + } + return new ClassEntry((ClassEntry) parent, name, javadocs); + } + case ENTRY_FIELD: { + if (!(parent instanceof ClassEntry)) { + throw new IOException("Field requires class parent"); + } + TypeDescriptor desc = new TypeDescriptor(readString(input)); + return new FieldEntry((ClassEntry) parent, name, desc, javadocs); + } + case ENTRY_METHOD: { + if (!(parent instanceof ClassEntry)) { + throw new IOException("Method requires class parent"); + } + MethodDescriptor desc = new MethodDescriptor(readString(input)); + return new MethodEntry((ClassEntry) parent, name, desc, javadocs); + } + case ENTRY_LOCAL_VAR: { + if (!(parent instanceof MethodEntry)) { + throw new IOException("Local variable requires method parent"); + } + int index = input.readUnsignedShort(); + boolean parameter = input.readBoolean(); + return new LocalVariableEntry((MethodEntry) parent, index, name, parameter, javadocs); + } + default: throw new IOException("Received unknown entry type " + type); + } + } + + public static void writeEntry(DataOutput output, Entry entry) throws IOException { + writeEntry(output, entry, true); + } + + public static void writeEntry(DataOutput output, Entry entry, boolean includeParent) throws IOException { + // type + if (entry instanceof ClassEntry) { + output.writeByte(ENTRY_CLASS); + } else if (entry instanceof FieldEntry) { + output.writeByte(ENTRY_FIELD); + } else if (entry instanceof MethodEntry) { + output.writeByte(ENTRY_METHOD); + } else if (entry instanceof LocalVariableEntry) { + output.writeByte(ENTRY_LOCAL_VAR); + } else { + throw new IOException("Don't know how to serialize entry of type " + entry.getClass().getSimpleName()); + } + + // parent + if (includeParent) { + output.writeBoolean(entry.getParent() != null); + if (entry.getParent() != null) { + writeEntry(output, entry.getParent(), true); + } + } + + // name + writeString(output, entry.getName()); + + // javadocs + output.writeBoolean(entry.getJavadocs() != null); + if (entry.getJavadocs() != null) { + writeString(output, entry.getJavadocs()); + } + + // type-specific stuff + if (entry instanceof FieldEntry) { + writeString(output, ((FieldEntry) entry).getDesc().toString()); + } else if (entry instanceof MethodEntry) { + writeString(output, ((MethodEntry) entry).getDesc().toString()); + } else if (entry instanceof LocalVariableEntry) { + LocalVariableEntry localVar = (LocalVariableEntry) entry; + output.writeShort(localVar.getIndex()); + output.writeBoolean(localVar.isArgument()); + } + } + + public static String readString(DataInput input) throws IOException { + int length = input.readUnsignedShort(); + byte[] bytes = new byte[length]; + input.readFully(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + public static void writeString(DataOutput output, String str) throws IOException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + if (bytes.length > MAX_STRING_LENGTH) { + throw new IOException("String too long, was " + bytes.length + " bytes, max " + MAX_STRING_LENGTH + " allowed"); + } + output.writeShort(bytes.length); + output.write(bytes); + } + +} diff --git a/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java b/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java new file mode 100644 index 0000000..ba5d9de --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java @@ -0,0 +1,64 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.network.ServerPacketHandler; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class PacketRegistry { + + private static final Map>, Integer> c2sPacketIds = new HashMap<>(); + private static final Map>> c2sPacketCreators = new HashMap<>(); + private static final Map>, Integer> s2cPacketIds = new HashMap<>(); + private static final Map>> s2cPacketCreators = new HashMap<>(); + + private static > void registerC2S(int id, Class clazz, Supplier creator) { + c2sPacketIds.put(clazz, id); + c2sPacketCreators.put(id, creator); + } + + private static > void registerS2C(int id, Class clazz, Supplier creator) { + s2cPacketIds.put(clazz, id); + s2cPacketCreators.put(id, creator); + } + + static { + registerC2S(0, LoginC2SPacket.class, LoginC2SPacket::new); + registerC2S(1, ConfirmChangeC2SPacket.class, ConfirmChangeC2SPacket::new); + registerC2S(2, RenameC2SPacket.class, RenameC2SPacket::new); + registerC2S(3, RemoveMappingC2SPacket.class, RemoveMappingC2SPacket::new); + registerC2S(4, ChangeDocsC2SPacket.class, ChangeDocsC2SPacket::new); + registerC2S(5, MarkDeobfuscatedC2SPacket.class, MarkDeobfuscatedC2SPacket::new); + registerC2S(6, MessageC2SPacket.class, MessageC2SPacket::new); + + registerS2C(0, KickS2CPacket.class, KickS2CPacket::new); + registerS2C(1, SyncMappingsS2CPacket.class, SyncMappingsS2CPacket::new); + registerS2C(2, RenameS2CPacket.class, RenameS2CPacket::new); + registerS2C(3, RemoveMappingS2CPacket.class, RemoveMappingS2CPacket::new); + registerS2C(4, ChangeDocsS2CPacket.class, ChangeDocsS2CPacket::new); + registerS2C(5, MarkDeobfuscatedS2CPacket.class, MarkDeobfuscatedS2CPacket::new); + registerS2C(6, MessageS2CPacket.class, MessageS2CPacket::new); + registerS2C(7, UserListS2CPacket.class, UserListS2CPacket::new); + } + + public static int getC2SId(Packet packet) { + return c2sPacketIds.get(packet.getClass()); + } + + public static Packet createC2SPacket(int id) { + Supplier> creator = c2sPacketCreators.get(id); + return creator == null ? null : creator.get(); + } + + public static int getS2CId(Packet packet) { + return s2cPacketIds.get(packet.getClass()); + } + + public static Packet createS2CPacket(int id) { + Supplier> creator = s2cPacketCreators.get(id); + return creator == null ? null : creator.get(); + } + +} diff --git a/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java new file mode 100644 index 0000000..a3f3d91 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java @@ -0,0 +1,55 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.throwables.IllegalNameException; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RemoveMappingC2SPacket implements Packet { + private Entry entry; + + RemoveMappingC2SPacket() { + } + + public RemoveMappingC2SPacket(Entry entry) { + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + + if (valid) { + try { + handler.getServer().getMappings().removeByObf(entry); + } catch (IllegalNameException e) { + valid = false; + } + } + + if (!valid) { + handler.getServer().sendCorrectMapping(handler.getClient(), entry, true); + return; + } + + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " removed the mapping for " + entry); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new RemoveMappingS2CPacket(syncId, entry)); + handler.getServer().sendMessage(Message.removeMapping(handler.getServer().getUsername(handler.getClient()), entry)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java new file mode 100644 index 0000000..7bb1b00 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java @@ -0,0 +1,40 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RemoveMappingS2CPacket implements Packet { + private int syncId; + private Entry entry; + + RemoveMappingS2CPacket() { + } + + public RemoveMappingS2CPacket(int syncId, Entry entry) { + this.syncId = syncId; + this.entry = entry; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + } + + @Override + public void handle(GuiController controller) { + controller.removeMapping(new EntryReference<>(entry, entry.getName()), false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java b/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java new file mode 100644 index 0000000..03e95d6 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java @@ -0,0 +1,64 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.throwables.IllegalNameException; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.utils.Message; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RenameC2SPacket implements Packet { + private Entry entry; + private String newName; + private boolean refreshClassTree; + + RenameC2SPacket() { + } + + public RenameC2SPacket(Entry entry, String newName, boolean refreshClassTree) { + this.entry = entry; + this.newName = newName; + this.refreshClassTree = refreshClassTree; + } + + @Override + public void read(DataInput input) throws IOException { + this.entry = PacketHelper.readEntry(input); + this.newName = PacketHelper.readString(input); + this.refreshClassTree = input.readBoolean(); + } + + @Override + public void write(DataOutput output) throws IOException { + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newName); + output.writeBoolean(refreshClassTree); + } + + @Override + public void handle(ServerPacketHandler handler) { + boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry); + + if (valid) { + try { + handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(newName)); + } catch (IllegalNameException e) { + valid = false; + } + } + + if (!valid) { + handler.getServer().sendCorrectMapping(handler.getClient(), entry, refreshClassTree); + return; + } + + handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " renamed " + entry + " to " + newName); + + int syncId = handler.getServer().lockEntry(handler.getClient(), entry); + handler.getServer().sendToAllExcept(handler.getClient(), new RenameS2CPacket(syncId, entry, newName, refreshClassTree)); + handler.getServer().sendMessage(Message.rename(handler.getServer().getUsername(handler.getClient()), entry, newName)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java new file mode 100644 index 0000000..058f0e5 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java @@ -0,0 +1,48 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +public class RenameS2CPacket implements Packet { + private int syncId; + private Entry entry; + private String newName; + private boolean refreshClassTree; + + RenameS2CPacket() { + } + + public RenameS2CPacket(int syncId, Entry entry, String newName, boolean refreshClassTree) { + this.syncId = syncId; + this.entry = entry; + this.newName = newName; + this.refreshClassTree = refreshClassTree; + } + + @Override + public void read(DataInput input) throws IOException { + this.syncId = input.readUnsignedShort(); + this.entry = PacketHelper.readEntry(input); + this.newName = PacketHelper.readString(input); + this.refreshClassTree = input.readBoolean(); + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(syncId); + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newName); + output.writeBoolean(refreshClassTree); + } + + @Override + public void handle(GuiController controller) { + controller.rename(new EntryReference<>(entry, entry.getName()), newName, refreshClassTree, false); + controller.sendPacket(new ConfirmChangeC2SPacket(syncId)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java new file mode 100644 index 0000000..e6378d1 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java @@ -0,0 +1,88 @@ +package cuchaz.enigma.network.packet; + +import cuchaz.enigma.gui.GuiController; +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.entry.Entry; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class SyncMappingsS2CPacket implements Packet { + private EntryTree mappings; + + SyncMappingsS2CPacket() { + } + + public SyncMappingsS2CPacket(EntryTree mappings) { + this.mappings = mappings; + } + + @Override + public void read(DataInput input) throws IOException { + mappings = new HashEntryTree<>(); + int size = input.readInt(); + for (int i = 0; i < size; i++) { + readEntryTreeNode(input, null); + } + } + + private void readEntryTreeNode(DataInput input, Entry parent) throws IOException { + Entry entry = PacketHelper.readEntry(input, parent, false); + EntryMapping mapping = null; + if (input.readBoolean()) { + String name = input.readUTF(); + if (input.readBoolean()) { + String javadoc = input.readUTF(); + mapping = new EntryMapping(name, javadoc); + } else { + mapping = new EntryMapping(name); + } + } + mappings.insert(entry, mapping); + int size = input.readUnsignedShort(); + for (int i = 0; i < size; i++) { + readEntryTreeNode(input, entry); + } + } + + @Override + public void write(DataOutput output) throws IOException { + List> roots = mappings.getRootNodes().collect(Collectors.toList()); + output.writeInt(roots.size()); + for (EntryTreeNode node : roots) { + writeEntryTreeNode(output, node); + } + } + + private static void writeEntryTreeNode(DataOutput output, EntryTreeNode node) throws IOException { + PacketHelper.writeEntry(output, node.getEntry(), false); + EntryMapping value = node.getValue(); + output.writeBoolean(value != null); + if (value != null) { + PacketHelper.writeString(output, value.getTargetName()); + output.writeBoolean(value.getJavadoc() != null); + if (value.getJavadoc() != null) { + PacketHelper.writeString(output, value.getJavadoc()); + } + } + Collection> children = node.getChildNodes(); + output.writeShort(children.size()); + for (EntryTreeNode child : children) { + writeEntryTreeNode(output, child); + } + } + + @Override + public void handle(GuiController controller) { + controller.openMappings(mappings); + controller.sendPacket(new ConfirmChangeC2SPacket(EnigmaServer.DUMMY_SYNC_ID)); + } +} diff --git a/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java b/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java new file mode 100644 index 0000000..8904848 --- /dev/null +++ b/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java @@ -0,0 +1,44 @@ +package cuchaz.enigma.network.packet; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import cuchaz.enigma.gui.GuiController; + +public class UserListS2CPacket implements Packet { + + private List users; + + UserListS2CPacket() { + } + + public UserListS2CPacket(List users) { + this.users = users; + } + + @Override + public void read(DataInput input) throws IOException { + int len = input.readUnsignedShort(); + users = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + users.add(input.readUTF()); + } + } + + @Override + public void write(DataOutput output) throws IOException { + output.writeShort(users.size()); + for (String user : users) { + PacketHelper.writeString(output, user); + } + } + + @Override + public void handle(GuiController handler) { + handler.updateUserList(users); + } + +} -- cgit v1.2.3