summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.gradle2
-rw-r--r--docs/protocol.md366
-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
-rw-r--r--src/test/java/cuchaz/enigma/TestDeobfed.java3
43 files changed, 3087 insertions, 79 deletions
diff --git a/build.gradle b/build.gradle
index a42b2257..0d744a36 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ plugins {
5} 5}
6 6
7group = 'cuchaz' 7group = 'cuchaz'
8version = '0.15.3' 8version = '0.16.0'
9 9
10def generatedSourcesDir = "$buildDir/generated-src" 10def generatedSourcesDir = "$buildDir/generated-src"
11 11
diff --git a/docs/protocol.md b/docs/protocol.md
new file mode 100644
index 00000000..c14ecb81
--- /dev/null
+++ b/docs/protocol.md
@@ -0,0 +1,366 @@
1# Enigma protocol
2Enigma uses TCP sockets for communication. Data is sent in each direction as a continuous stream, with packets being
3concatenated one after the other.
4
5In this document, data will be represented in C-like pseudocode. The primitive data types will be the same as those
6defined by Java's [DataOutputStream](https://docs.oracle.com/javase/7/docs/api/java/io/DataOutputStream.html), i.e. in
7big-endian order for multi-byte integers (`short`, `int` and `long`). The one exception is for Strings, which do *not*
8use the same modified UTF format as in `DataOutputStream`, I repeat, the normal `writeUTF` method in `DataOutputStream`
9(and the corresponding method in `DataInputStream`) should *not* be used. Instead, there is a custom `utf` struct for
10Strings, see below.
11
12## Login protocol
13```
14Client Server
15| |
16| Login |
17| >>>>>>>>>>>>> |
18| |
19| SyncMappings |
20| <<<<<<<<<<<<< |
21| |
22| ConfirmChange |
23| >>>>>>>>>>>>> |
24```
251. On connect, the client sends a login packet to the server. This allows the server to test the validity of the client,
26 as well as allowing the client to declare metadata about itself, such as the username.
271. After validating the login packet, the server sends all its mappings to the client, and the client will apply them.
281. Upon receiving the mappings, the client sends a `ConfirmChange` packet with `sync_id` set to 0, to confirm that it
29 has received the mappings and is in sync with the server. Once the server receives this packet, the client will be
30 allowed to modify mappings.
31
32The server will not accept any other packets from the client until this entire exchange has been completed.
33
34## Kicking clients
35When the server kicks a client, it may optionally send a `Kick` packet immediately before closing the connection, which
36contains the reason why the client was kicked (so the client can display it to the user). This is not required though -
37the server may simply terminate the connection.
38
39## Changing mappings
40This section uses the example of renaming, but the same pattern applies to all mapping changes.
41```
42Client A Server Client B
43| | |
44| RenameC2S | |
45| >>>>>>>>> | |
46| | |
47| | RenameS2C |
48| | >>>>>>>>>>>>> |
49| | |
50| | ConfirmChange |
51| | <<<<<<<<<<<<< |
52```
53
541. Client A validates the name and updates the mapping client-side to give the impression there is no latency >:)
551. Client A sends a rename packet to the server, notifying it of the rename.
561. The server assesses the validity of the rename. If it is invalid for whatever reason (e.g. the mapping was locked or
57 the name contains invalid characters), then the server sends an appropriate packet back to client A to revert the
58 change, with `sync_id` set to 0. The server will ignore any `ConfirmChange` packets it receives in response to this.
591. If the rename was valid, the server will lock all clients except client A from being able to modify this mapping, and
60 then send an appropriate packet to all clients except client A notifying them of this rename. The `sync_id` will be a
61 unique non-zero value identifying this change.
621. Each client responds to this packet by updating their mappings locally to reflect this change, then sending a
63 `ConfirmChange` packet with the same `sync_id` as the one in the packet they received, to confirm that they have
64 received the change.
651. When the server receives the `ConfirmChange` packet, and another change to that mapping hasn't occurred since, the
66 server will unlock that mapping for that client and allow them to make changes again.
67
68## Packets
69```c
70struct Packet {
71 unsigned short packet_id;
72 data[]; // depends on packet_id
73}
74```
75The IDs for client-to-server packets are as follows:
76- 0: `Login`
77- 1: `ConfirmChange`
78- 2: `Rename`
79- 3: `RemoveMapping`
80- 4: `ChangeDocs`
81- 5: `MarkDeobfuscated`
82- 6: `Message`
83
84The IDs for server-to-client packets are as follows:
85- 0: `Kick`
86- 1: `SyncMappings`
87- 2: `Rename`
88- 3: `RemoveMapping`
89- 4: `ChangeDocs`
90- 5: `MarkDeobfuscated`
91- 6: `Message`
92- 7: `UserList`
93
94### The utf struct
95```c
96struct utf {
97 unsigned short length;
98 byte data[length];
99}
100```
101- `length`: The number of bytes in the UTF-8 encoding of the string. Note, this may not be the same as the number of
102 Unicode characters in the string.
103- `data`: A standard UTF-8 encoded byte array representing the string.
104
105### The Entry struct
106```c
107enum EntryType {
108 ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3;
109}
110struct Entry {
111 unsigned byte type;
112 boolean has_parent;
113 if<has_parent> {
114 Entry parent;
115 }
116 utf name;
117 boolean has_javadoc;
118 if<has_javadoc> {
119 utf javadoc;
120 }
121 if<type == ENTRY_FIELD || type == ENTRY_METHOD> {
122 utf descriptor;
123 }
124 if<type == ENTRY_LOCAL_VAR> {
125 unsigned short index;
126 boolean parameter;
127 }
128}
129```
130- `type`: The type of entry this is. One of `ENTRY_CLASS`, `ENTRY_FIELD`, `ENTRY_METHOD` or `ENTRY_LOCAL_VAR`.
131- `parent`: The parent entry. Only class entries may have no parent. fields, methods and inner classes must have their
132 containing class as their parent. Local variables have a method as a parent.
133- `name`: The class/field/method/variable name.
134- `javadoc`: The javadoc of an entry, if present.
135- `descriptor`: The field/method descriptor.
136- `index`: The index of the local variable in the local variable table.
137- `parameter`: Whether the local variable is a parameter.
138
139### The Message struct
140```c
141enum MessageType {
142 MESSAGE_CHAT = 0,
143 MESSAGE_CONNECT = 1,
144 MESSAGE_DISCONNECT = 2,
145 MESSAGE_EDIT_DOCS = 3,
146 MESSAGE_MARK_DEOBF = 4,
147 MESSAGE_REMOVE_MAPPING = 5,
148 MESSAGE_RENAME = 6
149};
150typedef unsigned byte message_type_t;
151
152struct Message {
153 message_type_t type;
154 union { // Note that the size of this varies depending on type, it is not constant size
155 struct {
156 utf user;
157 utf message;
158 } chat;
159 struct {
160 utf user;
161 } connect;
162 struct {
163 utf user;
164 } disconnect;
165 struct {
166 utf user;
167 Entry entry;
168 } edit_docs;
169 struct {
170 utf user;
171 Entry entry;
172 } mark_deobf;
173 struct {
174 utf user;
175 Entry entry;
176 } remove_mapping;
177 struct {
178 utf user;
179 Entry entry;
180 utf new_name;
181 } rename;
182 } data;
183};
184```
185- `type`: The type of message this is. One of `MESSAGE_CHAT`, `MESSAGE_CONNECT`, `MESSAGE_DISCONNECT`,
186 `MESSAGE_EDIT_DOCS`, `MESSAGE_MARK_DEOBF`, `MESSAGE_REMOVE_MAPPING`, `MESSAGE_RENAME`.
187- `chat`: Chat message. Use in case `type` is `MESSAGE_CHAT`
188- `connect`: Sent when a user connects. Use in case `type` is `MESSAGE_CONNECT`
189- `disconnect`: Sent when a user disconnects. Use in case `type` is `MESSAGE_DISCONNECT`
190- `edit_docs`: Sent when a user edits the documentation of an entry. Use in case `type` is `MESSAGE_EDIT_DOCS`
191- `mark_deobf`: Sent when a user marks an entry as deobfuscated. Use in case `type` is `MESSAGE_MARK_DEOBF`
192- `remove_mapping`: Sent when a user removes a mapping. Use in case `type` is `MESSAGE_REMOVE_MAPPING`
193- `rename`: Sent when a user renames an entry. Use in case `type` is `MESSAGE_RENAME`
194- `user`: The user that performed the action.
195- `message`: The message the user sent.
196- `entry`: The entry that was modified.
197- `new_name`: The new name for the entry.
198
199
200### Login (client-to-server)
201```c
202struct LoginC2SPacket {
203 unsigned short protocol_version;
204 byte checksum[20];
205 unsigned byte password_length;
206 char password[password_length];
207 utf username;
208}
209```
210- `protocol_version`: the version of the protocol. If the version does not match on the server, then the client will be
211 kicked immediately. Currently always equal to 0.
212- `checksum`: the SHA-1 hash of the JAR file the client has open. If this does not match the SHA-1 hash of the JAR file
213 the server has open, the client will be kicked.
214- `password`: the password needed to log into the server. Note that each `char` is 2 bytes, as per the Java data type.
215 If this password is incorrect, the client will be kicked.
216- `username`: the username of the user logging in. If the username is not unique, the client will be kicked.
217
218### ConfirmChange (client-to-server)
219```c
220struct ConfirmChangeC2SPacket {
221 unsigned short sync_id;
222}
223```
224- `sync_id`: the sync ID to confirm.
225
226### Rename (client-to-server)
227```c
228struct RenameC2SPacket {
229 Entry obf_entry;
230 utf new_name;
231 boolean refresh_class_tree;
232}
233```
234- `obf_entry`: the obfuscated name and descriptor of the entry to rename.
235- `new_name`: what to rename the entry to.
236- `refresh_class_tree`: whether the class tree on the sidebar of Enigma needs refreshing as a result of this change.
237
238### RemoveMapping (client-to-server)
239```c
240struct RemoveMappingC2SPacket {
241 Entry obf_entry;
242}
243```
244- `obf_entry`: the obfuscated name and descriptor of the entry to remove the mapping for.
245
246### ChangeDocs (client-to-server)
247```c
248struct ChangeDocsC2SPacket {
249 Entry obf_entry;
250 utf new_docs;
251}
252```
253- `obf_entry`: the obfuscated name and descriptor of the entry to change the documentation for.
254- `new_docs`: the new documentation for this entry, or an empty string to remove the documentation.
255
256### MarkDeobfuscated (client-to-server)
257```c
258struct MarkDeobfuscatedC2SPacket {
259 Entry obf_entry;
260}
261```
262- `obf_entry`: the obfuscated name and descriptor of the entry to mark as deobfuscated.
263
264### Message (client-to-server)
265```c
266struct MessageC2SPacket {
267 utf message;
268}
269```
270- `message`: The text message the user sent.
271
272### Kick (server-to-client)
273```c
274struct KickS2CPacket {
275 utf reason;
276}
277```
278- `reason`: the reason for the kick, may or may not be a translation key for the client to display to the user.
279
280### SyncMappings (server-to-client)
281```c
282struct SyncMappingsS2CPacket {
283 int num_roots;
284 MappingNode roots[num_roots];
285}
286struct MappingNode {
287 NoParentEntry obf_entry;
288 boolean is_named;
289 if<is_named> {
290 utf name;
291 boolean has_javadoc;
292 if<has_javadoc> {
293 utf javadoc;
294 }
295 }
296 unsigned short children_count;
297 MappingNode children[children_count];
298}
299typedef { Entry but without the has_parent or parent fields } NoParentEntry;
300```
301- `roots`: The root mapping nodes, containing all the entries without parents.
302- `obf_entry`: The value of a node, containing the obfuscated name and descriptor of the entry.
303- `name`: The deobfuscated name of the entry, if it has a mapping.
304- `javadoc`: The documentation for the entry, if it is named and has documentation.
305- `children`: The children of this node
306
307### Rename (server-to-client)
308```c
309struct RenameS2CPacket {
310 unsigned short sync_id;
311 Entry obf_entry;
312 utf new_name;
313 boolean refresh_class_tree;
314}
315```
316- `sync_id`: the sync ID of the change for locking purposes.
317- `obf_entry`: the obfuscated name and descriptor of the entry to rename.
318- `new_name`: what to rename the entry to.
319- `refresh_class_tree`: whether the class tree on the sidebar of Enigma needs refreshing as a result of this change.
320
321### RemoveMapping (server-to-client)
322```c
323struct RemoveMappingS2CPacket {
324 unsigned short sync_id;
325 Entry obf_entry;
326}
327```
328- `sync_id`: the sync ID of the change for locking purposes.
329- `obf_entry`: the obfuscated name and descriptor of the entry to remove the mapping for.
330
331### ChangeDocs (server-to-client)
332```c
333struct ChangeDocsS2CPacket {
334 unsigned short sync_id;
335 Entry obf_entry;
336 utf new_docs;
337}
338```
339- `sync_id`: the sync ID of the change for locking purposes.
340- `obf_entry`: the obfuscated name and descriptor of the entry to change the documentation for.
341- `new_docs`: the new documentation for this entry, or an empty string to remove the documentation.
342
343### MarkDeobfuscated (server-to-client)
344```c
345struct MarkDeobfuscatedS2CPacket {
346 unsigned short sync_id;
347 Entry obf_entry;
348}
349```
350- `sync_id`: the sync ID of the change for locking purposes.
351- `obf_entry`: the obfuscated name and descriptor of the entry to mark as deobfuscated.
352
353### Message (server-to-client)
354```c
355struct MessageS2CPacket {
356 Message message;
357}
358```
359
360### UserList (server-to-client)
361```c
362struct UserListS2CPacket {
363 unsigned short len;
364 utf user[len];
365}
366```
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é ! =(",
diff --git a/src/test/java/cuchaz/enigma/TestDeobfed.java b/src/test/java/cuchaz/enigma/TestDeobfed.java
index c88b0eb6..d64a745b 100644
--- a/src/test/java/cuchaz/enigma/TestDeobfed.java
+++ b/src/test/java/cuchaz/enigma/TestDeobfed.java
@@ -13,6 +13,7 @@ package cuchaz.enigma;
13 13
14import cuchaz.enigma.analysis.ClassCache; 14import cuchaz.enigma.analysis.ClassCache;
15import cuchaz.enigma.analysis.index.JarIndex; 15import cuchaz.enigma.analysis.index.JarIndex;
16import cuchaz.enigma.network.EnigmaServer;
16import cuchaz.enigma.source.Decompiler; 17import cuchaz.enigma.source.Decompiler;
17import cuchaz.enigma.source.Decompilers; 18import cuchaz.enigma.source.Decompilers;
18import cuchaz.enigma.source.SourceSettings; 19import cuchaz.enigma.source.SourceSettings;
@@ -70,7 +71,7 @@ public class TestDeobfed {
70 71
71 @Test 72 @Test
72 public void decompile() { 73 public void decompile() {
73 EnigmaProject project = new EnigmaProject(enigma, classCache, index); 74 EnigmaProject project = new EnigmaProject(enigma, classCache, index, new byte[EnigmaServer.CHECKSUM_SIZE]);
74 Decompiler decompiler = Decompilers.PROCYON.create(project.getClassCache(), new SourceSettings(false, false)); 75 Decompiler decompiler = Decompilers.PROCYON.create(project.getClassCache(), new SourceSettings(false, false));
75 76
76 decompiler.getSource("a"); 77 decompiler.getSource("a");