System CommandParser

Discussion in 'Systems and Snippets' started by quraji, Nov 21, 2009.

  1. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    CommandParser v1.33
    by quraji​

    Requires:
    -Recent JassHelper (one that compiles Zinc)
    -Ascii by TheDamien

    About:
    CommandParser is an easy to use system that allows you to register functions to a chat command. When a player enters that command, the function will be called and passed an array of the entered arguments. You may then access these arguments in string form, or have the system try to typecast the argument into another type (as of right now, those are integer, real, boolean and player).

    Code (it's zinc):
    JASS:
    /*
    	CommandParser v1.33
    		by quraji
    
    		About:
    			CommandParser is an easy to use system that allows you to register functions to a chat command. When a player enters that command,
    			the functions will be called and passed an array of the entered arguments. You may then access these arguments in string form, 
    			or have the system try to typecast the argument into another type (as of right now, those are integer, real, boolean and player).
    			Read below for more details and usage examples.
    			
    		Config:
    			You will find configuration constants at the top of the library. Change them only if you know what you're doing.
    			
    			constant boolean START_PLAYER_INDECES_AT_ZERO = false;
    				If false, player indeces start at 1, otherwise they start at 0. Keep this as false if you want "1" to be player 1 (red).
    				Change to true if you want "1" to be player 2 (blue).
    			
    			constant string ARG_DELIMITER = " ";
    				This defines the string that separates arguments in a command. The default is the space character.
    				ARG_DELIMITER is automatically appended to the command when it is registered.
    				You can change this if you feel the need.
    			
    			constant boolean DEFAULT_PERMISSION = true;
    				This defines the default permission for all players when a command is registered. If true, all players
    				may activate the command as soon as it is created. If false, you must allow certain players to use it yourself
    				using the functions below.
    
    		Command Functions:
    			These are the functions you use to implement the system. x is any string you want to use for a command, like "-mycommand".
    			
    			Command[x].register(commandFunc func)
    				Call this to register a function to command x. This function must take type Args and return nothing.
    				Ex:
    				function myfunc takes Args args returns nothing
    					//stuff
    				endfunction
    				
    				function registercommand takes nothing returns nothing
    					call Command["-mycommand"].register(myfunc)
    				endfunction
    				
    				Note: Only one function at a time may be registered to a command (this may change)
    				
    			Command[x].unregister(commandFunc func)
    				Unregisters the function func from the command x.
    				
    			Command[x].enable(boolean flag)
    				Enables the command x if flag is true, else disables the command.
    				
    			Command[x].isEnabled() -> boolean
    				Returns true if the command is enabled, false if it isn't.
    				
    			Command[x].setPermission(player p, boolean flag)
    				Enables the command for player p if flag is true, or disables it if false.
    			
    			Command[x].setPermissionAll(boolean flag)
    				Allows the command for all players if flag is true, else disallows it for all players.
    				
    			Command[x].getPermission(player p) -> boolean
    				Returns true if the command is enabled for player p, false if not.
    		
    			Command[x].remove() or Command[x].destroy()
    				Removes the command entirely.
    
    			GetEnteredCommand() -> string
    				Call this from within your commandFunc to get the command string that was entered.
    			
    		Args Methods:
    			These are methods that you may use within your callback function on the Args parameter. x is the array index of the argument you want.
    			x can be an integer from 0 (the first argument) to args.size()-1 (the last argument).
    			
    			args.size() -> integer
    				Returns the size of the array.
    			
    			args[x].getStr() -> string
    				This will return a string matching the argument as the player typed it.
    			
    			args[x].isBool() -> boolean
    				This will return true if the argument can be interpreted as a boolean (it is "true", "false", "yes", "no", "on", "off", "1", or "0").
    				It will return false otherwise.
    			
    			args[x].getBool() -> boolean
    				Will return true if the argument is "true", "yes", "no", or "1". Will return false if is "false", "no", "off", "0", or something undefined.
    				You should use .isBool() first to see if it is a boolean value.
    				
    			args[x].isInt() -> boolean
    				Will return true if the argument is an integer value, or false if it isn't.
    				
    			args[x].getInt() -> integer
    				Will return the integer value of the argument. You should use .isInt() first to see if it is an integer.
    				If you use this and the argument is not an integer, it will return 0
    				
    			args[x].isReal() -> boolean
    				Will return true if the argument is a real value (an integer or a number with a decimal point), or false if it isn't.
    				
    			args[x].getReal() -> real
    				Will return the real value of the argument. You should use .isReal() first to see if it is a real.
    				If you use this and the argument is not a real, it will return 0.000
    				
    			args[x].isPlayer() -> boolean
    				Will return true if the arg can be interpreted as a player (it is a player number, name or color).
    				This will recognize if an argument is only a substring at the start of a player's name ("worl" can match "WorldEdit").
    				Returns false if not.
    				
    			args[x].getPlayer() -> player
    				Will return the a player matching the argument. You should use .isPlayer() first to see if it may be interpreted as a player.
    				This will recognize if an argument is only a substring at the start of a player's name ("worl" can match "WorldEdit").
    				If you use this and the system can't find a matching player, it returns null.
    				
    		Useage:
    			Once you register a function to a command, that function will be called when a player enters the command.
    			A pseudo-array of type Args holding the entered parameters will be passed to the function, which you can then access, just like an array.
    			You may use the .size() method to get the size of the array.
    			Ex:
    			function mycommandfunc takes Args args returns nothing
    				local integer i = 0
    				
    				// this loop displays all the arguments that the player entered
    				loop
    					exitwhen (i==args.size())
    					call BJDebugMsg(args[i].getStr())
    					set i = i + 1
    				endloop
    			endfunction
    		
    		Notes:
    			You may use GetTriggerPlayer() to get the player who entered the command.
    			You may use waits inside the callback function, but be aware both GetTriggerPlayer() and the Args array will not persist after it, 
    			so if you need to use a wait make sure you save all the values you want to keep in variables beforehand.
    		
    		Thanks:
    			Executor for pointing out a bug that allowed multiple commands to execute if a command contained another command inside it.
    			Prozix for inspiring me to add argument type-checking and type-casting.
    					
    		And that's it.
    */
    
    //! zinc
    library CommandParser requires Ascii
    // v1.33 - by quraji
    {
    	/* CONFIG */
    	
    	// If false, "1" refers to Player 1 (red), if true "1" refers to Player 2 (blue).
    	// default value: false
    	constant boolean START_PLAYER_NUMBERS_AT_ZERO = false;
    	
    	// The string that separates arguments when a command is typed.
    	// default value: " "
    	constant string ARG_DELIMITER = " ";
    	
    	// The default permission for all players when a command is first registered.
    	// If true, all players have access to any new command by default.
    	// default value: true
    	constant boolean DEFAULT_PERMISSION = true;
    	
    	/* ENDCONFIG */
    
    	private constant string MSG_COLOR = "|cffCC33CC";
    
    	// Display a CommandParser error.
    	private function msg_error (string msg)
    	{
    		DisplayTimedTextToPlayer(GetLocalPlayer(), .0, .0, 60., MSG_COLOR + "CommandParser Error:|r " + msg);
    	}
    	// Display a CommandParser warning.
    	private function msg_warning (string msg)
    	{
    		DisplayTimedTextToPlayer(GetLocalPlayer(), .0, .0, 60., MSG_COLOR + "CommandParser Warning:|r " + msg);
    	}	
    	
    	// Global string to record the entered command, and the function to return it
    	string EnteredCommand;
    	public function GetEnteredCommand()-> string { return EnteredCommand; }
    
    	// Function interface for functions to run on command
    	public type commandFunc extends function(Args);
    	
    	// Struct to store/typecheck/typecast values
    	public struct ArgsValue
    	{
    		private static string DEF_BOOL_TRUE[], DEF_BOOL_FALSE[];
    		private static constant integer DEF_BOOL_SIZE = 4;
    		
    		private static string DEF_PLAYERCOLORS[];
    		private static constant integer DEF_PLAYERCOLORS_SIZE = 15;
    		
    		private static integer MIN_PLAYER_INDEX = 1, MAX_PLAYER_INDEX = 12;
    		
    		private string source;
    		
    		// Returns the source string
    		method getStr ()-> string
    		{
    			return source;
    		}
    		
    		// Checks to see if source can be interpreted as a boolean.
    		// Returns true if source can be found in the array of "true" definitions, false if not.
    		method isBool ()-> boolean
    		{
    			integer i;
    			string s = StringCase(source, false);
    			for (i=0; i<DEF_BOOL_SIZE; i+=1)
    			{
    				if (s==DEF_BOOL_TRUE[i] || s==DEF_BOOL_FALSE[i])
    					return true;
    			}
    			return false;
    		}
    		
    		// Returns a boolean value interpreted from source.
    		// Returns true if source is found to be a "true" value, false otherwise.
    		method getBool ()-> boolean
    		{
    			integer i;
    			string s = StringCase(source, false);
    			for (i=0; i<DEF_BOOL_SIZE; i+=1)
    			{
    				if (s==DEF_BOOL_TRUE[i])
    					return true;
    			}
    			return false;
    		}
    		
    		
    		// Checks to see if source can be interpreted as an integer.
    		// Returns true if it only contains number characters, false if not.
    		method isInt ()-> boolean
    		{
    			string s = source;
    			integer ascii;
    			while (s!="")
    			{
    				ascii = Char2Ascii(SubString(s, 0, 1));
    				s = SubString(s, 1, StringLength(s));
    				if (!(ascii>=48 && ascii<=57)) // 0-9
    					return false;
    			}
    			return true;                                                       
    		}
    		
    		// Returns source as an integer.
    		// Returns 0 if source can't be interpreted as an integer.
    		method getInt ()-> integer
    		{
    			return S2I(source);
    		}
    		
    		// Checks to see if source can be interpreted as a real.
    		// Returns true if source only contains number characters, and at max 1 decimal point.
    		method isReal ()-> boolean
    		{
    			string s = source;
    			integer ascii, decimal_count = 0;
    			while (s!="")
    			{
    				ascii = Char2Ascii(SubString(s, 0, 1));
    				s = SubString(s, 1, StringLength(s));
    				if (ascii==46) // decimal point
    					decimal_count+=1;
    				else if (!(ascii>=48 && ascii<=57)) // 0-9
    					return false;
    			}
    			return (decimal_count<=1); // no self respecting real number has more than one decimal point
    		}
    		
    		// Returns source as a real.
    		// Returns 0.000 if source can't be interpreted as a real.
    		method getReal ()-> real
    		{
    			return S2R(source);
    		}
    		
    		// Checks to see if source can be interpreted as a player.
    		// Returns true if source matches a player number 1-12, or 0-11 if START_PLAYER_NUMBERS_AT_ZERO is true.
    		// Returns true if source matches a player color.
    		// Returns true if source matches a player's name, or is a substring at the start of a player's name.
    		method isPlayer ()-> boolean
    		{
    			integer i, length;
    			string s = StringCase(source, false);
    			
    			// check if source is a player number
    			if (isInt())
    			{
    				i = getInt();
    				if (i>=MIN_PLAYER_INDEX && i<=MAX_PLAYER_INDEX)
    					return true;
    			}
    			// check if source is a player color
    			for (i=0; i<DEF_PLAYERCOLORS_SIZE; i+=1)
    			{
    				if (s==DEF_PLAYERCOLORS[i])
    					return true;
    			}
    			// check to see if source matches or starts a player name
    			length = StringLength(s);
    			for (i=0; i<bj_MAX_PLAYERS; i+=1)
    			{
    				if (s==SubString(StringCase(GetPlayerName(Player(i)), false), 0, length))
    					return true;
    			}
    			return false;
    		}
    		
    		// Returns source as a player.
    		// Returns null if source can not be interpreted as a player.
    		method getPlayer ()-> player
    		{
    			integer i, j, length;
    			string s = StringCase(source, false);
    			
    			// check if source is a player number
    			if (isInt())
    			{
    				i = getInt();
    				if (i>=MIN_PLAYER_INDEX && i<=MAX_PLAYER_INDEX)
    					return Player(i-MIN_PLAYER_INDEX);
    			}
    			
    			if (s==DEF_PLAYERCOLORS[12]) // cyan -> teal
    			{
    				s = DEF_PLAYERCOLORS[2];
    			}
    			if (s==DEF_PLAYERCOLORS[13]) // grey -> gray
    			{
    				s = DEF_PLAYERCOLORS[8];
    			}
    			// check if source is a player color
    			for (i=0; i<bj_MAX_PLAYERS; i+=1)
    			{
    				if (s==DEF_PLAYERCOLORS[i])
    				{
    					for (j=0; j<bj_MAX_PLAYERS; j+=1)
    					{
    						if (GetPlayerColor(Player(j))==ConvertPlayerColor(i))
    						{
    							return Player(j);
    						}
    					}
    				}
    			}
    			
    			// Check if source matches or starts a player name
    			length = StringLength(s);
    			for (i=0; i<bj_MAX_PLAYERS; i+=1)
    			{
    				if (s==SubString(StringCase(GetPlayerName(Player(i)), false), 0, length))
    					return Player(i);
    			}
    			
    			return null;
    		}
    		
    		// Deallocate this struct and it's child.
    		method destroy ()
    		{
    			deallocate();
    		}
    		
    		// Allocate thistype struct.
    		static method create (string s)-> thistype
    		{
    			thistype vi = thistype.allocate();
    			vi.source = s;
    			return vi;
    		}
    		
    		// Initialize data
    		private static method onInit ()
    		{
    			// stole these definitions from Prozix :)
    			DEF_BOOL_TRUE[0] = "true";
    			DEF_BOOL_TRUE[1] = "yes";
    			DEF_BOOL_TRUE[2] = "1";
    			DEF_BOOL_TRUE[3] = "on";
    			DEF_BOOL_FALSE[0] = "false";
    			DEF_BOOL_FALSE[1] = "no";
    			DEF_BOOL_FALSE[2] = "0";
    			DEF_BOOL_FALSE[3] = "off";
    			
    			// player color strings
    			DEF_PLAYERCOLORS[0] = "red";
    			DEF_PLAYERCOLORS[1] = "blue";
    			DEF_PLAYERCOLORS[2] = "teal";
    			DEF_PLAYERCOLORS[3] = "purple";
    			DEF_PLAYERCOLORS[4] = "yellow";
    			DEF_PLAYERCOLORS[5] = "orange";
    			DEF_PLAYERCOLORS[6] = "green";
    			DEF_PLAYERCOLORS[7] = "pink";
    			DEF_PLAYERCOLORS[8] = "gray";
    			DEF_PLAYERCOLORS[9] = "lightblue";
    			DEF_PLAYERCOLORS[10] = "darkgreen";
    			DEF_PLAYERCOLORS[11] = "brown";
    			
    			// extra spellings..
    			DEF_PLAYERCOLORS[12] = "cyan";
    			DEF_PLAYERCOLORS[13] = "grey";
    			
    			static if (START_PLAYER_NUMBERS_AT_ZERO)
    			{
    				MIN_PLAYER_INDEX = 0;
    				MAX_PLAYER_INDEX = 11;
    			}
    		}
    	}
    	
    	// List of ArgsValue structs
    	public struct Args
    	{
    		private thistype last;
    		private thistype next;
    		private ArgsValue arg;
    		
    		private integer list_size;
    		method size ()-> integer { return list_size; }
    			
    		method operator[] (integer index)-> ArgsValue
    		{
    			thistype a = this;
    			integer i = 0;
    			if (index<0 || index>size())
    			{
    				debug msg_error("Attempt to access Args element out of bounds.");
    				return 0;
    			}
    			while (i<=index)
    			{
    				a = a.next;
    				i +=1;
    			}
    			return a.arg;
    		}
    		
    		// Allocate a new node, add it to the list and set it's value.
    		method add_arg (string s)
    		{
    			thistype a = thistype.allocate();
    			last.next = a;
    			last = a;
    			a.next = -1;
    			a.arg = ArgsValue.create(s);
    			list_size += 1;
    		}
    		
    		// Deallocate this struct, and members of it's list.
    		method destroy ()
    		{
    			thistype a = this, b;
    			while (a.next!=-1)
    			{
    				a = a.next;
    				b = a;
    				a.deallocate();
    				a = b;
    			}
    			deallocate();
    		}
    		
    		// Allocate thistype and initialize variables.
    		static method create ()-> thistype
    		{
    			thistype a = thistype.allocate();
    			a.last = a;
    			a.next = -1;
    			a.list_size = 0;
    			return a;
    		}
    	}
    	
    	// List of commandFuncs
    	struct Commandfuncs
    	{
    		private thistype last;
    		private thistype next;
    		private commandFunc func;
    		
    		private integer list_size;
    		method size ()-> integer { return list_size; }
    			
    		// Executes all commandFuncs in the list
    		method execute (Args args)
    		{
    			thistype c = this;
    			while (c.next!=-1)
    			{
    				c = c.next;
    				c.func.execute(args);
    			}
    		}
    		
    		// Attempts to find f in the list. If it is found, it is removed.
    		method remove_func (commandFunc f)
    		{
    			thistype prev, c = this;
    			while (c.next!=-1)
    			{
    				prev = c;
    				c = c.next;
    				if (c.func==f) // f was found, unlink it.
    				{
    					prev.next = c.next;
    					c.deallocate();
    					list_size -= 1;
    					return;
    				}
    			}
    			debug msg_warning("Attempt to unregister a function that didn't exist from a command.");
    		}
    			
    		
    		// Allocate a new node, add it to the list and set it's value.
    		method add_func (commandFunc f)
    		{
    			thistype c = this;
    			// Check for duplicate function
    			while (c.next!=-1)
    			{
    				c = c.next;
    				if (c.func == f)
    				{
    					debug msg_warning("Attempt to register function twice to a command.");
    					return;
    				}
    			}
    			// A duplicate was not found, allocate the node
    			c = thistype.allocate();
    			last.next = c;
    			last = c;
    			c.next = -1;
    			c.func = f;
    			list_size += 1;
    		}
    		
    		// Deallocate this struct, and members of it's list.
    		method destroy ()
    		{
    			thistype a = this, b;
    			while(a.next!=-1)
    			{
    				a = a.next;
    				b = a;
    				a.deallocate();
    				a = b;
    			}
    			deallocate();
    		}
    		
    		// Allocate thistype and initialize variables.
    		static method create ()-> thistype
    		{
    			thistype c = thistype.allocate();
    			c.last = c;
    			c.next = -1;
    			c.list_size = 0;
    			return c;
    		}
    		
    	}
    
    	public struct Command
    	{
    		private static hashtable Table = InitHashtable();
    		private static key KEY_COMMANDS;
    
    		private trigger trig;
    		private Commandfuncs funcs;
    		private string cmd;
    		private boolean player_permissions[12];
    		
    		static method operator[] (string s) -> thistype
    		{
    			thistype c = thistype(LoadInteger(Table, KEY_COMMANDS, StringHash(s)));
    			if (integer(c)==0)
    			{
    				// c doesn't exist, create it
    				c = thistype.create(s);
    			}
    			return c;
    		}
    		
    		private static method parse (Args args, string input)
    		{
    			integer i = 0;
    			while(i<StringLength(input))
    			{
    				if (SubString(input, i, i+1)==ARG_DELIMITER)
    				{
    					if (i>0) // there's something before the delimiter
    					{
    						args.add_arg(SubString(input, 0, i));
    					}
    					input = SubString(input, i+1, StringLength(input));
    					i = -1;
    				}
    				i += 1;
    			}
    			if (i>0) // there's stuff left
    			{
    				args.add_arg(input);
    			}
    		}
    		
    		private static method onChat ()-> boolean
    		{
    			Args args;
    			string input = GetEventPlayerChatString();
    			string command = GetEventPlayerChatStringMatched();
    			thistype c;
    			
    			// the command was found in the input, but it isn't the first thing, so exit
    			if (SubString(input, 0, StringLength(command))!=command) 
    				return false;
    
    			EnteredCommand = SubString(command, 0, StringLength(command)-StringLength(ARG_DELIMITER));
    			command = EnteredCommand;
    			c = thistype(LoadInteger(Table, KEY_COMMANDS, StringHash(command)));
    			
    			// the player is not allowed to use this command, exit
    			if (c.player_permissions[GetPlayerId(GetTriggerPlayer())]==false) 
    				return false;
    			
    			input = SubString(input, StringLength(command), StringLength(input));
    			
    			args = Args.create();
    			parse(args, input);
    			c.funcs.execute(args);
    			args.destroy();
    			
    			return false;
    		}
    		
    		private static method create (string s)-> thistype
    		{
    			thistype c = thistype.allocate();
    			integer i;
    			
    			// create the trigger and register the chat event for it
    			c.trig = CreateTrigger();
    			for (i=0; i<bj_MAX_PLAYERS; i+=1)
    			{
    				TriggerRegisterPlayerChatEvent(c.trig, Player(i), s+ARG_DELIMITER, false);
    				c.player_permissions[i] = DEFAULT_PERMISSION;
    			}
    			TriggerAddCondition(c.trig, static method thistype.onChat);
    			
    			c.funcs = funcs.create();
    			c.cmd = s;
    			
    			SaveInteger(Table, KEY_COMMANDS, StringHash(c.cmd), integer(c)); // save the struct into the table, using the command string as a key
    			
    			return c;
    		}
    		
    		method destroy ()
    		{
    			RemoveSavedInteger(Table, KEY_COMMANDS, StringHash(cmd));
    			DisableTrigger(trig);
    			DestroyTrigger(trig);
    			funcs.destroy();
    			deallocate();
    		}
    		method remove () { destroy(); } // alias for destroy
    		
    		// enable/disable the command
    		method enable (boolean flag)
    		{
    			if (flag) EnableTrigger(trig);
    			else DisableTrigger(trig);
    		}
    		// get whether or not the command is enabled
    		method isEnabled ()-> boolean
    		{
    			return IsTriggerEnabled(trig);
    		}
    		
    		// set permission for all players
    		method setPermissionAll (boolean flag)
    		{
    			integer i;
    			for (i=0; i<bj_MAX_PLAYERS; i+=1)
    			{
    				player_permissions[i] = flag;
    			}
    		}
    		// set permission to use a command for one player
    		method setPermission (player p, boolean flag)
    		{
    			player_permissions[GetPlayerId(p)] = flag;
    		}
    		// get command permission for a player
    		method getPermission (player p)-> boolean
    		{
    			return player_permissions[GetPlayerId(p)];
    		}
    		
    		// register a func to the command
    		method register (commandFunc func)
    		{
    			if (func == 0)
    			{
    				debug msg_warning("Attempt to register a null commandFunc to command: \""+cmd+"\"");
    			}
    			else
    			{
    				funcs.add_func(func);
    			}
    		}
    		// unregister a func from the command
    		method unregister (commandFunc func)
    		{
    			if (func == 0)
    			{
    				debug msg_warning("Attempt to unregister a null commandFunc from command: \""+cmd+"\"");
    			}
    			else
    			{
    				funcs.remove_func(func);
    				if (funcs.size()==0)
    				{
    					destroy();
    				}
    			}
    		}	
    	}
    }
    //! endzinc

    Sorry for the big indents...it's my text editor.

    Example Code:
    JASS:
    library goldwood initializer init requires CommandParser
    
        private function ShowHelp takes nothing returns nothing
            call ClearTextMessages()
            call BJDebugMsg("Type \"-give gold/wood #\" to give additional gold or wood to yourself.")
            call BJDebugMsg("Ex: -give gold 500")
        endfunction
        
        private function GiveStuff takes Args args returns nothing
            local player p = GetTriggerPlayer()
            local playerstate state
            local integer val
            
            call ClearTextMessages()
            
            // does player want help?
            if (args[0].getStr()=="?") then
                call ShowHelp()
                return
            endif
            
            // check if they typed "gold" or "wood"
            if (args[0].getStr()=="gold") then
                set state = PLAYER_STATE_RESOURCE_GOLD
            elseif (args[0].getStr()=="wood") then
                set state = PLAYER_STATE_RESOURCE_LUMBER
            else
                call BJDebugMsg("Invalid input, please enter \"wood\" or \"gold\"!")
                return
            endif
            
            // make sure they gave a proper number
            if ((args[1].isInt())) then
                set val = args[1].getInt()
            else
                call BJDebugMsg("Invalid input, please enter an integer value!")
                return
            endif
            
            // give them their reward!
            call SetPlayerState(p, state, GetPlayerState(p, state) + val)
            call BJDebugMsg("Added " + args[1].getStr() + " to " + GetPlayerName(p) + "'s stockpile of " + args[0].getStr() + "!")
        endfunction
        
        private function init takes nothing returns nothing
            call BJDebugMsg("Type \"-give ?\" to get help for the \"-give\" command.")
            call Command["-give"].register(GiveStuff)
        endfunction
    
    endlibrary


    Changelog:
    (version numbers are arbitrary)
    v1.33:
    -The system now matches player substrings (if they start the string). Ex: "qur" will match "quraji" as a player name.
    -Neatened code a bit.
    -Removed the encapsulation structs (I didn't like them).
    -Removed the WC3-style functions (I didn't like them).
    -Renamed a couple Command functions, and added a couple:
    • Command[x].isEnabled() -> boolean
    • Command[x].allow -> Command[x].setPermission
    • Command[x].allowAll -> Command[x].setPermissionAll
    • Command[x].getPermission(player p) -> boolean
    • Command[x].remove()
    v1.32:
    -The system should now match player colors ("red", "green", etc.) to the proper players even if player colors are changed in-game.

    v1.31:
    -Changed the calling method for command functions to .execute. This makes it possible to use waits in the command function (but if you do, the Args variable will be destroyed, so if you need to use it after a wait store the arguments in an array or something).
    -It is now possible to register multiple functions to a command, and unregister functions from a command.

    v1.30:
    -Changed Args a bit. You now have to use args[index].getStr() to get it's value.
    -But! There are some new methods that allow you to typecheck/typecast the arguments. Available types: integer, real, boolean, player. Check the documentation to see them.

    v1.20:
    -Fixed an issue that fired two (or more) commands if one command's text contained another. ("-kill" and "-killunit" would both fire if you typed "-killunit", for example. Theoretically this could still happen if you used a command with two words separated by spaces...but it's a lot less unlikely because of this.
    -Enabled allowing/disallowing a command on a per player basis.
    -Made Args a wrapper struct for Args_internal. This is for encapsulation purposes.
    -Added some WC3 style functions.

    v1.01:
    -Fixed an error that caused a double-free when someone used a command without arguments
    -Fixed an error that caused a command to be fired even if it wasn't at the beginning of the input (thanks to Executor for pointing it out!)

    v1.0:
    -Release


    Check out the demo map, it's cool! :D
     

    Attached Files:

    • Like Like x 4
  2. Executor

    Executor I see you

    Ratings:
    +57 / 0 / -0
    JASS:
            private integer list_size;
            method size () -> integer { return list_size; }


    JASS:
    readonly integer size


    Could this be faster?

    Anyway, nice idea and I like the []. syntax! +rep

    //Edit:

    for ex.:
    JASS:
    call Command["force"].register(someFunction)
    call Command["kill"].register(someFunction)


    What happens when typing:

    "force kill Unit1 Unit2"
     
  3. Jesus4Lyf

    Jesus4Lyf Good Idea™

    Ratings:
    +394 / 0 / -0
    Yeah, cool. But...
    I don't see why not. Change to execute? :)
     
  4. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    It shouldn't be faster, since size() should get inlined. I kind of like args.size() as opposed to args.size :)

    Thanks for the comment.

    Edit:
    It will activate the "force" command. The command is activated only if it is typed exactly as it is registered, and first. So, typing "force" will work but not " force" or "-force". I like it rigid like that :p

    At first I used execute, but then the called function must destroy the Args variable itself (or I keep it as and gamble that a new Args won't take it's place until the function is done! :p). So which is better? Not having to destroy the Args on your own or being able to use waits?
     
  5. Executor

    Executor I see you

    Ratings:
    +57 / 0 / -0
    JASS:
    		private static method onChat ()-> boolean
    		{
    			Args args = Args.create();
    			string input = GetEventPlayerChatString();
    			string command = GetEventPlayerChatStringMatched();
    			thistype c = thistype(LoadInteger(Table, KEY_COMMANDS, StringHash(command)));
    			
    			// remove the command itself from the input
    			input = SubString(input, StringLength(command), StringLength(input));
    			
    			// parse the input
    			parse(args, input);
    			
    			// call the registered function
    			c.cmdFunc.evaluate(args);
    			
    			// destroy args
    			args.destroy();
    			
    			return false;
    		}


    Well imo the use of this string will lead to both functions being executed, as you don't check whether the key word is at the front of the string or not.


    Edit:

    ================ Did some testing ==========

    JASS:
    scope Test initializer init
        private function test1 takes Args args returns nothing
            call BJDebugMsg("1")
        endfunction
        
        private function test2 takes Args args returns nothing
            call BJDebugMsg("2")
        endfunction
        
        private function init takes nothing returns nothing
            call Command["force"].register(test1)
            call Command["kill"].register(test2)
        endfunction
    endscope


    "kill" leads to
    • "2"
    • "Double free of type: Args"

    "force" leads to
    • "1"
    • "Double free of type: Args"

    "force kill force kill" leads to
    • 1
    • 2
    • 1
    • 2
     
  6. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    I think you're right...for some reason I was thinking that the trigger event would only match the front of the string. Let me make sure...

    Thanks for pointing that out.

    Edit: Yep, you're absolutely right, I'll change it after I eat ^.^
     
  7. Executor

    Executor I see you

    Ratings:
    +57 / 0 / -0
    No problem, + don't forget, to fix that only "force" raises an error.
     
  8. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    Fixed, and also fixed a double-free error.
    v1.01 :p

    Edit: Added to the documentation that you shouldn't register a command that contains another command, as it will fire both. I'll have to fix this in the future.
     
    • Like Like x 1
  9. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    Updated to v1.2 :thup:

    Just need to add functionality for registering multiple functions to a command, and I think this will be complete.
    Too tired to do it now though, goodnight :p
     
  10. Executor

    Executor I see you

    Ratings:
    +57 / 0 / -0
    Well it isn't that hard. You simply have to check if GetEventPlayerChatStringMatched() is at the front of the string.
     
  11. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    It isn't that simple, since if you have two commands, "-kill", and "-killunit", and you type "-killunit", both will fire since "-kill" is contained in "-killunit". Better yet, it's right in the front which will defeat that check you suggest :p

    Anyways, I fixed it by appending an ARG_DELIMITER (space by default) character to the command as it is being registered. This way only typing the previous commands as such will be allowed: "-kill " "-killunit ". The drawback is that you can't write the comand and argument as one word...but that's not much of a drawback is it? :p
     
  12. Executor

    Executor I see you

    Ratings:
    +57 / 0 / -0
    But does it make sense to have "-kill" and "-killunit"? I mean you get the "unit" as a parameter and so the "-killunit"-process should be doen in the "-kill" process.

    Imo you should not allow "-kill" and "-killunit" and allow to use parameters not seperated by delimiters.
     
  13. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    >But does it make sense to have "-kill" and "-killunit"? I mean you get the "unit" as a parameter and so the "-killunit"-process should be doen in the "-kill" process.

    That's probably how I would do it ("-kill unit xxx", "-kill player xxx", etc.), but I won't dictate that aspect. If you want a "-kill" and "-killplayer" command, go ahead.

    >Imo you should not allow "-kill" and "-killunit" and allow to use parameters not seperated by delimiters.

    I agree, and as of the last version this is no longer allowed. The way I fixed it means you can also not use a command with no parameters (unless you type the space after it). I don't mind that because that's not what this system is for, since you can easily trigger your own command like that.
     
  14. Tru_Power22

    Tru_Power22 You can change this now in User CP.

    Ratings:
    +144 / 0 / -0
    Very cool. Neat system. +rep.
     
  15. Jesus4Lyf

    Jesus4Lyf Good Idea™

    Ratings:
    +394 / 0 / -0
    >At first I used execute, but then the called function must destroy the Args variable itself

    You know how some event responses don't work after waits? I think it actually makes sense for Args to be like that. If you document that arguments will not be available after waits, I'm pretty sure that should resolve this.

    The alternative is you use a GetArgs() function which is like using GetTargetLocation or whatever, in that you must destroy the resulting object. It doesn't make sense to have an object passed as a parameter which you must then destroy, though.
     
  16. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    I was thinking of making it like this.. but I haven't yet because I'm not sure I can guarantee that the lifespan of Args will be the length of the callback function with .execute(), even if you don't use waits. I'll have to test it..

    Anyways, v1.3! :thup:
    Now has a set of awesome Args methods that will let you typecheck and typecast the arguments! Available typechecks/typecasts:
    • integer
    • real
    • boolean
    • player

    Check out the new demo map!
     
  17. Jesus4Lyf

    Jesus4Lyf Good Idea™

    Ratings:
    +394 / 0 / -0
    >player
    If you stole or used the algorithm from Darthfett's system, you're a hero. (Of course, credit him if you do. I think that's a good way to do things... :))
     
  18. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    I've been dreading having to make the typechecking/typecasting to player work even if colors/names have changed...does it do that?! :D
    I'll check it out.

    Also, it looks like the lifespan of Args lasts through the whole callback function, with .execute, so that's good. It will even last through a wait if a new Args isn't allocated in it's spot (not that you should depend on it...). I think I'll change to .execute next update :thup:
     
  19. Jesus4Lyf

    Jesus4Lyf Good Idea™

    Ratings:
    +394 / 0 / -0
    >It will even last through a wait if a new Args isn't allocated in it's spot (not that you should depend on it...).
    So that is unreliable and should not be used, maybe even throw errors in debug mode.

    You could have an autoDestroy boolean member that you could set to false in case you really want to access parameters after a wait. Then the user could optionally destroy it themselves. I think that might be nice.

    It's up to you. Personally, I'm happy with any solution you come up with. I just think ideally waits should be allowed in the callback...

    The sad thing is if you implemented this as a struct you could actually destroy the args at the end, including after waits. o.o

    In fact, why doesn't vJass allow you to call FunctionA.execute(params).after(FunctionB.execute(params))? :D
     
  20. quraji

    quraji zap

    Ratings:
    +143 / 0 / -0
    I think I'm going to switch to .execute, and not allow destroying at all from outside the library. Then it should act like an event response, which makes sense.

    Although, I kinda like that .autoDestroy thing...argh, I hate making decisions.

    Maybe more freedom is better than less? The worst that could happen is that the user forgets to destroy it (even after explicitly typing args.autoDestroy=false, must have amnesia), and a few Args instances never get recycled..who cares. How many times could that possibly happen in one game? And don't even talk about auto-recycling or something... :p
     

Share This Page