Thread: [Elvarg] Commands using reflection

Results 1 to 10 of 10
  1. #1 [Elvarg] Commands using reflection 
    Registered Member
    Optimum's Avatar
    Join Date
    Apr 2012
    Posts
    3,570
    Thanks given
    871
    Thanks received
    1,745
    Rep Power
    5000
    Download this and add as a dependency: https://oss.sonatype.org/service/loc...ner-2.0.19.jar
    or if you are using Maven (doubt it):
    Code:
    <dependency>
        <groupId>io.github.lukehutch</groupId>
        <artifactId>fast-classpath-scanner</artifactId>
        <version>LATEST</version>
    </dependency>
    in com.elvarg.net.impl.command add these classes
    Spoiler for Command.java:
    Code:
    package com.elvarg.net.packet.impl.command;
    
    import com.elvarg.world.entity.impl.player.Player;
    
    public interface Command {
    	public boolean execute(Player player, String command);
    }


    Spoiler for CommandHandler.java:
    Code:
    package com.elvarg.net.packet.impl.command;
    
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.logging.Logger;
    
    import com.elvarg.world.entity.impl.player.Player;
    import com.elvarg.world.model.PlayerRights;
    
    import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
    import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo;
    import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult;
    
    public class CommandHandler {
    
    	private static final Logger LOGGER = Logger.getLogger(CommandHandler.class.getName());
    	
    	private final Map<String, ReflectionCommand> commands = new HashMap<>();
    	
    	private static CommandHandler instance;
    
    	public void executeCommand(Player player, String command) {
    		if (command.equalsIgnoreCase("updatecommands")) {
    			commands.clear();
    			try {
    				loadCommands();
    			} catch (InstantiationException | IllegalAccessException e) {
    				e.printStackTrace();
    			}
    			player.getPacketSender().sendMessage("Loaded " + commands.size() + " commands");
    			return;
    		}
    
    		String commandString = command.split("-")[0].toLowerCase();
    		ReflectionCommand rfc = commands.get(commandString);
    
    		if (rfc != null) {
    			for (PlayerRights r : rfc.getRights()) {
    				if (r.higherRight(player.getRights())) {
    					if (!rfc.execute(player, command)) {
    						player.getPacketSender().sendMessage("try using: " + rfc.getSyntaxHelp());
    					}
    					return;
    				}
    			}
    		}
    	}
    	
    	/**
    	 * Loads all commands based on reflection
    	 */
    	public void loadCommands() throws InstantiationException, IllegalAccessException {
    		ScanResult scanResult = new FastClasspathScanner("com.elvarg.net.packet.impl.command.impl").scan();
    		Map<String, ClassInfo> map = scanResult.getClassNameToClassInfo();
    		List<String> subs = scanResult.getNamesOfAllClasses();
    		for(Class<?> c : scanResult.classNamesToClassRefs(subs)) {
    			if(map.get(c.getName()).implementsInterface("com.elvarg.net.packet.impl.command.Command")) {
    				Object command = c.newInstance();
    				// The annotation
    				CommandInfo annotation = ((CommandInfo) c.getDeclaredAnnotations()[0]);
    				// The reflection command
    				ReflectionCommand reflectionCommand = new ReflectionCommand((Command) command, annotation.rights(),
    						annotation.syntaxHelp());
    				// Puts the command in the commands map
    				getInstance().commands.put(annotation.syntax().toLowerCase(), reflectionCommand);
    
    				// Log info
    				LOGGER.info("Loaded command: " + c.getSimpleName() + " with the following attributes: " + "syntax="
    						+ annotation.syntax() + ", syntaxHelp=" + annotation.syntaxHelp() + ", rights="
    						+ Arrays.toString(annotation.rights()));
    			}
    		}
    	}
    
    	public static CommandHandler getInstance() {
    		if (instance == null) {
    			instance = new CommandHandler();
    		}
    		return instance;
    	}
    
    	private CommandHandler() {
    	}
    
    	public Map<String, ReflectionCommand> getCommandsMap() {
    		return commands;
    	}
    }


    Spoiler for CommandInfo.java:
    Code:
    package com.elvarg.net.packet.impl.command;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    import com.elvarg.world.model.PlayerRights;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CommandInfo {
    	
    	/**
    	 * The syntax of the command, ie what the player would type ::notimplemented
    	 */
    	public String syntax();
    	
    	/**
    	 * The syntax help
    	 */
    	public String syntaxHelp();
    
    	/**
    	 * What rights the player requires to execute a command <br>
    	 * Default = {@code PlayerRights#PLAYER}
    	 */
    	public PlayerRights[] rights() default {PlayerRights.PLAYER};
    	
    
    }


    Spoiler for RelfectionCommand.java:

    Code:
    package com.elvarg.net.packet.impl.command;
    
    import com.elvarg.world.entity.impl.player.Player;
    import com.elvarg.world.model.PlayerRights;
    
    public final class ReflectionCommand implements Command {
    
    	private final Command command;
    	private final PlayerRights[] rights;
    	private final String syntaxHelp;
    
    	public ReflectionCommand(Command command, PlayerRights[] rights, String syntaxHelp) {
    		this.command = command;
    		this.rights = rights;
    		this.syntaxHelp = syntaxHelp;
    	}
    
    	public final Command getCommand() {
    		return command;
    	}
    
    	public final String getSyntaxHelp() {
    		return syntaxHelp;
    	}
    
    	public final PlayerRights[] getRights() {
    		return rights;
    	}
    
    	@Override
    	public boolean execute(Player player, String command) {
    		return this.command.execute(player, command);
    	}
    
    }


    Now you need to go to com.elvarg.net.packet.impl and replace CommandPacketListener.java to
    Spoiler for CommandPacketListener.java:

    Code:
    package com.elvarg.net.packet.impl;
    
    import com.elvarg.net.packet.Packet;
    import com.elvarg.net.packet.PacketListener;
    import com.elvarg.net.packet.impl.command.CommandHandler;
    import com.elvarg.world.entity.impl.player.Player;
    
    /**
     * This packet listener manages commands a player uses by using the command
     * console prompted by using the "`" char.
     * 
     * @author Gabriel Hannason
     */
    
    public class CommandPacketListener implements PacketListener {
    
    	@Override
    	public void handleMessage(Player player, Packet packet) {
    		String command = packet.readString();
    		CommandHandler.getInstance().executeCommand(player, command);
    	}
    
    }


    Final step: go to Elvarg.java and add
    Code:
    CommandHandler.getInstance().loadCommands();
    under
    Code:
    logger.info("Initializing the game...");

    Adding commands
    create a package in com.elvarg.net.packet.impl.command.impl

    and create a name that implements Command and has a @CommandInfo annotation
    for example:
    Spoiler for Commands:


    Spoiler for Commands.java:

    Code:
    package com.elvarg.net.packet.impl.command.impl;
    
    import java.util.Map.Entry;
    
    import com.elvarg.net.packet.impl.command.Command;
    import com.elvarg.net.packet.impl.command.CommandHandler;
    import com.elvarg.net.packet.impl.command.CommandInfo;
    import com.elvarg.net.packet.impl.command.ReflectionCommand;
    import com.elvarg.world.entity.impl.player.Player;
    import com.elvarg.world.model.PlayerRights;
    
    @CommandInfo(
    		syntax = "commands", 
    		syntaxHelp = "::commands for a list of all commands")
    public class Commands implements Command {
    
    	@Override
    	public boolean execute(Player player, String command) {
    		for(Entry<String, ReflectionCommand> entry : CommandHandler.getInstance().getCommandsMap().entrySet()) {
    			if(!(entry.getValue().getCommand() instanceof Commands)) {
    				for(PlayerRights r : entry.getValue().getRights()) {
    					if(r.higherRight(player.getRights())) {
    						player.getPacketSender().sendMessage("Command syntax: " + entry.getValue().getSyntaxHelp());
    					}
    				}
    			}
    		}
    		return true;
    	}
    
    }


    Spoiler for AddItemCommand.java:

    Code:
    package com.elvarg.net.packet.impl.command.impl;
    
    import com.elvarg.net.packet.impl.command.Command;
    import com.elvarg.net.packet.impl.command.CommandInfo;
    import com.elvarg.world.entity.impl.player.Player;
    import com.elvarg.world.model.Item;
    import com.elvarg.world.model.PlayerRights;
    
    @CommandInfo(
    		syntax = "additem", 
    		syntaxHelp = "::additem-id-amount to add an item to your inventory",
    		rights={PlayerRights.DEVELOPER})
    public class AddItemCommand implements Command{
    
    	@Override
    	public boolean execute(Player player, String command) {
    		String[] segments = command.split("-");
    		try {
    			int id = Integer.parseInt(segments[1]);
    			int amount = Integer.parseInt(segments[2]);
    			player.getInventory().add(new Item(id, amount));
    		} catch(Exception ex) {
    			player.getPacketSender().sendMessage("error executing command: "+ segments[0]);
    			return false;
    		}
    		return true;
    	}
    	
    
    }

    Reply With Quote  
     

  2. #2  
    Registered Member Stevenhax's Avatar
    Join Date
    Jul 2014
    Posts
    387
    Thanks given
    55
    Thanks received
    64
    Rep Power
    42
    Code looks clean, however I've never worked with reflection before as far as I'm aware.
    Would you be so kind to explain me the benefits of using this opposed to the system most RSPS use?
    Reply With Quote  
     

  3. #3  
    Registered Member
    Optimum's Avatar
    Join Date
    Apr 2012
    Posts
    3,570
    Thanks given
    871
    Thanks received
    1,745
    Rep Power
    5000
    Quote Originally Posted by Stevenhax View Post
    Code looks clean, however I've never worked with reflection before as far as I'm aware.
    Would you be so kind to explain me the benefits of using this opposed to the system most RSPS use?
    as far as my example it is easier to add commands without the need to manually add it to a list, sort of dynamic + cleaner. Probably not recommend but it only happens at run-time once.
    Reply With Quote  
     

  4. #4  
    Theory Wins?
    Greyfield's Avatar
    Join Date
    Nov 2008
    Age
    32
    Posts
    1,585
    Thanks given
    61
    Thanks received
    265
    Rep Power
    310
    You don't need a third party library to write a class loader, just pointing that out.

    Similarities - https://www.rune-server.ee/runescape...-commands.html

    But aside from that, the annotation schema is a pretty cool idea(as I'm the one that originally did it), but when it comes down to it; it's just unnecessary, just syntax sugar.

    EDIT:
    I personally threw out the implementation I had a while ago as the plugin design that now replaced it has no need for annotated implementations; since the deployed PODs are dynamically loaded into memory during runtime anyways. But just a reason why something like this is uneccessary.



    Reply With Quote  
     

  5. Thankful user:


  6. #5  
    Registered Member
    Optimum's Avatar
    Join Date
    Apr 2012
    Posts
    3,570
    Thanks given
    871
    Thanks received
    1,745
    Rep Power
    5000
    Quote Originally Posted by Greyfield View Post
    You don't need a third party library to write a class loader, just pointing that out.

    Similarities - https://www.rune-server.ee/runescape...-commands.html

    But aside from that, the annotation schema is a pretty cool idea(as I'm the one that originally did it), but when it comes down to it; it's just unnecessary, just syntax sugar.

    EDIT:
    I personally threw out the implementation I had a while ago as the plugin design that now replaced it has no need for annotated implementations; since the deployed PODs are dynamically loaded into memory during runtime anyways. But just a reason why something like this is uneccessary.
    I love the idea of annotations also. It could really bring a better design. Thanks for your feedback.

    Your example does show that you are still entitled to add
    registerCommand(new PickupItemCommand());
    Where as I've eliminated that purpose.

    Still a great example although.
    Reply With Quote  
     

  7. #6  
    Blurite

    Corey's Avatar
    Join Date
    Feb 2012
    Age
    26
    Posts
    1,491
    Thanks given
    1,245
    Thanks received
    1,729
    Rep Power
    5000
    I love the annotations aspect, nice work.
    Attached image
    Reply With Quote  
     

  8. #7  
    Registered Member
    Optimum's Avatar
    Join Date
    Apr 2012
    Posts
    3,570
    Thanks given
    871
    Thanks received
    1,745
    Rep Power
    5000
    Quote Originally Posted by Tesla View Post
    I love the annotations aspect, nice work.
    Thanks a lot
    Reply With Quote  
     

  9. #8  
    Registered Member

    Join Date
    Nov 2014
    Posts
    253
    Thanks given
    39
    Thanks received
    146
    Rep Power
    248
    The reflection and loading parts are nice. Annotations are also nice.

    However you really need to get rid of this monstrosity in CommandHandler#executeCommand:

    Code:
    		for (Entry<String, ReflectionCommand> entry : commands.entrySet()) {
    			for (PlayerRights r : entry.getValue().getRights()) {
    				if (r.higherRight(player.getRights())) {
    					if (entry.getKey().startsWith(command.split("-")[0].toLowerCase())) {
    						if (!entry.getValue().getCommand().execute(player, command)) {
    							player.getPacketSender().sendMessage("try using: " + entry.getValue().getSyntaxHelp());
    						}
    						return;
    					}
    				}
    			}
    		}
    Utilize your map as map :/

    final String commandString = command.split("-")[0].toLowerCase(); //I'd split it up further for clarity
    final ReflectionCommand rfc = map.get(commandString);
    if(rfc.higherRights(player.getRights))
    //bla

    ALSO, there is no reason for ReflectionCommand and Command to be different, ESPECIALLY considering that ReflectionCommand doesn't even inherent from Command. Its super confusing. I see what you're doing with the classes, but you don't have to do it like that. (You can get annotations without making an instance iirc, then just make the new instance using Constructor - you can put in arguments via reflections too).
    Reply With Quote  
     

  10. Thankful user:


  11. #9  
    Registered Member
    Optimum's Avatar
    Join Date
    Apr 2012
    Posts
    3,570
    Thanks given
    871
    Thanks received
    1,745
    Rep Power
    5000
    Quote Originally Posted by Intrice Joe View Post
    The reflection and loading parts are nice. Annotations are also nice.

    However you really need to get rid of this monstrosity in CommandHandler#executeCommand:

    Code:
    		for (Entry<String, ReflectionCommand> entry : commands.entrySet()) {
    			for (PlayerRights r : entry.getValue().getRights()) {
    				if (r.higherRight(player.getRights())) {
    					if (entry.getKey().startsWith(command.split("-")[0].toLowerCase())) {
    						if (!entry.getValue().getCommand().execute(player, command)) {
    							player.getPacketSender().sendMessage("try using: " + entry.getValue().getSyntaxHelp());
    						}
    						return;
    					}
    				}
    			}
    		}
    Utilize your map as map :/

    final String commandString = command.split("-")[0].toLowerCase(); //I'd split it up further for clarity
    final ReflectionCommand rfc = map.get(commandString);
    if(rfc.higherRights(player.getRights))
    //bla

    ALSO, there is no reason for ReflectionCommand and Command to be different, ESPECIALLY considering that ReflectionCommand doesn't even inherent from Command. Its super confusing. I see what you're doing with the classes, but you don't have to do it like that. (You can get annotations without making an instance iirc, then just make the new instance using Constructor - you can put in arguments via reflections too).
    Thanks a lot for your feedback, looking at it yea, i should probably have made Command abstract passing in the values via constructor, that way i can get the correct syntax without having the ReflectionClass

    Map itself is an interface, hashmap is the easiest and probably most recommended in this instance (i'm sure)

    edit- updated
    Reply With Quote  
     

  12. #10  
    Registered Member

    Join Date
    Nov 2014
    Posts
    253
    Thanks given
    39
    Thanks received
    146
    Rep Power
    248
    Quote Originally Posted by Optimum View Post
    Thanks a lot for your feedback, looking at it yea, i should probably have made Command abstract passing in the values via constructor, that way i can get the correct syntax without having the ReflectionClass

    Map itself is an interface, hashmap is the easiest and probably most recommended in this instance (i'm sure)

    edit- updated
    I don't think you understand what I meant. Pretty much there is no reason for the "Command" class to even exist. Making ReflectionCommand inherent from Command and then TAKE Command as an argument in its constructor is counter-intuitive (weird mix of composition and inheritance). ReflectionCommand does nothing special for reflection, but simply takes PlayerRights and syntax - whcih is something needed for ALL commands

    Let me show you what I mean:

    Instead of reflectioncommand & command have this:

    Code:
    public abstract class Command {
    
    	private String syntaxHelp;
    	private PlayerRights[] rights;
    
    	
    	public Command() {
    	}
    
    	public String getSyntaxHelp() {
    		return syntaxHelp;
    	}
    
    	public PlayerRights[] getRights() {
    		return rights;
    	}
    	
    	
    	//These will act as setters that can be chained in the construction of
    	//Command, this is so you're able to keep your annotation structure
    
    	public final Command setSyntaxHelp(String syntaxHelp) {
    		this.syntaxHelp = syntaxHelp;
    	}
    	
    	
    	public final Command setRights(PlayerRights[] rights) {
    		this.rights = rights;
    		return this;
    	}
    
    	
    	@Override
    	public abstract boolean execute(Player player, String command);
    
    }
    and then inside your load commands do this instead:

    Code:
    			//not too familiar with this library, idk if its still implementsInterface for
    			//an abstract class
    			if(map.get(c.getName()).implementsInterface("com.elvarg.net.packet.impl.command.Command")) {
    				//^ I assume this makes sure that it must be type command, for safe cast
    				//Get constructor with correct parameters
    				@SuppressWarnings("unchecked")
    				Constructor<Command> constructor = (Constructor<Command>)c.getDeclaredConstructor();
    				
    				//Don't assume this will be annotation 0, make it certain!
    				CommandInfo annotation = (c.getDeclaredAnnotation(CommandInfo.class));
    				
    				// THIS will make an instance of command using the annotation variables
    				
    				Command reflectionCommand = constructor.newInstance().
    						setRights(annotation.rights()).setSyntaxHelp(annotation.syntaxHelp());
    				// Puts the command in the commands map
    				getInstance().commands.put(annotation.syntax().toLowerCase(), reflectionCommand);
    ...
    You could also use a constructor instead of using a "builder pattern" to construct Command, I just HATE having to implement default constructors in all my subclasses (also you assume that constructor args are in same order with
    Code:
     c.getDeclaredConstructor(String.class, PlayerRights[].class)
    Which is fair to assume, but can be annoying if someone decides to flip the order around or something in a subclass

    Also, I know map is an interface. It is also a data structure. Every implementation of Map will still implement the methods to effectively do the same things (just in "different ways" - which aren't going to be important 99% of the time and you just default to hashmap).
    Reply With Quote  
     


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. Using reflection to get data
    By EpicMeme in forum Old School RS (2007)
    Replies: 0
    Last Post: 06-14-2013, 01:30 AM
  2. adding a alltome command using case's
    By scullboy491 in forum Help
    Replies: 12
    Last Post: 06-01-2013, 02:56 PM
  3. Definition List Shower Thingy using Reflection
    By Wiffles in forum Snippets
    Replies: 6
    Last Post: 10-25-2012, 05:36 PM
  4. Item Dumper using Reflection
    By Yarnova in forum Snippets
    Replies: 7
    Last Post: 03-27-2010, 05:54 PM
  5. Command used once?
    By CTucker in forum Help
    Replies: 7
    Last Post: 02-05-2009, 07: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
  •