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 --- src/main/java/cuchaz/enigma/gui/GuiController.java | 150 +++++++++++++++++++-- 1 file changed, 137 insertions(+), 13 deletions(-) (limited to 'src/main/java/cuchaz/enigma/gui/GuiController.java') diff --git a/src/main/java/cuchaz/enigma/gui/GuiController.java b/src/main/java/cuchaz/enigma/gui/GuiController.java index 742d6b8..cccc9e8 100644 --- a/src/main/java/cuchaz/enigma/gui/GuiController.java +++ b/src/main/java/cuchaz/enigma/gui/GuiController.java @@ -24,24 +24,33 @@ import cuchaz.enigma.gui.dialog.ProgressDialog; import cuchaz.enigma.gui.stats.StatsGenerator; import cuchaz.enigma.gui.stats.StatsMember; import cuchaz.enigma.gui.util.History; +import cuchaz.enigma.network.EnigmaClient; +import cuchaz.enigma.network.EnigmaServer; +import cuchaz.enigma.network.IntegratedEnigmaServer; +import cuchaz.enigma.network.ServerPacketHandler; +import cuchaz.enigma.network.packet.LoginC2SPacket; +import cuchaz.enigma.network.packet.Packet; import cuchaz.enigma.source.*; import cuchaz.enigma.throwables.MappingParseException; import cuchaz.enigma.translation.Translator; import cuchaz.enigma.translation.mapping.*; import cuchaz.enigma.translation.mapping.serde.MappingFormat; import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; 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.MethodEntry; import cuchaz.enigma.utils.I18n; +import cuchaz.enigma.utils.Message; import cuchaz.enigma.utils.ReadableToken; import cuchaz.enigma.utils.Utils; import org.objectweb.asm.tree.ClassNode; import javax.annotation.Nullable; import javax.swing.JOptionPane; -import java.awt.Desktop; +import javax.swing.SwingUtilities; +import java.awt.*; import java.awt.event.ItemEvent; import java.io.*; import java.nio.file.Path; @@ -76,6 +85,9 @@ public class GuiController { private DecompiledClassSource currentSource; private Source uncommentedSource; + private EnigmaClient client; + private EnigmaServer server; + public GuiController(Gui gui, EnigmaProfile profile) { this.gui = gui; this.enigma = Enigma.builder() @@ -143,6 +155,14 @@ public class GuiController { }); } + public void openMappings(EntryTree mappings) { + if (project == null) return; + + project.setMappings(mappings); + refreshClasses(); + refreshCurrentClass(); + } + public CompletableFuture saveMappings(Path path) { return saveMappings(path, loadedMappingFormat); } @@ -388,11 +408,39 @@ public class GuiController { private void refreshCurrentClass(EntryReference, Entry> reference, RefreshMode mode) { if (currentSource != null) { - loadClass(currentSource.getEntry(), () -> { - if (reference != null) { - showReference(reference); + if (reference == null) { + int obfSelectionStart = currentSource.getObfuscatedOffset(gui.editor.getSelectionStart()); + int obfSelectionEnd = currentSource.getObfuscatedOffset(gui.editor.getSelectionEnd()); + + Rectangle viewportBounds = gui.sourceScroller.getViewport().getViewRect(); + // Here we pick an "anchor position", which we want to stay in the same vertical location on the screen after the new text has been set + int anchorModelPos = gui.editor.getSelectionStart(); + Rectangle anchorViewPos = Utils.safeModelToView(gui.editor, anchorModelPos); + if (anchorViewPos.y < viewportBounds.y || anchorViewPos.y >= viewportBounds.y + viewportBounds.height) { + anchorModelPos = gui.editor.viewToModel(new Point(0, viewportBounds.y)); + anchorViewPos = Utils.safeModelToView(gui.editor, anchorModelPos); } - }, mode); + int obfAnchorPos = currentSource.getObfuscatedOffset(anchorModelPos); + Rectangle anchorViewPos_f = anchorViewPos; + int scrollX = gui.sourceScroller.getHorizontalScrollBar().getValue(); + + loadClass(currentSource.getEntry(), () -> SwingUtilities.invokeLater(() -> { + int newAnchorModelPos = currentSource.getDeobfuscatedOffset(obfAnchorPos); + Rectangle newAnchorViewPos = Utils.safeModelToView(gui.editor, newAnchorModelPos); + int newScrollY = newAnchorViewPos.y - (anchorViewPos_f.y - viewportBounds.y); + + gui.editor.select(currentSource.getDeobfuscatedOffset(obfSelectionStart), currentSource.getDeobfuscatedOffset(obfSelectionEnd)); + // Changing the selection scrolls to the caret position inside a SwingUtilities.invokeLater call, so + // we need to wrap our change to the scroll position inside another invokeLater so it happens after + // the caret's own scrolling. + SwingUtilities.invokeLater(() -> { + gui.sourceScroller.getHorizontalScrollBar().setValue(Math.min(scrollX, gui.sourceScroller.getHorizontalScrollBar().getMaximum())); + gui.sourceScroller.getVerticalScrollBar().setValue(Math.min(newScrollY, gui.sourceScroller.getVerticalScrollBar().getMaximum())); + }); + }), mode); + } else { + loadClass(currentSource.getEntry(), () -> showReference(reference), mode); + } } } @@ -528,43 +576,59 @@ public class GuiController { } public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree) { + rename(reference, newName, refreshClassTree, true); + } + + public void rename(EntryReference, Entry> reference, String newName, boolean refreshClassTree, boolean jumpToReference) { Entry entry = reference.getNameableEntry(); project.getMapper().mapFromObf(entry, new EntryMapping(newName)); if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) this.gui.moveClassTree(reference, newName); - refreshCurrentClass(reference); + refreshCurrentClass(jumpToReference ? reference : null); } public void removeMapping(EntryReference, Entry> reference) { + removeMapping(reference, true); + } + + public void removeMapping(EntryReference, Entry> reference, boolean jumpToReference) { project.getMapper().removeByObf(reference.getNameableEntry()); if (reference.entry instanceof ClassEntry) this.gui.moveClassTree(reference, false, true); - refreshCurrentClass(reference); + refreshCurrentClass(jumpToReference ? reference : null); } public void changeDocs(EntryReference, Entry> reference, String updatedDocs) { - changeDoc(reference.entry, updatedDocs); + changeDocs(reference, updatedDocs, true); + } - refreshCurrentClass(reference, RefreshMode.JAVADOCS); + public void changeDocs(EntryReference, Entry> reference, String updatedDocs, boolean jumpToReference) { + changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs); + + refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS); } - public void changeDoc(Entry obfEntry, String newDoc) { + private void changeDoc(Entry obfEntry, String newDoc) { EntryRemapper mapper = project.getMapper(); if (mapper.getDeobfMapping(obfEntry) == null) { - markAsDeobfuscated(obfEntry,false); // NPE + markAsDeobfuscated(obfEntry, false); // NPE } mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false); } - public void markAsDeobfuscated(Entry obfEntry, boolean renaming) { + private void markAsDeobfuscated(Entry obfEntry, boolean renaming) { EntryRemapper mapper = project.getMapper(); mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming); } public void markAsDeobfuscated(EntryReference, Entry> reference) { + markAsDeobfuscated(reference, true); + } + + public void markAsDeobfuscated(EntryReference, Entry> reference, boolean jumpToReference) { EntryRemapper mapper = project.getMapper(); Entry entry = reference.getNameableEntry(); mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName())); @@ -572,7 +636,7 @@ public class GuiController { if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) this.gui.moveClassTree(reference, true, false); - refreshCurrentClass(reference); + refreshCurrentClass(jumpToReference ? reference : null); } public void openStats(Set includedMembers) { @@ -602,4 +666,64 @@ public class GuiController { decompiler = createDecompiler(); refreshCurrentClass(null, RefreshMode.FULL); } + + public EnigmaClient getClient() { + return client; + } + + public EnigmaServer getServer() { + return server; + } + + public void createClient(String username, String ip, int port, char[] password) throws IOException { + client = new EnigmaClient(this, ip, port); + client.connect(); + client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, username)); + gui.setConnectionState(ConnectionState.CONNECTED); + } + + public void createServer(int port, char[] password) throws IOException { + server = new IntegratedEnigmaServer(project.getJarChecksum(), password, EntryRemapper.mapped(project.getJarIndex(), new HashEntryTree<>(project.getMapper().getObfToDeobf())), port); + server.start(); + client = new EnigmaClient(this, "127.0.0.1", port); + client.connect(); + client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, EnigmaServer.OWNER_USERNAME)); + gui.setConnectionState(ConnectionState.HOSTING); + } + + public synchronized void disconnectIfConnected(String reason) { + if (client == null && server == null) { + return; + } + + if (client != null) { + client.disconnect(); + } + if (server != null) { + server.stop(); + } + client = null; + server = null; + SwingUtilities.invokeLater(() -> { + if (reason != null) { + JOptionPane.showMessageDialog(gui.getFrame(), I18n.translate(reason), I18n.translate("disconnect.disconnected"), JOptionPane.INFORMATION_MESSAGE); + } + gui.setConnectionState(ConnectionState.NOT_CONNECTED); + }); + } + + public void sendPacket(Packet packet) { + if (client != null) { + client.sendPacket(packet); + } + } + + public void addMessage(Message message) { + gui.addMessage(message); + } + + public void updateUserList(List users) { + gui.setUserList(users); + } + } -- cgit v1.2.3