Minecraft Modding: Making A Mod Version Checker

Update


I haven't tried this yet, but in the @Mod annotation class, there is now an updateJSON parameter that can contain a "URL to a JSON file that will be checked once per launch to determine if there is an updated version of this mod and notify the end user. The format is defined here: https://gist.github.com/LexManos/7aacb9aa991330523884.

Background


Thanks to Jwosty for suggesting the approach.

It is often nice to notify people when they are running an old version of the mod.  It is possible to have the mod compare its version against information posted somewhere on the web.  So if you are able to host a web page, you can publish the latest version there for the mod to check.

You will want to do this on the client side, so will need to perform this someplace like your client proxy class (probably in the post-init handler method). Furthermore, since it will go off and check a web site it is considered better practice to have the code run in its own thread so it doesn't block rest of execution.

Create The Version Checking Class (Runnable As Separate Thread)


So first you need to make a VersionChecker class that implements Runnable (this will allow it to run in its own thread):

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

import org.apache.commons.io.IOUtils;

/**
 * @author jabelar
 *
 */
public class VersionChecker implements Runnable
{
    private static boolean isLatestVersion = false;
    private static String latestVersion = "";

    /* (non-Javadoc)
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() 
    {
        InputStream in = null;
        try 
        {
            in = new URL("https://raw.githubusercontent.com/jabelar/MagicBeans-1.7.10/master/src/main/java/com/blogspot/jabelarminecraft/magicbeans/version_file").openStream();
        } 
        catch 
        (MalformedURLException e) 
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } 
        catch (IOException e) 
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        try 
        {
            latestVersion = IOUtils.readLines(in).get(0);
        } 
        catch (IOException e) 
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } 
        finally 
        {
            IOUtils.closeQuietly(in);
        }
        System.out.println("Latest mod version = "+latestVersion);
        isLatestVersion = MagicBeans.MODVERSION.equals(latestVersion);
        System.out.println("Are you running latest version = "+isLatestVersion);
    }
    
    public boolean isLatestVersion()
    {
     return isLatestVersion;
    }
    
    public String getLatestVersion()
    {
     return latestVersion;
    }

Of course "MagicBeans" should be replaced with your mod's main class, and this assumes that you use the standard practice of making constants like MODVERSION in your main class.  In any case, you just need to check for the version you have against the version from the URL.

Post Latest Version Information On Some Web Site


Next you need to get the information to your URL.  An easy way if you're using github is to create a text file in your project that contains the version information on the first line.  Then sync your project to the github, go to github website and find your file, then copy the URL (use the raw view) and put that into your VersionChecker class above.

Start The Version Checker Thread In Your Client Proxy Post-Init Handling


Next you need code to actually start the thread.  So in your post-init handling method (i.e. the @EventHandler method that takes FMLPostInitializationEvent parameter) in your client proxy you need to add this:

MagicBeans.versionChecker = new VersionChecker();
Thread versionCheckThread = new Thread(MagicBeans.versionChecker, "Version Check");
versionCheckThread.start();

Again, replace "MagicBeans" with the name of your main class.

Tip: The version checking should only be run on client side.  It won't technically hurt to run it on server, but is a waste of resources and generally bad practice if you don't need it.

Create the Clickable Warning Message When Player First Plays


In the case where the version isn't latest, you probably want a chat message to appear that is clickable to allow people to go to your site to download the latest version.  You want the message to only appear once, so need to remember if you've already shown the message.

Clickable chat messages are available through the ChatStyle.setChatClickEvent() method where the click event can have an action enumerated type of OPEN_URL.

To display a chat message, you need to make sure that you're actually in the game and not just in the loading screen.  Furthermore, I tried various events like player joining and found that I think the behavior is best to just use the PlayerTickEvent but remember if you've already warned them.

Putting it all together, in your main class you want to create some public fields for the version checker instance as well as a field to remember if you've already warned the player.  So in your main class:

// Version checking instance
public static VersionChecker versionChecker;
public static boolean haveWarnedVersionOutOfDate = false;

Then you need to handle the PlayerTickEvent by putting something like this into a method that is registered as a handler to the FML event bus (see my tutorial on events for more information on how to do that):

@SubscribeEvent(priority=EventPriority.NORMAL, receiveCanceled=true)
public void onEvent(PlayerTickEvent event)
{
  
    if (!MagicBeans.haveWarnedVersionOutOfDate && event.player.worldObj.isRemote 
          && !MagicBeans.versionChecker.isLatestVersion())
    {
        ClickEvent versionCheckChatClickEvent = new ClickEvent(ClickEvent.Action.OPEN_URL, 
              "http://jabelarminecraft.blogspot.com");
        ChatStyle clickableChatStyle = new ChatStyle().setChatClickEvent(versionCheckChatClickEvent);
        ChatComponentText versionWarningChatComponent = 
              new ChatComponentText("Your Magic Beans Mod is not latest version!  Click here to update.");
        versionWarningChatComponent.setChatStyle(clickableChatStyle);
        event.player.addChatMessage(versionWarningChatComponent);
        MagicBeans.haveWarnedVersionOutOfDate = true;
    }
  
}

Hopefully you can follow along with the code.  Honestly, the chat component stuff is pretty confusing (I'll make tutorial soon).  But basically I'm just trying to send a message that is clickable and to do that I am creating a style that is associated with a click event that opens a URL.

Key Point: Change the URL to point to your own page to download the latest version of the mod.  Of course also change the references to Magic Beans to match your own mod's name.

Set Your MODVERSION String

The VersionChecker class compares the string value it finds on your website with the MODVERSION string field from your mod.   So you need to make sure your mod has something like this in the main class:

public static final String MODVERSION = "0.0.1";

Testing It Out


Okay, to test that it is working.  You can follow these steps to confirm things are working properly:

  1. Make sure your version information is properly at the website that will be checked.
  2. Run Minecraft while checking the console.  During loading at some point you should see messages from the VersionChecker class indicating what version that it found and whether it is considered the latest version.
  3. Create a single player game and look for chat message.  If the version is latest (meaning your website string matches your MODVERSION string) then no chat message will display.  If it doesn't match, a chat message should display.  
  4. If warning message appears, press "T" to enter chat mode and then use the mouse to click on the text.  It should bring up a dialog asking if you really want to follow the link, then should open your browser to the link you put in.

Try testing with and without the versions matching to confirm proper operation.

Conclusion And Further Thoughts


I hope you found this helpful.  There are certainly areas where this can be further improved:
  • The exception handling in my example is pretty lame (should really have custom handling)
  • My approach is assuming thread-safe interaction because the version checking thread should be completed long before the fields are accessed.  But it would be better to do more to ensure thread-safe interaction with the fields shared between threads.
As always, please write if you have questions or corrections.


14 comments:

  1. does this works for 1.8? i can't get it to work

    ReplyDelete
  2. Yes, I am using it for 1.8 as well. What doesn't work for you? Is it getting an error, or just doing nothing? Do you have code I can review?

    ReplyDelete
    Replies
    1. it just does nothing, it does says in the launcher that the version is "out of date". But in-game, there is no text message popping up.

      Delete
    2. Hi. The best thing to do in these cases (when something doesn't happen as expected in your code) is to "trace" the execution of the code to follow each step to find out where the problem occurs. You can use the debug mode of Eclipse but I prefer to use System.out.println() statements at each point in the code to print out useful info to the console. I prefer these over the debug mode because in debug mode the entire execution of the code stops while you step through, whereas the console statements allow you to simply play the game and get a log record of what happened that you can analyze.

      So what you need to confirm is that the line where it sends the chat message is actually running. Note, I suppose maybe there is an issue with client and server and maybe you're sending the message from the wrong side. The console statements will help figure this out because it will say which side each code is running.

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Why getLatestVersion method is never used ?

    ReplyDelete
    Replies
    1. In my example, the message I give only says if it is out of date or not. But you could call the getLatestVersion() method if you wanted to tell people exactly what version they should have.

      Delete
  5. This is a great tutorial, but there is one thing that I take issue with. The PlayerTickEvent is run every game tick, so 20 times per second you are checking to see if the mod has warned the player which is a waste of CPU cycles. I would suggest using PlayerLoggedInEvent and change the remote world check to !event.player.worldObj.isRemote as the PlayerLoggedInEvent only runs when the player joins.

    ReplyDelete
  6. I'm back again 1 year later with another question xD

    How can I get this to work in 1.10.2? All the chat functions that the script used are gone?

    ReplyDelete
  7. Worked for 1.12.2. Thanks!

    For the chat message part, replace all this
    [code]
    ClickEvent versionCheckChatClickEvent = new ClickEvent(ClickEvent.Action.OPEN_URL,
    "http://jabelarminecraft.blogspot.com");
    ChatStyle clickableChatStyle = new ChatStyle().setChatClickEvent(versionCheckChatClickEvent);
    ChatComponentText versionWarningChatComponent =
    new ChatComponentText("Your Magic Beans Mod is not latest version! Click here to update.");
    versionWarningChatComponent.setChatStyle(clickableChatStyle);
    event.player.addChatMessage(versionWarningChatComponent);
    [/code]

    With something like this
    [code]
    String updateUrl = "https://YourUpdateUrl.com/"
    ClickEvent versionCheckChatClickEvent = new ClickEvent(ClickEvent.Action.OPEN_URL, updateURL);
    String m2 = (Object)TextFormatting.GOLD + "Update Available: " + (Object)TextFormatting.DARK_AQUA + "[" + (Object)TextFormatting.YELLOW + "MODNAME" + (Object)TextFormatting.WHITE + " v" + VersionChecker.getLatestVersion() + (Object)TextFormatting.DARK_AQUA + "]";
    TextComponentString component = new TextComponentString(m2);
    Style s = component.getStyle();
    s.setClickEvent(versionCheckChatClickEvent);
    component.setStyle(s);
    player.sendMessage((ITextComponent) component);
    [/code]

    Change updateURL to where you want to direct the user.
    Change "MODNAME" to the name of your mod.

    ReplyDelete
    Replies
    1. Hi, I think it's a bit late but..... could you help me please ?

      Delete