From 854f4d49407e45d67dd5754afd21a7e59970ca5b Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Sun, 3 May 2020 21:06:38 +0100 Subject: Multiplayer support (#221) * First pass on multiplayer * Apply review suggestions * Dedicated Enigma server * Don't jump to references when other users do stuff * Better UI + translations * french translation * Apply review suggestions * Document the protocol * Fix most issues with scrolling. * Apply review suggestions * Fix zip hash issues + add a bit more logging * Optimize zip hash * Fix a couple of login bugs * Add message log and user list * Make Message an abstract class * Make status bar work, add chat box * Hide message log/users list when not connected * Fix status bar not resetting entirely * Run stop server task on server thread to prevent multithreading race conditions * Add c2s message to packet id list * Fix message scroll bar not scrolling to the end * Formatting * User list size -> ushort * Combine contains and remove check * Check removal before sending packet * Add password to login packet * Fix the GUI closing the rename text field when someone else renames something * Update fr_fr.json * oups * Make connection/server create dialogs not useless if it fails once * Refactor UI state updating * Fix imports * Fix Collab menu * Fix NPE when rename not allowed * Make the log file a configurable option * Don't use modified UTF * Update fr_fr.json * Bump version to 0.15.4 * Apparently I can't spell neither words nor semantic versions Co-authored-by: Yanis48 Co-authored-by: 2xsaiko --- src/main/java/cuchaz/enigma/utils/Message.java | 392 +++++++++++++++++++++++++ src/main/java/cuchaz/enigma/utils/Utils.java | 64 +++- 2 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 src/main/java/cuchaz/enigma/utils/Message.java (limited to 'src/main/java/cuchaz/enigma/utils') 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 0000000..d7c5f23 --- /dev/null +++ b/src/main/java/cuchaz/enigma/utils/Message.java @@ -0,0 +1,392 @@ +package cuchaz.enigma.utils; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Objects; + +import cuchaz.enigma.network.packet.PacketHelper; +import cuchaz.enigma.translation.representation.entry.Entry; + +public abstract class Message { + + public final String user; + + public static Chat chat(String user, String message) { + return new Chat(user, message); + } + + public static Connect connect(String user) { + return new Connect(user); + } + + public static Disconnect disconnect(String user) { + return new Disconnect(user); + } + + public static EditDocs editDocs(String user, Entry entry) { + return new EditDocs(user, entry); + } + + public static MarkDeobf markDeobf(String user, Entry entry) { + return new MarkDeobf(user, entry); + } + + public static RemoveMapping removeMapping(String user, Entry entry) { + return new RemoveMapping(user, entry); + } + + public static Rename rename(String user, Entry entry, String newName) { + return new Rename(user, entry, newName); + } + + public abstract String translate(); + + public abstract Type getType(); + + public static Message read(DataInput input) throws IOException { + byte typeId = input.readByte(); + if (typeId < 0 || typeId >= Type.values().length) { + throw new IOException(String.format("Invalid message type ID %d", typeId)); + } + Type type = Type.values()[typeId]; + String user = input.readUTF(); + switch (type) { + case CHAT: + String message = input.readUTF(); + return chat(user, message); + case CONNECT: + return connect(user); + case DISCONNECT: + return disconnect(user); + case EDIT_DOCS: + Entry entry = PacketHelper.readEntry(input); + return editDocs(user, entry); + case MARK_DEOBF: + entry = PacketHelper.readEntry(input); + return markDeobf(user, entry); + case REMOVE_MAPPING: + entry = PacketHelper.readEntry(input); + return removeMapping(user, entry); + case RENAME: + entry = PacketHelper.readEntry(input); + String newName = input.readUTF(); + return rename(user, entry, newName); + default: + throw new IllegalStateException("unreachable"); + } + } + + public void write(DataOutput output) throws IOException { + output.writeByte(getType().ordinal()); + PacketHelper.writeString(output, user); + } + + private Message(String user) { + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Message message = (Message) o; + return Objects.equals(user, message.user); + } + + @Override + public int hashCode() { + return Objects.hash(user); + } + + public enum Type { + CHAT, + CONNECT, + DISCONNECT, + EDIT_DOCS, + MARK_DEOBF, + REMOVE_MAPPING, + RENAME, + } + + public static final class Chat extends Message { + + public final String message; + + private Chat(String user, String message) { + super(user); + this.message = message; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeString(output, message); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.chat.text"), user, message); + } + + @Override + public Type getType() { + return Type.CHAT; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Chat chat = (Chat) o; + return Objects.equals(message, chat.message); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), message); + } + + @Override + public String toString() { + return String.format("Message.Chat { user: '%s', message: '%s' }", user, message); + } + + } + + public static final class Connect extends Message { + + private Connect(String user) { + super(user); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.connect.text"), user); + } + + @Override + public Type getType() { + return Type.CONNECT; + } + + @Override + public String toString() { + return String.format("Message.Connect { user: '%s' }", user); + } + + } + + public static final class Disconnect extends Message { + + private Disconnect(String user) { + super(user); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.disconnect.text"), user); + } + + @Override + public Type getType() { + return Type.DISCONNECT; + } + + @Override + public String toString() { + return String.format("Message.Disconnect { user: '%s' }", user); + } + + } + + public static final class EditDocs extends Message { + + public final Entry entry; + + private EditDocs(String user, Entry entry) { + super(user); + this.entry = entry; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.edit_docs.text"), user, entry); + } + + @Override + public Type getType() { + return Type.EDIT_DOCS; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + EditDocs editDocs = (EditDocs) o; + return Objects.equals(entry, editDocs.entry); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry); + } + + @Override + public String toString() { + return String.format("Message.EditDocs { user: '%s', entry: %s }", user, entry); + } + + } + + public static final class MarkDeobf extends Message { + + public final Entry entry; + + private MarkDeobf(String user, Entry entry) { + super(user); + this.entry = entry; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.mark_deobf.text"), user, entry); + } + + @Override + public Type getType() { + return Type.MARK_DEOBF; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + MarkDeobf markDeobf = (MarkDeobf) o; + return Objects.equals(entry, markDeobf.entry); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry); + } + + @Override + public String toString() { + return String.format("Message.MarkDeobf { user: '%s', entry: %s }", user, entry); + } + + } + + public static final class RemoveMapping extends Message { + + public final Entry entry; + + private RemoveMapping(String user, Entry entry) { + super(user); + this.entry = entry; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.remove_mapping.text"), user, entry); + } + + @Override + public Type getType() { + return Type.REMOVE_MAPPING; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + RemoveMapping that = (RemoveMapping) o; + return Objects.equals(entry, that.entry); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry); + } + + @Override + public String toString() { + return String.format("Message.RemoveMapping { user: '%s', entry: %s }", user, entry); + } + + } + + public static final class Rename extends Message { + + public final Entry entry; + public final String newName; + + private Rename(String user, Entry entry, String newName) { + super(user); + this.entry = entry; + this.newName = newName; + } + + @Override + public void write(DataOutput output) throws IOException { + super.write(output); + PacketHelper.writeEntry(output, entry); + PacketHelper.writeString(output, newName); + } + + @Override + public String translate() { + return String.format(I18n.translate("message.rename.text"), user, entry, newName); + } + + @Override + public Type getType() { + return Type.RENAME; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Rename rename = (Rename) o; + return Objects.equals(entry, rename.entry) && + Objects.equals(newName, rename.newName); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entry, newName); + } + + @Override + public String toString() { + return String.format("Message.Rename { user: '%s', entry: %s, newName: '%s' }", user, entry, newName); + } + + } + +} diff --git a/src/main/java/cuchaz/enigma/utils/Utils.java b/src/main/java/cuchaz/enigma/utils/Utils.java index b8f2ec2..b45b00d 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; import org.objectweb.asm.Opcodes; import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.event.MouseEvent; import java.io.IOException; @@ -22,13 +24,16 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Comparator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; import java.util.List; -import java.util.Locale; -import java.util.StringJoiner; import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; public class Utils { @@ -98,6 +103,19 @@ public class Utils { manager.setInitialDelay(oldDelay); } + public static Rectangle safeModelToView(JTextComponent component, int modelPos) { + if (modelPos < 0) { + modelPos = 0; + } else if (modelPos >= component.getText().length()) { + modelPos = component.getText().length(); + } + try { + return component.modelToView(modelPos); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + public static boolean getSystemPropertyAsBoolean(String property, boolean defValue) { String value = System.getProperty(property); return value == null ? defValue : Boolean.parseBoolean(value); @@ -111,6 +129,34 @@ public class Utils { } } + public static byte[] zipSha1(Path path) throws IOException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + // Algorithm guaranteed to be supported + throw new RuntimeException(e); + } + try (ZipFile zip = new ZipFile(path.toFile())) { + List entries = Collections.list(zip.entries()); + // only compare classes (some implementations may not generate directory entries) + entries.removeIf(entry -> !entry.getName().toLowerCase(Locale.ROOT).endsWith(".class")); + // different implementations may add zip entries in a different order + entries.sort(Comparator.comparing(ZipEntry::getName)); + byte[] buffer = new byte[8192]; + for (ZipEntry entry : entries) { + digest.update(entry.getName().getBytes(StandardCharsets.UTF_8)); + try (InputStream in = zip.getInputStream(entry)) { + int n; + while ((n = in.read(buffer)) != -1) { + digest.update(buffer, 0, n); + } + } + } + } + return digest.digest(); + } + public static String caplisiseCamelCase(String input){ StringJoiner stringJoiner = new StringJoiner(" "); for (String word : input.toLowerCase(Locale.ROOT).split("_")) { @@ -118,4 +164,16 @@ public class Utils { } return stringJoiner.toString(); } + + public static boolean isBlank(String input) { + if (input == null) { + return true; + } + for (int i = 0; i < input.length(); i++) { + if (!Character.isWhitespace(input.charAt(i))) { + return false; + } + } + return true; + } } -- cgit v1.2.3