Thread: Kotlin plugin system, best practice

Results 1 to 5 of 5
  1. #1 Kotlin plugin system, best practice 
    Registered Member
    Join Date
    Dec 2017
    Posts
    31
    Thanks given
    12
    Thanks received
    4
    Rep Power
    23
    can anyone give me tips on implementing a kotlin plugin system? i need to avoid reflection (during runtime) and i want it to be concise.. i have implemented a way to achieve it already but it's very verbose, requiring a fully visible class, class annotation, and function implementation per event.

    Current method for registering an event subscriber
    Code:
    @SubscribesTo(ServerInitEvent.class)
    public class ExampleListener implements EventSubscriber<ServerInitEvent> {
        @Override
        public void subscribe(EventContext context, ServerInitEvent event) {
            
        }
    }
    Reply With Quote  
     

  2. #2  
    Ex Rune-Scaper

    Join Date
    Jun 2008
    Posts
    3,534
    Thanks given
    457
    Thanks received
    1,257
    Rep Power
    990
    Quote Originally Posted by Lefwek View Post
    can anyone give me tips on implementing a kotlin plugin system? i need to avoid reflection (during runtime) and i want it to be concise.. i have implemented a way to achieve it already but it's very verbose, requiring a fully visible class, class annotation, and function implementation per event.

    Current method for registering an event subscriber
    Code:
    @SubscribesTo(ServerInitEvent.class)
    public class ExampleListener implements EventSubscriber<ServerInitEvent> {
        @Override
        public void subscribe(EventContext context, ServerInitEvent event) {
            
        }
    }
    First I wanna say look at Apollo for a nice design. They use Kotlin scripts which is even better then what I'm going to show you. (See here)

    Some advice:
    1. Don't use generics because one of Java's biggest flaws is type erasures. (Ryley's old plugin system in AJ8 did this)
    2. Don't use annotations to subscribe to classes like that. You're going to limit yourself to a 1-to-1 relationship in which you should support 1-to-many. The only acceptable way to use annotations in Plugins is by having a Plugin meta data where you can declare the author, description, version of the plugin in the annotation itself. These would only be used where you're creating a public api for people to use and you want the community to create plugins for your api. (OSBot does this)

    Flaws in other peoples plugin systems
    • Arios: Only supports 1-to-1
    • AJ8: Type erasures, and 1-to-1
    • OSRoyale: Not efficient O( n ) (Don't use publish-subscribe, consuming is not as efficient as O(1) map also crappy way of handling 1-to-many)


    Here's an example of what I do for SoulPlay.

    E.g A plugin such as a CrystalChest and be invoked by many extensions. Such as ObjectClickExtension and ItemOnObjectExtension. You can even combine ItemOnItemExtension (creating the CrystalKey in the same class)

    On startup use reflection to instantiate the plugins (You can use FastClasspathScanner or better use a custom class loader to support class reloading) and call onInit to initialize the plugins. onInit I map the keys to the type of extension therefore I can retrieve the plugin with O(1) complexity.

    You can obviously write these in Kotlin too but here's a Java version.

    Code:
    package plugin.object;
    
    import com.soulplay.game.model.item.Item;
    import com.soulplay.game.model.player.Player;
    import com.soulplay.plugin.Plugin;
    import com.soulplay.plugin.extension.ItemOnItemExtension;
    import com.soulplay.plugin.extension.ItemOnObjectExtension;
    import com.soulplay.plugin.extension.ObjectClickExtension;
    import com.soulplay.util.Misc;
    
    public class CrystalChestPlugin extends Plugin implements ItemOnItemExtension, ItemOnObjectExtension, ObjectClickExtension {
    
        @Override
        public void onInit() {
            map(ObjectClickExtension.class, 172);
            map(ItemOnObjectExtension.class, CRYSTAL_KEY, 172);
            map(ItemOnItemExtension.class, TOOTH_HALF, LOOP_HALF);
            map(ItemOnItemExtension.class, LOOP_HALF, TOOTH_HALF);
        }
    
        @Override
        public void onItemOnItem(Player player, Item used, int usedSlot, Item with, int withSlot) {
            if (player.getItems().playerHasItem(TOOTH_HALF, 1)
                    && player.getItems().playerHasItem(LOOP_HALF, 1)) {
                player.getItems().deleteItemInOneSlot(TOOTH_HALF, 1);
                player.getItems().deleteItemInOneSlot(LOOP_HALF, 1);
                player.getItems().addItem(CRYSTAL_KEY, 1);
            }
        }
    
        @Override
        public void onItemOnObject(Player player, int objectID, int objectX, int objectY, int itemId, int itemSlot) {
            searchChest(player);
        }
    
        @Override
        public void onObjectOption1(Player player, int objectType, int obX, int obY) {
            searchChest(player);
        }
    
        private static void searchChest(Player player) {
            if (!player.getItems().playerHasItem(CRYSTAL_KEY)) {
                player.sendMessage("The chest is locked");
                return;
            }
    
            player.getExtraHiscores().incCrystalChestsOpened();
            player.sendMessage("You unlock the chest with your key.");
            player.getItems().deleteItemInOneSlot(CRYSTAL_KEY, 1);
            player.getItems().addItem(995, Misc.random(213800));
            player.getItems().addItem(CHEST_REWARDS[Misc.random(CHEST_REWARDS.length - 1)],1);
            player.getItems().addItem(CHEST_REWARDS1[Misc.random(CHEST_REWARDS1.length - 1)], 1);
            player.startAnimation(OPEN_ANIMATION);
            player.sendMessage("You find some treasure in the chest.");
            player.getAchievement().openCrystalChests();
        }
    
        private static final int OPEN_ANIMATION = 881;
        private static final int CRYSTAL_KEY = 989;
    
        private static final int TOOTH_HALF = 985;
        private static final int LOOP_HALF = 987;
    
        private static final int[] CHEST_REWARDS1 = {1623, 1623, 1623, 1623, 1623,
                1621, 1621, 1621, 1621, 1619, 1619, 1619, 1617, 1617, 1631, 1631,
                6571};
    
        private static final int[] CHEST_REWARDS = {4224, 4224, 4224, 4212, 4212,
                4212, 18830, 389, 219, 217, 1513, 3040, 2440, 2444, 2436, 2442,
                1969, 451, 2361, 18830, 389, 219, 217, 1513, 3040, 2440, 2444, 2436,
                2442, 1969, 451, 2361, 18830, 389, 219, 217, 1513, 3040, 2440, 2444,
                2436, 2442, 1969, 451, 2361, 2631, 13107, 13109, 13111, 13113,
                13115, 10398, 2633, 2635, 2637, 10400, 10402, 10408, 10410, 10412,
                10414, 10404, 10406, 7390, 7392, 7394, 7396, 9629, 7372, 7370, 7374,
                7376, 7378, 7380, 7382, 7384, 2607, 2609, 2611, 2613, 2615, 2617,
                2619, 2621, 2599, 2601, 2603, 2605, 2623, 2625, 2627, 2629, 2583,
                2585, 2587, 2589, 2591, 2593, 2595, 2597};
    
    }
    Use default methods so in your plugin you can choose which method you want to override.

    Code:
    public interface ObjectClickExtension extends PluginExtension {
    
        default void onObjectOption1(Player player, int objectType, int obX, int obY) {
    
        }
    
        default void onObjectOption2(Player player, int objectType, int obX, int obY) {
    
        }
    
        default void onObjectOption3(Player player, int objectType, int obX, int obY) {
    
        }
    
        default void onObjectOption4(Player player, int objectType, int obX, int obY) {
    
        }
        
        default void onObjectOption5(Player player, int objectType, int obX, int obY) {
    
        }
    
    }
    Call like this

    Code:
    		final PluginResult<ObjectClickExtension> pluginResult = PluginManager.search(ObjectClickExtension.class, clickedObjectId);
                    pluginResult.invoke(() -> pluginResult.extension.onObjectOption1(c, clickedObjectId, obX, obY));
    Last edited by CrazyPanda; 03-30-2018 at 08:19 PM.
    Attached image
    Reply With Quote  
     

  3. Thankful users:


  4. #3  
    Registered Member
    Join Date
    Dec 2017
    Posts
    31
    Thanks given
    12
    Thanks received
    4
    Rep Power
    23
    Quote Originally Posted by nshusa View Post
    Code:
    package plugin.object;
    
    import com.soulplay.game.model.item.Item;
    import com.soulplay.game.model.player.Player;
    import com.soulplay.plugin.Plugin;
    import com.soulplay.plugin.extension.ItemOnItemExtension;
    import com.soulplay.plugin.extension.ItemOnObjectExtension;
    import com.soulplay.plugin.extension.ObjectClickExtension;
    import com.soulplay.util.Misc;
    
    public class CrystalChestPlugin extends Plugin implements ItemOnItemExtension, ItemOnObjectExtension, ObjectClickExtension {
    
        @Override
        public void onInit() {
            map(ObjectClickExtension.class, 172);
            map(ItemOnObjectExtension.class, CRYSTAL_KEY, 172);
            map(ItemOnItemExtension.class, TOOTH_HALF, LOOP_HALF);
            map(ItemOnItemExtension.class, LOOP_HALF, TOOTH_HALF);
        }
    
        @Override
        public void onItemOnItem(Player player, Item used, int usedSlot, Item with, int withSlot) {
            if (player.getItems().playerHasItem(TOOTH_HALF, 1)
                    && player.getItems().playerHasItem(LOOP_HALF, 1)) {
                player.getItems().deleteItemInOneSlot(TOOTH_HALF, 1);
                player.getItems().deleteItemInOneSlot(LOOP_HALF, 1);
                player.getItems().addItem(CRYSTAL_KEY, 1);
            }
        }
    
        @Override
        public void onItemOnObject(Player player, int objectID, int objectX, int objectY, int itemId, int itemSlot) {
            searchChest(player);
        }
    
        @Override
        public void onObjectOption1(Player player, int objectType, int obX, int obY) {
            searchChest(player);
        }
    
        private static void searchChest(Player player) {
            if (!player.getItems().playerHasItem(CRYSTAL_KEY)) {
                player.sendMessage("The chest is locked");
                return;
            }
    
            player.getExtraHiscores().incCrystalChestsOpened();
            player.sendMessage("You unlock the chest with your key.");
            player.getItems().deleteItemInOneSlot(CRYSTAL_KEY, 1);
            player.getItems().addItem(995, Misc.random(213800));
            player.getItems().addItem(CHEST_REWARDS[Misc.random(CHEST_REWARDS.length - 1)],1);
            player.getItems().addItem(CHEST_REWARDS1[Misc.random(CHEST_REWARDS1.length - 1)], 1);
            player.startAnimation(OPEN_ANIMATION);
            player.sendMessage("You find some treasure in the chest.");
            player.getAchievement().openCrystalChests();
        }
    
        private static final int OPEN_ANIMATION = 881;
        private static final int CRYSTAL_KEY = 989;
    
        private static final int TOOTH_HALF = 985;
        private static final int LOOP_HALF = 987;
    
        private static final int[] CHEST_REWARDS1 = {1623, 1623, 1623, 1623, 1623,
                1621, 1621, 1621, 1621, 1619, 1619, 1619, 1617, 1617, 1631, 1631,
                6571};
    
        private static final int[] CHEST_REWARDS = {4224, 4224, 4224, 4212, 4212,
                4212, 18830, 389, 219, 217, 1513, 3040, 2440, 2444, 2436, 2442,
                1969, 451, 2361, 18830, 389, 219, 217, 1513, 3040, 2440, 2444, 2436,
                2442, 1969, 451, 2361, 18830, 389, 219, 217, 1513, 3040, 2440, 2444,
                2436, 2442, 1969, 451, 2361, 2631, 13107, 13109, 13111, 13113,
                13115, 10398, 2633, 2635, 2637, 10400, 10402, 10408, 10410, 10412,
                10414, 10404, 10406, 7390, 7392, 7394, 7396, 9629, 7372, 7370, 7374,
                7376, 7378, 7380, 7382, 7384, 2607, 2609, 2611, 2613, 2615, 2617,
                2619, 2621, 2599, 2601, 2603, 2605, 2623, 2625, 2627, 2629, 2583,
                2585, 2587, 2589, 2591, 2593, 2595, 2597};
    
    }
    Use default methods so in your plugin you can choose which method you want to override.

    Code:
    public interface ObjectClickExtension extends PluginExtension {
    
        default void onObjectOption1(Player player, int objectType, int obX, int obY) {
    
        }
    
        default void onObjectOption2(Player player, int objectType, int obX, int obY) {
    
        }
    
        default void onObjectOption3(Player player, int objectType, int obX, int obY) {
    
        }
    
        default void onObjectOption4(Player player, int objectType, int obX, int obY) {
    
        }
        
        default void onObjectOption5(Player player, int objectType, int obX, int obY) {
    
        }
    
    }
    Call like this

    Code:
    		final PluginResult<ObjectClickExtension> pluginResult = PluginManager.search(ObjectClickExtension.class, clickedObjectId);
                    pluginResult.invoke(() -> pluginResult.extension.onObjectOption1(c, clickedObjectId, obX, obY));
    Thank you, this is exactly the kind of setup I was looking for. The only change I will make is to use the command pattern for method parameters.
    Reply With Quote  
     

  5. #4  
    Renown Programmer & Respected Member

    Ryley's Avatar
    Join Date
    Aug 2011
    Posts
    596
    Thanks given
    254
    Thanks received
    521
    Rep Power
    1332
    Quote Originally Posted by nshusa View Post
    First I wanna say look at Apollo for a nice design. They use Kotlin scripts which is even better then what I'm going to show you. (See here)

    Some advice:
    1. Don't use generics because one of Java's biggest flaws is type erasures. (Don't look at Ryley's old plugin system in AJ8 which did this)
    2. Don't use annotations to subscribe to classes like that. You're going to limit yourself to a 1-to-1 relationship in which you should support 1-to-many. The only acceptable way to use annotations in Plugins is by having a Plugin meta data where you can declare the author, description, version of the plugin in the annotation itself. These would only be used where you're creating a public api for people to use and you want the community to create plugins for your api. (OSBot does this)

    Flaws in other peoples plugin systems
    • Arios: Only supports 1-to-1
    • AJ8: Type erasures, and 1-to-1
    • OSRoyale: Not efficient (Don't use publish-subscribe, consuming is not as efficient as O(1) map)


    *snip*
    +1, was young and stupid. Still stupid.
    Reply With Quote  
     

  6. Thankful users:


  7. #5  
    Ex Rune-Scaper

    Join Date
    Jun 2008
    Posts
    3,534
    Thanks given
    457
    Thanks received
    1,257
    Rep Power
    990
    Quote Originally Posted by Ryley View Post
    +1, was young and stupid. Still stupid
    Nah bro, only way we learn is from our mistakes.
    Attached image
    Reply With Quote  
     

  8. Thankful users:



Thread Information
Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)


User Tag List

Similar Threads

  1. Replies: 1
    Last Post: 08-06-2013, 11:10 PM
  2. Plugin system
    By tommo in forum Show-off
    Replies: 23
    Last Post: 01-13-2012, 09:22 AM
  3. Replies: 11
    Last Post: 11-05-2011, 07:20 PM
  4. Replies: 13
    Last Post: 04-22-2011, 06:57 AM
  5. Simple PHP Plugin System
    By Anthony` in forum Website Development
    Replies: 5
    Last Post: 11-20-2010, 06:42 PM
Posting Permissions
  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •