diff options
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 | ||
| 7 | group = 'cuchaz' | 7 | group = 'cuchaz' |
| 8 | version = '0.15.3' | 8 | version = '0.16.0' |
| 9 | 9 | ||
| 10 | def generatedSourcesDir = "$buildDir/generated-src" | 10 | def 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 | ||
| 2 | Enigma uses TCP sockets for communication. Data is sent in each direction as a continuous stream, with packets being | ||
| 3 | concatenated one after the other. | ||
| 4 | |||
| 5 | In this document, data will be represented in C-like pseudocode. The primitive data types will be the same as those | ||
| 6 | defined by Java's [DataOutputStream](https://docs.oracle.com/javase/7/docs/api/java/io/DataOutputStream.html), i.e. in | ||
| 7 | big-endian order for multi-byte integers (`short`, `int` and `long`). The one exception is for Strings, which do *not* | ||
| 8 | use 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 | ||
| 10 | Strings, see below. | ||
| 11 | |||
| 12 | ## Login protocol | ||
| 13 | ``` | ||
| 14 | Client Server | ||
| 15 | | | | ||
| 16 | | Login | | ||
| 17 | | >>>>>>>>>>>>> | | ||
| 18 | | | | ||
| 19 | | SyncMappings | | ||
| 20 | | <<<<<<<<<<<<< | | ||
| 21 | | | | ||
| 22 | | ConfirmChange | | ||
| 23 | | >>>>>>>>>>>>> | | ||
| 24 | ``` | ||
| 25 | 1. 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. | ||
| 27 | 1. After validating the login packet, the server sends all its mappings to the client, and the client will apply them. | ||
| 28 | 1. 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 | |||
| 32 | The server will not accept any other packets from the client until this entire exchange has been completed. | ||
| 33 | |||
| 34 | ## Kicking clients | ||
| 35 | When the server kicks a client, it may optionally send a `Kick` packet immediately before closing the connection, which | ||
| 36 | contains the reason why the client was kicked (so the client can display it to the user). This is not required though - | ||
| 37 | the server may simply terminate the connection. | ||
| 38 | |||
| 39 | ## Changing mappings | ||
| 40 | This section uses the example of renaming, but the same pattern applies to all mapping changes. | ||
| 41 | ``` | ||
| 42 | Client A Server Client B | ||
| 43 | | | | | ||
| 44 | | RenameC2S | | | ||
| 45 | | >>>>>>>>> | | | ||
| 46 | | | | | ||
| 47 | | | RenameS2C | | ||
| 48 | | | >>>>>>>>>>>>> | | ||
| 49 | | | | | ||
| 50 | | | ConfirmChange | | ||
| 51 | | | <<<<<<<<<<<<< | | ||
| 52 | ``` | ||
| 53 | |||
| 54 | 1. Client A validates the name and updates the mapping client-side to give the impression there is no latency >:) | ||
| 55 | 1. Client A sends a rename packet to the server, notifying it of the rename. | ||
| 56 | 1. 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. | ||
| 59 | 1. 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. | ||
| 62 | 1. 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. | ||
| 65 | 1. 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 | ||
| 70 | struct Packet { | ||
| 71 | unsigned short packet_id; | ||
| 72 | data[]; // depends on packet_id | ||
| 73 | } | ||
| 74 | ``` | ||
| 75 | The 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 | |||
| 84 | The 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 | ||
| 96 | struct 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 | ||
| 107 | enum EntryType { | ||
| 108 | ENTRY_CLASS = 0, ENTRY_FIELD = 1, ENTRY_METHOD = 2, ENTRY_LOCAL_VAR = 3; | ||
| 109 | } | ||
| 110 | struct 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 | ||
| 141 | enum 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 | }; | ||
| 150 | typedef unsigned byte message_type_t; | ||
| 151 | |||
| 152 | struct 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 | ||
| 202 | struct 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 | ||
| 220 | struct ConfirmChangeC2SPacket { | ||
| 221 | unsigned short sync_id; | ||
| 222 | } | ||
| 223 | ``` | ||
| 224 | - `sync_id`: the sync ID to confirm. | ||
| 225 | |||
| 226 | ### Rename (client-to-server) | ||
| 227 | ```c | ||
| 228 | struct 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 | ||
| 240 | struct 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 | ||
| 248 | struct 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 | ||
| 258 | struct 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 | ||
| 266 | struct MessageC2SPacket { | ||
| 267 | utf message; | ||
| 268 | } | ||
| 269 | ``` | ||
| 270 | - `message`: The text message the user sent. | ||
| 271 | |||
| 272 | ### Kick (server-to-client) | ||
| 273 | ```c | ||
| 274 | struct 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 | ||
| 282 | struct SyncMappingsS2CPacket { | ||
| 283 | int num_roots; | ||
| 284 | MappingNode roots[num_roots]; | ||
| 285 | } | ||
| 286 | struct 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 | } | ||
| 299 | typedef { 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 | ||
| 309 | struct 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 | ||
| 323 | struct 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 | ||
| 333 | struct 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 | ||
| 345 | struct 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 | ||
| 355 | struct MessageS2CPacket { | ||
| 356 | Message message; | ||
| 357 | } | ||
| 358 | ``` | ||
| 359 | |||
| 360 | ### UserList (server-to-client) | ||
| 361 | ```c | ||
| 362 | struct 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; | |||
| 21 | import cuchaz.enigma.api.service.EnigmaServiceFactory; | 21 | import cuchaz.enigma.api.service.EnigmaServiceFactory; |
| 22 | import cuchaz.enigma.api.service.EnigmaServiceType; | 22 | import cuchaz.enigma.api.service.EnigmaServiceType; |
| 23 | import cuchaz.enigma.api.service.JarIndexerService; | 23 | import cuchaz.enigma.api.service.JarIndexerService; |
| 24 | import cuchaz.enigma.utils.Utils; | ||
| 24 | 25 | ||
| 25 | import java.io.IOException; | 26 | import java.io.IOException; |
| 26 | import java.nio.file.Path; | 27 | import 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; | |||
| 14 | import cuchaz.enigma.translation.mapping.MappingFileNameFormat; | 14 | import cuchaz.enigma.translation.mapping.MappingFileNameFormat; |
| 15 | import cuchaz.enigma.translation.mapping.MappingSaveParameters; | 15 | import cuchaz.enigma.translation.mapping.MappingSaveParameters; |
| 16 | 16 | ||
| 17 | import javax.annotation.Nullable; | ||
| 18 | import java.io.BufferedReader; | ||
| 19 | import java.io.IOException; | ||
| 20 | import java.io.InputStreamReader; | ||
| 17 | import java.io.Reader; | 21 | import java.io.Reader; |
| 18 | import java.lang.reflect.Type; | 22 | import java.lang.reflect.Type; |
| 23 | import java.nio.charset.StandardCharsets; | ||
| 24 | import java.nio.file.Files; | ||
| 25 | import java.nio.file.Path; | ||
| 19 | import java.util.Collections; | 26 | import java.util.Collections; |
| 20 | import java.util.List; | 27 | import java.util.List; |
| 21 | import java.util.Map; | 28 | import 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 @@ | |||
| 1 | package cuchaz.enigma; | 1 | package cuchaz.enigma; |
| 2 | 2 | ||
| 3 | import com.google.common.base.Functions; | 3 | import com.google.common.base.Functions; |
| 4 | import com.google.common.base.Preconditions; | ||
| 4 | import cuchaz.enigma.analysis.ClassCache; | 5 | import cuchaz.enigma.analysis.ClassCache; |
| 5 | import cuchaz.enigma.analysis.EntryReference; | 6 | import cuchaz.enigma.analysis.EntryReference; |
| 6 | import cuchaz.enigma.analysis.index.JarIndex; | 7 | import cuchaz.enigma.analysis.index.JarIndex; |
| 7 | import cuchaz.enigma.api.service.NameProposalService; | 8 | import cuchaz.enigma.api.service.NameProposalService; |
| 8 | import cuchaz.enigma.bytecode.translators.SourceFixVisitor; | 9 | import cuchaz.enigma.bytecode.translators.SourceFixVisitor; |
| 9 | import cuchaz.enigma.bytecode.translators.TranslationClassVisitor; | 10 | import cuchaz.enigma.bytecode.translators.TranslationClassVisitor; |
| 11 | import cuchaz.enigma.network.EnigmaServer; | ||
| 10 | import cuchaz.enigma.source.*; | 12 | import cuchaz.enigma.source.*; |
| 11 | import cuchaz.enigma.translation.Translator; | 13 | import cuchaz.enigma.translation.Translator; |
| 12 | import cuchaz.enigma.translation.mapping.*; | 14 | import 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 | ||
| 18 | import joptsimple.*; | 18 | import joptsimple.*; |
| 19 | 19 | ||
| 20 | import java.io.BufferedReader; | ||
| 21 | import java.io.IOException; | 20 | import java.io.IOException; |
| 22 | import java.io.InputStreamReader; | ||
| 23 | import java.nio.charset.StandardCharsets; | ||
| 24 | import java.nio.file.Files; | 21 | import java.nio.file.Files; |
| 25 | import java.nio.file.Path; | 22 | import java.nio.file.Path; |
| 26 | import java.nio.file.Paths; | 23 | import 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | public 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; | |||
| 34 | import cuchaz.enigma.gui.dialog.CrashDialog; | 34 | import cuchaz.enigma.gui.dialog.CrashDialog; |
| 35 | import cuchaz.enigma.gui.dialog.JavadocDialog; | 35 | import cuchaz.enigma.gui.dialog.JavadocDialog; |
| 36 | import cuchaz.enigma.gui.dialog.SearchDialog; | 36 | import cuchaz.enigma.gui.dialog.SearchDialog; |
| 37 | import cuchaz.enigma.gui.elements.CollapsibleTabbedPane; | ||
| 37 | import cuchaz.enigma.gui.elements.MenuBar; | 38 | import cuchaz.enigma.gui.elements.MenuBar; |
| 38 | import cuchaz.enigma.gui.elements.PopupMenuBar; | 39 | import cuchaz.enigma.gui.elements.PopupMenuBar; |
| 39 | import cuchaz.enigma.gui.filechooser.FileChooserAny; | 40 | import cuchaz.enigma.gui.filechooser.FileChooserAny; |
| @@ -46,10 +47,12 @@ import cuchaz.enigma.gui.panels.PanelEditor; | |||
| 46 | import cuchaz.enigma.gui.panels.PanelIdentifier; | 47 | import cuchaz.enigma.gui.panels.PanelIdentifier; |
| 47 | import cuchaz.enigma.gui.panels.PanelObf; | 48 | import cuchaz.enigma.gui.panels.PanelObf; |
| 48 | import cuchaz.enigma.gui.util.History; | 49 | import cuchaz.enigma.gui.util.History; |
| 50 | import cuchaz.enigma.network.packet.*; | ||
| 49 | import cuchaz.enigma.throwables.IllegalNameException; | 51 | import cuchaz.enigma.throwables.IllegalNameException; |
| 50 | import cuchaz.enigma.translation.mapping.*; | 52 | import cuchaz.enigma.translation.mapping.*; |
| 51 | import cuchaz.enigma.translation.representation.entry.*; | 53 | import cuchaz.enigma.translation.representation.entry.*; |
| 52 | import cuchaz.enigma.utils.I18n; | 54 | import cuchaz.enigma.utils.I18n; |
| 55 | import cuchaz.enigma.utils.Message; | ||
| 53 | import cuchaz.enigma.gui.util.ScaleUtil; | 56 | import cuchaz.enigma.gui.util.ScaleUtil; |
| 54 | import cuchaz.enigma.utils.Utils; | 57 | import cuchaz.enigma.utils.Utils; |
| 55 | import de.sciss.syntaxpane.DefaultSyntaxKit; | 58 | import 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; | |||
| 24 | import cuchaz.enigma.gui.stats.StatsGenerator; | 24 | import cuchaz.enigma.gui.stats.StatsGenerator; |
| 25 | import cuchaz.enigma.gui.stats.StatsMember; | 25 | import cuchaz.enigma.gui.stats.StatsMember; |
| 26 | import cuchaz.enigma.gui.util.History; | 26 | import cuchaz.enigma.gui.util.History; |
| 27 | import cuchaz.enigma.network.EnigmaClient; | ||
| 28 | import cuchaz.enigma.network.EnigmaServer; | ||
| 29 | import cuchaz.enigma.network.IntegratedEnigmaServer; | ||
| 30 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 31 | import cuchaz.enigma.network.packet.LoginC2SPacket; | ||
| 32 | import cuchaz.enigma.network.packet.Packet; | ||
| 27 | import cuchaz.enigma.source.*; | 33 | import cuchaz.enigma.source.*; |
| 28 | import cuchaz.enigma.throwables.MappingParseException; | 34 | import cuchaz.enigma.throwables.MappingParseException; |
| 29 | import cuchaz.enigma.translation.Translator; | 35 | import cuchaz.enigma.translation.Translator; |
| 30 | import cuchaz.enigma.translation.mapping.*; | 36 | import cuchaz.enigma.translation.mapping.*; |
| 31 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | 37 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; |
| 32 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | 38 | import cuchaz.enigma.translation.mapping.tree.EntryTree; |
| 39 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 33 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | 40 | import cuchaz.enigma.translation.representation.entry.ClassEntry; |
| 34 | import cuchaz.enigma.translation.representation.entry.Entry; | 41 | import cuchaz.enigma.translation.representation.entry.Entry; |
| 35 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | 42 | import cuchaz.enigma.translation.representation.entry.FieldEntry; |
| 36 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | 43 | import cuchaz.enigma.translation.representation.entry.MethodEntry; |
| 37 | import cuchaz.enigma.utils.I18n; | 44 | import cuchaz.enigma.utils.I18n; |
| 45 | import cuchaz.enigma.utils.Message; | ||
| 38 | import cuchaz.enigma.utils.ReadableToken; | 46 | import cuchaz.enigma.utils.ReadableToken; |
| 39 | import cuchaz.enigma.utils.Utils; | 47 | import cuchaz.enigma.utils.Utils; |
| 40 | import org.objectweb.asm.tree.ClassNode; | 48 | import org.objectweb.asm.tree.ClassNode; |
| 41 | 49 | ||
| 42 | import javax.annotation.Nullable; | 50 | import javax.annotation.Nullable; |
| 43 | import javax.swing.JOptionPane; | 51 | import javax.swing.JOptionPane; |
| 44 | import java.awt.Desktop; | 52 | import javax.swing.SwingUtilities; |
| 53 | import java.awt.*; | ||
| 45 | import java.awt.event.ItemEvent; | 54 | import java.awt.event.ItemEvent; |
| 46 | import java.io.*; | 55 | import java.io.*; |
| 47 | import java.nio.file.Path; | 56 | import 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 @@ | |||
| 1 | package cuchaz.enigma.gui; | ||
| 2 | |||
| 3 | import java.awt.Component; | ||
| 4 | |||
| 5 | import javax.swing.DefaultListCellRenderer; | ||
| 6 | import javax.swing.JList; | ||
| 7 | |||
| 8 | import cuchaz.enigma.utils.Message; | ||
| 9 | |||
| 10 | // For now, just render the translated text. | ||
| 11 | // TODO: Icons or something later? | ||
| 12 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.dialog; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.EnigmaServer; | ||
| 4 | import cuchaz.enigma.utils.I18n; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import java.awt.Frame; | ||
| 8 | |||
| 9 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.dialog; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.EnigmaServer; | ||
| 4 | import cuchaz.enigma.utils.I18n; | ||
| 5 | |||
| 6 | import javax.swing.*; | ||
| 7 | import java.awt.*; | ||
| 8 | |||
| 9 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.elements; | ||
| 2 | |||
| 3 | import java.awt.event.MouseEvent; | ||
| 4 | |||
| 5 | import javax.swing.JTabbedPane; | ||
| 6 | |||
| 7 | public 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 @@ | |||
| 1 | package cuchaz.enigma.gui.elements; | 1 | package cuchaz.enigma.gui.elements; |
| 2 | 2 | ||
| 3 | import cuchaz.enigma.config.Config; | ||
| 4 | import cuchaz.enigma.config.Themes; | ||
| 5 | import cuchaz.enigma.gui.Gui; | ||
| 6 | import cuchaz.enigma.gui.dialog.AboutDialog; | ||
| 7 | import cuchaz.enigma.gui.dialog.ConnectToServerDialog; | ||
| 8 | import cuchaz.enigma.gui.dialog.CreateServerDialog; | ||
| 9 | import cuchaz.enigma.gui.dialog.SearchDialog; | ||
| 10 | import cuchaz.enigma.gui.stats.StatsMember; | ||
| 11 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 12 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 13 | import cuchaz.enigma.utils.I18n; | ||
| 14 | import cuchaz.enigma.utils.Pair; | ||
| 15 | |||
| 3 | import java.awt.Container; | 16 | import java.awt.Container; |
| 4 | import java.awt.Desktop; | 17 | import java.awt.Desktop; |
| 5 | import java.awt.FlowLayout; | 18 | import java.awt.FlowLayout; |
| @@ -13,21 +26,11 @@ import java.nio.file.Files; | |||
| 13 | import java.nio.file.Path; | 26 | import java.nio.file.Path; |
| 14 | import java.nio.file.Paths; | 27 | import java.nio.file.Paths; |
| 15 | import java.util.*; | 28 | import java.util.*; |
| 29 | import java.util.List; | ||
| 16 | import java.util.stream.Collectors; | 30 | import java.util.stream.Collectors; |
| 17 | import java.util.stream.IntStream; | 31 | import java.util.stream.IntStream; |
| 18 | |||
| 19 | import javax.swing.*; | 32 | import javax.swing.*; |
| 20 | 33 | ||
| 21 | import cuchaz.enigma.config.Config; | ||
| 22 | import cuchaz.enigma.config.Themes; | ||
| 23 | import cuchaz.enigma.gui.Gui; | ||
| 24 | import cuchaz.enigma.gui.dialog.AboutDialog; | ||
| 25 | import cuchaz.enigma.gui.dialog.SearchDialog; | ||
| 26 | import cuchaz.enigma.gui.stats.StatsMember; | ||
| 27 | import cuchaz.enigma.gui.util.ScaleUtil; | ||
| 28 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 29 | import cuchaz.enigma.utils.I18n; | ||
| 30 | import cuchaz.enigma.utils.Pair; | ||
| 31 | 34 | ||
| 32 | import javax.swing.*; | 35 | import 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 @@ | |||
| 1 | package cuchaz.enigma.network; | ||
| 2 | |||
| 3 | import com.google.common.io.MoreFiles; | ||
| 4 | import cuchaz.enigma.*; | ||
| 5 | import cuchaz.enigma.throwables.MappingParseException; | ||
| 6 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 7 | import cuchaz.enigma.translation.mapping.serde.MappingFormat; | ||
| 8 | import cuchaz.enigma.utils.Utils; | ||
| 9 | import joptsimple.OptionParser; | ||
| 10 | import joptsimple.OptionSet; | ||
| 11 | import joptsimple.OptionSpec; | ||
| 12 | |||
| 13 | import java.io.IOException; | ||
| 14 | import java.io.PrintWriter; | ||
| 15 | import java.nio.file.Files; | ||
| 16 | import java.nio.file.Path; | ||
| 17 | import java.nio.file.Paths; | ||
| 18 | import java.util.concurrent.BlockingQueue; | ||
| 19 | import java.util.concurrent.Executors; | ||
| 20 | import java.util.concurrent.LinkedBlockingDeque; | ||
| 21 | import java.util.concurrent.TimeUnit; | ||
| 22 | |||
| 23 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.GuiController; | ||
| 4 | import cuchaz.enigma.network.packet.LoginC2SPacket; | ||
| 5 | import cuchaz.enigma.network.packet.Packet; | ||
| 6 | import cuchaz.enigma.network.packet.PacketRegistry; | ||
| 7 | |||
| 8 | import javax.swing.SwingUtilities; | ||
| 9 | import java.io.DataInput; | ||
| 10 | import java.io.DataInputStream; | ||
| 11 | import java.io.DataOutput; | ||
| 12 | import java.io.DataOutputStream; | ||
| 13 | import java.io.EOFException; | ||
| 14 | import java.io.IOException; | ||
| 15 | import java.net.Socket; | ||
| 16 | import java.net.SocketException; | ||
| 17 | |||
| 18 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.GuiController; | ||
| 4 | import cuchaz.enigma.network.packet.KickS2CPacket; | ||
| 5 | import cuchaz.enigma.network.packet.MessageS2CPacket; | ||
| 6 | import cuchaz.enigma.network.packet.Packet; | ||
| 7 | import cuchaz.enigma.network.packet.PacketRegistry; | ||
| 8 | import cuchaz.enigma.network.packet.RemoveMappingS2CPacket; | ||
| 9 | import cuchaz.enigma.network.packet.RenameS2CPacket; | ||
| 10 | import cuchaz.enigma.network.packet.UserListS2CPacket; | ||
| 11 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 12 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 13 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 14 | import cuchaz.enigma.utils.Message; | ||
| 15 | |||
| 16 | import java.io.DataInput; | ||
| 17 | import java.io.DataInputStream; | ||
| 18 | import java.io.DataOutput; | ||
| 19 | import java.io.DataOutputStream; | ||
| 20 | import java.io.EOFException; | ||
| 21 | import java.io.IOException; | ||
| 22 | import java.net.ServerSocket; | ||
| 23 | import java.net.Socket; | ||
| 24 | import java.net.SocketException; | ||
| 25 | import java.util.ArrayList; | ||
| 26 | import java.util.Collections; | ||
| 27 | import java.util.HashMap; | ||
| 28 | import java.util.HashSet; | ||
| 29 | import java.util.List; | ||
| 30 | import java.util.Map; | ||
| 31 | import java.util.Set; | ||
| 32 | import java.util.concurrent.CopyOnWriteArrayList; | ||
| 33 | |||
| 34 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.mapping.EntryRemapper; | ||
| 4 | |||
| 5 | import javax.swing.*; | ||
| 6 | |||
| 7 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network; | ||
| 2 | |||
| 3 | import java.net.Socket; | ||
| 4 | |||
| 5 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.EnigmaServer; | ||
| 4 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 5 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 6 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 7 | import cuchaz.enigma.utils.Message; | ||
| 8 | import cuchaz.enigma.utils.Utils; | ||
| 9 | |||
| 10 | import java.io.DataInput; | ||
| 11 | import java.io.DataOutput; | ||
| 12 | import java.io.IOException; | ||
| 13 | |||
| 14 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.analysis.EntryReference; | ||
| 4 | import cuchaz.enigma.gui.GuiController; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | |||
| 7 | import java.io.DataInput; | ||
| 8 | import java.io.DataOutput; | ||
| 9 | import java.io.IOException; | ||
| 10 | |||
| 11 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 4 | |||
| 5 | import java.io.DataInput; | ||
| 6 | import java.io.DataOutput; | ||
| 7 | import java.io.IOException; | ||
| 8 | |||
| 9 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.GuiController; | ||
| 4 | |||
| 5 | import java.io.DataInput; | ||
| 6 | import java.io.DataOutput; | ||
| 7 | import java.io.IOException; | ||
| 8 | |||
| 9 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.EnigmaServer; | ||
| 4 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 5 | import cuchaz.enigma.utils.Message; | ||
| 6 | |||
| 7 | import java.io.DataInput; | ||
| 8 | import java.io.DataOutput; | ||
| 9 | import java.io.IOException; | ||
| 10 | import java.util.Arrays; | ||
| 11 | |||
| 12 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 4 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | import cuchaz.enigma.utils.Message; | ||
| 7 | |||
| 8 | import java.io.DataInput; | ||
| 9 | import java.io.DataOutput; | ||
| 10 | import java.io.IOException; | ||
| 11 | |||
| 12 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.analysis.EntryReference; | ||
| 4 | import cuchaz.enigma.gui.GuiController; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | |||
| 7 | import java.io.DataInput; | ||
| 8 | import java.io.DataOutput; | ||
| 9 | import java.io.IOException; | ||
| 10 | |||
| 11 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import java.io.DataInput; | ||
| 4 | import java.io.DataOutput; | ||
| 5 | import java.io.IOException; | ||
| 6 | |||
| 7 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 8 | import cuchaz.enigma.utils.Message; | ||
| 9 | |||
| 10 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import java.io.DataInput; | ||
| 4 | import java.io.DataOutput; | ||
| 5 | import java.io.IOException; | ||
| 6 | |||
| 7 | import cuchaz.enigma.gui.GuiController; | ||
| 8 | import cuchaz.enigma.utils.Message; | ||
| 9 | |||
| 10 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import java.io.DataInput; | ||
| 4 | import java.io.DataOutput; | ||
| 5 | import java.io.IOException; | ||
| 6 | |||
| 7 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.translation.representation.MethodDescriptor; | ||
| 4 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 6 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 7 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 8 | import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; | ||
| 9 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 10 | |||
| 11 | import java.io.DataInput; | ||
| 12 | import java.io.DataOutput; | ||
| 13 | import java.io.IOException; | ||
| 14 | import java.nio.charset.StandardCharsets; | ||
| 15 | |||
| 16 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.GuiController; | ||
| 4 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 5 | |||
| 6 | import java.util.HashMap; | ||
| 7 | import java.util.Map; | ||
| 8 | import java.util.function.Supplier; | ||
| 9 | |||
| 10 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 4 | import cuchaz.enigma.throwables.IllegalNameException; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | import cuchaz.enigma.utils.Message; | ||
| 7 | |||
| 8 | import java.io.DataInput; | ||
| 9 | import java.io.DataOutput; | ||
| 10 | import java.io.IOException; | ||
| 11 | |||
| 12 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.analysis.EntryReference; | ||
| 4 | import cuchaz.enigma.gui.GuiController; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | |||
| 7 | import java.io.DataInput; | ||
| 8 | import java.io.DataOutput; | ||
| 9 | import java.io.IOException; | ||
| 10 | |||
| 11 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.network.ServerPacketHandler; | ||
| 4 | import cuchaz.enigma.throwables.IllegalNameException; | ||
| 5 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 6 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 7 | import cuchaz.enigma.utils.Message; | ||
| 8 | |||
| 9 | import java.io.DataInput; | ||
| 10 | import java.io.DataOutput; | ||
| 11 | import java.io.IOException; | ||
| 12 | |||
| 13 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.analysis.EntryReference; | ||
| 4 | import cuchaz.enigma.gui.GuiController; | ||
| 5 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 6 | |||
| 7 | import java.io.DataInput; | ||
| 8 | import java.io.DataOutput; | ||
| 9 | import java.io.IOException; | ||
| 10 | |||
| 11 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import cuchaz.enigma.gui.GuiController; | ||
| 4 | import cuchaz.enigma.network.EnigmaServer; | ||
| 5 | import cuchaz.enigma.translation.mapping.EntryMapping; | ||
| 6 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 7 | import cuchaz.enigma.translation.mapping.tree.EntryTreeNode; | ||
| 8 | import cuchaz.enigma.translation.mapping.tree.HashEntryTree; | ||
| 9 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 10 | |||
| 11 | import java.io.DataInput; | ||
| 12 | import java.io.DataOutput; | ||
| 13 | import java.io.IOException; | ||
| 14 | import java.util.Collection; | ||
| 15 | import java.util.List; | ||
| 16 | import java.util.stream.Collectors; | ||
| 17 | |||
| 18 | public 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 @@ | |||
| 1 | package cuchaz.enigma.network.packet; | ||
| 2 | |||
| 3 | import java.io.DataInput; | ||
| 4 | import java.io.DataOutput; | ||
| 5 | import java.io.IOException; | ||
| 6 | import java.util.ArrayList; | ||
| 7 | import java.util.List; | ||
| 8 | |||
| 9 | import cuchaz.enigma.gui.GuiController; | ||
| 10 | |||
| 11 | public 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 @@ | |||
| 1 | package cuchaz.enigma.utils; | ||
| 2 | |||
| 3 | import java.io.DataInput; | ||
| 4 | import java.io.DataOutput; | ||
| 5 | import java.io.IOException; | ||
| 6 | import java.util.Objects; | ||
| 7 | |||
| 8 | import cuchaz.enigma.network.packet.PacketHelper; | ||
| 9 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 10 | |||
| 11 | public 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; | |||
| 15 | import org.objectweb.asm.Opcodes; | 15 | import org.objectweb.asm.Opcodes; |
| 16 | 16 | ||
| 17 | import javax.swing.*; | 17 | import javax.swing.*; |
| 18 | import javax.swing.text.BadLocationException; | ||
| 19 | import javax.swing.text.JTextComponent; | ||
| 18 | import java.awt.*; | 20 | import java.awt.*; |
| 19 | import java.awt.event.MouseEvent; | 21 | import java.awt.event.MouseEvent; |
| 20 | import java.io.IOException; | 22 | import java.io.IOException; |
| @@ -22,13 +24,16 @@ import java.io.InputStream; | |||
| 22 | import java.io.InputStreamReader; | 24 | import java.io.InputStreamReader; |
| 23 | import java.net.URI; | 25 | import java.net.URI; |
| 24 | import java.net.URISyntaxException; | 26 | import java.net.URISyntaxException; |
| 27 | import java.nio.charset.StandardCharsets; | ||
| 25 | import java.nio.file.Files; | 28 | import java.nio.file.Files; |
| 26 | import java.nio.file.Path; | 29 | import java.nio.file.Path; |
| 27 | import java.util.Comparator; | 30 | import java.security.MessageDigest; |
| 31 | import java.security.NoSuchAlgorithmException; | ||
| 32 | import java.util.*; | ||
| 28 | import java.util.List; | 33 | import java.util.List; |
| 29 | import java.util.Locale; | ||
| 30 | import java.util.StringJoiner; | ||
| 31 | import java.util.stream.Collectors; | 34 | import java.util.stream.Collectors; |
| 35 | import java.util.zip.ZipEntry; | ||
| 36 | import java.util.zip.ZipFile; | ||
| 32 | 37 | ||
| 33 | public class Utils { | 38 | public 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 | ||
| 14 | import cuchaz.enigma.analysis.ClassCache; | 14 | import cuchaz.enigma.analysis.ClassCache; |
| 15 | import cuchaz.enigma.analysis.index.JarIndex; | 15 | import cuchaz.enigma.analysis.index.JarIndex; |
| 16 | import cuchaz.enigma.network.EnigmaServer; | ||
| 16 | import cuchaz.enigma.source.Decompiler; | 17 | import cuchaz.enigma.source.Decompiler; |
| 17 | import cuchaz.enigma.source.Decompilers; | 18 | import cuchaz.enigma.source.Decompilers; |
| 18 | import cuchaz.enigma.source.SourceSettings; | 19 | import 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"); |