//  [java]
    public void translate(GeyserSession session, BlockEntityDataPacket packet) {
        NbtMap tag = packet.getData();
        if (tag.getString("id").equals("Sign")) {
            // This is the reason why this all works - Bedrock sends packets every time you update the sign, Java only wants the final packet
            // But Bedrock sends one final packet when you're done editing the sign, which should be equal to the last message since there's no edits
            // So if the latest update does not match the last cached update then it's still being edited
            if (!tag.getString("Text").equals(session.getLastSignMessage())) {
                session.setLastSignMessage(tag.getString("Text"));
                return;
            }
            // Otherwise the two messages are identical and we can get to work deconstructing
            StringBuilder newMessage = new StringBuilder();
            // While Bedrock's sign lines are one string, Java's is an array of each line
            // (Initialized all with empty strings because it complains about null)
            String[] lines = new String[] {"", "", "", ""};
            int iterator = 0;
            // Keep track of the width of each character
            // If it goes over the maximum, we need to start a new line to match Java
            int widthCount = 0;
            // This converts the message into the array'd message Java wants
            for (char character : tag.getString("Text").toCharArray()) {
                widthCount += SignUtils.getCharacterWidth(character);
                // If we get a return in Bedrock, or go over the character width max, that signals to use the next line.
                if (character == '\n' || widthCount > SignUtils.JAVA_CHARACTER_WIDTH_MAX) {
                    // We need to apply some more logic if we went over the character width max
                    boolean wentOverMax = widthCount > SignUtils.JAVA_CHARACTER_WIDTH_MAX && character != '\n';
                    widthCount = 0;
                    // Saves if we're moving a word to the next line
                    String word = null;
                    if (wentOverMax && iterator < lines.length - 1) {
                        // If we went over the max, we want to try to wrap properly like Bedrock does.
                        // So we look for a space in the Bedrock user's text to imply a word.
                        int index = newMessage.lastIndexOf(" ");
                        if (index != -1) {
                            // There is indeed a space in this line; let's get it
                            word = newMessage.substring(index + 1);
                            // 'Delete' that word from the string builder
                            newMessage.delete(index, newMessage.length());
                        }
                    }
                    lines[iterator] = newMessage.toString();
                    iterator++;
                    // Bedrock, for whatever reason, can hold a message out of the bounds of the four lines
                    // We don't care about that so we discard that
                    if (iterator > lines.length - 1) {
                        break;
                    }
                    newMessage = new StringBuilder();
                    if (wentOverMax) {
                        // Apply the wrapped word to the new line
                        if (word != null) {
                            newMessage.append(word);
                            // And apply the width count
                            for (char wordCharacter : word.toCharArray()) {
                                widthCount += SignUtils.getCharacterWidth(wordCharacter);
                            }
                        }
                        // If we went over the max, we want to append the character to the new line.
                        newMessage.append(character);
                        widthCount += SignUtils.getCharacterWidth(character);
                    }
                } else newMessage.append(character);
            }
            // Put the final line on since it isn't done in the for loop
            if (iterator < lines.length) lines[iterator] = newMessage.toString();
            Position pos = new Position(tag.getInt("x"), tag.getInt("y"), tag.getInt("z"));
            ClientUpdateSignPacket clientUpdateSignPacket = new ClientUpdateSignPacket(pos, lines);
            session.sendDownstreamPacket(clientUpdateSignPacket);

            // We set the sign text cached in the session to null to indicate there is no work-in-progress sign
            session.setLastSignMessage(null);

        } else if (tag.getString("id").equals("JigsawBlock")) {
            // Client has just sent a jigsaw block update
            Position pos = new Position(tag.getInt("x"), tag.getInt("y"), tag.getInt("z"));
            String name = tag.getString("name");
            String target = tag.getString("target");
            String pool = tag.getString("target_pool");
            String finalState = tag.getString("final_state");
            String joint = tag.getString("joint");
            ClientUpdateJigsawBlockPacket jigsawPacket = new ClientUpdateJigsawBlockPacket(pos, name, target, pool,
                    finalState, joint);
            session.sendDownstreamPacket(jigsawPacket);
        }

    }