[LIB] Fanciful: pleasant chat message formatting

Discussion in 'Resources' started by mkremins, Nov 15, 2013.

Thread Status:
Not open for further replies.
  1. Offline

    mkremins

    Fanciful – originally inspired by @afistofirony's SpecialMessage library – provides your plugins with a developer-friendly way to access the new chat formatting features introduced by the Minecraft 1.7 chat protocol.

    Instead of using XML-ish strings to format chat messages, Fanciful provides a fluent builder for the same purpose. To create a formatted chat message, instantiate a FancyMessage object and start calling methods to tweak formatting or add more text. Then, once everything is set up, just call .toJSONString() on the builder and send the returned JSON off to a player or three.

    The result looks a little like this.
    Code:
    import static org.bukkit.ChatColor.*;
    import mkremins.fanciful.FancyMessage;
    
    public final class Example {
    
        public static void main(String[] args) {
            System.out.println(welcome("Orbixitron"));
            System.out.println(advertisement());
            System.out.println(gui("Starbux42", 413000));
        }
    
        static String welcome(String playername) {
            return new FancyMessage("Hello, ")
                .color(YELLOW)
            .then(playername)
                .color(LIGHT_PURPLE)
                .style(ITALIC, UNDERLINE)
            .then("!")
                .color(YELLOW)
                .style(ITALIC)
            .toJSONString();
        }
    
        static String advertisement() {
            return new FancyMessage("Visit ")
                .color(GREEN)
            .then("our website")
                .color(YELLOW)
                .style(UNDERLINE)
                .link("http://awesome-server.net")
                .tooltip("AwesomeServer Forums")
            .then(" to win ")
                .color(GREEN)
            .then("big prizes!")
                .color(AQUA)
                .style(BOLD)
                .tooltip("Terms and conditions may apply. Offer not valid in Sweden.")
            .toJSONString();
        }
    
        static String gui(String playername, int blocksEdited) {
            return new FancyMessage("Player ")
                .color(DARK_RED)
            .then(playername)
                .color(RED)
                .style(ITALIC)
            .then(" changed ").color(DARK_RED)
            .then(blocksEdited).color(AQUA)
            .then(" blocks. ").color(DARK_RED)
            .then("Roll back?")
                .color(GOLD)
                .style(UNDERLINE)
                .suggest("/rollenbacken " + playername)
                .tooltip("Be careful, this might undo legitimate edits!")
            .then(" ")
            .then("Ban?")
                .color(RED)
                .style(UNDERLINE)
                .suggest("/banhammer " + playername)
                .tooltip("Remember: only ban if you have photographic evidence of grief.")
            .toJSONString();
        }
    
    }
    
    Under the hood, this works because every one of the method calls prior to that last invocation of .toJSONString() returns the FancyMessage object after operating on it. As a result, you can engage in method chaining indefinitely before deciding to cap off the message at the end. Fans of the StringBuilder will probably find this pretty familiar.

    Want to install? For best results, use Maven:
    Code:
    <repository>
      <id>fanciful-mvn-repo</id>
      <url>http://repo.franga2000.com/artifactory/public</url>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>always</updatePolicy>
      </snapshots>
    </repository>
    
    <dependency>
      <groupId>mkremins</groupId>
      <artifactId>fanciful</artifactId>
      <version>0.3.1</version>
    </dependency>
    
    Fanciful is far from complete, and there's a bunch of stuff I'd like to do with it if I have the time:
    • Use a proper JSON library, like Google's GSON, to generate JSON. Right now, generation is completely ad-hoc and done entirely with StringBuilders, and there's all kinds of potential escaping issues as a result.
    • Make it available through a public Maven repository so that people can use it as a proper dependency.
    • Properly document the public API.
    • Dump the ad-hoc "ChatStyle" enum I created to have a quick way of distinguishing between color formatting and style formatting (red/green vs italic/bold, etc) and do proper checks against Bukkit's ChatColor enum instead.
    • Optimizations and even moar features.
    It might be a while before I get around to some of this stuff, but Fanciful is open source (of course), and pull requests are always appreciated ;)
     
    Last edited: Jan 9, 2015
    GrandmaJam, leon3001, 4ever and 23 others like this.
  2. Offline

    macguy8

    How does one then send the JSON string to the player?
     
  3. Offline

    afistofirony

    mkremins Very nice! I was originally considering doing it this way, but I chose to use the XML format in order to support customisable message files. :p

    Also, you may want to consider the following:
    • Auto-escape for apostrophes, quotes, and commas
    • New-line support for tooltips (see this: lines 123-50)
     
    mkremins likes this.
  4. Offline

    mkremins

    That makes a lot of sense, and it's an advantage of the XML-ish format that I hadn't fully considered. Seems like each of our libraries has a niche of its own in that regard – yours is way easier to expose to server administrators for use in configuration files, whereas mine makes life easier for plugin developers who are hard-coding most of their messages at compile time.

    Might be neat to build some interop with your library into mine – allow people to substitute XML-ish strings into a FancyMessage object in place of an ordinary message section with hard-coded styles at run time. I'll add it to the TODO list :p

    This is one of the things that switching over to a proper JSON library (which I'm intending to do ASAP) will probably give me for free. Definitely important to address, though – right now there's all sorts of potential escaping issues in the JSON I'm putting out.

    Oh wow, that's clever – using item lore for multi-line tooltips is one of those hacks I wish I'd thought of myself. Fluent formatting for text inside tooltips would be nice to have as well; that's another idea I'll have to toss onto the pile.

    I'm not certain if the Bukkit API includes a way to do this yet; most of the new Minecraft chat protocol features this library leverages weren't added until 1.7.x, and Bukkit hasn't yet been updated to properly support the new protocol. However, people seem to be reporting that there's a way to send JSON messages to players and have them be formatted correctly on the client side by manipulating packets.

    I'll add an explanation of whatever becomes the idiomatic way to send raw JSON messages to players to the thread's OP once some of the dust settles around the 1.7 update and it becomes more reasonable to use this library in production.
     
  5. Offline

    afistofirony


    I'm already hard at work on that. Since my library supports adding custom element mappers, I can easily create a code-based processor. :p

    Also, you can send these messages via (player).sendRawMessage(String) (I think).

    EDIT: Also, if you do plan to add support for multi-line tooltips, be wary of colons (and commas, but those can be escaped with the backslash) any lines other than the first - although colons in Strings are considered valid JSON, Minecraft's JSON parser crashes and burns when trying to parse colons inside the Lore array (you'll get "Invalid Item" instead of the lines you want).

    The workaround I found (almost by accident) was to introduce a key before the actual text, so you'd have something like a:"Colon:" (the name of the key does not matter).
     
    mkremins likes this.
  6. Offline

    mkremins

    Today's progress: instead of forcing you to pull in a separate ChatStyle enum to use non-color text formatting features, you can now use the appropriate constants from Bukkit's own ChatColor enum instead. Additionally, you can now use Fanciful as a Maven dependency. Just add the repository and dependency information to your POM.
    Code:
    <repository>
      <id>fanciful-mvn-repo</id>
      <url>https://raw.github.com/mkremins/fanciful/mvn-repo/</url>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>always</updatePolicy>
      </snapshots>
    </repository>
     
    <dependency>
      <groupId>mkremins</groupId>
      <artifactId>fanciful</artifactId>
      <version>0.1-SNAPSHOT</version>
    </dependency>
    
    Note that I'm using a branch of the project's public GitHub repository as a Maven repo to host the project's artifacts. This is a non-idiomatic way to use Maven, and probably falls well into the territory of "terrible hack". That said, it's also dead simple compared to trying to get into an existing Maven repository, and I might post a tutorial on how it's done pretty soon ;)
     
  7. Offline

    RyanTheLeach

    This is nice, but I can't help but feel that Bukkit won't be exposing the API for the JSON any time soon if NBT is anything to go by, it's an implementation detail.

    The good news is that your library and perhaps afistofirony 's could be combined to make a killer pull request for the new API and have it native to bukkit.
     
  8. Offline

    mkremins

    Today's progress: Fanciful now uses the external library org.json/json to generate JSON output, meaning that all emitted JSON is now properly escaped (no more issues with quotes inside strings) and conforms to the official JSON specification (keys are double-quoted, strings are wrapped in double rather than single quotes).

    Next up is better documentation (including Javadocs for all the public API methods), and then on to new features like multi-line tooltips and a tooltip-formatting API.
     
  9. Offline

    ftbastler

    mkremins Really cool stuff. Do I need to include the json library now?
     
  10. Offline

    mkremins

    As long as you use Fanciful as a Maven dependency, Maven will take care of the JSON library for you – you don't have to include it in your own project.
     
  11. Offline

    Garris0n

    Submitted a PR with a .send() method because player.sendRawMessage() functions about the same as .sendMessage() (sendMessage actually uses sendRawMessage, it just adds in some conversation check).
     
    mkremins likes this.
  12. Offline

    ftbastler

    mkremins Jeah but I don't use Maven with my project.
     
  13. Offline

    mkremins

    In that case, you'll probably need to include the JSON library in your own project. I'm not really able to help you much there; I don't know much about Java dependency resolution and prefer to let build tools handle that sort of thing on my behalf. I do know that you can locate the JSON library's source on GitHub.

    Today's progress: thanks to Garris0n, we've now got a working FancyMessage.send(Player) method that you can use to send your messages to online players. Unfortunately, this does introduce OBC and NMS dependencies into the library, something I'm hoping to find a way around in the near future.

    The version's been bumped to 0.1.1, and I've pushed a build of the new version to the Maven repository. Happy hacking!

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Oct 29, 2015
  14. Offline

    Garris0n

    It should be possible to do through reflection instead of NMS/OBC if anybody wants to make it version-independent.
     
  15. Offline

    SoThatsIt

    fixed it to work using reflection, so there is no dependency for NBC/OBC.

    I have just grabbed the methods i require from my reflectionutils class.

    Code:
    public static boolean ClassListEqual(Class< ? >[] l1, Class< ? >[] l2)
        {
            boolean equal = true;
         
            if ( l1.length != l2.length )
                return false;
            for ( int i = 0; i < l1.length; i++ )
            {
                if ( l1[i] != l2[i] )
                {
                    equal = false;
                    break;
                }
            }
         
            return equal;
        }
     
        public static Method getMethod(Class< ? > cl, String method)
        {
            for ( Method m : cl.getMethods() )
            {
                if ( m.getName().equals(method) )
                {
                    return m;
                }
            }
            return null;
        }
     
        public static void sendPacket(Player p, Object packet)
        {
            try
            {
                Object nmsPlayer = getHandle(p);
                Field con_field = nmsPlayer.getClass().getField("playerConnection");
                Object con = con_field.get(nmsPlayer);
                Method packet_method = getMethod(con.getClass() , "sendPacket");
                packet_method.invoke(con , packet);
            }
            catch (SecurityException e)
            {
                e.printStackTrace();
            }
            catch (IllegalArgumentException e)
            {
                e.printStackTrace();
            }
            catch (IllegalAccessException e)
            {
                e.printStackTrace();
            }
            catch (InvocationTargetException e)
            {
                e.printStackTrace();
            }
            catch (NoSuchFieldException e)
            {
                e.printStackTrace();
            }
        }
     
        public static Class< ? > getCraftClass(String ClassName)
        {
            String name = Bukkit.getServer().getClass().getPackage().getName();
            String version = name.substring(name.lastIndexOf('.') + 1) + ".";
            String className = "net.minecraft.server." + version + ClassName;
            Class< ? > c = null;
            try
            {
                c = Class.forName(className);
            }
            catch (ClassNotFoundException e)
            {
                e.printStackTrace();
            }
            return c;
        }
     
        public static Object getHandle(Object entity)
        {
            Object nms_entity = null;
            Method entity_getHandle = getMethod(entity.getClass() , "getHandle");
            try
            {
                nms_entity = entity_getHandle.invoke(entity);
            }
            catch (IllegalArgumentException e)
            {
                e.printStackTrace();
            }
            catch (IllegalAccessException e)
            {
                e.printStackTrace();
            }
            catch (InvocationTargetException e)
            {
                e.printStackTrace();
            }
            return nms_entity;
        }
     
        public void send(Player player)
        {
            Class< ? > IChatBaseComponent = getCraftClass("IChatBaseComponent");
            Class< ? > PacketPlayOutChat = getCraftClass("PacketPlayOutChat");
            Class< ? > ChatSerializer = getCraftClass("ChatSerializer");
            Method a = ChatSerializer.getDeclaredMethod("a" , IChatBaseComponent);
         
            Object json = a.invoke(null , toJSONString());
            Object packet = PacketPlayOutChat.getConstructor(IChatBaseComponent).newInstance(json);
         
            sendPacket(player , packet);
        }
     
    mkremins and Garris0n like this.
  16. Offline

    xTrollxDudex

    mkremins
    Better post the tutorial on a Github-Maven repo somewhere not on the resources ;)

    For Bukkit related content only. Where'd you learn about the github-maven repos from?
    SoThatsIt
    Reflection is something to avoid at certain times, although convinient, changes to the obfuscation causes a whole console of errors.
     
  17. Offline

    mkremins

    I don't have time to write up a full explanation of the process right now, but this StackOverflow thread and this blog post both pointed me in the right direction. I used the GitHub Maven plugins to get everything set up such that mvn deploy pushes an updated Maven artifact to the mvn-repo branch of the Fanciful GitHub repository.

    Thanks for this, I'll take a look at integrating it into the library as soon as I get a chance :)
     
  18. Offline

    xTrollxDudex

    mkremins
    No way.... That's almost the exact way I did it, except I added a CI Server and automatically pushed a JavaDoc to GitHub Pages :O
     
  19. Offline

    SoThatsIt

    although reflection may cause errors on some updates, its better than updating your plugins every update
     
    CraftThatBlock likes this.
  20. Offline

    jorisk322

    The Bukkit team doesn't agree (and neither do I). They made NMS packages include the version for a reason. Changed internal methods can really cause big problems.
     
    Paxination and Garris0n like this.
  21. Offline

    Paxination

    jorisk322

    There for forcing you to update to make sure your plugin doesnt do something that could just crash your server and you lose all your data! Basically adding a safety check if you decide to use the volatile NMS code.
     
  22. Offline

    rbrick

    very nice! i will be using this :)
     
  23. Offline

    viper_monster

    mkremins I have imported this using the maven report, but every time when I execute code that has something to do with your FancyMessage class it throws an error that that class can't be found? :O

    EDIT: Umm, nvm...
     
  24. Offline

    Garris0n

  25. Offline

    mkremins

    Thanks to this patch, I've just released version 0.1.2, which adds an overloaded FancyMessage.itemTooltip(ItemStack item) method. You can now specify an item tooltip using an ItemStack directly, rather than a hand-serialized string representation of the ItemStack in question.
     
    Garris0n likes this.
  26. Offline

    MrVerece

    Is it possible to use your libraries to execute own Java methods when a Player clicks on a text in chat?
     
  27. Offline

    Stoux

    Has anyone figured out how to add enters (\n) to show text on hover? If so, would anyone be so kind to point me in the right direction :)?
     
  28. Offline

    Garris0n

    You can use the item display to do that. I've been meaning to make it do that automatically with \n, but I haven't gotten around to it :p
     
  29. Offline

    mkremins

    The straightforward answer is no, because that's not something the Minecraft chat protocol allows. The more roundabout answer is yes, in an indirect and sort of hacky way: you can add a click action that causes the player to execute a command (something the Minecraft chat protocol does allow) when part of a message is clicked, then run arbitrary code (e.g. execute whatever Java methods you'd like) in the body of your plugin's handler for the executed command.

    If you want to actually pull this off, you can use the FancyMessage.command(String command) method to add a click action that executes a command to part of your message.

    This isn't yet supported in the library proper, but it's definitely in the cards for the future (I'd love to see a pull request for it). Anyone who wants to try their hand at implementing it should probably take a look at this snippet that afistofirony posted further up in this thread; it's his own library's implementation of multi-line tooltips using \n and item descriptions, and it accounts for a handful of potentially ugly edge cases that an implementor would probably want to know about.

    Quick update (@Stoux, Garris0n): I had a little bit of extra time and decided to try my hand at implementing a really naïve version of multiline tooltips. I wouldn't recommend anyone try to use it in production yet as it's all but untested, but if you want to play around with it you can update your Fanciful dependency version to 0.1.3-RC1 and try it out.

    In the process, I also fixed an issue that was causing every one of my Maven deployments of the library to overwrite past versions in the repository, effectively preventing people from depending on any but the latest version. Now that that's been fixed, I'll be able to safely deploy "release candidates" (like 0.1.3-RC1) for testing purposes without screwing up anyone's setup who's still depending on the previous stable release.

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Oct 29, 2015
  30. Offline

    Aikar

    mkremins likes this.
Thread Status:
Not open for further replies.

Share This Page