Support multiple Minecraft versions with abstraction/maven

Discussion in 'Resources' started by mbaxter, Dec 13, 2012.

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

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    For 1.4-5-R1.0 and beyond, CraftBukkit's internals (net.minecraft.server, or NMS, and org.bukkit.craftbukkit, or OBC) have their package version-tagged. This means that your plugins poking into the volatile NMS/OBC internals will need updating each and every Minecraft version.

    For plugin developers looking for an easy way to handle this code change and support multiple versions (while not putting server admins in harms way), I wrote a very simple abstraction demonstration. You can also view my approach in my plugin TagAPI.

    The code is available here: https://github.com/mbax/AbstractionExamplePlugin

    Benefits:
    • Version checking! If you call the version checker/loader in your onEnable, you can handle incompatibility (outdated plugin version) immediately.
    • Backwards compatibility! Minecraft internals do change. This code lets you support multiple versions in one version of your plugin. On release day, you can quickly write the new implementation for the new version and upload and can still label your plugin as compatible with the previous version of minecraft!
    • Maven! Maven is the coolest way to automate building of your plugin. Except perhaps building a robotic army to handle your job.
    There are several pieces to this setup.

    1) The parent pom.

    The pom.xml at the root of the project is the parent. It doesn't create any project itself, but instead defines the child modules that will be built. The modules defined are the relative paths to the folders where the modules are located. It is possible to (and I do this in my other plugins) put all of the modules inside a folder named "modules" and then reference them as modules/modulename. The order of the modules doesn't matter as the system will determine the order necessary to build, but for sanity I ordered them in the way it will be done.


    2) The API

    In order to call your NMS-interacting methods without specifically referencing a ton of versions in your main code, interfaces and abstract classes are a great solution. In the API module, I have defined an interface that very importantly does not reference ANY other code in the other modules. This module will be built first and thus cannot rely on other modules. In the example, I define a single method that takes a Bukkit object and a String.
    The pom.xml for this module defines the parent


    3) The implementations

    Now, for any versions we wish to support, let's implement that interface in a package named after that version package (Why that is, is covered in step 4), with the file name the same (I used NMSHandler) for each implementation. This is an easy way to track supported versions.

    In each of these modules, create a class implementing the interface for that version of minecraft. This is where all the internal calls go. In the pom.xml the parent is again defined, and this time the dependencies are CraftBukkit for that specific implementation version as well as the API.


    4) The final project.

    Lastly, we want to build everything together. The largest module in this example is the Plugin module. This is where the majority of the code goes. Anything that isn't the interfaces and abstract classes for the NMS implementation or the NMS implementation itself goes here. This is the module where you can refer to any other module. As you can observe in the pom.xml, all of the other modules are dependencies. In the main plugin class, I have defined a field "NMSHandler" of type NMS, which is the interface I defined in the API. In onEnable, the code first determines what version is running from the package found, and then attempts to load a class in the expected location. If anything goes wrong in that loading, for instance if the file is not found because the plugin version does not support that version of minecraft, it will disable itself and inform the server admin that it is time to update.

    In this module's pom.xml there are some additional things. First, this is the only pom.xml where I define a version number, as this is the one that will count. Second, I define the target directory as one level up, so that when the project is built the final JAR result of the plugin will be in the target folder on the same level as the parent pom, which is how non-modulized maven-built plugins work. Lastly, I am using the shade plugin to include the compiled files from all of the dependencies with a compile scope into this jar. This builds the complete plugin!


    Updating
    Future updates are simple - Just create a new implementation module (copy an existing implementation pom and rename a few lines!) and new implementation of your interfaces. Add the module to two places. 1) The parent pom, so it builds and 2) The final product pom, so it is shaded into the final jar.


    Some additional topics:
    Do I have to use separate packages for the implementations?
    No, an easy alternative to this would be to put all of your implementation classes in one package and name the classes after the version instead. I opted to use packages to mimic how NMS is labeling with packages.

    This feels far too complicated to maintain
    The only challenge in this approach is the initial setup. See the above description on updating.



    If you have any more questions feel free to ask.
     
  2. Offline

    Lolmewn

    That might be useful :)
    Not for me, anyway, but since like 10% of all plugins use it... :p
     
  3. Offline

    Shiny Quagsire

    I personally think it would be easier to just change with each update. I have a private server anyhow, so I can't really forget to update.
     
  4. Offline

    Deathmarine

  5. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    I have massively improved the main post. Woo.
     
    slipcor likes this.
  6. Offline

    Ne0nx3r0

    That does seem far simpler... Though to be fair, having all the files in one package also simplifies the process of reviewing it.
     
  7. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    His example is the same as mine, without all the maven assistance that really helps automate the process ;)

    As for reviewing it, we don't really care as long as the code is easy to follow.
     
  8. Offline

    Ne0nx3r0

    I think you said what I said but you said it after I said it again when you said it. :)

    Notably I meant reviewing the example, not actually developing that way, but I suppose I didn't say so explicitly.
     
  9. Offline

    desht

    Yep, this seems like the Right Way To Do It (tm). Although the way I'm doing it right now for my plugins is any methods which call NMS code are in a separate library (https://github.com/desht/dhutils) which I shade in to my plugins with Maven. The approach here should be adaptable to that, possibly with a NMSCompat helper class in dhutils - I guess a lot of the code here (including what you have n your onEnable() to get the NMS interface object) can go in dhutils rather than my actual plugins.

    Time for a bit of refactoring...

    mbaxter You might wish to find a new set of NMS calls to abstract, because your example plugin doesn't compile right now :) v1_4_5/src/main/java/org/kitteh/example/plugin/nms/v1_4_5/NMSHandler.java makes reference to a non-existent getHandle() method in CraftItemStack.

    Edit: or more simply, just change the v1_4_5 implementation to carry out the operation in terms of the ItemMeta API.

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 30, 2016
  10. Offline

    Uniclaw

    Usefull - But why not just remove the cb change, so that its like befor .. :eek:?
     
  11. Offline

    desht

  12. Offline

    Uniclaw

    I hate newer version.. just breaks all the things and is good for nothing >.< And what, if bukkit is no longer needed? All Plugins will be programmed for nothing! -.-
     
  13. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    desht it's an example for maven, I'm totally unconcerned that something which was replaced by bukkit api is broken.
     
  14. Offline

    desht

    I understand, but surely even examples should compile properly? :)
     
  15. Offline

    Ne0nx3r0

    It doesn't need to in order for people to learn from its technique, and don't call him Shirley.
     
    blha303, desht and robinjam like this.
  16. Offline

    agaricus

    Trying to get the example to build and I'm hitting the same error:

    Code:
    [ERROR] COMPILATION ERROR :
    [INFO] -------------------------------------------------------------
    [ERROR] /Users/admin/minecraft/1.4.x/AbstractionExamplePlugin/v1_4_5/src/main/java/org/kitteh/example/plugin/nms/v1_4_5/NMSHandler.java:[13,53] error: cannot find symbol
    [INFO] 1 error
    [INFO] -------------------------------------------------------------
    [INFO] ------------------------------------------------------------------------
    [INFO] Reactor Summary:
    [INFO]
    [INFO] AbstractionExamplePlugin Parent ................... SUCCESS [0.003s]
    [INFO] Abstraction Example Plugin API .................... SUCCESS [0.972s]
    [INFO] Abstraction Example Plugin implementation pre-refactor SUCCESS [0.265s]
    [INFO] Abstraction Example Plugin implementation v1_4_5 .. FAILURE [0.309s]
    [INFO] AbstractionExamplePlugin .......................... SKIPPED
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD FAILURE
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time: 1.660s
    [INFO] Finished at: Sat Jan 19 21:11:27 PST 2013
    [INFO] Final Memory: 15M/207M
    [INFO] ------------------------------------------------------------------------
    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project abstractionexampleplugin-v1_4_5: Compilation failure
    [ERROR] /Users/admin/minecraft/1.4.x/AbstractionExamplePlugin/v1_4_5/src/main/java/org/kitteh/example/plugin/nms/v1_4_5/NMSHandler.java:[13,53] error: cannot find symbol
    [ERROR] -> [Help 1]
    org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project abstractionexampleplugin-v1_4_5: Compilation failure
    /Users/admin/minecraft/1.4.x/AbstractionExamplePlugin/v1_4_5/src/main/java/org/kitteh/example/plugin/nms/v1_4_5/NMSHandler.java:[13,53] error: cannot find symbol
     
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:213)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:153)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:145)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:84)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:59)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.singleThreadedBuild(LifecycleStarter.java:183)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:161)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:319)
    at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:156)
    at org.apache.maven.cli.MavenCli.execute(MavenCli.java:537)
    at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:196)
    at org.apache.maven.cli.MavenCli.main(MavenCli.java:141)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:290)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:230)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:409)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:352)
    Caused by: org.apache.maven.plugin.CompilationFailureException: Compilation failure
    /Users/admin/minecraft/1.4.x/AbstractionExamplePlugin/v1_4_5/src/main/java/org/kitteh/example/plugin/nms/v1_4_5/NMSHandler.java:[13,53] error: cannot find symbol
     
    at org.apache.maven.plugin.AbstractCompilerMojo.execute(AbstractCompilerMojo.java:656)
    at org.apache.maven.plugin.CompilerMojo.execute(CompilerMojo.java:128)
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:101)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:209)
    ... 19 more
    [ERROR]
    [ERROR]
    [ERROR] For more information about the errors and possible solutions, please read the following articles:
    [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
    [ERROR]
    [ERROR] After correcting the problems, you can resume the build with the command
    [ERROR] mvn <goals> -rf :abstractionexampleplugin-v1_4_5
    
    Not that I mind the breakage but I'm confused as to why it broke – wasn't the purpose of this abstraction technique to allow your plugin to work on multiple versions, and since its building the NMS handler against CraftBukkit 1.4.5 (which does have CraftItemStack getHandle) shouldn't it still work? Can anyone shed some light on this problem or how to avoid it?
     
  17. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    My example was a poor example :3
     
  18. Offline

    EnvisionRed

    How would one go about having a method which returns a CraftBukkit/NMS class instance?
     
  19. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    If you can't figure that out you shouldn't be writing against NMS
     
  20. Offline

    EnvisionRed

    Fair enough. I've figured that out now.
     
  21. Offline

    CyberianTiger

    Just done the same, without realizing this thread existed, moving all my projects to maven (mostly because it makes it easier for other people to depend on them if they want, and makes configuring Jenkins a breeze).

    Previously I just added multiple versions of craftbukkit to the compile classpath (yes, that's legal, if dirty). Maven does not like multiple versions of the same artifact in the classpath though :(

    Ended up with this:

    https://github.com/cybertiger/Bukkit-Lib

    For my NBT poking library, quite a similar layout.

    Oops, thread necromancy...
     
  22. Offline

    DerFlash

    Hey mbax,

    just trying to use your great idea within a project here. Works like a charm besides one important thing. Maybe you (or anyone else) can throw me a hint:

    Both, VNP and my project can be imported into eclipse and also build just fine through all modules up to the final jar. But eclipse does not seem to recognize the project as a java project when importing as "existing maven project" (both, VPN and mine). Any idea?
     
  23. DerFlash If eclipse doesn't recoginize the jar project than you can always do: right click on project | properties | project facets | create new facet | (click the java thingy) | apply | ok. Now your project should be fixed. (I'm not an Eclipse-expert since I hate it but this helped me in the past when I used Eclipse)
     
  24. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    DerFlash I never use git/maven integration in my IDE, because it always ends in tears.
     
    CaptainBern likes this.
  25. Offline

    Ultimate_n00b

    Not maven? I understand git (as I love their windows client) but don't you need maven for dependencies?
     
  26. Offline

    mbaxter ʇıʞʞnq ɐ sɐɥ ı

    I manually add the dependencies to the build path. One time, takes one or two minutes.
     
  27. Offline

    Ultimate_n00b

    Well, I suppose it's up to you. I find it easier to just have maven update it inside the IDE.
     
  28. Offline

    xTrollxDudex

    mbaxter
    Continuing to bump this thread, I discovered that going through several folders and finding:
    A SINGLE class
    7 lines

    It makes me wonder if I'm making the best use of my time :p
     
  29. Offline

    DerFlash


    Hehe. Sure, it's sometimes a pain in the xxx, but when it works, it really does and helps a lot. I've almost 30 projects in eclipse running as imported maven projects - none of them with modules yet - and all work fine so far. Would be really cool to get the 'moduled' one to work too. I really love the fact to just download a project and hit one small "import" and be ready to click on "Run as>Maven install".

    CaptainBern: Thanks for the hint. The facet option was not there, so I googled that I need some WTP eclipse add ons first. I'll check that. Maybe there will be some easy way to check in some basic eclipse .project file then and still adding it to the ignore. Lets see.

    [edit]
    I did it! :) Btw, this also may improve your idea, mbax:
    I've used WTPs facet conversion to convert it to a java project and looked at the .classpath file of an existing maven project to find out why it still did not recognize the dependency jars, finding that I needed to add 'org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER' in there.

    Then I noted that I may move up my external repository declaration and the bukkit base artifact (not the ones in the v1_6_R3, ... modules out of the API-module & <projectName>-module up into the main pom.
    This way the modules still find everything while the special CB modules still override that with their needed artifact. The improvement here is that you'll only need to change that in one place instead of two later. And the second improvement is that the eclipse project will know about those dependencies and now compiles right out of the box :)

    See: https://github.com/nisovin/Shopkeepers/commit/2cb60e684ae92e2fd50a1b3768fd3ae4cade8225
     
    mbaxter likes this.
  30. Offline

    epicfacecreeper

    Would you consider writing a maven guide/tutorial? It seems like you can do a lot of stuff with it, but I don't know how to start.
     
Thread Status:
Not open for further replies.

Share This Page