Solved Create a multi version plugin

Discussion in 'Spigot Plugin Development' started by MaximePremont, Jan 30, 2020.

  1. Hello, I would like to create a plugin supporting minecraft versions from 1.8 to 1.15, I have seen some developers use methods like this :

    [​IMG]


    How exactly does this method work? And how could I set it up?
    Thanks !
     
  2. thats maven modularisation, you setup multiple smaller sub projects. When compiling everything is put into one jar file, so you can use reflective invocation to access the nms adapter for the given version. You can just copy the project structure used, its nothing particularly special.
     
    • Like Like x 1
  3. TeamBergerhealer

    Supporter

    Easier is creating an interface class, which you implement with version-specific code, and compile as submodules linking against that version's spigot server jar. With a generic function to create the implementation of that interface.

    Code (Text):
    // Public interface
    public interface NMSStuff {
        void doThing();
    }

    // Implementation inside submodule
    class NMSStuff_1_9 implements NMSStuff {
        public void doThing() {
            // 1.9 code
        }
    }

    // Some factory method inside the submodule, must be in another class
    // This avoids the ClassLoader loading the 1_9 submodule class on a different version
    public static NMSStuff create_1_9() {
        return new NMSStuff_1_9();
    }

    // In your plugin implemenation code:
    NMSStuff nms;
    if (version.equals("1.9")) {
        nms = some.package.path.compatible.1_9.Factory.create_1_9();
    }
    Only be careful where you put the new NMSStuff_1_9() code. The Java classloader can be a bit unpredictable sometimes when the class is initialized. You will want to avoid initializing the 1.12 NMSStuff implementation when initializing 1.9 and etc.
     
    #3 TeamBergerhealer, Jan 30, 2020
    Last edited: Jan 30, 2020
    • Agree Agree x 3
  4. Thats ... what I described? You setup an interface, which is what you will access from your code. You need a reflective invocation to match your server version with the specific implementation necessary. I would like to see a working maven project structure that allows you to call the create_1_9(); method without relying on a reflective invocation of it, because I couldn't pull it off.
     
    • Like Like x 1
  5. TeamBergerhealer

    Supporter

    Why would you need reflection for this? The NMSStuff implementation class isnt initialized until the create_1_9() is called. Just don't put the create() function inside the NMSStuff implementation class itself, otherwise it will attempt loading the members and methods inside, which likely contain 1.9 package path classes. The unreliable part is whether the class create_1_9() is inside of is loaded early on as part of jvm optimization. Hence the separate Factory class.

    If you only expose the interface, there should be no errors if the factory class is self-contained, since the method body which contains the constructor, and the implementation class type, is only loaded once the method is first called.

    Further, you can do the following:
    Code (Text):
    Map<String, Supplier<NMSStuff>> by_version = new HashMap<>();
    by_version .put("1.9", some.path.Factory::create_1_9);
    by_version .put("1.12", some.path.Factory::create_1_12);

    // Initialize. TODO: Unsupported version handling
    NMSStuff nms = by_version.get(version).get();
     
    #5 TeamBergerhealer, Jan 30, 2020
    Last edited: Jan 30, 2020
  6. Or just instantiate it like that.
    https://github.com/yannicklamprecht...cht/worldborder/plugin/WorldBorderPlugin.java
     
  7. Thank you for your answers, I already know how to use the interfaces but I would like to know more about this principle of "maven modularisation", how does it work and how can we set it up?
    The problem is that I find myself too limited with only the use of interfaces, because I would like my plugin to change depending on the versions of minecraft used and I think that this other solution might be more suitable.
     
    • Like Like x 2
  8. TeamBergerhealer

    Supporter

    Should work fine, I just dont trust that the jvm optimizer wont decide to initialize all the Impl classes for all switch case statements the moment it hits the switch. I've had weird things like this in the past where it ended up loading classes meant for the test environment because of a stray if somewhere that it wouldn't even branch into.

    @MaximePremont I thought I could find some easy tutorials to follow online, but bar none Maven tutorials that touch this subject show the pom.xml as its written. Annoying.

    Theres a few threads about this on these forums though.
    https://www.spigotmc.org/threads/multi-version-maven-project.168485/

    Most people just link to their own plugin projects on github

    This one looks pretty good at first glance: https://mkyong.com/maven/maven-how-to-create-a-multi-module-project/

    Relevant is the directory tree structure, the use of <parent> in the submodules, and adding the interface submodule as a dependency in the implementations' pom.xml.
     
    #8 TeamBergerhealer, Jan 30, 2020
    Last edited: Jan 30, 2020
  9. Thank you very much I will watch!
    I have another question, so that the plugins work correctly in 1.13 and more, I believe that we have to add "api-version: 1.13", doesn't that pose problems for multi version?
     
  10. TeamBergerhealer

    Supporter

    Versions 1.12.2 and before don't interpret api-version and ignore it completely. Just use api-version: 1.13, the 1.12.2 and before modules will function just fine. Omitting it will cause problems though. Just avoid using 1.12.2 and before Material apis in the 1.13+ implementations when you do.
     
  11. So if I understand correctly, I just can't use the old materials if I put the "api-version: 1.13"? So, for example, should I create an item management class depending on the version of minecraft used?
     
  12. TeamBergerhealer

    Supporter

    Yes you should do that. You can continue using the legacy material API on 1.13 and later and omit the api-version, but it'll show that warning and eventually legacy material api support will be removed entirely. The least you need is something to obtain Material api constants. For example, if you want to check whether a Block is a particular type, either:
    • A method to check that a block or type is <type>
    • A getter method for the Material enum value for that type
    For example:
    Code (Text):
    public interface NMSStuff {
        boolean isWallSign(Block block);
        boolean isSignPost(Block block);
    }
    You'll have to read up a little what differences exist. For example, on newer minecraft versions there are many more different Material types for signs (different wood types), so the classic 'WALL_SIGN' material is no longer sufficient.

    Another tip: make use of java8 default interface methods, and create a base interface for all the Material API methods pre-1.13 and post-1.13 so you don't have to copy-paste the same helper methods into all your implementations.
     
    • Useful Useful x 1
  13. Thank you ! So for example if I want to have a gray dye item (which has changed its name during new spigot versions), I create an interface which returns the corresponding material depending on the version?
     
  14. TeamBergerhealer

    Supporter

    Yup, that works fine so long the meaning of 'gray dye item' stayed the same between versions. For some materials for example, there was a material type like 'log' and the block data changed what material it was made of ('spruce, oak'). In newer versions each material type got its own type ('SPRUCE_LOG') In that case what you said wouldn't work very well.
     
  15. So I understood in the case of wood, I'm just going to create an ItemFactory class which when I ask it for wood, gives me the right material depending on the version.

    I will also look for the multi-module management of maven and I come back here in case of problems.
     
    • Like Like x 2
  16. Because using reflection to instantiate the implementation has two advantages over explicitely calling every single implementation.

    First, maintainability. Adding a new implementation does not require changes to the main module if it's accessed with reflection.

    Second, no circular dependencies. If the main module uses reflection to instantiate the implementations, then the implementations may call the classes of the main module, which can make the implementation just so much easier. But if the main module calls the implementations, then the implementations can't call the main module. You'd need another useless abstraction module that the main module implements and that the implementation module depends on (which should not be the public API module). It is reasonable for the implementation module to be allowed to call implementation specific methods from the main module.
     
  17. You can avoid circular dependencies by separating your projects right.

    • API
    • Impl_1_14 depends on API
    • Impl_1_15 depends on API
    • Plugin depends on API, Impl_1_14, Impl_1_15
    So there is no circular dependency.
     
    • Agree Agree x 1
  18. This crams two different purposes into the API: Interfaces for support of multiple versions and hooks for other plugins. This is not ideal. Why would I expose utility methods for things I used internally that the Bukkit API doesn't provide (or changed) to developers hooking into my plugin. You shouldn't put things into the API because you need them in the implementation. That's basically thinking from the specific to the abstract, from the implementation to the API - the opposite of the way programmers are supposed to think.
    And it still doesn't explain why the version specific part of the implementation should not be allowed to call the part that is not version specific.
     
  19. TeamBergerhealer

    Supporter

    @Sataniel the NMS API is for internal use, maybe API is the wrong word for it. Its an interface. But in Maven module land, its an API. Can call it 'MyPlugin-NMSAPI'. I definitely wouldn't make it part of your plugin's main API that is exposed to others that use your plugin that way, since this NMS API can change frequently.

    I agree that reflection is also a viable option, since you can simply get a class by path, with the path containing the version string, and then instantiate it. Doesn't require any extra factory, switch statement or otherwise. But you still need some default interface implemented by all version implementations.
     
    • Agree Agree x 1