Help with direction of plugin

Discussion in 'Spigot Plugin Development' started by chmod_777, Jan 6, 2020.

  1. Hi all,

    I just started developing a new plugin called "seasons" that works a lot like the battle pass idea that has been implemented into a lot of games recently. I have a general idea of how I am going to structure the plugin, but I thought I would ask if anyone has any ideas on how to most effectively structure this.

    A tier can be reached only after completing the tier before it. This is done by completing a challenge like 'kill x mobs' or 'vote x times', stuff like that

    I have a 'Tier' class which defines the parameters for a single tier: reward, reward type, and the challenge required to reach the tier. I also have a tierHandler class which takes all the data from the 'Tier' section of my config and instantiates each tier defined in the config and puts them into an arrayList.

    This is the general structure of my config file:
    Code (Text):
    Tiers:
      0:
        challenge:
          - type: 'KILLS'
          - entity: 'PIG'
          - count: '10'
        reward:
          - Type: 'BLOCK'
          - Data: 'STONE'
       1:
         next tier and so on...
     

    I guess my most pressing question is what is the best way to parse all of this data into a single tier class?
     
  2. I would make a method for each section, and have a switch for different types of challenges, where all challenge share a common interface.

    Something of this sort:
    Code (Text):
    Challenge getChallenge(ConfigurationSection challenge) {
         String type = challenge.getString("type");
         switch (type) {
             case "KILLS": return getKillChallenge(challenge);
             case ...
             default: throw new IllegalStateException("Unsupported type for challenge '"+type+"'");
         }
    }
    Similar for rewards etc. and then construct a new Tier object from those.

    It's a bit unclear what you're asking since it seems that you have the structure already fleshed out.

    You'll probably need to figure out how to keep track of which challenges have been fulfilled. i.e. If the configuration changes, how to handle the changes in challenges for those who already have done challenges.
     
  3. I would agree mostly. I think that it would be easier to make an event based implementation though. For example:

    PlayerRespawnEvent:

    (Run challenges that have PlayerRespawnEvent as it's activation event.)

    EntityDeathEvent:

    (Run challenges that have EntityDeathEvent as it's activation event.)



    Any information needed from the event, can be passed to a processor, which will process these parameters and grab any challenges that work for them. A lot can be done with a system like this.

    One example can be:

    EntityDeathEvent:

    Parameters passed:

    - Victim EntityType

    - Player who killed the entity.

    - Player's item they killed with.

    Here's where I prove a lot can be done with this. You can check if the player killed a skeleton with a diamond sword, you can check if they killed a zombie with an iron axe, and you can even check if they killed a bat with a wooden shovel :ROFLMAO:. The more parameters you give, the more options that are configurable. You don't even have to force the server owner to use all the parameters. (I'd call them conditions if you don't want a coding word when describing this to owners).


    To add more events, just support the event, and what conditions the owner can configure.


    In my opinion, an implementation like this is easier to write, as it does not require a switch statement and a method for each challenge type. It allows the server owner to configure what one they want, and any events that are supported by the developer can easily be used. I've made a system like this for the Skill system in my treasure pets plugin, and I can support any event I want with very little code, and it all automatically works itself out.
     
    #3 BourneDev, Jan 6, 2020
    Last edited: Jan 6, 2020
  4. Thanks for the idea. Im fairly new to spigot development and im not too familiar with the Event interface. If I understand correctly, when an event occurs, then what I define in that event class will be executed? In my case, say someone kills a cow, I would make an event called 'EntityDeathEvent' that increments the kill count of cows for that player whenever that players kills a cow.

    As far as the config goes, I should fetch each tier/challenge as its own configurationSection and parse from there?
     
  5. by 'supporting' an event do you mean extending it?

    Edit: And challenges would be defined as listeners that wait for that event to occur?
     
  6. Basically, here is the example again:

    Code (Text):
    One example can be:

    EntityDeathEvent:

    Parameters passed:

    - Victim EntityType

    - Player who killed the entity.

    - Player's item they killed with.
    So this is the implementation of something I call an activation event. The EntityDeathEvent is an event, but it is critical to the activation of a challenge for the player. Whenever an entity dies, we want to give the player data from the event that we want to be configurable. In the example, I've listed three options that you could make configurable, but you can get creative of course.

    Now, back to the question you've asked. How do I actually use this data that we now know? Well now's where we take the config into consideration. Say we have a slightly modified version of your config:

    Code (Text):
    Tiers:
      0:
        challenge:
          activationEvent: 'EntityDeathEvent'
          conditions: 'EntityType:PIG'
          count: '10'
        rewards:
          1:
              material: "STONE"
              amount: 1
    Then, this challenge will ONLY activate, every time the Victim is a pig. If we wanted the challenge to activate when you kill a skeleton, just switch it to skeleton. Maybe we'll also add killing it with a diamond sword. Well, you would do this.

    Code (Text):
    Tiers:
      0:
        challenge:
          activationEvent: 'EntityDeathEvent'
          conditions: 'EntityType:SKELETON Material:DIAMOND_SWORD'
          count: '10'
        rewards:
          1:
              material: "STONE"
              amount: 1
    Now, every time you kill a skeleton, with a diamond sword material, the challenge will run.

    EDIT:

    I'm just going to get even more into how configurable this is. You can add as many of the same type of condition as you want to support multiple different creatures, items, and more. For example, if you want all wooden tools to be supported, and for the challenge to work on all passive mobs, you just add all those materials and all those entity types to the conditions section. This concept really is that configurable.
     
    #6 BourneDev, Jan 6, 2020
    Last edited: Jan 6, 2020
  7. Ok that makes sense. I guess the only thing I'm confused about is the calling of the event. If I extend an existing event that follows your example, an entity death, does that event just occur or do I have to call It when needed? Sorry, trying to understand how Event API works and spigot & bukkit don't have great explanations.

    Edit: I guess my question is: Do I call this custom event whenever the EntityDeathEvent is called?
     
    #7 chmod_777, Jan 6, 2020
    Last edited: Jan 6, 2020
  8. I'll just show you some code I have written for treasure pets that runs the entity damage event as an example.

    Code (Text):
        @EventHandler
        public void processEntityDamage(EntityDamageEvent event) {
            if (!event.getEventName().equals("EntityDamageEvent")) {
                return;
            }
            if (event.getEntity() instanceof Player) {
                Player player = (Player) event.getEntity();
                ArrayList<CPet> activatablePets = CPetUtil.getActivatablePets(player, event);

                for (CPet cPet : activatablePets) {
                    SkillProcessor skillProcessor = new SkillProcessor(cPet, player, event, "DamageCause:" + event.getCause().name());
                    skillProcessor.run();
                }
            }
        }
    Every time an entity damages another entity, we check if the entity is a player. Then, I basically get a list of all pets that run off the EntityDamageEvent activation event. For you it would be grab all the challenges that have this activation event. Then, I process all the skills of the pet that come from this specific event.

    The skill processor takes the pet (for you it's a challenge) and the player that it runs on. It also takes the event because some skills are exclusive to that event but yours is a challenge system not a skills system so don't worry about that. Lastly, it takes a list of conditions. That's the key. It takes the damage cause. So the player can now configure whether the Skill runs whether they FALL, whether they are hit by an entity (ENTITY_ATTACK), and any other enums from the DamageCause enum.

    My EntityDamageByEntityEvent activation method is a little bit more sophisticated than this one because it has the player's item in hand as a condition as well as the victim, but the EntityDamageEvent activationEvent is a simple example so that is the one I wanted to show. Your specific skill will probably want EntityDamageByEntityEvent.

    Lastly, this code:

    Code (Text):
            if (!event.getEventName().equals("EntityDamageEvent")) {
                return;
            }
    Is only needed if you don't want the code in the event to run when any events that extend it run. There was an issue where EntityDamageByEntityEvent would also activate EntityDamageEvent skills because EntityDamageByEntityEvent extends EntityDamageEvent, so that code fixes it.

    Your system is a challenge system and mine is a skills system, but they are extremely similar. In the end, think of it this way.

    - An event runs.

    - Process specific information (conditions)

    - If the conditions processed are met, we can run the challenge.
     
    #8 BourneDev, Jan 6, 2020
    Last edited: Jan 6, 2020
  9. gotcha. So when/where would I do ‘event.call(params)’ so that the event takes place?
    So when do I trigger the event? ie.
    Code (Text):
    getPluginManager().callEvent(event);
     
  10. Events are a regular spigot thing. I guess if you need to know that, the official wiki would be the best at explaining. So here's their wiki. https://www.spigotmc.org/wiki/using-the-event-api/


    Here's another wiki if you need even more information https://bukkit.gamepedia.com/Event_API_Reference


    Basically, the events listen for things that happen in the world, and the methods are called when they actually happen. In the end, my solution does the 3 steps I explained in my last post, after the listening takes place.
     

  11. Ok, so I think I got a grasp on this. If you have time, please take a look at what I've written so far:

    Code (Java):
    public class Tier {

        private String activationEvent;
        private String conditions;
        private int count;
        private Reward reward;

        public static final String[] VALID_REWARD_TYPES = {"ITEM", "MONEY"};
        public static final String[] VALID_CHALLENGE_TYPES = {"EntityDeathChallengeEvent"};

        public Tier(String activationEvent, String conditions, int count, Reward reward){
            this.activationEvent = activationEvent;
            this.conditions = conditions;
            this.count = count;
            this.reward = reward;
        }

        public boolean checkRewardType(String rewardType){
            int check = 0;
            for(int i=0; i<VALID_REWARD_TYPES.length; i++){
                if(rewardType.equals(VALID_REWARD_TYPES[i])){
                    check++;
                }
            }
            if(check != 1){
                return false;
            } return true;
        }

        public boolean checkChallengeType(String challengeType){
            int check = 0;
            for(int i = 0; i< VALID_CHALLENGE_TYPES.length; i++){
                if(challengeType.equals(VALID_CHALLENGE_TYPES[i])) {
                    check++;
                }
            }
            if(check != 1){
                return false;
            } return true;
        }
    }

    Code (Java):
    public class Reward {

        private String material;
        private int amount;

        public Reward(String material, int amount){
            this.amount = amount;
            this.material = material;

        }

        public ItemStack stringToStack(String name){
            ItemStack stack;
            if(!Material.valueOf(name).isItem()){
                stack = new ItemStack(Material.STONE, amount);
            } else {
                stack = new ItemStack(Material.getMaterial(name), amount);
            }
            return stack;
        }
    }
     

    Code (Java):
    public class TierHandler {

        private FileConfiguration config;

        public TierHandler(Main plugin){
            config = plugin.getConfig();
        }

        public Tier buildTier(ConfigurationSection section){
            ConfigurationSection challengeSection = section.getConfigurationSection("challenge");
            String activationEvent = challengeSection.getString("activationEvent");
            String conditions = challengeSection.getString("conditions");
            int count = challengeSection.getInt("count");

            ConfigurationSection rewardSection = section.getConfigurationSection("rewards");
            String material = rewardSection.getString("material");
            int amount = rewardSection.getInt("amount");

            Reward reward = new Reward(material, amount);

            Tier tier = new Tier(activationEvent, conditions, count, reward);
            return tier;
        }

        public ArrayList<Tier> buildTierList(){
            ArrayList<Tier> tierList = new ArrayList<Tier>();

            for(String sec : config.getConfigurationSection("Tiers").getKeys(false)){
                ConfigurationSection subSec = config.getConfigurationSection("Tiers." + sec);
                tierList.add(buildTier(subSec));
            }
            return tierList;
        }
    }

    Code (Text):
    public final class EntityDeathChallengeEvent extends Event {

        private Player player;
        private EntityType entityType;
        private Material tool;
        private boolean cancelled;

        public EntityDeathChallengeEvent(Player player, EntityType entityType, Material tool){
            this.player = player;
            this.entityType = entityType;
            this.tool = tool;
        }

        public Player getPlayer(){
            return player;
        }

        public EntityType getEntityType(){
            return entityType;
        }

        public Material getTool(){
            return tool;
        }

        private static final HandlerList handlers = new HandlerList();

        public HandlerList getHandlers(){
            return handlers;
        }

        public static HandlerList getHandlerList(){
            return handlers;
        }

        public void setCancelled(boolean cancel){
            cancelled = cancel;
        }

        public boolean isCancelled(){
            return cancelled;
        }

    }

    Code (Java):
    public final class EntityDeathListener implements Listener {

        @EventHandler
        public void onEntityDeath(EntityDeathEvent e){
            if(e.getEntity().getKiller() == null){
                return;
            }
            Player player = e.getEntity().getKiller();
            EntityType entityType = e.getEntityType();
            Material tool = e.getEntity().getKiller().getInventory().getItemInMainHand().getType();

            EntityDeathChallengeEvent event = new EntityDeathChallengeEvent(player, entityType, tool);
            Bukkit.getServer().getPluginManager().callEvent(event);
        }

        @EventHandler
        public void onEntityDeathChallengeEvent(EntityDeathChallengeEvent e){
            //use event data for challenges
        }
    }

    As far as rewards go, Im not entirely sure how to implement more than one type of reward (maybe with subclasses?).

    Also, Im not entirely sure how to link what is in the configuration file for each tier to the event. I know ill have to make a storage medium (mySQL or just flat yml) to store player data like what tier they're currently on and look at that every time a desired event is called.
     
  12. Yeah, you just need a processor to process the conditions based on the information given in the EntityDeathEvent listener to get whether the challenge conditions fit what the player has just killed among other things. To implement more than 1 type of reward, you do it the same way you wrote your tiers in the config. You can process them as ItemStack objects from string. To do this, you need all the information like the amount, the material, the name, the lore, and anything else you need in each rewards config section kinda like I showed in my earlier example.

    The string to stack method is close, but it needs more parameters like I've stated. That's all.
     
  13. Awesome. Thanks for all the help. Could you show me an example of a processor if you have one? ie. the skill processor in one of your examples? That would be a huge help.
     
  14. Here is my method that processes commands.

    Code (Text):
        //Returns whether or not the command meets the Processor's conditions.
        public boolean conditionsMet(String command) {

            for (String condition : conditions) {

                String[] split = condition.split(":");

                String className = split[0];

                //Checks if the condition type is even there.
                if (command.contains(className)) {
                    //Checks whether the command contains the exact condition. If not, return false.
                    if (!command.contains(condition)) {
                        return false;
                    }
                }
            }

            return true;
        }
    Essentially, the conditions are like the ones I showed above like EntityType:SKELETON or Material:IRON_SWORD. The SkillProcessor object gets passed commands in the example I showed above (the final parameter of the initialization), and each command is "checked" to see if all of them are meeting the conditions in the config. How it does that, is it checks to see if the conditions string even requires the type of object it is checking, and then it checks if the specific condition is the command that it's checking. If so, then the condition is met. Ideally, check to see if all conditions are met by the commands passed into your command processor before running the Challenge.

    I don't want to give the whole skill processor class because my resource is premium, however this is all you will need for challenges specifically. Most of the stuff in the skill processor is pets related anyways.