summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorGravatar Joseph Burton2020-05-03 21:06:38 +0100
committerGravatar GitHub2020-05-03 21:06:38 +0100
commit854f4d49407e45d67dd5754afd21a7e59970ca5b (patch)
tree582e3245786f9723b5895b0c8d41087b0e6bb416 /src/main
parentRewrite search dialog (#233) (diff)
downloadenigma-854f4d49407e45d67dd5754afd21a7e59970ca5b.tar.gz
enigma-854f4d49407e45d67dd5754afd21a7e59970ca5b.tar.xz
enigma-854f4d49407e45d67dd5754afd21a7e59970ca5b.zip
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 <doublecraft.official@gmail.com> Co-authored-by: 2xsaiko <git@dblsaiko.net>
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/cuchaz/enigma/Enigma.java3
-rw-r--r--src/main/java/cuchaz/enigma/EnigmaProfile.java22
-rw-r--r--src/main/java/cuchaz/enigma/EnigmaProject.java11
-rw-r--r--src/main/java/cuchaz/enigma/Main.java22
-rw-r--r--src/main/java/cuchaz/enigma/gui/ConnectionState.java7
-rw-r--r--src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java26
-rw-r--r--src/main/java/cuchaz/enigma/gui/Gui.java193
-rw-r--r--src/main/java/cuchaz/enigma/gui/GuiController.java150
-rw-r--r--src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java24
-rw-r--r--src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java82
-rw-r--r--src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java65
-rw-r--r--src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java40
-rw-r--r--src/main/java/cuchaz/enigma/gui/elements/MenuBar.java79
-rw-r--r--src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java164
-rw-r--r--src/main/java/cuchaz/enigma/network/EnigmaClient.java85
-rw-r--r--src/main/java/cuchaz/enigma/network/EnigmaServer.java292
-rw-r--r--src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java16
-rw-r--r--src/main/java/cuchaz/enigma/network/ServerPacketHandler.java22
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java59
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java44
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java33
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java33
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java75
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java48
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java40
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java39
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java36
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/Packet.java15
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/PacketHelper.java135
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java64
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java55
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java40
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java64
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java48
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java88
-rw-r--r--src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java44
-rw-r--r--src/main/java/cuchaz/enigma/utils/Message.java392
-rw-r--r--src/main/java/cuchaz/enigma/utils/Utils.java64
-rw-r--r--src/main/resources/lang/en_us.json38
-rw-r--r--src/main/resources/lang/fr_fr.json38
40 files changed, 2718 insertions, 77 deletions
diff --git a/src/main/java/cuchaz/enigma/Enigma.java b/src/main/java/cuchaz/enigma/Enigma.java
index b8887c29..f5f06491 100644
--- a/src/main/java/cuchaz/enigma/Enigma.java
+++ b/src/main/java/cuchaz/enigma/Enigma.java
@@ -21,6 +21,7 @@ import cuchaz.enigma.api.service.EnigmaService;
21import cuchaz.enigma.api.service.EnigmaServiceFactory; 21import cuchaz.enigma.api.service.EnigmaServiceFactory;
22import cuchaz.enigma.api.service.EnigmaServiceType; 22import cuchaz.enigma.api.service.EnigmaServiceType;
23import cuchaz.enigma.api.service.JarIndexerService; 23import cuchaz.enigma.api.service.JarIndexerService;
24import cuchaz.enigma.utils.Utils;
24 25
25import java.io.IOException; 26import java.io.IOException;
26import java.nio.file.Path; 27import java.nio.file.Path;
@@ -50,7 +51,7 @@ public class Enigma {
50 51
51 services.get(JarIndexerService.TYPE).forEach(indexer -> indexer.acceptJar(classCache, jarIndex)); 52 services.get(JarIndexerService.TYPE).forEach(indexer -> indexer.acceptJar(classCache, jarIndex));
52 53
53 return new EnigmaProject(this, classCache, jarIndex); 54 return new EnigmaProject(this, classCache, jarIndex, Utils.zipSha1(path));
54 } 55 }
55 56
56 public EnigmaProfile getProfile() { 57 public EnigmaProfile getProfile() {
diff --git a/src/main/java/cuchaz/enigma/EnigmaProfile.java b/src/main/java/cuchaz/enigma/EnigmaProfile.java
index 5a68be14..09b90f5f 100644
--- a/src/main/java/cuchaz/enigma/EnigmaProfile.java
+++ b/src/main/java/cuchaz/enigma/EnigmaProfile.java
@@ -14,8 +14,15 @@ import cuchaz.enigma.api.service.EnigmaServiceType;
14import cuchaz.enigma.translation.mapping.MappingFileNameFormat; 14import cuchaz.enigma.translation.mapping.MappingFileNameFormat;
15import cuchaz.enigma.translation.mapping.MappingSaveParameters; 15import cuchaz.enigma.translation.mapping.MappingSaveParameters;
16 16
17import javax.annotation.Nullable;
18import java.io.BufferedReader;
19import java.io.IOException;
20import java.io.InputStreamReader;
17import java.io.Reader; 21import java.io.Reader;
18import java.lang.reflect.Type; 22import java.lang.reflect.Type;
23import java.nio.charset.StandardCharsets;
24import java.nio.file.Files;
25import java.nio.file.Path;
19import java.util.Collections; 26import java.util.Collections;
20import java.util.List; 27import java.util.List;
21import java.util.Map; 28import java.util.Map;
@@ -41,6 +48,21 @@ public final class EnigmaProfile {
41 this.serviceProfiles = serviceProfiles; 48 this.serviceProfiles = serviceProfiles;
42 } 49 }
43 50
51 public static EnigmaProfile read(@Nullable Path file) throws IOException {
52 if (file != null) {
53 try (BufferedReader reader = Files.newBufferedReader(file)) {
54 return EnigmaProfile.parse(reader);
55 }
56 } else {
57 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Main.class.getResourceAsStream("/profile.json"), StandardCharsets.UTF_8))) {
58 return EnigmaProfile.parse(reader);
59 } catch (IOException ex) {
60 System.err.println("Failed to load default profile, will use empty profile: " + ex.getMessage());
61 return EnigmaProfile.EMPTY;
62 }
63 }
64 }
65
44 public static EnigmaProfile parse(Reader reader) { 66 public static EnigmaProfile parse(Reader reader) {
45 return GSON.fromJson(reader, EnigmaProfile.class); 67 return GSON.fromJson(reader, EnigmaProfile.class);
46 } 68 }
diff --git a/src/main/java/cuchaz/enigma/EnigmaProject.java b/src/main/java/cuchaz/enigma/EnigmaProject.java
index 852bfc49..b345fb39 100644
--- a/src/main/java/cuchaz/enigma/EnigmaProject.java
+++ b/src/main/java/cuchaz/enigma/EnigmaProject.java
@@ -1,12 +1,14 @@
1package cuchaz.enigma; 1package cuchaz.enigma;
2 2
3import com.google.common.base.Functions; 3import com.google.common.base.Functions;
4import com.google.common.base.Preconditions;
4import cuchaz.enigma.analysis.ClassCache; 5import cuchaz.enigma.analysis.ClassCache;
5import cuchaz.enigma.analysis.EntryReference; 6import cuchaz.enigma.analysis.EntryReference;
6import cuchaz.enigma.analysis.index.JarIndex; 7import cuchaz.enigma.analysis.index.JarIndex;
7import cuchaz.enigma.api.service.NameProposalService; 8import cuchaz.enigma.api.service.NameProposalService;
8import cuchaz.enigma.bytecode.translators.SourceFixVisitor; 9import cuchaz.enigma.bytecode.translators.SourceFixVisitor;
9import cuchaz.enigma.bytecode.translators.TranslationClassVisitor; 10import cuchaz.enigma.bytecode.translators.TranslationClassVisitor;
11import cuchaz.enigma.network.EnigmaServer;
10import cuchaz.enigma.source.*; 12import cuchaz.enigma.source.*;
11import cuchaz.enigma.translation.Translator; 13import cuchaz.enigma.translation.Translator;
12import cuchaz.enigma.translation.mapping.*; 14import cuchaz.enigma.translation.mapping.*;
@@ -39,13 +41,16 @@ public class EnigmaProject {
39 41
40 private final ClassCache classCache; 42 private final ClassCache classCache;
41 private final JarIndex jarIndex; 43 private final JarIndex jarIndex;
44 private final byte[] jarChecksum;
42 45
43 private EntryRemapper mapper; 46 private EntryRemapper mapper;
44 47
45 public EnigmaProject(Enigma enigma, ClassCache classCache, JarIndex jarIndex) { 48 public EnigmaProject(Enigma enigma, ClassCache classCache, JarIndex jarIndex, byte[] jarChecksum) {
49 Preconditions.checkArgument(jarChecksum.length == EnigmaServer.CHECKSUM_SIZE);
46 this.enigma = enigma; 50 this.enigma = enigma;
47 this.classCache = classCache; 51 this.classCache = classCache;
48 this.jarIndex = jarIndex; 52 this.jarIndex = jarIndex;
53 this.jarChecksum = jarChecksum;
49 54
50 this.mapper = EntryRemapper.empty(jarIndex); 55 this.mapper = EntryRemapper.empty(jarIndex);
51 } 56 }
@@ -70,6 +75,10 @@ public class EnigmaProject {
70 return jarIndex; 75 return jarIndex;
71 } 76 }
72 77
78 public byte[] getJarChecksum() {
79 return jarChecksum;
80 }
81
73 public EntryRemapper getMapper() { 82 public EntryRemapper getMapper() {
74 return mapper; 83 return mapper;
75 } 84 }
diff --git a/src/main/java/cuchaz/enigma/Main.java b/src/main/java/cuchaz/enigma/Main.java
index 1d63ec1c..7c87669f 100644
--- a/src/main/java/cuchaz/enigma/Main.java
+++ b/src/main/java/cuchaz/enigma/Main.java
@@ -17,10 +17,7 @@ import cuchaz.enigma.translation.mapping.serde.MappingFormat;
17 17
18import joptsimple.*; 18import joptsimple.*;
19 19
20import java.io.BufferedReader;
21import java.io.IOException; 20import java.io.IOException;
22import java.io.InputStreamReader;
23import java.nio.charset.StandardCharsets;
24import java.nio.file.Files; 21import java.nio.file.Files;
25import java.nio.file.Path; 22import java.nio.file.Path;
26import java.nio.file.Paths; 23import java.nio.file.Paths;
@@ -54,20 +51,7 @@ public class Main {
54 return; 51 return;
55 } 52 }
56 53
57 EnigmaProfile parsedProfile; 54 EnigmaProfile parsedProfile = EnigmaProfile.read(options.valueOf(profile));
58 if (options.has(profile)) {
59 Path profilePath = options.valueOf(profile);
60 try (BufferedReader reader = Files.newBufferedReader(profilePath)) {
61 parsedProfile = EnigmaProfile.parse(reader);
62 }
63 } else {
64 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Main.class.getResourceAsStream("/profile.json"), StandardCharsets.UTF_8))){
65 parsedProfile = EnigmaProfile.parse(reader);
66 } catch (IOException ex) {
67 System.out.println("Failed to load default profile, will use empty profile: " + ex.getMessage());
68 parsedProfile = EnigmaProfile.EMPTY;
69 }
70 }
71 55
72 Gui gui = new Gui(parsedProfile); 56 Gui gui = new Gui(parsedProfile);
73 GuiController controller = gui.getController(); 57 GuiController controller = gui.getController();
@@ -95,8 +79,8 @@ public class Main {
95 } 79 }
96 } 80 }
97 81
98 private static class PathConverter implements ValueConverter<Path> { 82 public static class PathConverter implements ValueConverter<Path> {
99 static final ValueConverter<Path> INSTANCE = new PathConverter(); 83 public static final ValueConverter<Path> INSTANCE = new PathConverter();
100 84
101 PathConverter() { 85 PathConverter() {
102 } 86 }
diff --git a/src/main/java/cuchaz/enigma/gui/ConnectionState.java b/src/main/java/cuchaz/enigma/gui/ConnectionState.java
new file mode 100644
index 00000000..db6590de
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/gui/ConnectionState.java
@@ -0,0 +1,7 @@
1package cuchaz.enigma.gui;
2
3public enum ConnectionState {
4 NOT_CONNECTED,
5 HOSTING,
6 CONNECTED,
7}
diff --git a/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java
index f7097f0e..08df3e75 100644
--- a/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java
+++ b/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java
@@ -126,6 +126,32 @@ public class DecompiledClassSource {
126 highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token); 126 highlightedTokens.computeIfAbsent(highlightType, t -> new ArrayList<>()).add(token);
127 } 127 }
128 128
129 public int getObfuscatedOffset(int deobfOffset) {
130 return getOffset(remappedIndex, obfuscatedIndex, deobfOffset);
131 }
132
133 public int getDeobfuscatedOffset(int obfOffset) {
134 return getOffset(obfuscatedIndex, remappedIndex, obfOffset);
135 }
136
137 private static int getOffset(SourceIndex fromIndex, SourceIndex toIndex, int fromOffset) {
138 int relativeOffset = 0;
139
140 Iterator<Token> fromTokenItr = fromIndex.referenceTokens().iterator();
141 Iterator<Token> toTokenItr = toIndex.referenceTokens().iterator();
142 while (fromTokenItr.hasNext() && toTokenItr.hasNext()) {
143 Token fromToken = fromTokenItr.next();
144 Token toToken = toTokenItr.next();
145 if (fromToken.end > fromOffset) {
146 break;
147 }
148
149 relativeOffset = toToken.end - fromToken.end;
150 }
151
152 return fromOffset + relativeOffset;
153 }
154
129 @Override 155 @Override
130 public String toString() { 156 public String toString() {
131 return remappedIndex.getSource(); 157 return remappedIndex.getSource();
diff --git a/src/main/java/cuchaz/enigma/gui/Gui.java b/src/main/java/cuchaz/enigma/gui/Gui.java
index 3412cd51..3adabaee 100644
--- a/src/main/java/cuchaz/enigma/gui/Gui.java
+++ b/src/main/java/cuchaz/enigma/gui/Gui.java
@@ -34,6 +34,7 @@ import cuchaz.enigma.config.Themes;
34import cuchaz.enigma.gui.dialog.CrashDialog; 34import cuchaz.enigma.gui.dialog.CrashDialog;
35import cuchaz.enigma.gui.dialog.JavadocDialog; 35import cuchaz.enigma.gui.dialog.JavadocDialog;
36import cuchaz.enigma.gui.dialog.SearchDialog; 36import cuchaz.enigma.gui.dialog.SearchDialog;
37import cuchaz.enigma.gui.elements.CollapsibleTabbedPane;
37import cuchaz.enigma.gui.elements.MenuBar; 38import cuchaz.enigma.gui.elements.MenuBar;
38import cuchaz.enigma.gui.elements.PopupMenuBar; 39import cuchaz.enigma.gui.elements.PopupMenuBar;
39import cuchaz.enigma.gui.filechooser.FileChooserAny; 40import cuchaz.enigma.gui.filechooser.FileChooserAny;
@@ -46,10 +47,12 @@ import cuchaz.enigma.gui.panels.PanelEditor;
46import cuchaz.enigma.gui.panels.PanelIdentifier; 47import cuchaz.enigma.gui.panels.PanelIdentifier;
47import cuchaz.enigma.gui.panels.PanelObf; 48import cuchaz.enigma.gui.panels.PanelObf;
48import cuchaz.enigma.gui.util.History; 49import cuchaz.enigma.gui.util.History;
50import cuchaz.enigma.network.packet.*;
49import cuchaz.enigma.throwables.IllegalNameException; 51import cuchaz.enigma.throwables.IllegalNameException;
50import cuchaz.enigma.translation.mapping.*; 52import cuchaz.enigma.translation.mapping.*;
51import cuchaz.enigma.translation.representation.entry.*; 53import cuchaz.enigma.translation.representation.entry.*;
52import cuchaz.enigma.utils.I18n; 54import cuchaz.enigma.utils.I18n;
55import cuchaz.enigma.utils.Message;
53import cuchaz.enigma.gui.util.ScaleUtil; 56import cuchaz.enigma.gui.util.ScaleUtil;
54import cuchaz.enigma.utils.Utils; 57import cuchaz.enigma.utils.Utils;
55import de.sciss.syntaxpane.DefaultSyntaxKit; 58import de.sciss.syntaxpane.DefaultSyntaxKit;
@@ -63,8 +66,11 @@ public class Gui {
63 private final MenuBar menuBar; 66 private final MenuBar menuBar;
64 // state 67 // state
65 public History<EntryReference<Entry<?>, Entry<?>>> referenceHistory; 68 public History<EntryReference<Entry<?>, Entry<?>>> referenceHistory;
69 public EntryReference<Entry<?>, Entry<?>> renamingReference;
66 public EntryReference<Entry<?>, Entry<?>> cursorReference; 70 public EntryReference<Entry<?>, Entry<?>> cursorReference;
67 private boolean shouldNavigateOnClick; 71 private boolean shouldNavigateOnClick;
72 private ConnectionState connectionState;
73 private boolean isJarOpen;
68 74
69 public FileDialog jarFileChooser; 75 public FileDialog jarFileChooser;
70 public FileDialog tinyMappingsFileChooser; 76 public FileDialog tinyMappingsFileChooser;
@@ -76,6 +82,7 @@ public class Gui {
76 private JFrame frame; 82 private JFrame frame;
77 public Config.LookAndFeel editorFeel; 83 public Config.LookAndFeel editorFeel;
78 public PanelEditor editor; 84 public PanelEditor editor;
85 public JScrollPane sourceScroller;
79 private JPanel classesPanel; 86 private JPanel classesPanel;
80 private JSplitPane splitClasses; 87 private JSplitPane splitClasses;
81 private PanelIdentifier infoPanel; 88 private PanelIdentifier infoPanel;
@@ -87,6 +94,20 @@ public class Gui {
87 private JList<Token> tokens; 94 private JList<Token> tokens;
88 private JTabbedPane tabs; 95 private JTabbedPane tabs;
89 96
97 private JSplitPane splitRight;
98 private JSplitPane logSplit;
99 private CollapsibleTabbedPane logTabs;
100 private JList<String> users;
101 private DefaultListModel<String> userModel;
102 private JScrollPane messageScrollPane;
103 private JList<Message> messages;
104 private DefaultListModel<Message> messageModel;
105 private JTextField chatBox;
106
107 private JPanel statusBar;
108 private JLabel connectionStatusLabel;
109 private JLabel statusLabel;
110
90 public JTextField renameTextField; 111 public JTextField renameTextField;
91 public JTextArea javadocTextArea; 112 public JTextArea javadocTextArea;
92 113
@@ -150,7 +171,7 @@ public class Gui {
150 // init editor 171 // init editor
151 selectionHighlightPainter = new SelectionHighlightPainter(); 172 selectionHighlightPainter = new SelectionHighlightPainter();
152 this.editor = new PanelEditor(this); 173 this.editor = new PanelEditor(this);
153 JScrollPane sourceScroller = new JScrollPane(this.editor); 174 this.sourceScroller = new JScrollPane(this.editor);
154 this.editor.setContentType("text/enigma-sources"); 175 this.editor.setContentType("text/enigma-sources");
155 this.editor.setBackground(new Color(Config.getInstance().editorBackground)); 176 this.editor.setBackground(new Color(Config.getInstance().editorBackground));
156 DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit(); 177 DefaultSyntaxKit kit = (DefaultSyntaxKit) this.editor.getEditorKit();
@@ -283,7 +304,34 @@ public class Gui {
283 tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel); 304 tabs.addTab(I18n.translate("info_panel.tree.inheritance"), inheritancePanel);
284 tabs.addTab(I18n.translate("info_panel.tree.implementations"), implementationsPanel); 305 tabs.addTab(I18n.translate("info_panel.tree.implementations"), implementationsPanel);
285 tabs.addTab(I18n.translate("info_panel.tree.calls"), callPanel); 306 tabs.addTab(I18n.translate("info_panel.tree.calls"), callPanel);
286 JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, tabs); 307 logTabs = new CollapsibleTabbedPane(JTabbedPane.BOTTOM);
308 userModel = new DefaultListModel<>();
309 users = new JList<>(userModel);
310 messageModel = new DefaultListModel<>();
311 messages = new JList<>(messageModel);
312 messages.setCellRenderer(new MessageListCellRenderer());
313 JPanel messagePanel = new JPanel(new BorderLayout());
314 messageScrollPane = new JScrollPane(this.messages);
315 messagePanel.add(messageScrollPane, BorderLayout.CENTER);
316 JPanel chatPanel = new JPanel(new BorderLayout());
317 chatBox = new JTextField();
318 AbstractAction sendListener = new AbstractAction("Send") {
319 @Override
320 public void actionPerformed(ActionEvent e) {
321 sendMessage();
322 }
323 };
324 chatBox.addActionListener(sendListener);
325 JButton chatSendButton = new JButton(sendListener);
326 chatPanel.add(chatBox, BorderLayout.CENTER);
327 chatPanel.add(chatSendButton, BorderLayout.EAST);
328 messagePanel.add(chatPanel, BorderLayout.SOUTH);
329 logTabs.addTab(I18n.translate("log_panel.users"), new JScrollPane(this.users));
330 logTabs.addTab(I18n.translate("log_panel.messages"), messagePanel);
331 logSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, tabs, logTabs);
332 logSplit.setResizeWeight(0.5);
333 logSplit.resetToPreferredSizes();
334 splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, this.logSplit);
287 splitRight.setResizeWeight(1); // let the left side take all the slack 335 splitRight.setResizeWeight(1); // let the left side take all the slack
288 splitRight.resetToPreferredSizes(); 336 splitRight.resetToPreferredSizes();
289 JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.classesPanel, splitRight); 337 JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, this.classesPanel, splitRight);
@@ -294,7 +342,17 @@ public class Gui {
294 this.menuBar = new MenuBar(this); 342 this.menuBar = new MenuBar(this);
295 this.frame.setJMenuBar(this.menuBar); 343 this.frame.setJMenuBar(this.menuBar);
296 344
345 // init status bar
346 statusBar = new JPanel(new BorderLayout());
347 statusBar.setBorder(BorderFactory.createLoweredBevelBorder());
348 connectionStatusLabel = new JLabel();
349 statusLabel = new JLabel();
350 statusBar.add(statusLabel, BorderLayout.CENTER);
351 statusBar.add(connectionStatusLabel, BorderLayout.EAST);
352 pane.add(statusBar, BorderLayout.SOUTH);
353
297 // init state 354 // init state
355 setConnectionState(ConnectionState.NOT_CONNECTED);
298 onCloseJar(); 356 onCloseJar();
299 357
300 this.frame.addWindowListener(new WindowAdapter() { 358 this.frame.addWindowListener(new WindowAdapter() {
@@ -334,18 +392,14 @@ public class Gui {
334 setEditorText(null); 392 setEditorText(null);
335 393
336 // update menu 394 // update menu
337 this.menuBar.closeJarMenu.setEnabled(true); 395 isJarOpen = true;
338 this.menuBar.openMappingsMenus.forEach(item -> item.setEnabled(true));
339 this.menuBar.saveMappingsMenu.setEnabled(false);
340 this.menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(true));
341 this.menuBar.closeMappingsMenu.setEnabled(true);
342 this.menuBar.exportSourceMenu.setEnabled(true);
343 this.menuBar.exportJarMenu.setEnabled(true);
344 396
397 updateUiState();
345 redraw(); 398 redraw();
346 } 399 }
347 400
348 public void onCloseJar() { 401 public void onCloseJar() {
402
349 // update gui 403 // update gui
350 this.frame.setTitle(Constants.NAME); 404 this.frame.setTitle(Constants.NAME);
351 setObfClasses(null); 405 setObfClasses(null);
@@ -354,14 +408,10 @@ public class Gui {
354 this.classesPanel.removeAll(); 408 this.classesPanel.removeAll();
355 409
356 // update menu 410 // update menu
357 this.menuBar.closeJarMenu.setEnabled(false); 411 isJarOpen = false;
358 this.menuBar.openMappingsMenus.forEach(item -> item.setEnabled(false)); 412 setMappingsFile(null);
359 this.menuBar.saveMappingsMenu.setEnabled(false);
360 this.menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(false));
361 this.menuBar.closeMappingsMenu.setEnabled(false);
362 this.menuBar.exportSourceMenu.setEnabled(false);
363 this.menuBar.exportJarMenu.setEnabled(false);
364 413
414 updateUiState();
365 redraw(); 415 redraw();
366 } 416 }
367 417
@@ -375,7 +425,7 @@ public class Gui {
375 425
376 public void setMappingsFile(Path path) { 426 public void setMappingsFile(Path path) {
377 this.enigmaMappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null); 427 this.enigmaMappingsFileChooser.setSelectedFile(path != null ? path.toFile() : null);
378 this.menuBar.saveMappingsMenu.setEnabled(path != null); 428 updateUiState();
379 } 429 }
380 430
381 public void setEditorText(String source) { 431 public void setEditorText(String source) {
@@ -561,10 +611,12 @@ public class Gui {
561 boolean isConstructorEntry = isToken && referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor(); 611 boolean isConstructorEntry = isToken && referenceEntry instanceof MethodEntry && ((MethodEntry) referenceEntry).isConstructor();
562 boolean isRenamable = isToken && this.controller.project.isRenamable(cursorReference); 612 boolean isRenamable = isToken && this.controller.project.isRenamable(cursorReference);
563 613
564 if (isToken) { 614 if (!isRenaming()) {
565 showCursorReference(cursorReference); 615 if (isToken) {
566 } else { 616 showCursorReference(cursorReference);
567 infoPanel.clearReference(); 617 } else {
618 infoPanel.clearReference();
619 }
568 } 620 }
569 621
570 this.popupMenu.renameMenu.setEnabled(isRenamable); 622 this.popupMenu.renameMenu.setEnabled(isRenamable);
@@ -586,6 +638,11 @@ public class Gui {
586 } 638 }
587 639
588 public void startDocChange() { 640 public void startDocChange() {
641 EntryReference<Entry<?>, Entry<?>> curReference = cursorReference;
642 if (isRenaming()) {
643 finishRename(false);
644 }
645 renamingReference = curReference;
589 646
590 // init the text box 647 // init the text box
591 javadocTextArea = new JTextArea(10, 40); 648 javadocTextArea = new JTextArea(10, 40);
@@ -603,7 +660,8 @@ public class Gui {
603 String newName = javadocTextArea.getText(); 660 String newName = javadocTextArea.getText();
604 if (saveName) { 661 if (saveName) {
605 try { 662 try {
606 this.controller.changeDocs(cursorReference, newName); 663 this.controller.changeDocs(renamingReference, newName);
664 this.controller.sendPacket(new ChangeDocsC2SPacket(renamingReference.getNameableEntry(), newName));
607 } catch (IllegalNameException ex) { 665 } catch (IllegalNameException ex) {
608 javadocTextArea.setBorder(BorderFactory.createLineBorder(Color.red, 1)); 666 javadocTextArea.setBorder(BorderFactory.createLineBorder(Color.red, 1));
609 javadocTextArea.setToolTipText(ex.getReason()); 667 javadocTextArea.setToolTipText(ex.getReason());
@@ -665,14 +723,19 @@ public class Gui {
665 else 723 else
666 renameTextField.selectAll(); 724 renameTextField.selectAll();
667 725
726 renamingReference = cursorReference;
727
668 redraw(); 728 redraw();
669 } 729 }
670 730
671 private void finishRename(boolean saveName) { 731 private void finishRename(boolean saveName) {
672 String newName = renameTextField.getText(); 732 String newName = renameTextField.getText();
733
673 if (saveName && newName != null && !newName.isEmpty()) { 734 if (saveName && newName != null && !newName.isEmpty()) {
674 try { 735 try {
675 this.controller.rename(cursorReference, newName, true); 736 this.controller.rename(renamingReference, newName, true);
737 this.controller.sendPacket(new RenameC2SPacket(renamingReference.getNameableEntry(), newName, true));
738 renameTextField = null;
676 } catch (IllegalNameException ex) { 739 } catch (IllegalNameException ex) {
677 renameTextField.setBorder(BorderFactory.createLineBorder(Color.red, 1)); 740 renameTextField.setBorder(BorderFactory.createLineBorder(Color.red, 1));
678 renameTextField.setToolTipText(ex.getReason()); 741 renameTextField.setToolTipText(ex.getReason());
@@ -681,18 +744,20 @@ public class Gui {
681 return; 744 return;
682 } 745 }
683 746
684 // abort the rename
685 JPanel panel = (JPanel) infoPanel.getComponent(0);
686 panel.remove(panel.getComponentCount() - 1);
687 panel.add(Utils.unboldLabel(new JLabel(cursorReference.getNameableName(), JLabel.LEFT)));
688
689 renameTextField = null; 747 renameTextField = null;
690 748
749 // abort the rename
750 showCursorReference(cursorReference);
751
691 this.editor.grabFocus(); 752 this.editor.grabFocus();
692 753
693 redraw(); 754 redraw();
694 } 755 }
695 756
757 private boolean isRenaming() {
758 return renameTextField != null;
759 }
760
696 public void showInheritance() { 761 public void showInheritance() {
697 762
698 if (cursorReference == null) { 763 if (cursorReference == null) {
@@ -783,8 +848,10 @@ public class Gui {
783 848
784 if (!Objects.equals(obfEntry, deobfEntry)) { 849 if (!Objects.equals(obfEntry, deobfEntry)) {
785 this.controller.removeMapping(cursorReference); 850 this.controller.removeMapping(cursorReference);
851 this.controller.sendPacket(new RemoveMappingC2SPacket(cursorReference.getNameableEntry()));
786 } else { 852 } else {
787 this.controller.markAsDeobfuscated(cursorReference); 853 this.controller.markAsDeobfuscated(cursorReference);
854 this.controller.sendPacket(new MarkDeobfuscatedC2SPacket(cursorReference.getNameableEntry()));
788 } 855 }
789 } 856 }
790 857
@@ -850,6 +917,7 @@ public class Gui {
850 ClassEntry prevDataChild = (ClassEntry) childNode.getUserObject(); 917 ClassEntry prevDataChild = (ClassEntry) childNode.getUserObject();
851 ClassEntry dataChild = new ClassEntry(data + "/" + prevDataChild.getSimpleName()); 918 ClassEntry dataChild = new ClassEntry(data + "/" + prevDataChild.getSimpleName());
852 this.controller.rename(new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false); 919 this.controller.rename(new EntryReference<>(prevDataChild, prevDataChild.getFullName()), dataChild.getFullName(), false);
920 this.controller.sendPacket(new RenameC2SPacket(prevDataChild, dataChild.getFullName(), false));
853 childNode.setUserObject(dataChild); 921 childNode.setUserObject(dataChild);
854 } 922 }
855 node.setUserObject(data); 923 node.setUserObject(data);
@@ -857,8 +925,10 @@ public class Gui {
857 this.deobfPanel.deobfClasses.reload(); 925 this.deobfPanel.deobfClasses.reload();
858 } 926 }
859 // class rename 927 // class rename
860 else if (data instanceof ClassEntry) 928 else if (data instanceof ClassEntry) {
861 this.controller.rename(new EntryReference<>((ClassEntry) prevData, ((ClassEntry) prevData).getFullName()), ((ClassEntry) data).getFullName(), false); 929 this.controller.rename(new EntryReference<>((ClassEntry) prevData, ((ClassEntry) prevData).getFullName()), ((ClassEntry) data).getFullName(), false);
930 this.controller.sendPacket(new RenameC2SPacket((ClassEntry) prevData, ((ClassEntry) data).getFullName(), false));
931 }
862 } 932 }
863 933
864 public void moveClassTree(EntryReference<Entry<?>, Entry<?>> obfReference, String newName) { 934 public void moveClassTree(EntryReference<Entry<?>, Entry<?>> obfReference, String newName) {
@@ -920,4 +990,69 @@ public class Gui {
920 return searchDialog; 990 return searchDialog;
921 } 991 }
922 992
993
994 public MenuBar getMenuBar() {
995 return menuBar;
996 }
997
998 public void addMessage(Message message) {
999 JScrollBar verticalScrollBar = messageScrollPane.getVerticalScrollBar();
1000 boolean isAtBottom = verticalScrollBar.getValue() >= verticalScrollBar.getMaximum() - verticalScrollBar.getModel().getExtent();
1001 messageModel.addElement(message);
1002 if (isAtBottom) {
1003 SwingUtilities.invokeLater(() -> verticalScrollBar.setValue(verticalScrollBar.getMaximum() - verticalScrollBar.getModel().getExtent()));
1004 }
1005 statusLabel.setText(message.translate());
1006 }
1007
1008 public void setUserList(List<String> users) {
1009 userModel.clear();
1010 users.forEach(userModel::addElement);
1011 connectionStatusLabel.setText(String.format(I18n.translate("status.connected_user_count"), users.size()));
1012 }
1013
1014 private void sendMessage() {
1015 String text = chatBox.getText().trim();
1016 if (!text.isEmpty()) {
1017 getController().sendPacket(new MessageC2SPacket(text));
1018 }
1019 chatBox.setText("");
1020 }
1021
1022 /**
1023 * Updates the state of the UI elements (button text, enabled state, ...) to reflect the current program state.
1024 * This is a central place to update the UI state to prevent multiple code paths from changing the same state,
1025 * causing inconsistencies.
1026 */
1027 public void updateUiState() {
1028 menuBar.connectToServerMenu.setEnabled(isJarOpen && connectionState != ConnectionState.HOSTING);
1029 menuBar.connectToServerMenu.setText(I18n.translate(connectionState != ConnectionState.CONNECTED ? "menu.collab.connect" : "menu.collab.disconnect"));
1030 menuBar.startServerMenu.setEnabled(isJarOpen && connectionState != ConnectionState.CONNECTED);
1031 menuBar.startServerMenu.setText(I18n.translate(connectionState != ConnectionState.HOSTING ? "menu.collab.server.start" : "menu.collab.server.stop"));
1032
1033 menuBar.closeJarMenu.setEnabled(isJarOpen);
1034 menuBar.openMappingsMenus.forEach(item -> item.setEnabled(isJarOpen));
1035 menuBar.saveMappingsMenu.setEnabled(isJarOpen && enigmaMappingsFileChooser.getSelectedFile() != null && connectionState != ConnectionState.CONNECTED);
1036 menuBar.saveMappingsMenus.forEach(item -> item.setEnabled(isJarOpen));
1037 menuBar.closeMappingsMenu.setEnabled(isJarOpen);
1038 menuBar.exportSourceMenu.setEnabled(isJarOpen);
1039 menuBar.exportJarMenu.setEnabled(isJarOpen);
1040
1041 connectionStatusLabel.setText(I18n.translate(connectionState == ConnectionState.NOT_CONNECTED ? "status.disconnected" : "status.connected"));
1042
1043 if (connectionState == ConnectionState.NOT_CONNECTED) {
1044 logSplit.setLeftComponent(null);
1045 splitRight.setRightComponent(tabs);
1046 } else {
1047 splitRight.setRightComponent(logSplit);
1048 logSplit.setLeftComponent(tabs);
1049 }
1050 }
1051
1052 public void setConnectionState(ConnectionState state) {
1053 connectionState = state;
1054 statusLabel.setText(I18n.translate("status.ready"));
1055 updateUiState();
1056 }
1057
923} 1058}
diff --git a/src/main/java/cuchaz/enigma/gui/GuiController.java b/src/main/java/cuchaz/enigma/gui/GuiController.java
index 742d6b8d..cccc9e8a 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;
24import cuchaz.enigma.gui.stats.StatsGenerator; 24import cuchaz.enigma.gui.stats.StatsGenerator;
25import cuchaz.enigma.gui.stats.StatsMember; 25import cuchaz.enigma.gui.stats.StatsMember;
26import cuchaz.enigma.gui.util.History; 26import cuchaz.enigma.gui.util.History;
27import cuchaz.enigma.network.EnigmaClient;
28import cuchaz.enigma.network.EnigmaServer;
29import cuchaz.enigma.network.IntegratedEnigmaServer;
30import cuchaz.enigma.network.ServerPacketHandler;
31import cuchaz.enigma.network.packet.LoginC2SPacket;
32import cuchaz.enigma.network.packet.Packet;
27import cuchaz.enigma.source.*; 33import cuchaz.enigma.source.*;
28import cuchaz.enigma.throwables.MappingParseException; 34import cuchaz.enigma.throwables.MappingParseException;
29import cuchaz.enigma.translation.Translator; 35import cuchaz.enigma.translation.Translator;
30import cuchaz.enigma.translation.mapping.*; 36import cuchaz.enigma.translation.mapping.*;
31import cuchaz.enigma.translation.mapping.serde.MappingFormat; 37import cuchaz.enigma.translation.mapping.serde.MappingFormat;
32import cuchaz.enigma.translation.mapping.tree.EntryTree; 38import cuchaz.enigma.translation.mapping.tree.EntryTree;
39import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
33import cuchaz.enigma.translation.representation.entry.ClassEntry; 40import cuchaz.enigma.translation.representation.entry.ClassEntry;
34import cuchaz.enigma.translation.representation.entry.Entry; 41import cuchaz.enigma.translation.representation.entry.Entry;
35import cuchaz.enigma.translation.representation.entry.FieldEntry; 42import cuchaz.enigma.translation.representation.entry.FieldEntry;
36import cuchaz.enigma.translation.representation.entry.MethodEntry; 43import cuchaz.enigma.translation.representation.entry.MethodEntry;
37import cuchaz.enigma.utils.I18n; 44import cuchaz.enigma.utils.I18n;
45import cuchaz.enigma.utils.Message;
38import cuchaz.enigma.utils.ReadableToken; 46import cuchaz.enigma.utils.ReadableToken;
39import cuchaz.enigma.utils.Utils; 47import cuchaz.enigma.utils.Utils;
40import org.objectweb.asm.tree.ClassNode; 48import org.objectweb.asm.tree.ClassNode;
41 49
42import javax.annotation.Nullable; 50import javax.annotation.Nullable;
43import javax.swing.JOptionPane; 51import javax.swing.JOptionPane;
44import java.awt.Desktop; 52import javax.swing.SwingUtilities;
53import java.awt.*;
45import java.awt.event.ItemEvent; 54import java.awt.event.ItemEvent;
46import java.io.*; 55import java.io.*;
47import java.nio.file.Path; 56import java.nio.file.Path;
@@ -76,6 +85,9 @@ public class GuiController {
76 private DecompiledClassSource currentSource; 85 private DecompiledClassSource currentSource;
77 private Source uncommentedSource; 86 private Source uncommentedSource;
78 87
88 private EnigmaClient client;
89 private EnigmaServer server;
90
79 public GuiController(Gui gui, EnigmaProfile profile) { 91 public GuiController(Gui gui, EnigmaProfile profile) {
80 this.gui = gui; 92 this.gui = gui;
81 this.enigma = Enigma.builder() 93 this.enigma = Enigma.builder()
@@ -143,6 +155,14 @@ public class GuiController {
143 }); 155 });
144 } 156 }
145 157
158 public void openMappings(EntryTree<EntryMapping> mappings) {
159 if (project == null) return;
160
161 project.setMappings(mappings);
162 refreshClasses();
163 refreshCurrentClass();
164 }
165
146 public CompletableFuture<Void> saveMappings(Path path) { 166 public CompletableFuture<Void> saveMappings(Path path) {
147 return saveMappings(path, loadedMappingFormat); 167 return saveMappings(path, loadedMappingFormat);
148 } 168 }
@@ -388,11 +408,39 @@ public class GuiController {
388 408
389 private void refreshCurrentClass(EntryReference<Entry<?>, Entry<?>> reference, RefreshMode mode) { 409 private void refreshCurrentClass(EntryReference<Entry<?>, Entry<?>> reference, RefreshMode mode) {
390 if (currentSource != null) { 410 if (currentSource != null) {
391 loadClass(currentSource.getEntry(), () -> { 411 if (reference == null) {
392 if (reference != null) { 412 int obfSelectionStart = currentSource.getObfuscatedOffset(gui.editor.getSelectionStart());
393 showReference(reference); 413 int obfSelectionEnd = currentSource.getObfuscatedOffset(gui.editor.getSelectionEnd());
414
415 Rectangle viewportBounds = gui.sourceScroller.getViewport().getViewRect();
416 // 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
417 int anchorModelPos = gui.editor.getSelectionStart();
418 Rectangle anchorViewPos = Utils.safeModelToView(gui.editor, anchorModelPos);
419 if (anchorViewPos.y < viewportBounds.y || anchorViewPos.y >= viewportBounds.y + viewportBounds.height) {
420 anchorModelPos = gui.editor.viewToModel(new Point(0, viewportBounds.y));
421 anchorViewPos = Utils.safeModelToView(gui.editor, anchorModelPos);
394 } 422 }
395 }, mode); 423 int obfAnchorPos = currentSource.getObfuscatedOffset(anchorModelPos);
424 Rectangle anchorViewPos_f = anchorViewPos;
425 int scrollX = gui.sourceScroller.getHorizontalScrollBar().getValue();
426
427 loadClass(currentSource.getEntry(), () -> SwingUtilities.invokeLater(() -> {
428 int newAnchorModelPos = currentSource.getDeobfuscatedOffset(obfAnchorPos);
429 Rectangle newAnchorViewPos = Utils.safeModelToView(gui.editor, newAnchorModelPos);
430 int newScrollY = newAnchorViewPos.y - (anchorViewPos_f.y - viewportBounds.y);
431
432 gui.editor.select(currentSource.getDeobfuscatedOffset(obfSelectionStart), currentSource.getDeobfuscatedOffset(obfSelectionEnd));
433 // Changing the selection scrolls to the caret position inside a SwingUtilities.invokeLater call, so
434 // we need to wrap our change to the scroll position inside another invokeLater so it happens after
435 // the caret's own scrolling.
436 SwingUtilities.invokeLater(() -> {
437 gui.sourceScroller.getHorizontalScrollBar().setValue(Math.min(scrollX, gui.sourceScroller.getHorizontalScrollBar().getMaximum()));
438 gui.sourceScroller.getVerticalScrollBar().setValue(Math.min(newScrollY, gui.sourceScroller.getVerticalScrollBar().getMaximum()));
439 });
440 }), mode);
441 } else {
442 loadClass(currentSource.getEntry(), () -> showReference(reference), mode);
443 }
396 } 444 }
397 } 445 }
398 446
@@ -528,43 +576,59 @@ public class GuiController {
528 } 576 }
529 577
530 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree) { 578 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree) {
579 rename(reference, newName, refreshClassTree, true);
580 }
581
582 public void rename(EntryReference<Entry<?>, Entry<?>> reference, String newName, boolean refreshClassTree, boolean jumpToReference) {
531 Entry<?> entry = reference.getNameableEntry(); 583 Entry<?> entry = reference.getNameableEntry();
532 project.getMapper().mapFromObf(entry, new EntryMapping(newName)); 584 project.getMapper().mapFromObf(entry, new EntryMapping(newName));
533 585
534 if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) 586 if (refreshClassTree && reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass())
535 this.gui.moveClassTree(reference, newName); 587 this.gui.moveClassTree(reference, newName);
536 588
537 refreshCurrentClass(reference); 589 refreshCurrentClass(jumpToReference ? reference : null);
538 } 590 }
539 591
540 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference) { 592 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference) {
593 removeMapping(reference, true);
594 }
595
596 public void removeMapping(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference) {
541 project.getMapper().removeByObf(reference.getNameableEntry()); 597 project.getMapper().removeByObf(reference.getNameableEntry());
542 598
543 if (reference.entry instanceof ClassEntry) 599 if (reference.entry instanceof ClassEntry)
544 this.gui.moveClassTree(reference, false, true); 600 this.gui.moveClassTree(reference, false, true);
545 refreshCurrentClass(reference); 601 refreshCurrentClass(jumpToReference ? reference : null);
546 } 602 }
547 603
548 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs) { 604 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs) {
549 changeDoc(reference.entry, updatedDocs); 605 changeDocs(reference, updatedDocs, true);
606 }
550 607
551 refreshCurrentClass(reference, RefreshMode.JAVADOCS); 608 public void changeDocs(EntryReference<Entry<?>, Entry<?>> reference, String updatedDocs, boolean jumpToReference) {
609 changeDoc(reference.entry, Utils.isBlank(updatedDocs) ? null : updatedDocs);
610
611 refreshCurrentClass(jumpToReference ? reference : null, RefreshMode.JAVADOCS);
552 } 612 }
553 613
554 public void changeDoc(Entry<?> obfEntry, String newDoc) { 614 private void changeDoc(Entry<?> obfEntry, String newDoc) {
555 EntryRemapper mapper = project.getMapper(); 615 EntryRemapper mapper = project.getMapper();
556 if (mapper.getDeobfMapping(obfEntry) == null) { 616 if (mapper.getDeobfMapping(obfEntry) == null) {
557 markAsDeobfuscated(obfEntry,false); // NPE 617 markAsDeobfuscated(obfEntry, false); // NPE
558 } 618 }
559 mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false); 619 mapper.mapFromObf(obfEntry, mapper.getDeobfMapping(obfEntry).withDocs(newDoc), false);
560 } 620 }
561 621
562 public void markAsDeobfuscated(Entry<?> obfEntry, boolean renaming) { 622 private void markAsDeobfuscated(Entry<?> obfEntry, boolean renaming) {
563 EntryRemapper mapper = project.getMapper(); 623 EntryRemapper mapper = project.getMapper();
564 mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming); 624 mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()), renaming);
565 } 625 }
566 626
567 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference) { 627 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference) {
628 markAsDeobfuscated(reference, true);
629 }
630
631 public void markAsDeobfuscated(EntryReference<Entry<?>, Entry<?>> reference, boolean jumpToReference) {
568 EntryRemapper mapper = project.getMapper(); 632 EntryRemapper mapper = project.getMapper();
569 Entry<?> entry = reference.getNameableEntry(); 633 Entry<?> entry = reference.getNameableEntry();
570 mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName())); 634 mapper.mapFromObf(entry, new EntryMapping(mapper.deobfuscate(entry).getName()));
@@ -572,7 +636,7 @@ public class GuiController {
572 if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass()) 636 if (reference.entry instanceof ClassEntry && !((ClassEntry) reference.entry).isInnerClass())
573 this.gui.moveClassTree(reference, true, false); 637 this.gui.moveClassTree(reference, true, false);
574 638
575 refreshCurrentClass(reference); 639 refreshCurrentClass(jumpToReference ? reference : null);
576 } 640 }
577 641
578 public void openStats(Set<StatsMember> includedMembers) { 642 public void openStats(Set<StatsMember> includedMembers) {
@@ -602,4 +666,64 @@ public class GuiController {
602 decompiler = createDecompiler(); 666 decompiler = createDecompiler();
603 refreshCurrentClass(null, RefreshMode.FULL); 667 refreshCurrentClass(null, RefreshMode.FULL);
604 } 668 }
669
670 public EnigmaClient getClient() {
671 return client;
672 }
673
674 public EnigmaServer getServer() {
675 return server;
676 }
677
678 public void createClient(String username, String ip, int port, char[] password) throws IOException {
679 client = new EnigmaClient(this, ip, port);
680 client.connect();
681 client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, username));
682 gui.setConnectionState(ConnectionState.CONNECTED);
683 }
684
685 public void createServer(int port, char[] password) throws IOException {
686 server = new IntegratedEnigmaServer(project.getJarChecksum(), password, EntryRemapper.mapped(project.getJarIndex(), new HashEntryTree<>(project.getMapper().getObfToDeobf())), port);
687 server.start();
688 client = new EnigmaClient(this, "127.0.0.1", port);
689 client.connect();
690 client.sendPacket(new LoginC2SPacket(project.getJarChecksum(), password, EnigmaServer.OWNER_USERNAME));
691 gui.setConnectionState(ConnectionState.HOSTING);
692 }
693
694 public synchronized void disconnectIfConnected(String reason) {
695 if (client == null && server == null) {
696 return;
697 }
698
699 if (client != null) {
700 client.disconnect();
701 }
702 if (server != null) {
703 server.stop();
704 }
705 client = null;
706 server = null;
707 SwingUtilities.invokeLater(() -> {
708 if (reason != null) {
709 JOptionPane.showMessageDialog(gui.getFrame(), I18n.translate(reason), I18n.translate("disconnect.disconnected"), JOptionPane.INFORMATION_MESSAGE);
710 }
711 gui.setConnectionState(ConnectionState.NOT_CONNECTED);
712 });
713 }
714
715 public void sendPacket(Packet<ServerPacketHandler> packet) {
716 if (client != null) {
717 client.sendPacket(packet);
718 }
719 }
720
721 public void addMessage(Message message) {
722 gui.addMessage(message);
723 }
724
725 public void updateUserList(List<String> users) {
726 gui.setUserList(users);
727 }
728
605} 729}
diff --git a/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java b/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java
new file mode 100644
index 00000000..c9e38cbf
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/gui/MessageListCellRenderer.java
@@ -0,0 +1,24 @@
1package cuchaz.enigma.gui;
2
3import java.awt.Component;
4
5import javax.swing.DefaultListCellRenderer;
6import javax.swing.JList;
7
8import cuchaz.enigma.utils.Message;
9
10// For now, just render the translated text.
11// TODO: Icons or something later?
12public class MessageListCellRenderer extends DefaultListCellRenderer {
13
14 @Override
15 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
16 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
17 Message message = (Message) value;
18 if (message != null) {
19 setText(message.translate());
20 }
21 return this;
22 }
23
24}
diff --git a/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java b/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java
new file mode 100644
index 00000000..c5f505cf
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/gui/dialog/ConnectToServerDialog.java
@@ -0,0 +1,82 @@
1package cuchaz.enigma.gui.dialog;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.utils.I18n;
5
6import javax.swing.*;
7import java.awt.Frame;
8
9public class ConnectToServerDialog {
10
11 public static Result show(Frame parentComponent) {
12 JTextField usernameField = new JTextField(System.getProperty("user.name"), 20);
13 JPanel usernameRow = new JPanel();
14 usernameRow.add(new JLabel(I18n.translate("prompt.connect.username")));
15 usernameRow.add(usernameField);
16 JTextField ipField = new JTextField(20);
17 JPanel ipRow = new JPanel();
18 ipRow.add(new JLabel(I18n.translate("prompt.connect.ip")));
19 ipRow.add(ipField);
20 JTextField portField = new JTextField(String.valueOf(EnigmaServer.DEFAULT_PORT), 10);
21 JPanel portRow = new JPanel();
22 portRow.add(new JLabel(I18n.translate("prompt.port")));
23 portRow.add(portField);
24 JPasswordField passwordField = new JPasswordField(20);
25 JPanel passwordRow = new JPanel();
26 passwordRow.add(new JLabel(I18n.translate("prompt.password")));
27 passwordRow.add(passwordField);
28
29 int response = JOptionPane.showConfirmDialog(parentComponent, new Object[]{usernameRow, ipRow, portRow, passwordRow}, I18n.translate("prompt.connect.title"), JOptionPane.OK_CANCEL_OPTION);
30 if (response != JOptionPane.OK_OPTION) {
31 return null;
32 }
33
34 String username = usernameField.getText();
35 String ip = ipField.getText();
36 int port;
37 try {
38 port = Integer.parseInt(portField.getText());
39 } catch (NumberFormatException e) {
40 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.nan"), I18n.translate("prompt.connect.title"), JOptionPane.ERROR_MESSAGE);
41 return null;
42 }
43 if (port < 0 || port >= 65536) {
44 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.invalid"), I18n.translate("prompt.connect.title"), JOptionPane.ERROR_MESSAGE);
45 return null;
46 }
47 char[] password = passwordField.getPassword();
48
49 return new Result(username, ip, port, password);
50 }
51
52 public static class Result {
53 private final String username;
54 private final String ip;
55 private final int port;
56 private final char[] password;
57
58 public Result(String username, String ip, int port, char[] password) {
59 this.username = username;
60 this.ip = ip;
61 this.port = port;
62 this.password = password;
63 }
64
65 public String getUsername() {
66 return username;
67 }
68
69 public String getIp() {
70 return ip;
71 }
72
73 public int getPort() {
74 return port;
75 }
76
77 public char[] getPassword() {
78 return password;
79 }
80 }
81
82}
diff --git a/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java b/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java
new file mode 100644
index 00000000..eea1dff1
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/gui/dialog/CreateServerDialog.java
@@ -0,0 +1,65 @@
1package cuchaz.enigma.gui.dialog;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.utils.I18n;
5
6import javax.swing.*;
7import java.awt.*;
8
9public class CreateServerDialog {
10
11 public static Result show(Frame parentComponent) {
12 JTextField portField = new JTextField(String.valueOf(EnigmaServer.DEFAULT_PORT), 10);
13 JPanel portRow = new JPanel();
14 portRow.add(new JLabel(I18n.translate("prompt.port")));
15 portRow.add(portField);
16 JPasswordField passwordField = new JPasswordField(20);
17 JPanel passwordRow = new JPanel();
18 passwordRow.add(new JLabel(I18n.translate("prompt.password")));
19 passwordRow.add(passwordField);
20
21 int response = JOptionPane.showConfirmDialog(parentComponent, new Object[]{portRow, passwordRow}, I18n.translate("prompt.create_server.title"), JOptionPane.OK_CANCEL_OPTION);
22 if (response != JOptionPane.OK_OPTION) {
23 return null;
24 }
25
26 int port;
27 try {
28 port = Integer.parseInt(portField.getText());
29 } catch (NumberFormatException e) {
30 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.nan"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE);
31 return null;
32 }
33 if (port < 0 || port >= 65536) {
34 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.port.invalid"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE);
35 return null;
36 }
37
38 char[] password = passwordField.getPassword();
39 if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) {
40 JOptionPane.showMessageDialog(parentComponent, I18n.translate("prompt.password.too_long"), I18n.translate("prompt.create_server.title"), JOptionPane.ERROR_MESSAGE);
41 return null;
42 }
43
44 return new Result(port, password);
45 }
46
47 public static class Result {
48 private final int port;
49 private final char[] password;
50
51 public Result(int port, char[] password) {
52 this.port = port;
53 this.password = password;
54 }
55
56 public int getPort() {
57 return port;
58 }
59
60 public char[] getPassword() {
61 return password;
62 }
63 }
64
65}
diff --git a/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java b/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java
new file mode 100644
index 00000000..fb497b11
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/gui/elements/CollapsibleTabbedPane.java
@@ -0,0 +1,40 @@
1package cuchaz.enigma.gui.elements;
2
3import java.awt.event.MouseEvent;
4
5import javax.swing.JTabbedPane;
6
7public class CollapsibleTabbedPane extends JTabbedPane {
8
9 public CollapsibleTabbedPane() {
10 }
11
12 public CollapsibleTabbedPane(int tabPlacement) {
13 super(tabPlacement);
14 }
15
16 public CollapsibleTabbedPane(int tabPlacement, int tabLayoutPolicy) {
17 super(tabPlacement, tabLayoutPolicy);
18 }
19
20 @Override
21 protected void processMouseEvent(MouseEvent e) {
22 int id = e.getID();
23 if (id == MouseEvent.MOUSE_PRESSED) {
24 if (!isEnabled()) return;
25 int tabIndex = getUI().tabForCoordinate(this, e.getX(), e.getY());
26 if (tabIndex >= 0 && isEnabledAt(tabIndex)) {
27 if (tabIndex == getSelectedIndex()) {
28 if (isFocusOwner() && isRequestFocusEnabled()) {
29 requestFocus();
30 } else {
31 setSelectedIndex(-1);
32 }
33 return;
34 }
35 }
36 }
37 super.processMouseEvent(e);
38 }
39
40}
diff --git a/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java b/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java
index 8098178b..f8e4f7e8 100644
--- a/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java
+++ b/src/main/java/cuchaz/enigma/gui/elements/MenuBar.java
@@ -1,5 +1,18 @@
1package cuchaz.enigma.gui.elements; 1package cuchaz.enigma.gui.elements;
2 2
3import cuchaz.enigma.config.Config;
4import cuchaz.enigma.config.Themes;
5import cuchaz.enigma.gui.Gui;
6import cuchaz.enigma.gui.dialog.AboutDialog;
7import cuchaz.enigma.gui.dialog.ConnectToServerDialog;
8import cuchaz.enigma.gui.dialog.CreateServerDialog;
9import cuchaz.enigma.gui.dialog.SearchDialog;
10import cuchaz.enigma.gui.stats.StatsMember;
11import cuchaz.enigma.gui.util.ScaleUtil;
12import cuchaz.enigma.translation.mapping.serde.MappingFormat;
13import cuchaz.enigma.utils.I18n;
14import cuchaz.enigma.utils.Pair;
15
3import java.awt.Container; 16import java.awt.Container;
4import java.awt.Desktop; 17import java.awt.Desktop;
5import java.awt.FlowLayout; 18import java.awt.FlowLayout;
@@ -13,21 +26,11 @@ import java.nio.file.Files;
13import java.nio.file.Path; 26import java.nio.file.Path;
14import java.nio.file.Paths; 27import java.nio.file.Paths;
15import java.util.*; 28import java.util.*;
29import java.util.List;
16import java.util.stream.Collectors; 30import java.util.stream.Collectors;
17import java.util.stream.IntStream; 31import java.util.stream.IntStream;
18
19import javax.swing.*; 32import javax.swing.*;
20 33
21import cuchaz.enigma.config.Config;
22import cuchaz.enigma.config.Themes;
23import cuchaz.enigma.gui.Gui;
24import cuchaz.enigma.gui.dialog.AboutDialog;
25import cuchaz.enigma.gui.dialog.SearchDialog;
26import cuchaz.enigma.gui.stats.StatsMember;
27import cuchaz.enigma.gui.util.ScaleUtil;
28import cuchaz.enigma.translation.mapping.serde.MappingFormat;
29import cuchaz.enigma.utils.I18n;
30import cuchaz.enigma.utils.Pair;
31 34
32import javax.swing.*; 35import javax.swing.*;
33 36
@@ -49,6 +52,8 @@ public class MenuBar extends JMenuBar {
49 public final JMenuItem dropMappingsMenu; 52 public final JMenuItem dropMappingsMenu;
50 public final JMenuItem exportSourceMenu; 53 public final JMenuItem exportSourceMenu;
51 public final JMenuItem exportJarMenu; 54 public final JMenuItem exportJarMenu;
55 public final JMenuItem connectToServerMenu;
56 public final JMenuItem startServerMenu;
52 private final Gui gui; 57 private final Gui gui;
53 58
54 public MenuBar(Gui gui) { 59 public MenuBar(Gui gui) {
@@ -343,6 +348,58 @@ public class MenuBar extends JMenuBar {
343 } 348 }
344 349
345 /* 350 /*
351 * Collab menu
352 */
353 {
354 JMenu menu = new JMenu(I18n.translate("menu.collab"));
355 this.add(menu);
356 {
357 JMenuItem item = new JMenuItem(I18n.translate("menu.collab.connect"));
358 menu.add(item);
359 item.addActionListener(event -> {
360 if (this.gui.getController().getClient() != null) {
361 this.gui.getController().disconnectIfConnected(null);
362 return;
363 }
364 ConnectToServerDialog.Result result = ConnectToServerDialog.show(this.gui.getFrame());
365 if (result == null) {
366 return;
367 }
368 this.gui.getController().disconnectIfConnected(null);
369 try {
370 this.gui.getController().createClient(result.getUsername(), result.getIp(), result.getPort(), result.getPassword());
371 } catch (IOException e) {
372 JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.connect.error"), JOptionPane.ERROR_MESSAGE);
373 this.gui.getController().disconnectIfConnected(null);
374 }
375 Arrays.fill(result.getPassword(), (char)0);
376 });
377 this.connectToServerMenu = item;
378 }
379 {
380 JMenuItem item = new JMenuItem(I18n.translate("menu.collab.server.start"));
381 menu.add(item);
382 item.addActionListener(event -> {
383 if (this.gui.getController().getServer() != null) {
384 this.gui.getController().disconnectIfConnected(null);
385 return;
386 }
387 CreateServerDialog.Result result = CreateServerDialog.show(this.gui.getFrame());
388 if (result == null) {
389 return;
390 }
391 this.gui.getController().disconnectIfConnected(null);
392 try {
393 this.gui.getController().createServer(result.getPort(), result.getPassword());
394 } catch (IOException e) {
395 JOptionPane.showMessageDialog(this.gui.getFrame(), e.toString(), I18n.translate("menu.collab.server.start.error"), JOptionPane.ERROR_MESSAGE);
396 this.gui.getController().disconnectIfConnected(null);
397 }
398 });
399 this.startServerMenu = item;
400 }
401 }
402 /*
346 * Help menu 403 * Help menu
347 */ 404 */
348 { 405 {
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 00000000..2cfe8233
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/DedicatedEnigmaServer.java
@@ -0,0 +1,164 @@
1package cuchaz.enigma.network;
2
3import com.google.common.io.MoreFiles;
4import cuchaz.enigma.*;
5import cuchaz.enigma.throwables.MappingParseException;
6import cuchaz.enigma.translation.mapping.EntryRemapper;
7import cuchaz.enigma.translation.mapping.serde.MappingFormat;
8import cuchaz.enigma.utils.Utils;
9import joptsimple.OptionParser;
10import joptsimple.OptionSet;
11import joptsimple.OptionSpec;
12
13import java.io.IOException;
14import java.io.PrintWriter;
15import java.nio.file.Files;
16import java.nio.file.Path;
17import java.nio.file.Paths;
18import java.util.concurrent.BlockingQueue;
19import java.util.concurrent.Executors;
20import java.util.concurrent.LinkedBlockingDeque;
21import java.util.concurrent.TimeUnit;
22
23public class DedicatedEnigmaServer extends EnigmaServer {
24
25 private final EnigmaProfile profile;
26 private final MappingFormat mappingFormat;
27 private final Path mappingsFile;
28 private final PrintWriter log;
29 private BlockingQueue<Runnable> tasks = new LinkedBlockingDeque<>();
30
31 public DedicatedEnigmaServer(
32 byte[] jarChecksum,
33 char[] password,
34 EnigmaProfile profile,
35 MappingFormat mappingFormat,
36 Path mappingsFile,
37 PrintWriter log,
38 EntryRemapper mappings,
39 int port
40 ) {
41 super(jarChecksum, password, mappings, port);
42 this.profile = profile;
43 this.mappingFormat = mappingFormat;
44 this.mappingsFile = mappingsFile;
45 this.log = log;
46 }
47
48 @Override
49 protected void runOnThread(Runnable task) {
50 tasks.add(task);
51 }
52
53 @Override
54 public void log(String message) {
55 super.log(message);
56 log.println(message);
57 }
58
59 public static void main(String[] args) {
60 OptionParser parser = new OptionParser();
61
62 OptionSpec<Path> jarOpt = parser.accepts("jar", "Jar file to open at startup")
63 .withRequiredArg()
64 .required()
65 .withValuesConvertedBy(Main.PathConverter.INSTANCE);
66
67 OptionSpec<Path> mappingsOpt = parser.accepts("mappings", "Mappings file to open at startup")
68 .withRequiredArg()
69 .required()
70 .withValuesConvertedBy(Main.PathConverter.INSTANCE);
71
72 OptionSpec<Path> profileOpt = parser.accepts("profile", "Profile json to apply at startup")
73 .withRequiredArg()
74 .withValuesConvertedBy(Main.PathConverter.INSTANCE);
75
76 OptionSpec<Integer> portOpt = parser.accepts("port", "Port to run the server on")
77 .withOptionalArg()
78 .ofType(Integer.class)
79 .defaultsTo(EnigmaServer.DEFAULT_PORT);
80
81 OptionSpec<String> passwordOpt = parser.accepts("password", "The password to join the server")
82 .withRequiredArg()
83 .defaultsTo("");
84
85 OptionSpec<Path> logFileOpt = parser.accepts("log", "The log file to write to")
86 .withRequiredArg()
87 .withValuesConvertedBy(Main.PathConverter.INSTANCE)
88 .defaultsTo(Paths.get("log.txt"));
89
90 OptionSet parsedArgs = parser.parse(args);
91 Path jar = parsedArgs.valueOf(jarOpt);
92 Path mappingsFile = parsedArgs.valueOf(mappingsOpt);
93 Path profileFile = parsedArgs.valueOf(profileOpt);
94 int port = parsedArgs.valueOf(portOpt);
95 char[] password = parsedArgs.valueOf(passwordOpt).toCharArray();
96 if (password.length > EnigmaServer.MAX_PASSWORD_LENGTH) {
97 System.err.println("Password too long, must be at most " + EnigmaServer.MAX_PASSWORD_LENGTH + " characters");
98 System.exit(1);
99 }
100 Path logFile = parsedArgs.valueOf(logFileOpt);
101
102 System.out.println("Starting Enigma server");
103 DedicatedEnigmaServer server;
104 try {
105 byte[] checksum = Utils.zipSha1(parsedArgs.valueOf(jarOpt));
106
107 EnigmaProfile profile = EnigmaProfile.read(profileFile);
108 Enigma enigma = Enigma.builder().setProfile(profile).build();
109 System.out.println("Indexing Jar...");
110 EnigmaProject project = enigma.openJar(jar, ProgressListener.none());
111
112 MappingFormat mappingFormat = MappingFormat.ENIGMA_DIRECTORY;
113 EntryRemapper mappings;
114 if (!Files.exists(mappingsFile)) {
115 mappings = EntryRemapper.empty(project.getJarIndex());
116 } else {
117 System.out.println("Reading mappings...");
118 if (Files.isDirectory(mappingsFile)) {
119 mappingFormat = MappingFormat.ENIGMA_DIRECTORY;
120 } else if ("zip".equalsIgnoreCase(MoreFiles.getFileExtension(mappingsFile))) {
121 mappingFormat = MappingFormat.ENIGMA_ZIP;
122 } else {
123 mappingFormat = MappingFormat.ENIGMA_FILE;
124 }
125 mappings = EntryRemapper.mapped(project.getJarIndex(), mappingFormat.read(mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters()));
126 }
127
128 PrintWriter log = new PrintWriter(Files.newBufferedWriter(logFile));
129
130 server = new DedicatedEnigmaServer(checksum, password, profile, mappingFormat, mappingsFile, log, mappings, port);
131 server.start();
132 System.out.println("Server started");
133 } catch (IOException | MappingParseException e) {
134 System.err.println("Error starting server!");
135 e.printStackTrace();
136 System.exit(1);
137 return;
138 }
139
140 // noinspection RedundantSuppression
141 // noinspection Convert2MethodRef - javac 8 bug
142 Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> server.runOnThread(() -> server.saveMappings()), 0, 1, TimeUnit.MINUTES);
143 Runtime.getRuntime().addShutdownHook(new Thread(server::saveMappings));
144
145 while (true) {
146 try {
147 server.tasks.take().run();
148 } catch (InterruptedException e) {
149 break;
150 }
151 }
152 }
153
154 @Override
155 public synchronized void stop() {
156 super.stop();
157 System.exit(0);
158 }
159
160 private void saveMappings() {
161 mappingFormat.write(getMappings().getObfToDeobf(), getMappings().takeMappingDelta(), mappingsFile, ProgressListener.none(), profile.getMappingSaveParameters());
162 log.flush();
163 }
164}
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 00000000..bfa53d73
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/EnigmaClient.java
@@ -0,0 +1,85 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.gui.GuiController;
4import cuchaz.enigma.network.packet.LoginC2SPacket;
5import cuchaz.enigma.network.packet.Packet;
6import cuchaz.enigma.network.packet.PacketRegistry;
7
8import javax.swing.SwingUtilities;
9import java.io.DataInput;
10import java.io.DataInputStream;
11import java.io.DataOutput;
12import java.io.DataOutputStream;
13import java.io.EOFException;
14import java.io.IOException;
15import java.net.Socket;
16import java.net.SocketException;
17
18public class EnigmaClient {
19
20 private final GuiController controller;
21
22 private final String ip;
23 private final int port;
24 private Socket socket;
25 private DataOutput output;
26
27 public EnigmaClient(GuiController controller, String ip, int port) {
28 this.controller = controller;
29 this.ip = ip;
30 this.port = port;
31 }
32
33 public void connect() throws IOException {
34 socket = new Socket(ip, port);
35 output = new DataOutputStream(socket.getOutputStream());
36 Thread thread = new Thread(() -> {
37 try {
38 DataInput input = new DataInputStream(socket.getInputStream());
39 while (true) {
40 int packetId;
41 try {
42 packetId = input.readUnsignedByte();
43 } catch (EOFException | SocketException e) {
44 break;
45 }
46 Packet<GuiController> packet = PacketRegistry.createS2CPacket(packetId);
47 if (packet == null) {
48 throw new IOException("Received invalid packet id " + packetId);
49 }
50 packet.read(input);
51 SwingUtilities.invokeLater(() -> packet.handle(controller));
52 }
53 } catch (IOException e) {
54 controller.disconnectIfConnected(e.toString());
55 return;
56 }
57 controller.disconnectIfConnected("Disconnected");
58 });
59 thread.setName("Client I/O thread");
60 thread.setDaemon(true);
61 thread.start();
62 }
63
64 public synchronized void disconnect() {
65 if (socket != null && !socket.isClosed()) {
66 try {
67 socket.close();
68 } catch (IOException e1) {
69 System.err.println("Failed to close socket");
70 e1.printStackTrace();
71 }
72 }
73 }
74
75
76 public void sendPacket(Packet<ServerPacketHandler> packet) {
77 try {
78 output.writeByte(PacketRegistry.getC2SId(packet));
79 packet.write(output);
80 } catch (IOException e) {
81 controller.disconnectIfConnected(e.toString());
82 }
83 }
84
85}
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 00000000..b0e15a3c
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/EnigmaServer.java
@@ -0,0 +1,292 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.gui.GuiController;
4import cuchaz.enigma.network.packet.KickS2CPacket;
5import cuchaz.enigma.network.packet.MessageS2CPacket;
6import cuchaz.enigma.network.packet.Packet;
7import cuchaz.enigma.network.packet.PacketRegistry;
8import cuchaz.enigma.network.packet.RemoveMappingS2CPacket;
9import cuchaz.enigma.network.packet.RenameS2CPacket;
10import cuchaz.enigma.network.packet.UserListS2CPacket;
11import cuchaz.enigma.translation.mapping.EntryMapping;
12import cuchaz.enigma.translation.mapping.EntryRemapper;
13import cuchaz.enigma.translation.representation.entry.Entry;
14import cuchaz.enigma.utils.Message;
15
16import java.io.DataInput;
17import java.io.DataInputStream;
18import java.io.DataOutput;
19import java.io.DataOutputStream;
20import java.io.EOFException;
21import java.io.IOException;
22import java.net.ServerSocket;
23import java.net.Socket;
24import java.net.SocketException;
25import java.util.ArrayList;
26import java.util.Collections;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.List;
30import java.util.Map;
31import java.util.Set;
32import java.util.concurrent.CopyOnWriteArrayList;
33
34public abstract class EnigmaServer {
35
36 // https://discordapp.com/channels/507304429255393322/566418023372816394/700292322918793347
37 public static final int DEFAULT_PORT = 34712;
38 public static final int PROTOCOL_VERSION = 0;
39 public static final String OWNER_USERNAME = "Owner";
40 public static final int CHECKSUM_SIZE = 20;
41 public static final int MAX_PASSWORD_LENGTH = 255; // length is written as a byte in the login packet
42
43 private final int port;
44 private ServerSocket socket;
45 private List<Socket> clients = new CopyOnWriteArrayList<>();
46 private Map<Socket, String> usernames = new HashMap<>();
47 private Set<Socket> unapprovedClients = new HashSet<>();
48
49 private final byte[] jarChecksum;
50 private final char[] password;
51
52 public static final int DUMMY_SYNC_ID = 0;
53 private final EntryRemapper mappings;
54 private Map<Entry<?>, Integer> syncIds = new HashMap<>();
55 private Map<Integer, Entry<?>> inverseSyncIds = new HashMap<>();
56 private Map<Integer, Set<Socket>> clientsNeedingConfirmation = new HashMap<>();
57 private int nextSyncId = DUMMY_SYNC_ID + 1;
58
59 private static int nextIoId = 0;
60
61 public EnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) {
62 this.jarChecksum = jarChecksum;
63 this.password = password;
64 this.mappings = mappings;
65 this.port = port;
66 }
67
68 public void start() throws IOException {
69 socket = new ServerSocket(port);
70 log("Server started on " + socket.getInetAddress() + ":" + port);
71 Thread thread = new Thread(() -> {
72 try {
73 while (!socket.isClosed()) {
74 acceptClient();
75 }
76 } catch (SocketException e) {
77 System.out.println("Server closed");
78 } catch (IOException e) {
79 e.printStackTrace();
80 }
81 });
82 thread.setName("Server client listener");
83 thread.setDaemon(true);
84 thread.start();
85 }
86
87 private void acceptClient() throws IOException {
88 Socket client = socket.accept();
89 clients.add(client);
90 Thread thread = new Thread(() -> {
91 try {
92 DataInput input = new DataInputStream(client.getInputStream());
93 while (true) {
94 int packetId;
95 try {
96 packetId = input.readUnsignedByte();
97 } catch (EOFException | SocketException e) {
98 break;
99 }
100 Packet<ServerPacketHandler> packet = PacketRegistry.createC2SPacket(packetId);
101 if (packet == null) {
102 throw new IOException("Received invalid packet id " + packetId);
103 }
104 packet.read(input);
105 runOnThread(() -> packet.handle(new ServerPacketHandler(client, this)));
106 }
107 } catch (IOException e) {
108 kick(client, e.toString());
109 e.printStackTrace();
110 return;
111 }
112 kick(client, "disconnect.disconnected");
113 });
114 thread.setName("Server I/O thread #" + (nextIoId++));
115 thread.setDaemon(true);
116 thread.start();
117 }
118
119 public void stop() {
120 runOnThread(() -> {
121 if (socket != null && !socket.isClosed()) {
122 for (Socket client : clients) {
123 kick(client, "disconnect.server_closed");
124 }
125 try {
126 socket.close();
127 } catch (IOException e) {
128 System.err.println("Failed to close server socket");
129 e.printStackTrace();
130 }
131 }
132 });
133 }
134
135 public void kick(Socket client, String reason) {
136 if (!clients.remove(client)) return;
137
138 sendPacket(client, new KickS2CPacket(reason));
139
140 clientsNeedingConfirmation.values().removeIf(list -> {
141 list.remove(client);
142 return list.isEmpty();
143 });
144 String username = usernames.remove(client);
145 try {
146 client.close();
147 } catch (IOException e) {
148 System.err.println("Failed to close server client socket");
149 e.printStackTrace();
150 }
151
152 if (username != null) {
153 System.out.println("Kicked " + username + " because " + reason);
154 sendMessage(Message.disconnect(username));
155 }
156 sendUsernamePacket();
157 }
158
159 public boolean isUsernameTaken(String username) {
160 return usernames.containsValue(username);
161 }
162
163 public void setUsername(Socket client, String username) {
164 usernames.put(client, username);
165 sendUsernamePacket();
166 }
167
168 private void sendUsernamePacket() {
169 List<String> usernames = new ArrayList<>(this.usernames.values());
170 Collections.sort(usernames);
171 sendToAll(new UserListS2CPacket(usernames));
172 }
173
174 public String getUsername(Socket client) {
175 return usernames.get(client);
176 }
177
178 public void sendPacket(Socket client, Packet<GuiController> packet) {
179 if (!client.isClosed()) {
180 int packetId = PacketRegistry.getS2CId(packet);
181 try {
182 DataOutput output = new DataOutputStream(client.getOutputStream());
183 output.writeByte(packetId);
184 packet.write(output);
185 } catch (IOException e) {
186 if (!(packet instanceof KickS2CPacket)) {
187 kick(client, e.toString());
188 e.printStackTrace();
189 }
190 }
191 }
192 }
193
194 public void sendToAll(Packet<GuiController> packet) {
195 for (Socket client : clients) {
196 sendPacket(client, packet);
197 }
198 }
199
200 public void sendToAllExcept(Socket excluded, Packet<GuiController> packet) {
201 for (Socket client : clients) {
202 if (client != excluded) {
203 sendPacket(client, packet);
204 }
205 }
206 }
207
208 public boolean canModifyEntry(Socket client, Entry<?> entry) {
209 if (unapprovedClients.contains(client)) {
210 return false;
211 }
212
213 Integer syncId = syncIds.get(entry);
214 if (syncId == null) {
215 return true;
216 }
217 Set<Socket> clients = clientsNeedingConfirmation.get(syncId);
218 return clients == null || !clients.contains(client);
219 }
220
221 public int lockEntry(Socket exception, Entry<?> entry) {
222 int syncId = nextSyncId;
223 nextSyncId++;
224 // sync id is sent as an unsigned short, can't have more than 65536
225 if (nextSyncId == 65536) {
226 nextSyncId = DUMMY_SYNC_ID + 1;
227 }
228 Integer oldSyncId = syncIds.get(entry);
229 if (oldSyncId != null) {
230 clientsNeedingConfirmation.remove(oldSyncId);
231 }
232 syncIds.put(entry, syncId);
233 inverseSyncIds.put(syncId, entry);
234 Set<Socket> clients = new HashSet<>(this.clients);
235 clients.remove(exception);
236 clientsNeedingConfirmation.put(syncId, clients);
237 return syncId;
238 }
239
240 public void confirmChange(Socket client, int syncId) {
241 if (usernames.containsKey(client)) {
242 unapprovedClients.remove(client);
243 }
244
245 Set<Socket> clients = clientsNeedingConfirmation.get(syncId);
246 if (clients != null) {
247 clients.remove(client);
248 if (clients.isEmpty()) {
249 clientsNeedingConfirmation.remove(syncId);
250 syncIds.remove(inverseSyncIds.remove(syncId));
251 }
252 }
253 }
254
255 public void sendCorrectMapping(Socket client, Entry<?> entry, boolean refreshClassTree) {
256 EntryMapping oldMapping = mappings.getDeobfMapping(entry);
257 String oldName = oldMapping == null ? null : oldMapping.getTargetName();
258 if (oldName == null) {
259 sendPacket(client, new RemoveMappingS2CPacket(DUMMY_SYNC_ID, entry));
260 } else {
261 sendPacket(client, new RenameS2CPacket(0, entry, oldName, refreshClassTree));
262 }
263 }
264
265 protected abstract void runOnThread(Runnable task);
266
267 public void log(String message) {
268 System.out.println(message);
269 }
270
271 protected boolean isRunning() {
272 return !socket.isClosed();
273 }
274
275 public byte[] getJarChecksum() {
276 return jarChecksum;
277 }
278
279 public char[] getPassword() {
280 return password;
281 }
282
283 public EntryRemapper getMappings() {
284 return mappings;
285 }
286
287 public void sendMessage(Message message) {
288 log(String.format("[MSG] %s", message.translate()));
289 sendToAll(new MessageS2CPacket(message));
290 }
291
292}
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 00000000..21c6825b
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/IntegratedEnigmaServer.java
@@ -0,0 +1,16 @@
1package cuchaz.enigma.network;
2
3import cuchaz.enigma.translation.mapping.EntryRemapper;
4
5import javax.swing.*;
6
7public class IntegratedEnigmaServer extends EnigmaServer {
8 public IntegratedEnigmaServer(byte[] jarChecksum, char[] password, EntryRemapper mappings, int port) {
9 super(jarChecksum, password, mappings, port);
10 }
11
12 @Override
13 protected void runOnThread(Runnable task) {
14 SwingUtilities.invokeLater(task);
15 }
16}
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 00000000..86185536
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/ServerPacketHandler.java
@@ -0,0 +1,22 @@
1package cuchaz.enigma.network;
2
3import java.net.Socket;
4
5public class ServerPacketHandler {
6
7 private final Socket client;
8 private final EnigmaServer server;
9
10 public ServerPacketHandler(Socket client, EnigmaServer server) {
11 this.client = client;
12 this.server = server;
13 }
14
15 public Socket getClient() {
16 return client;
17 }
18
19 public EnigmaServer getServer() {
20 return server;
21 }
22}
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 00000000..4d5d86f3
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/ChangeDocsC2SPacket.java
@@ -0,0 +1,59 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.network.ServerPacketHandler;
5import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.representation.entry.Entry;
7import cuchaz.enigma.utils.Message;
8import cuchaz.enigma.utils.Utils;
9
10import java.io.DataInput;
11import java.io.DataOutput;
12import java.io.IOException;
13
14public class ChangeDocsC2SPacket implements Packet<ServerPacketHandler> {
15 private Entry<?> entry;
16 private String newDocs;
17
18 ChangeDocsC2SPacket() {
19 }
20
21 public ChangeDocsC2SPacket(Entry<?> entry, String newDocs) {
22 this.entry = entry;
23 this.newDocs = newDocs;
24 }
25
26 @Override
27 public void read(DataInput input) throws IOException {
28 this.entry = PacketHelper.readEntry(input);
29 this.newDocs = PacketHelper.readString(input);
30 }
31
32 @Override
33 public void write(DataOutput output) throws IOException {
34 PacketHelper.writeEntry(output, entry);
35 PacketHelper.writeString(output, newDocs);
36 }
37
38 @Override
39 public void handle(ServerPacketHandler handler) {
40 EntryMapping mapping = handler.getServer().getMappings().getDeobfMapping(entry);
41
42 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
43 if (!valid) {
44 String oldDocs = mapping == null ? null : mapping.getJavadoc();
45 handler.getServer().sendPacket(handler.getClient(), new ChangeDocsS2CPacket(EnigmaServer.DUMMY_SYNC_ID, entry, oldDocs == null ? "" : oldDocs));
46 return;
47 }
48
49 if (mapping == null) {
50 mapping = new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName());
51 }
52 handler.getServer().getMappings().mapFromObf(entry, mapping.withDocs(Utils.isBlank(newDocs) ? null : newDocs));
53
54 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
55 handler.getServer().sendToAllExcept(handler.getClient(), new ChangeDocsS2CPacket(syncId, entry, newDocs));
56 handler.getServer().sendMessage(Message.editDocs(handler.getServer().getUsername(handler.getClient()), entry));
57 }
58
59}
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 00000000..bf5b7cb8
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/ChangeDocsS2CPacket.java
@@ -0,0 +1,44 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.gui.GuiController;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class ChangeDocsS2CPacket implements Packet<GuiController> {
12 private int syncId;
13 private Entry<?> entry;
14 private String newDocs;
15
16 ChangeDocsS2CPacket() {
17 }
18
19 public ChangeDocsS2CPacket(int syncId, Entry<?> entry, String newDocs) {
20 this.syncId = syncId;
21 this.entry = entry;
22 this.newDocs = newDocs;
23 }
24
25 @Override
26 public void read(DataInput input) throws IOException {
27 this.syncId = input.readUnsignedShort();
28 this.entry = PacketHelper.readEntry(input);
29 this.newDocs = PacketHelper.readString(input);
30 }
31
32 @Override
33 public void write(DataOutput output) throws IOException {
34 output.writeShort(syncId);
35 PacketHelper.writeEntry(output, entry);
36 PacketHelper.writeString(output, newDocs);
37 }
38
39 @Override
40 public void handle(GuiController controller) {
41 controller.changeDocs(new EntryReference<>(entry, entry.getName()), newDocs, false);
42 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
43 }
44}
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 00000000..78ef9645
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/ConfirmChangeC2SPacket.java
@@ -0,0 +1,33 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4
5import java.io.DataInput;
6import java.io.DataOutput;
7import java.io.IOException;
8
9public class ConfirmChangeC2SPacket implements Packet<ServerPacketHandler> {
10 private int syncId;
11
12 ConfirmChangeC2SPacket() {
13 }
14
15 public ConfirmChangeC2SPacket(int syncId) {
16 this.syncId = syncId;
17 }
18
19 @Override
20 public void read(DataInput input) throws IOException {
21 this.syncId = input.readUnsignedShort();
22 }
23
24 @Override
25 public void write(DataOutput output) throws IOException {
26 output.writeShort(syncId);
27 }
28
29 @Override
30 public void handle(ServerPacketHandler handler) {
31 handler.getServer().confirmChange(handler.getClient(), syncId);
32 }
33}
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 00000000..bd007d31
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/KickS2CPacket.java
@@ -0,0 +1,33 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.gui.GuiController;
4
5import java.io.DataInput;
6import java.io.DataOutput;
7import java.io.IOException;
8
9public class KickS2CPacket implements Packet<GuiController> {
10 private String reason;
11
12 KickS2CPacket() {
13 }
14
15 public KickS2CPacket(String reason) {
16 this.reason = reason;
17 }
18
19 @Override
20 public void read(DataInput input) throws IOException {
21 this.reason = PacketHelper.readString(input);
22 }
23
24 @Override
25 public void write(DataOutput output) throws IOException {
26 PacketHelper.writeString(output, reason);
27 }
28
29 @Override
30 public void handle(GuiController controller) {
31 controller.disconnectIfConnected(reason);
32 }
33}
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 00000000..722cbbf3
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/LoginC2SPacket.java
@@ -0,0 +1,75 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.EnigmaServer;
4import cuchaz.enigma.network.ServerPacketHandler;
5import cuchaz.enigma.utils.Message;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10import java.util.Arrays;
11
12public class LoginC2SPacket implements Packet<ServerPacketHandler> {
13 private byte[] jarChecksum;
14 private char[] password;
15 private String username;
16
17 LoginC2SPacket() {
18 }
19
20 public LoginC2SPacket(byte[] jarChecksum, char[] password, String username) {
21 this.jarChecksum = jarChecksum;
22 this.password = password;
23 this.username = username;
24 }
25
26 @Override
27 public void read(DataInput input) throws IOException {
28 if (input.readUnsignedShort() != EnigmaServer.PROTOCOL_VERSION) {
29 throw new IOException("Mismatching protocol");
30 }
31 this.jarChecksum = new byte[EnigmaServer.CHECKSUM_SIZE];
32 input.readFully(jarChecksum);
33 this.password = new char[input.readUnsignedByte()];
34 for (int i = 0; i < password.length; i++) {
35 password[i] = input.readChar();
36 }
37 this.username = PacketHelper.readString(input);
38 }
39
40 @Override
41 public void write(DataOutput output) throws IOException {
42 output.writeShort(EnigmaServer.PROTOCOL_VERSION);
43 output.write(jarChecksum);
44 output.writeByte(password.length);
45 for (char c : password) {
46 output.writeChar(c);
47 }
48 PacketHelper.writeString(output, username);
49 }
50
51 @Override
52 public void handle(ServerPacketHandler handler) {
53 boolean usernameTaken = handler.getServer().isUsernameTaken(username);
54 handler.getServer().setUsername(handler.getClient(), username);
55 handler.getServer().log(username + " logged in with IP " + handler.getClient().getInetAddress().toString() + ":" + handler.getClient().getPort());
56
57 if (!Arrays.equals(password, handler.getServer().getPassword())) {
58 handler.getServer().kick(handler.getClient(), "disconnect.wrong_password");
59 return;
60 }
61
62 if (usernameTaken) {
63 handler.getServer().kick(handler.getClient(), "disconnect.username_taken");
64 return;
65 }
66
67 if (!Arrays.equals(jarChecksum, handler.getServer().getJarChecksum())) {
68 handler.getServer().kick(handler.getClient(), "disconnect.wrong_jar");
69 return;
70 }
71
72 handler.getServer().sendPacket(handler.getClient(), new SyncMappingsS2CPacket(handler.getServer().getMappings().getObfToDeobf()));
73 handler.getServer().sendMessage(Message.connect(username));
74 }
75}
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 00000000..98d20d96
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedC2SPacket.java
@@ -0,0 +1,48 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.translation.mapping.EntryMapping;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.utils.Message;
7
8import java.io.DataInput;
9import java.io.DataOutput;
10import java.io.IOException;
11
12public class MarkDeobfuscatedC2SPacket implements Packet<ServerPacketHandler> {
13 private Entry<?> entry;
14
15 MarkDeobfuscatedC2SPacket() {
16 }
17
18 public MarkDeobfuscatedC2SPacket(Entry<?> entry) {
19 this.entry = entry;
20 }
21
22 @Override
23 public void read(DataInput input) throws IOException {
24 this.entry = PacketHelper.readEntry(input);
25 }
26
27 @Override
28 public void write(DataOutput output) throws IOException {
29 PacketHelper.writeEntry(output, entry);
30 }
31
32 @Override
33 public void handle(ServerPacketHandler handler) {
34 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
35 if (!valid) {
36 handler.getServer().sendCorrectMapping(handler.getClient(), entry, true);
37 return;
38 }
39
40 handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(handler.getServer().getMappings().deobfuscate(entry).getName()));
41 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " marked " + entry + " as deobfuscated");
42
43 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
44 handler.getServer().sendToAllExcept(handler.getClient(), new MarkDeobfuscatedS2CPacket(syncId, entry));
45 handler.getServer().sendMessage(Message.markDeobf(handler.getServer().getUsername(handler.getClient()), entry));
46
47 }
48}
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 00000000..b7d6eda3
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/MarkDeobfuscatedS2CPacket.java
@@ -0,0 +1,40 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.gui.GuiController;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class MarkDeobfuscatedS2CPacket implements Packet<GuiController> {
12 private int syncId;
13 private Entry<?> entry;
14
15 MarkDeobfuscatedS2CPacket() {
16 }
17
18 public MarkDeobfuscatedS2CPacket(int syncId, Entry<?> entry) {
19 this.syncId = syncId;
20 this.entry = entry;
21 }
22
23 @Override
24 public void read(DataInput input) throws IOException {
25 this.syncId = input.readUnsignedShort();
26 this.entry = PacketHelper.readEntry(input);
27 }
28
29 @Override
30 public void write(DataOutput output) throws IOException {
31 output.writeShort(syncId);
32 PacketHelper.writeEntry(output, entry);
33 }
34
35 @Override
36 public void handle(GuiController controller) {
37 controller.markAsDeobfuscated(new EntryReference<>(entry, entry.getName()), false);
38 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
39 }
40}
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 00000000..b8e0f14f
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/MessageC2SPacket.java
@@ -0,0 +1,39 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
7import cuchaz.enigma.network.ServerPacketHandler;
8import cuchaz.enigma.utils.Message;
9
10public class MessageC2SPacket implements Packet<ServerPacketHandler> {
11
12 private String message;
13
14 MessageC2SPacket() {
15 }
16
17 public MessageC2SPacket(String message) {
18 this.message = message;
19 }
20
21 @Override
22 public void read(DataInput input) throws IOException {
23 message = PacketHelper.readString(input);
24 }
25
26 @Override
27 public void write(DataOutput output) throws IOException {
28 PacketHelper.writeString(output, message);
29 }
30
31 @Override
32 public void handle(ServerPacketHandler handler) {
33 String message = this.message.trim();
34 if (!message.isEmpty()) {
35 handler.getServer().sendMessage(Message.chat(handler.getServer().getUsername(handler.getClient()), message));
36 }
37 }
38
39}
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 00000000..edeaae0b
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/MessageS2CPacket.java
@@ -0,0 +1,36 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
7import cuchaz.enigma.gui.GuiController;
8import cuchaz.enigma.utils.Message;
9
10public class MessageS2CPacket implements Packet<GuiController> {
11
12 private Message message;
13
14 MessageS2CPacket() {
15 }
16
17 public MessageS2CPacket(Message message) {
18 this.message = message;
19 }
20
21 @Override
22 public void read(DataInput input) throws IOException {
23 message = Message.read(input);
24 }
25
26 @Override
27 public void write(DataOutput output) throws IOException {
28 message.write(output);
29 }
30
31 @Override
32 public void handle(GuiController handler) {
33 handler.addMessage(message);
34 }
35
36}
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 00000000..2f16dfb9
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/Packet.java
@@ -0,0 +1,15 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6
7public interface Packet<H> {
8
9 void read(DataInput input) throws IOException;
10
11 void write(DataOutput output) throws IOException;
12
13 void handle(H handler);
14
15}
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 00000000..464606e0
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/PacketHelper.java
@@ -0,0 +1,135 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.translation.representation.MethodDescriptor;
4import cuchaz.enigma.translation.representation.TypeDescriptor;
5import cuchaz.enigma.translation.representation.entry.ClassEntry;
6import cuchaz.enigma.translation.representation.entry.Entry;
7import cuchaz.enigma.translation.representation.entry.FieldEntry;
8import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
9import cuchaz.enigma.translation.representation.entry.MethodEntry;
10
11import java.io.DataInput;
12import java.io.DataOutput;
13import java.io.IOException;
14import java.nio.charset.StandardCharsets;
15
16public class PacketHelper {
17
18 private static final int ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3;
19 private static final int MAX_STRING_LENGTH = 65535;
20
21 public static Entry<?> readEntry(DataInput input) throws IOException {
22 return readEntry(input, null, true);
23 }
24
25 public static Entry<?> readEntry(DataInput input, Entry<?> parent, boolean includeParent) throws IOException {
26 int type = input.readUnsignedByte();
27
28 if (includeParent && input.readBoolean()) {
29 parent = readEntry(input, null, true);
30 }
31
32 String name = readString(input);
33
34 String javadocs = null;
35 if (input.readBoolean()) {
36 javadocs = readString(input);
37 }
38
39 switch (type) {
40 case ENTRY_CLASS: {
41 if (parent != null && !(parent instanceof ClassEntry)) {
42 throw new IOException("Class requires class parent");
43 }
44 return new ClassEntry((ClassEntry) parent, name, javadocs);
45 }
46 case ENTRY_FIELD: {
47 if (!(parent instanceof ClassEntry)) {
48 throw new IOException("Field requires class parent");
49 }
50 TypeDescriptor desc = new TypeDescriptor(readString(input));
51 return new FieldEntry((ClassEntry) parent, name, desc, javadocs);
52 }
53 case ENTRY_METHOD: {
54 if (!(parent instanceof ClassEntry)) {
55 throw new IOException("Method requires class parent");
56 }
57 MethodDescriptor desc = new MethodDescriptor(readString(input));
58 return new MethodEntry((ClassEntry) parent, name, desc, javadocs);
59 }
60 case ENTRY_LOCAL_VAR: {
61 if (!(parent instanceof MethodEntry)) {
62 throw new IOException("Local variable requires method parent");
63 }
64 int index = input.readUnsignedShort();
65 boolean parameter = input.readBoolean();
66 return new LocalVariableEntry((MethodEntry) parent, index, name, parameter, javadocs);
67 }
68 default: throw new IOException("Received unknown entry type " + type);
69 }
70 }
71
72 public static void writeEntry(DataOutput output, Entry<?> entry) throws IOException {
73 writeEntry(output, entry, true);
74 }
75
76 public static void writeEntry(DataOutput output, Entry<?> entry, boolean includeParent) throws IOException {
77 // type
78 if (entry instanceof ClassEntry) {
79 output.writeByte(ENTRY_CLASS);
80 } else if (entry instanceof FieldEntry) {
81 output.writeByte(ENTRY_FIELD);
82 } else if (entry instanceof MethodEntry) {
83 output.writeByte(ENTRY_METHOD);
84 } else if (entry instanceof LocalVariableEntry) {
85 output.writeByte(ENTRY_LOCAL_VAR);
86 } else {
87 throw new IOException("Don't know how to serialize entry of type " + entry.getClass().getSimpleName());
88 }
89
90 // parent
91 if (includeParent) {
92 output.writeBoolean(entry.getParent() != null);
93 if (entry.getParent() != null) {
94 writeEntry(output, entry.getParent(), true);
95 }
96 }
97
98 // name
99 writeString(output, entry.getName());
100
101 // javadocs
102 output.writeBoolean(entry.getJavadocs() != null);
103 if (entry.getJavadocs() != null) {
104 writeString(output, entry.getJavadocs());
105 }
106
107 // type-specific stuff
108 if (entry instanceof FieldEntry) {
109 writeString(output, ((FieldEntry) entry).getDesc().toString());
110 } else if (entry instanceof MethodEntry) {
111 writeString(output, ((MethodEntry) entry).getDesc().toString());
112 } else if (entry instanceof LocalVariableEntry) {
113 LocalVariableEntry localVar = (LocalVariableEntry) entry;
114 output.writeShort(localVar.getIndex());
115 output.writeBoolean(localVar.isArgument());
116 }
117 }
118
119 public static String readString(DataInput input) throws IOException {
120 int length = input.readUnsignedShort();
121 byte[] bytes = new byte[length];
122 input.readFully(bytes);
123 return new String(bytes, StandardCharsets.UTF_8);
124 }
125
126 public static void writeString(DataOutput output, String str) throws IOException {
127 byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
128 if (bytes.length > MAX_STRING_LENGTH) {
129 throw new IOException("String too long, was " + bytes.length + " bytes, max " + MAX_STRING_LENGTH + " allowed");
130 }
131 output.writeShort(bytes.length);
132 output.write(bytes);
133 }
134
135}
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 00000000..ba5d9dec
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/PacketRegistry.java
@@ -0,0 +1,64 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.gui.GuiController;
4import cuchaz.enigma.network.ServerPacketHandler;
5
6import java.util.HashMap;
7import java.util.Map;
8import java.util.function.Supplier;
9
10public class PacketRegistry {
11
12 private static final Map<Class<? extends Packet<ServerPacketHandler>>, Integer> c2sPacketIds = new HashMap<>();
13 private static final Map<Integer, Supplier<? extends Packet<ServerPacketHandler>>> c2sPacketCreators = new HashMap<>();
14 private static final Map<Class<? extends Packet<GuiController>>, Integer> s2cPacketIds = new HashMap<>();
15 private static final Map<Integer, Supplier<? extends Packet<GuiController>>> s2cPacketCreators = new HashMap<>();
16
17 private static <T extends Packet<ServerPacketHandler>> void registerC2S(int id, Class<T> clazz, Supplier<T> creator) {
18 c2sPacketIds.put(clazz, id);
19 c2sPacketCreators.put(id, creator);
20 }
21
22 private static <T extends Packet<GuiController>> void registerS2C(int id, Class<T> clazz, Supplier<T> creator) {
23 s2cPacketIds.put(clazz, id);
24 s2cPacketCreators.put(id, creator);
25 }
26
27 static {
28 registerC2S(0, LoginC2SPacket.class, LoginC2SPacket::new);
29 registerC2S(1, ConfirmChangeC2SPacket.class, ConfirmChangeC2SPacket::new);
30 registerC2S(2, RenameC2SPacket.class, RenameC2SPacket::new);
31 registerC2S(3, RemoveMappingC2SPacket.class, RemoveMappingC2SPacket::new);
32 registerC2S(4, ChangeDocsC2SPacket.class, ChangeDocsC2SPacket::new);
33 registerC2S(5, MarkDeobfuscatedC2SPacket.class, MarkDeobfuscatedC2SPacket::new);
34 registerC2S(6, MessageC2SPacket.class, MessageC2SPacket::new);
35
36 registerS2C(0, KickS2CPacket.class, KickS2CPacket::new);
37 registerS2C(1, SyncMappingsS2CPacket.class, SyncMappingsS2CPacket::new);
38 registerS2C(2, RenameS2CPacket.class, RenameS2CPacket::new);
39 registerS2C(3, RemoveMappingS2CPacket.class, RemoveMappingS2CPacket::new);
40 registerS2C(4, ChangeDocsS2CPacket.class, ChangeDocsS2CPacket::new);
41 registerS2C(5, MarkDeobfuscatedS2CPacket.class, MarkDeobfuscatedS2CPacket::new);
42 registerS2C(6, MessageS2CPacket.class, MessageS2CPacket::new);
43 registerS2C(7, UserListS2CPacket.class, UserListS2CPacket::new);
44 }
45
46 public static int getC2SId(Packet<ServerPacketHandler> packet) {
47 return c2sPacketIds.get(packet.getClass());
48 }
49
50 public static Packet<ServerPacketHandler> createC2SPacket(int id) {
51 Supplier<? extends Packet<ServerPacketHandler>> creator = c2sPacketCreators.get(id);
52 return creator == null ? null : creator.get();
53 }
54
55 public static int getS2CId(Packet<GuiController> packet) {
56 return s2cPacketIds.get(packet.getClass());
57 }
58
59 public static Packet<GuiController> createS2CPacket(int id) {
60 Supplier<? extends Packet<GuiController>> creator = s2cPacketCreators.get(id);
61 return creator == null ? null : creator.get();
62 }
63
64}
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 00000000..a3f3d91d
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/RemoveMappingC2SPacket.java
@@ -0,0 +1,55 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.throwables.IllegalNameException;
5import cuchaz.enigma.translation.representation.entry.Entry;
6import cuchaz.enigma.utils.Message;
7
8import java.io.DataInput;
9import java.io.DataOutput;
10import java.io.IOException;
11
12public class RemoveMappingC2SPacket implements Packet<ServerPacketHandler> {
13 private Entry<?> entry;
14
15 RemoveMappingC2SPacket() {
16 }
17
18 public RemoveMappingC2SPacket(Entry<?> entry) {
19 this.entry = entry;
20 }
21
22 @Override
23 public void read(DataInput input) throws IOException {
24 this.entry = PacketHelper.readEntry(input);
25 }
26
27 @Override
28 public void write(DataOutput output) throws IOException {
29 PacketHelper.writeEntry(output, entry);
30 }
31
32 @Override
33 public void handle(ServerPacketHandler handler) {
34 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
35
36 if (valid) {
37 try {
38 handler.getServer().getMappings().removeByObf(entry);
39 } catch (IllegalNameException e) {
40 valid = false;
41 }
42 }
43
44 if (!valid) {
45 handler.getServer().sendCorrectMapping(handler.getClient(), entry, true);
46 return;
47 }
48
49 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " removed the mapping for " + entry);
50
51 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
52 handler.getServer().sendToAllExcept(handler.getClient(), new RemoveMappingS2CPacket(syncId, entry));
53 handler.getServer().sendMessage(Message.removeMapping(handler.getServer().getUsername(handler.getClient()), entry));
54 }
55}
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 00000000..7bb1b00d
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/RemoveMappingS2CPacket.java
@@ -0,0 +1,40 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.gui.GuiController;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class RemoveMappingS2CPacket implements Packet<GuiController> {
12 private int syncId;
13 private Entry<?> entry;
14
15 RemoveMappingS2CPacket() {
16 }
17
18 public RemoveMappingS2CPacket(int syncId, Entry<?> entry) {
19 this.syncId = syncId;
20 this.entry = entry;
21 }
22
23 @Override
24 public void read(DataInput input) throws IOException {
25 this.syncId = input.readUnsignedShort();
26 this.entry = PacketHelper.readEntry(input);
27 }
28
29 @Override
30 public void write(DataOutput output) throws IOException {
31 output.writeShort(syncId);
32 PacketHelper.writeEntry(output, entry);
33 }
34
35 @Override
36 public void handle(GuiController controller) {
37 controller.removeMapping(new EntryReference<>(entry, entry.getName()), false);
38 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
39 }
40}
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 00000000..03e95d6f
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/RenameC2SPacket.java
@@ -0,0 +1,64 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.network.ServerPacketHandler;
4import cuchaz.enigma.throwables.IllegalNameException;
5import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.representation.entry.Entry;
7import cuchaz.enigma.utils.Message;
8
9import java.io.DataInput;
10import java.io.DataOutput;
11import java.io.IOException;
12
13public class RenameC2SPacket implements Packet<ServerPacketHandler> {
14 private Entry<?> entry;
15 private String newName;
16 private boolean refreshClassTree;
17
18 RenameC2SPacket() {
19 }
20
21 public RenameC2SPacket(Entry<?> entry, String newName, boolean refreshClassTree) {
22 this.entry = entry;
23 this.newName = newName;
24 this.refreshClassTree = refreshClassTree;
25 }
26
27 @Override
28 public void read(DataInput input) throws IOException {
29 this.entry = PacketHelper.readEntry(input);
30 this.newName = PacketHelper.readString(input);
31 this.refreshClassTree = input.readBoolean();
32 }
33
34 @Override
35 public void write(DataOutput output) throws IOException {
36 PacketHelper.writeEntry(output, entry);
37 PacketHelper.writeString(output, newName);
38 output.writeBoolean(refreshClassTree);
39 }
40
41 @Override
42 public void handle(ServerPacketHandler handler) {
43 boolean valid = handler.getServer().canModifyEntry(handler.getClient(), entry);
44
45 if (valid) {
46 try {
47 handler.getServer().getMappings().mapFromObf(entry, new EntryMapping(newName));
48 } catch (IllegalNameException e) {
49 valid = false;
50 }
51 }
52
53 if (!valid) {
54 handler.getServer().sendCorrectMapping(handler.getClient(), entry, refreshClassTree);
55 return;
56 }
57
58 handler.getServer().log(handler.getServer().getUsername(handler.getClient()) + " renamed " + entry + " to " + newName);
59
60 int syncId = handler.getServer().lockEntry(handler.getClient(), entry);
61 handler.getServer().sendToAllExcept(handler.getClient(), new RenameS2CPacket(syncId, entry, newName, refreshClassTree));
62 handler.getServer().sendMessage(Message.rename(handler.getServer().getUsername(handler.getClient()), entry, newName));
63 }
64}
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 00000000..058f0e58
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/RenameS2CPacket.java
@@ -0,0 +1,48 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.analysis.EntryReference;
4import cuchaz.enigma.gui.GuiController;
5import cuchaz.enigma.translation.representation.entry.Entry;
6
7import java.io.DataInput;
8import java.io.DataOutput;
9import java.io.IOException;
10
11public class RenameS2CPacket implements Packet<GuiController> {
12 private int syncId;
13 private Entry<?> entry;
14 private String newName;
15 private boolean refreshClassTree;
16
17 RenameS2CPacket() {
18 }
19
20 public RenameS2CPacket(int syncId, Entry<?> entry, String newName, boolean refreshClassTree) {
21 this.syncId = syncId;
22 this.entry = entry;
23 this.newName = newName;
24 this.refreshClassTree = refreshClassTree;
25 }
26
27 @Override
28 public void read(DataInput input) throws IOException {
29 this.syncId = input.readUnsignedShort();
30 this.entry = PacketHelper.readEntry(input);
31 this.newName = PacketHelper.readString(input);
32 this.refreshClassTree = input.readBoolean();
33 }
34
35 @Override
36 public void write(DataOutput output) throws IOException {
37 output.writeShort(syncId);
38 PacketHelper.writeEntry(output, entry);
39 PacketHelper.writeString(output, newName);
40 output.writeBoolean(refreshClassTree);
41 }
42
43 @Override
44 public void handle(GuiController controller) {
45 controller.rename(new EntryReference<>(entry, entry.getName()), newName, refreshClassTree, false);
46 controller.sendPacket(new ConfirmChangeC2SPacket(syncId));
47 }
48}
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 00000000..e6378d1d
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/SyncMappingsS2CPacket.java
@@ -0,0 +1,88 @@
1package cuchaz.enigma.network.packet;
2
3import cuchaz.enigma.gui.GuiController;
4import cuchaz.enigma.network.EnigmaServer;
5import cuchaz.enigma.translation.mapping.EntryMapping;
6import cuchaz.enigma.translation.mapping.tree.EntryTree;
7import cuchaz.enigma.translation.mapping.tree.EntryTreeNode;
8import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
9import cuchaz.enigma.translation.representation.entry.Entry;
10
11import java.io.DataInput;
12import java.io.DataOutput;
13import java.io.IOException;
14import java.util.Collection;
15import java.util.List;
16import java.util.stream.Collectors;
17
18public class SyncMappingsS2CPacket implements Packet<GuiController> {
19 private EntryTree<EntryMapping> mappings;
20
21 SyncMappingsS2CPacket() {
22 }
23
24 public SyncMappingsS2CPacket(EntryTree<EntryMapping> mappings) {
25 this.mappings = mappings;
26 }
27
28 @Override
29 public void read(DataInput input) throws IOException {
30 mappings = new HashEntryTree<>();
31 int size = input.readInt();
32 for (int i = 0; i < size; i++) {
33 readEntryTreeNode(input, null);
34 }
35 }
36
37 private void readEntryTreeNode(DataInput input, Entry<?> parent) throws IOException {
38 Entry<?> entry = PacketHelper.readEntry(input, parent, false);
39 EntryMapping mapping = null;
40 if (input.readBoolean()) {
41 String name = input.readUTF();
42 if (input.readBoolean()) {
43 String javadoc = input.readUTF();
44 mapping = new EntryMapping(name, javadoc);
45 } else {
46 mapping = new EntryMapping(name);
47 }
48 }
49 mappings.insert(entry, mapping);
50 int size = input.readUnsignedShort();
51 for (int i = 0; i < size; i++) {
52 readEntryTreeNode(input, entry);
53 }
54 }
55
56 @Override
57 public void write(DataOutput output) throws IOException {
58 List<EntryTreeNode<EntryMapping>> roots = mappings.getRootNodes().collect(Collectors.toList());
59 output.writeInt(roots.size());
60 for (EntryTreeNode<EntryMapping> node : roots) {
61 writeEntryTreeNode(output, node);
62 }
63 }
64
65 private static void writeEntryTreeNode(DataOutput output, EntryTreeNode<EntryMapping> node) throws IOException {
66 PacketHelper.writeEntry(output, node.getEntry(), false);
67 EntryMapping value = node.getValue();
68 output.writeBoolean(value != null);
69 if (value != null) {
70 PacketHelper.writeString(output, value.getTargetName());
71 output.writeBoolean(value.getJavadoc() != null);
72 if (value.getJavadoc() != null) {
73 PacketHelper.writeString(output, value.getJavadoc());
74 }
75 }
76 Collection<? extends EntryTreeNode<EntryMapping>> children = node.getChildNodes();
77 output.writeShort(children.size());
78 for (EntryTreeNode<EntryMapping> child : children) {
79 writeEntryTreeNode(output, child);
80 }
81 }
82
83 @Override
84 public void handle(GuiController controller) {
85 controller.openMappings(mappings);
86 controller.sendPacket(new ConfirmChangeC2SPacket(EnigmaServer.DUMMY_SYNC_ID));
87 }
88}
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 00000000..89048485
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/network/packet/UserListS2CPacket.java
@@ -0,0 +1,44 @@
1package cuchaz.enigma.network.packet;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6import java.util.ArrayList;
7import java.util.List;
8
9import cuchaz.enigma.gui.GuiController;
10
11public class UserListS2CPacket implements Packet<GuiController> {
12
13 private List<String> users;
14
15 UserListS2CPacket() {
16 }
17
18 public UserListS2CPacket(List<String> users) {
19 this.users = users;
20 }
21
22 @Override
23 public void read(DataInput input) throws IOException {
24 int len = input.readUnsignedShort();
25 users = new ArrayList<>(len);
26 for (int i = 0; i < len; i++) {
27 users.add(input.readUTF());
28 }
29 }
30
31 @Override
32 public void write(DataOutput output) throws IOException {
33 output.writeShort(users.size());
34 for (String user : users) {
35 PacketHelper.writeString(output, user);
36 }
37 }
38
39 @Override
40 public void handle(GuiController handler) {
41 handler.updateUserList(users);
42 }
43
44}
diff --git a/src/main/java/cuchaz/enigma/utils/Message.java b/src/main/java/cuchaz/enigma/utils/Message.java
new file mode 100644
index 00000000..d7c5f23e
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/utils/Message.java
@@ -0,0 +1,392 @@
1package cuchaz.enigma.utils;
2
3import java.io.DataInput;
4import java.io.DataOutput;
5import java.io.IOException;
6import java.util.Objects;
7
8import cuchaz.enigma.network.packet.PacketHelper;
9import cuchaz.enigma.translation.representation.entry.Entry;
10
11public abstract class Message {
12
13 public final String user;
14
15 public static Chat chat(String user, String message) {
16 return new Chat(user, message);
17 }
18
19 public static Connect connect(String user) {
20 return new Connect(user);
21 }
22
23 public static Disconnect disconnect(String user) {
24 return new Disconnect(user);
25 }
26
27 public static EditDocs editDocs(String user, Entry<?> entry) {
28 return new EditDocs(user, entry);
29 }
30
31 public static MarkDeobf markDeobf(String user, Entry<?> entry) {
32 return new MarkDeobf(user, entry);
33 }
34
35 public static RemoveMapping removeMapping(String user, Entry<?> entry) {
36 return new RemoveMapping(user, entry);
37 }
38
39 public static Rename rename(String user, Entry<?> entry, String newName) {
40 return new Rename(user, entry, newName);
41 }
42
43 public abstract String translate();
44
45 public abstract Type getType();
46
47 public static Message read(DataInput input) throws IOException {
48 byte typeId = input.readByte();
49 if (typeId < 0 || typeId >= Type.values().length) {
50 throw new IOException(String.format("Invalid message type ID %d", typeId));
51 }
52 Type type = Type.values()[typeId];
53 String user = input.readUTF();
54 switch (type) {
55 case CHAT:
56 String message = input.readUTF();
57 return chat(user, message);
58 case CONNECT:
59 return connect(user);
60 case DISCONNECT:
61 return disconnect(user);
62 case EDIT_DOCS:
63 Entry<?> entry = PacketHelper.readEntry(input);
64 return editDocs(user, entry);
65 case MARK_DEOBF:
66 entry = PacketHelper.readEntry(input);
67 return markDeobf(user, entry);
68 case REMOVE_MAPPING:
69 entry = PacketHelper.readEntry(input);
70 return removeMapping(user, entry);
71 case RENAME:
72 entry = PacketHelper.readEntry(input);
73 String newName = input.readUTF();
74 return rename(user, entry, newName);
75 default:
76 throw new IllegalStateException("unreachable");
77 }
78 }
79
80 public void write(DataOutput output) throws IOException {
81 output.writeByte(getType().ordinal());
82 PacketHelper.writeString(output, user);
83 }
84
85 private Message(String user) {
86 this.user = user;
87 }
88
89 @Override
90 public boolean equals(Object o) {
91 if (this == o) return true;
92 if (o == null || getClass() != o.getClass()) return false;
93 Message message = (Message) o;
94 return Objects.equals(user, message.user);
95 }
96
97 @Override
98 public int hashCode() {
99 return Objects.hash(user);
100 }
101
102 public enum Type {
103 CHAT,
104 CONNECT,
105 DISCONNECT,
106 EDIT_DOCS,
107 MARK_DEOBF,
108 REMOVE_MAPPING,
109 RENAME,
110 }
111
112 public static final class Chat extends Message {
113
114 public final String message;
115
116 private Chat(String user, String message) {
117 super(user);
118 this.message = message;
119 }
120
121 @Override
122 public void write(DataOutput output) throws IOException {
123 super.write(output);
124 PacketHelper.writeString(output, message);
125 }
126
127 @Override
128 public String translate() {
129 return String.format(I18n.translate("message.chat.text"), user, message);
130 }
131
132 @Override
133 public Type getType() {
134 return Type.CHAT;
135 }
136
137 @Override
138 public boolean equals(Object o) {
139 if (this == o) return true;
140 if (o == null || getClass() != o.getClass()) return false;
141 if (!super.equals(o)) return false;
142 Chat chat = (Chat) o;
143 return Objects.equals(message, chat.message);
144 }
145
146 @Override
147 public int hashCode() {
148 return Objects.hash(super.hashCode(), message);
149 }
150
151 @Override
152 public String toString() {
153 return String.format("Message.Chat { user: '%s', message: '%s' }", user, message);
154 }
155
156 }
157
158 public static final class Connect extends Message {
159
160 private Connect(String user) {
161 super(user);
162 }
163
164 @Override
165 public String translate() {
166 return String.format(I18n.translate("message.connect.text"), user);
167 }
168
169 @Override
170 public Type getType() {
171 return Type.CONNECT;
172 }
173
174 @Override
175 public String toString() {
176 return String.format("Message.Connect { user: '%s' }", user);
177 }
178
179 }
180
181 public static final class Disconnect extends Message {
182
183 private Disconnect(String user) {
184 super(user);
185 }
186
187 @Override
188 public String translate() {
189 return String.format(I18n.translate("message.disconnect.text"), user);
190 }
191
192 @Override
193 public Type getType() {
194 return Type.DISCONNECT;
195 }
196
197 @Override
198 public String toString() {
199 return String.format("Message.Disconnect { user: '%s' }", user);
200 }
201
202 }
203
204 public static final class EditDocs extends Message {
205
206 public final Entry<?> entry;
207
208 private EditDocs(String user, Entry<?> entry) {
209 super(user);
210 this.entry = entry;
211 }
212
213 @Override
214 public void write(DataOutput output) throws IOException {
215 super.write(output);
216 PacketHelper.writeEntry(output, entry);
217 }
218
219 @Override
220 public String translate() {
221 return String.format(I18n.translate("message.edit_docs.text"), user, entry);
222 }
223
224 @Override
225 public Type getType() {
226 return Type.EDIT_DOCS;
227 }
228
229 @Override
230 public boolean equals(Object o) {
231 if (this == o) return true;
232 if (o == null || getClass() != o.getClass()) return false;
233 if (!super.equals(o)) return false;
234 EditDocs editDocs = (EditDocs) o;
235 return Objects.equals(entry, editDocs.entry);
236 }
237
238 @Override
239 public int hashCode() {
240 return Objects.hash(super.hashCode(), entry);
241 }
242
243 @Override
244 public String toString() {
245 return String.format("Message.EditDocs { user: '%s', entry: %s }", user, entry);
246 }
247
248 }
249
250 public static final class MarkDeobf extends Message {
251
252 public final Entry<?> entry;
253
254 private MarkDeobf(String user, Entry<?> entry) {
255 super(user);
256 this.entry = entry;
257 }
258
259 @Override
260 public void write(DataOutput output) throws IOException {
261 super.write(output);
262 PacketHelper.writeEntry(output, entry);
263 }
264
265 @Override
266 public String translate() {
267 return String.format(I18n.translate("message.mark_deobf.text"), user, entry);
268 }
269
270 @Override
271 public Type getType() {
272 return Type.MARK_DEOBF;
273 }
274
275 @Override
276 public boolean equals(Object o) {
277 if (this == o) return true;
278 if (o == null || getClass() != o.getClass()) return false;
279 if (!super.equals(o)) return false;
280 MarkDeobf markDeobf = (MarkDeobf) o;
281 return Objects.equals(entry, markDeobf.entry);
282 }
283
284 @Override
285 public int hashCode() {
286 return Objects.hash(super.hashCode(), entry);
287 }
288
289 @Override
290 public String toString() {
291 return String.format("Message.MarkDeobf { user: '%s', entry: %s }", user, entry);
292 }
293
294 }
295
296 public static final class RemoveMapping extends Message {
297
298 public final Entry<?> entry;
299
300 private RemoveMapping(String user, Entry<?> entry) {
301 super(user);
302 this.entry = entry;
303 }
304
305 @Override
306 public void write(DataOutput output) throws IOException {
307 super.write(output);
308 PacketHelper.writeEntry(output, entry);
309 }
310
311 @Override
312 public String translate() {
313 return String.format(I18n.translate("message.remove_mapping.text"), user, entry);
314 }
315
316 @Override
317 public Type getType() {
318 return Type.REMOVE_MAPPING;
319 }
320
321 @Override
322 public boolean equals(Object o) {
323 if (this == o) return true;
324 if (o == null || getClass() != o.getClass()) return false;
325 if (!super.equals(o)) return false;
326 RemoveMapping that = (RemoveMapping) o;
327 return Objects.equals(entry, that.entry);
328 }
329
330 @Override
331 public int hashCode() {
332 return Objects.hash(super.hashCode(), entry);
333 }
334
335 @Override
336 public String toString() {
337 return String.format("Message.RemoveMapping { user: '%s', entry: %s }", user, entry);
338 }
339
340 }
341
342 public static final class Rename extends Message {
343
344 public final Entry<?> entry;
345 public final String newName;
346
347 private Rename(String user, Entry<?> entry, String newName) {
348 super(user);
349 this.entry = entry;
350 this.newName = newName;
351 }
352
353 @Override
354 public void write(DataOutput output) throws IOException {
355 super.write(output);
356 PacketHelper.writeEntry(output, entry);
357 PacketHelper.writeString(output, newName);
358 }
359
360 @Override
361 public String translate() {
362 return String.format(I18n.translate("message.rename.text"), user, entry, newName);
363 }
364
365 @Override
366 public Type getType() {
367 return Type.RENAME;
368 }
369
370 @Override
371 public boolean equals(Object o) {
372 if (this == o) return true;
373 if (o == null || getClass() != o.getClass()) return false;
374 if (!super.equals(o)) return false;
375 Rename rename = (Rename) o;
376 return Objects.equals(entry, rename.entry) &&
377 Objects.equals(newName, rename.newName);
378 }
379
380 @Override
381 public int hashCode() {
382 return Objects.hash(super.hashCode(), entry, newName);
383 }
384
385 @Override
386 public String toString() {
387 return String.format("Message.Rename { user: '%s', entry: %s, newName: '%s' }", user, entry, newName);
388 }
389
390 }
391
392}
diff --git a/src/main/java/cuchaz/enigma/utils/Utils.java b/src/main/java/cuchaz/enigma/utils/Utils.java
index b8f2ec23..b45b00d1 100644
--- a/src/main/java/cuchaz/enigma/utils/Utils.java
+++ b/src/main/java/cuchaz/enigma/utils/Utils.java
@@ -15,6 +15,8 @@ import com.google.common.io.CharStreams;
15import org.objectweb.asm.Opcodes; 15import org.objectweb.asm.Opcodes;
16 16
17import javax.swing.*; 17import javax.swing.*;
18import javax.swing.text.BadLocationException;
19import javax.swing.text.JTextComponent;
18import java.awt.*; 20import java.awt.*;
19import java.awt.event.MouseEvent; 21import java.awt.event.MouseEvent;
20import java.io.IOException; 22import java.io.IOException;
@@ -22,13 +24,16 @@ import java.io.InputStream;
22import java.io.InputStreamReader; 24import java.io.InputStreamReader;
23import java.net.URI; 25import java.net.URI;
24import java.net.URISyntaxException; 26import java.net.URISyntaxException;
27import java.nio.charset.StandardCharsets;
25import java.nio.file.Files; 28import java.nio.file.Files;
26import java.nio.file.Path; 29import java.nio.file.Path;
27import java.util.Comparator; 30import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
32import java.util.*;
28import java.util.List; 33import java.util.List;
29import java.util.Locale;
30import java.util.StringJoiner;
31import java.util.stream.Collectors; 34import java.util.stream.Collectors;
35import java.util.zip.ZipEntry;
36import java.util.zip.ZipFile;
32 37
33public class Utils { 38public class Utils {
34 39
@@ -98,6 +103,19 @@ public class Utils {
98 manager.setInitialDelay(oldDelay); 103 manager.setInitialDelay(oldDelay);
99 } 104 }
100 105
106 public static Rectangle safeModelToView(JTextComponent component, int modelPos) {
107 if (modelPos < 0) {
108 modelPos = 0;
109 } else if (modelPos >= component.getText().length()) {
110 modelPos = component.getText().length();
111 }
112 try {
113 return component.modelToView(modelPos);
114 } catch (BadLocationException e) {
115 throw new RuntimeException(e);
116 }
117 }
118
101 public static boolean getSystemPropertyAsBoolean(String property, boolean defValue) { 119 public static boolean getSystemPropertyAsBoolean(String property, boolean defValue) {
102 String value = System.getProperty(property); 120 String value = System.getProperty(property);
103 return value == null ? defValue : Boolean.parseBoolean(value); 121 return value == null ? defValue : Boolean.parseBoolean(value);
@@ -111,6 +129,34 @@ public class Utils {
111 } 129 }
112 } 130 }
113 131
132 public static byte[] zipSha1(Path path) throws IOException {
133 MessageDigest digest;
134 try {
135 digest = MessageDigest.getInstance("SHA-1");
136 } catch (NoSuchAlgorithmException e) {
137 // Algorithm guaranteed to be supported
138 throw new RuntimeException(e);
139 }
140 try (ZipFile zip = new ZipFile(path.toFile())) {
141 List<? extends ZipEntry> entries = Collections.list(zip.entries());
142 // only compare classes (some implementations may not generate directory entries)
143 entries.removeIf(entry -> !entry.getName().toLowerCase(Locale.ROOT).endsWith(".class"));
144 // different implementations may add zip entries in a different order
145 entries.sort(Comparator.comparing(ZipEntry::getName));
146 byte[] buffer = new byte[8192];
147 for (ZipEntry entry : entries) {
148 digest.update(entry.getName().getBytes(StandardCharsets.UTF_8));
149 try (InputStream in = zip.getInputStream(entry)) {
150 int n;
151 while ((n = in.read(buffer)) != -1) {
152 digest.update(buffer, 0, n);
153 }
154 }
155 }
156 }
157 return digest.digest();
158 }
159
114 public static String caplisiseCamelCase(String input){ 160 public static String caplisiseCamelCase(String input){
115 StringJoiner stringJoiner = new StringJoiner(" "); 161 StringJoiner stringJoiner = new StringJoiner(" ");
116 for (String word : input.toLowerCase(Locale.ROOT).split("_")) { 162 for (String word : input.toLowerCase(Locale.ROOT).split("_")) {
@@ -118,4 +164,16 @@ public class Utils {
118 } 164 }
119 return stringJoiner.toString(); 165 return stringJoiner.toString();
120 } 166 }
167
168 public static boolean isBlank(String input) {
169 if (input == null) {
170 return true;
171 }
172 for (int i = 0; i < input.length(); i++) {
173 if (!Character.isWhitespace(input.charAt(i))) {
174 return false;
175 }
176 }
177 return true;
178 }
121} 179}
diff --git a/src/main/resources/lang/en_us.json b/src/main/resources/lang/en_us.json
index a8b33064..04f689c7 100644
--- a/src/main/resources/lang/en_us.json
+++ b/src/main/resources/lang/en_us.json
@@ -41,6 +41,13 @@
41 "menu.view.scale": "Scale", 41 "menu.view.scale": "Scale",
42 "menu.view.scale.custom": "Custom...", 42 "menu.view.scale.custom": "Custom...",
43 "menu.view.search": "Search", 43 "menu.view.search": "Search",
44 "menu.collab": "Collab",
45 "menu.collab.connect": "Connect to server",
46 "menu.collab.connect.error": "Error connecting to server",
47 "menu.collab.disconnect": "Disconnect",
48 "menu.collab.server.start": "Start server",
49 "menu.collab.server.start.error": "Error starting server",
50 "menu.collab.server.stop": "Stop server",
44 "menu.help": "Help", 51 "menu.help": "Help",
45 "menu.help.about": "About", 52 "menu.help.about": "About",
46 "menu.help.about.title": "%s - About", 53 "menu.help.about.title": "%s - About",
@@ -81,6 +88,9 @@
81 "info_panel.tree.implementations": "Implementations", 88 "info_panel.tree.implementations": "Implementations",
82 "info_panel.tree.calls": "Call Graph", 89 "info_panel.tree.calls": "Call Graph",
83 90
91 "log_panel.messages": "Messages",
92 "log_panel.users": "Users",
93
84 "progress.operation": "%s - Operation in progress", 94 "progress.operation": "%s - Operation in progress",
85 "progress.jar.indexing": "Indexing jar", 95 "progress.jar.indexing": "Indexing jar",
86 "progress.jar.indexing.entries": "Entries...", 96 "progress.jar.indexing.entries": "Entries...",
@@ -115,6 +125,34 @@
115 "prompt.close.cancel": "Cancel", 125 "prompt.close.cancel": "Cancel",
116 "prompt.open": "Open", 126 "prompt.open": "Open",
117 "prompt.cancel": "Cancel", 127 "prompt.cancel": "Cancel",
128 "prompt.connect.title": "Connect to server",
129 "prompt.connect.username": "Username",
130 "prompt.connect.ip": "IP",
131 "prompt.port": "Port",
132 "prompt.port.nan": "Port is not a number",
133 "prompt.port.invalid": "Port is out of range, should be between 0-65535",
134 "prompt.password": "Password",
135 "prompt.password.too_long": "Password is too long, it must be at most 255 characters.",
136 "prompt.create_server.title": "Create server",
137
138 "disconnect.disconnected": "Disconnected",
139 "disconnect.server_closed": "Server closed",
140 "disconnect.wrong_jar": "Jar checksums don't match (you have the wrong jar)!",
141 "disconnect.wrong_password": "Incorrect password",
142 "disconnect.username_taken": "Username is taken",
143
144 "message.chat.text": "%s: %s",
145 "message.connect.text": "[+] %s",
146 "message.disconnect.text": "[-] %s",
147 "message.edit_docs.text": "%s edited docs for %s",
148 "message.mark_deobf.text": "%s marked %s as deobfuscated",
149 "message.remove_mapping.text": "%s removed mappings for %s",
150 "message.rename.text": "%s renamed %s to %s",
151
152 "status.disconnected": "Disconnected.",
153 "status.connected": "Connected.",
154 "status.connected_user_count": "Connected (%d users).",
155 "status.ready": "Ready.",
118 156
119 "crash.title": "%s - Crash Report", 157 "crash.title": "%s - Crash Report",
120 "crash.summary": "%s has crashed! =(", 158 "crash.summary": "%s has crashed! =(",
diff --git a/src/main/resources/lang/fr_fr.json b/src/main/resources/lang/fr_fr.json
index 12214cf7..a1d55a28 100644
--- a/src/main/resources/lang/fr_fr.json
+++ b/src/main/resources/lang/fr_fr.json
@@ -39,6 +39,13 @@
39 "menu.view.languages.summary": "La nouvelle langue sera appliquée lors du prochain redémarrage.", 39 "menu.view.languages.summary": "La nouvelle langue sera appliquée lors du prochain redémarrage.",
40 "menu.view.languages.ok": "Ok", 40 "menu.view.languages.ok": "Ok",
41 "menu.view.search": "Rechercher", 41 "menu.view.search": "Rechercher",
42 "menu.collab": "Collab",
43 "menu.collab.connect": "Se connecter à un serveur",
44 "menu.collab.connect.error": "Erreur lors de la connexion au serveur",
45 "menu.collab.disconnect": "Se déconnecter",
46 "menu.collab.server.start": "Démarrer le serveur",
47 "menu.collab.server.start.error": "Erreur lors du démarrage du serveur",
48 "menu.collab.server.stop": "Arrêter le serveur",
42 "menu.help": "Aide", 49 "menu.help": "Aide",
43 "menu.help.about": "À propos", 50 "menu.help.about": "À propos",
44 "menu.help.about.title": "%s - À propos", 51 "menu.help.about.title": "%s - À propos",
@@ -79,6 +86,9 @@
79 "info_panel.tree.implementations": "Implémentations", 86 "info_panel.tree.implementations": "Implémentations",
80 "info_panel.tree.calls": "Graphique des appels", 87 "info_panel.tree.calls": "Graphique des appels",
81 88
89 "log_panel.messages": "Messages",
90 "log_panel.users": "Utilisateurs",
91
82 "progress.operation": "%s - Opération en cours", 92 "progress.operation": "%s - Opération en cours",
83 "progress.jar.indexing": "Indexation du jar", 93 "progress.jar.indexing": "Indexation du jar",
84 "progress.jar.indexing.entries": "Entrées...", 94 "progress.jar.indexing.entries": "Entrées...",
@@ -111,6 +121,34 @@
111 "prompt.close.save": "Enregistrer et fermer", 121 "prompt.close.save": "Enregistrer et fermer",
112 "prompt.close.discard": "Annuler les modifications", 122 "prompt.close.discard": "Annuler les modifications",
113 "prompt.close.cancel": "Annuler", 123 "prompt.close.cancel": "Annuler",
124 "prompt.connect.title": "Se connecter à un serveur",
125 "prompt.connect.username": "Nom d'utilisateur",
126 "prompt.connect.ip": "IP",
127 "prompt.port": "Port",
128 "prompt.port.nan": "Le port n'est pas un nombre",
129 "prompt.port.invalid": "Le port est hors de portée. Il doit être compris entre 0 et 65535.",
130 "prompt.password": "Mot de passe",
131 "prompt.password.too_long": "Le mot de passe est trop long. Il ne doit pas dépasser 255 caractères.",
132 "prompt.create_server.title": "Créer un serveur",
133
134 "disconnect.disconnected": "Déconnecté",
135 "disconnect.server_closed": "Serveur fermé",
136 "disconnect.wrong_jar": "Les sommes de contrôle du jar ne correspondent pas (vous avez le mauvais jar) !",
137 "disconnect.wrong_password": "Mot de passe incorrect",
138 "disconnect.username_taken": "Le nom d'utilisateur est déjà pris",
139
140 "message.chat.text": "%s : %s",
141 "message.connect.text": "[+] %s",
142 "message.disconnect.text": "[-] %s",
143 "message.edit_docs.text": "%s a édité les javadocs de %s",
144 "message.mark_deobf.text": "%s a marqué %s comme déobfusqué",
145 "message.remove_mapping.text": "%s a supprimé les mappings de %s",
146 "message.rename.text": "%s a renommé %s en %s",
147
148 "status.disconnected": "Déconnecté.",
149 "status.connected": "Connecté.",
150 "status.connected_user_count": "Connecté (%d utilisateurs).",
151 "status.ready": "Prêt.",
114 152
115 "crash.title": "%s - Rapport de plantage", 153 "crash.title": "%s - Rapport de plantage",
116 "crash.summary": "%s a planté ! =(", 154 "crash.summary": "%s a planté ! =(",