System CommandParser

quraji

zap
Reaction score
144
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&#039;s it.
*/

//! zinc
library CommandParser requires Ascii
// v1.33 - by quraji
{
	/* CONFIG */
	
	// If false, &quot;1&quot; refers to Player 1 (red), if true &quot;1&quot; 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: &quot; &quot;
	constant string ARG_DELIMITER = &quot; &quot;;
	
	// 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 = &quot;|cffCC33CC&quot;;

	// Display a CommandParser error.
	private function msg_error (string msg)
	{
		DisplayTimedTextToPlayer(GetLocalPlayer(), .0, .0, 60., MSG_COLOR + &quot;CommandParser Error:|r &quot; + msg);
	}
	// Display a CommandParser warning.
	private function msg_warning (string msg)
	{
		DisplayTimedTextToPlayer(GetLocalPlayer(), .0, .0, 60., MSG_COLOR + &quot;CommandParser Warning:|r &quot; + msg);
	}	
	
	// Global string to record the entered command, and the function to return it
	string EnteredCommand;
	public function GetEnteredCommand()-&gt; 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 ()-&gt; 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 &quot;true&quot; definitions, false if not.
		method isBool ()-&gt; boolean
		{
			integer i;
			string s = StringCase(source, false);
			for (i=0; i&lt;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 &quot;true&quot; value, false otherwise.
		method getBool ()-&gt; boolean
		{
			integer i;
			string s = StringCase(source, false);
			for (i=0; i&lt;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 ()-&gt; boolean
		{
			string s = source;
			integer ascii;
			while (s!=&quot;&quot;)
			{
				ascii = Char2Ascii(SubString(s, 0, 1));
				s = SubString(s, 1, StringLength(s));
				if (!(ascii&gt;=48 &amp;&amp; ascii&lt;=57)) // 0-9
					return false;
			}
			return true;                                                       
		}
		
		// Returns source as an integer.
		// Returns 0 if source can&#039;t be interpreted as an integer.
		method getInt ()-&gt; 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 ()-&gt; boolean
		{
			string s = source;
			integer ascii, decimal_count = 0;
			while (s!=&quot;&quot;)
			{
				ascii = Char2Ascii(SubString(s, 0, 1));
				s = SubString(s, 1, StringLength(s));
				if (ascii==46) // decimal point
					decimal_count+=1;
				else if (!(ascii&gt;=48 &amp;&amp; ascii&lt;=57)) // 0-9
					return false;
			}
			return (decimal_count&lt;=1); // no self respecting real number has more than one decimal point
		}
		
		// Returns source as a real.
		// Returns 0.000 if source can&#039;t be interpreted as a real.
		method getReal ()-&gt; 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&#039;s name, or is a substring at the start of a player&#039;s name.
		method isPlayer ()-&gt; boolean
		{
			integer i, length;
			string s = StringCase(source, false);
			
			// check if source is a player number
			if (isInt())
			{
				i = getInt();
				if (i&gt;=MIN_PLAYER_INDEX &amp;&amp; i&lt;=MAX_PLAYER_INDEX)
					return true;
			}
			// check if source is a player color
			for (i=0; i&lt;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&lt;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 ()-&gt; player
		{
			integer i, j, length;
			string s = StringCase(source, false);
			
			// check if source is a player number
			if (isInt())
			{
				i = getInt();
				if (i&gt;=MIN_PLAYER_INDEX &amp;&amp; i&lt;=MAX_PLAYER_INDEX)
					return Player(i-MIN_PLAYER_INDEX);
			}
			
			if (s==DEF_PLAYERCOLORS[12]) // cyan -&gt; teal
			{
				s = DEF_PLAYERCOLORS[2];
			}
			if (s==DEF_PLAYERCOLORS[13]) // grey -&gt; gray
			{
				s = DEF_PLAYERCOLORS[8];
			}
			// check if source is a player color
			for (i=0; i&lt;bj_MAX_PLAYERS; i+=1)
			{
				if (s==DEF_PLAYERCOLORS<i>)
				{
					for (j=0; j&lt;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&lt;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&#039;s child.
		method destroy ()
		{
			deallocate();
		}
		
		// Allocate thistype struct.
		static method create (string s)-&gt; thistype
		{
			thistype vi = thistype.allocate();
			vi.source = s;
			return vi;
		}
		
		// Initialize data
		private static method onInit ()
		{
			// stole these definitions from Prozix <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="smilie smilie--sprite smilie--sprite1" alt=":)" title="Smile    :)" loading="lazy" data-shortname=":)" />
			DEF_BOOL_TRUE[0] = &quot;true&quot;;
			DEF_BOOL_TRUE[1] = &quot;yes&quot;;
			DEF_BOOL_TRUE[2] = &quot;1&quot;;
			DEF_BOOL_TRUE[3] = &quot;on&quot;;
			DEF_BOOL_FALSE[0] = &quot;false&quot;;
			DEF_BOOL_FALSE[1] = &quot;no&quot;;
			DEF_BOOL_FALSE[2] = &quot;0&quot;;
			DEF_BOOL_FALSE[3] = &quot;off&quot;;
			
			// player color strings
			DEF_PLAYERCOLORS[0] = &quot;red&quot;;
			DEF_PLAYERCOLORS[1] = &quot;blue&quot;;
			DEF_PLAYERCOLORS[2] = &quot;teal&quot;;
			DEF_PLAYERCOLORS[3] = &quot;purple&quot;;
			DEF_PLAYERCOLORS[4] = &quot;yellow&quot;;
			DEF_PLAYERCOLORS[5] = &quot;orange&quot;;
			DEF_PLAYERCOLORS[6] = &quot;green&quot;;
			DEF_PLAYERCOLORS[7] = &quot;pink&quot;;
			DEF_PLAYERCOLORS[8] = &quot;gray&quot;;
			DEF_PLAYERCOLORS[9] = &quot;lightblue&quot;;
			DEF_PLAYERCOLORS[10] = &quot;darkgreen&quot;;
			DEF_PLAYERCOLORS[11] = &quot;brown&quot;;
			
			// extra spellings..
			DEF_PLAYERCOLORS[12] = &quot;cyan&quot;;
			DEF_PLAYERCOLORS[13] = &quot;grey&quot;;
			
			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 ()-&gt; integer { return list_size; }
			
		method operator[] (integer index)-&gt; ArgsValue
		{
			thistype a = this;
			integer i = 0;
			if (index&lt;0 || index&gt;size())
			{
				debug msg_error(&quot;Attempt to access Args element out of bounds.&quot;);
				return 0;
			}
			while (i&lt;=index)
			{
				a = a.next;
				i +=1;
			}
			return a.arg;
		}
		
		// Allocate a new node, add it to the list and set it&#039;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&#039;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 ()-&gt; 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 ()-&gt; 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(&quot;Attempt to unregister a function that didn&#039;t exist from a command.&quot;);
		}
			
		
		// Allocate a new node, add it to the list and set it&#039;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(&quot;Attempt to register function twice to a command.&quot;);
					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&#039;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 ()-&gt; 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) -&gt; thistype
		{
			thistype c = thistype(LoadInteger(Table, KEY_COMMANDS, StringHash(s)));
			if (integer(c)==0)
			{
				// c doesn&#039;t exist, create it
				c = thistype.create(s);
			}
			return c;
		}
		
		private static method parse (Args args, string input)
		{
			integer i = 0;
			while(i&lt;StringLength(input))
			{
				if (SubString(input, i, i+1)==ARG_DELIMITER)
				{
					if (i&gt;0) // there&#039;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&gt;0) // there&#039;s stuff left
			{
				args.add_arg(input);
			}
		}
		
		private static method onChat ()-&gt; boolean
		{
			Args args;
			string input = GetEventPlayerChatString();
			string command = GetEventPlayerChatStringMatched();
			thistype c;
			
			// the command was found in the input, but it isn&#039;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)-&gt; thistype
		{
			thistype c = thistype.allocate();
			integer i;
			
			// create the trigger and register the chat event for it
			c.trig = CreateTrigger();
			for (i=0; i&lt;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 ()-&gt; boolean
		{
			return IsTriggerEnabled(trig);
		}
		
		// set permission for all players
		method setPermissionAll (boolean flag)
		{
			integer i;
			for (i=0; i&lt;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)-&gt; boolean
		{
			return player_permissions[GetPlayerId(p)];
		}
		
		// register a func to the command
		method register (commandFunc func)
		{
			if (func == 0)
			{
				debug msg_warning(&quot;Attempt to register a null commandFunc to command: \&quot;&quot;+cmd+&quot;\&quot;&quot;);
			}
			else
			{
				funcs.add_func(func);
			}
		}
		// unregister a func from the command
		method unregister (commandFunc func)
		{
			if (func == 0)
			{
				debug msg_warning(&quot;Attempt to unregister a null commandFunc from command: \&quot;&quot;+cmd+&quot;\&quot;&quot;);
			}
			else
			{
				funcs.remove_func(func);
				if (funcs.size()==0)
				{
					destroy();
				}
			}
		}	
	}
}
//! endzinc</i></i></i></i></i></i></i></i>

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(&quot;Type \&quot;-give gold/wood #\&quot; to give additional gold or wood to yourself.&quot;)
        call BJDebugMsg(&quot;Ex: -give gold 500&quot;)
    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()==&quot;?&quot;) then
            call ShowHelp()
            return
        endif
        
        // check if they typed &quot;gold&quot; or &quot;wood&quot;
        if (args[0].getStr()==&quot;gold&quot;) then
            set state = PLAYER_STATE_RESOURCE_GOLD
        elseif (args[0].getStr()==&quot;wood&quot;) then
            set state = PLAYER_STATE_RESOURCE_LUMBER
        else
            call BJDebugMsg(&quot;Invalid input, please enter \&quot;wood\&quot; or \&quot;gold\&quot;!&quot;)
            return
        endif
        
        // make sure they gave a proper number
        if ((args[1].isInt())) then
            set val = args[1].getInt()
        else
            call BJDebugMsg(&quot;Invalid input, please enter an integer value!&quot;)
            return
        endif
        
        // give them their reward!
        call SetPlayerState(p, state, GetPlayerState(p, state) + val)
        call BJDebugMsg(&quot;Added &quot; + args[1].getStr() + &quot; to &quot; + GetPlayerName(p) + &quot;&#039;s stockpile of &quot; + args[0].getStr() + &quot;!&quot;)
    endfunction
    
    private function init takes nothing returns nothing
        call BJDebugMsg(&quot;Type \&quot;-give ?\&quot; to get help for the \&quot;-give\&quot; command.&quot;)
        call Command[&quot;-give&quot;].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
 

Attachments

  • CommandParser v1.33 (demo).w3m
    26.5 KB · Views: 377

Executor

I see you
Reaction score
57
JASS:
        private integer list_size;
        method size () -&gt; 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[&quot;force&quot;].register(someFunction)
call Command[&quot;kill&quot;].register(someFunction)


What happens when typing:

"force kill Unit1 Unit2"
 

quraji

zap
Reaction score
144
JASS:
        private integer list_size;
        method size () -&gt; integer { return list_size; }


JASS:
readonly integer size


Could this be faster?

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

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:
//Edit:

for ex.:
JASS:
call Command[&quot;force&quot;].register(someFunction)
call Command[&quot;kill&quot;].register(someFunction)


What happens when typing:

"force kill Unit1 Unit2"

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

I don't see why not. Change to execute? :)
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?
 

Executor

I see you
Reaction score
57
JASS:
		private static method onChat ()-&gt; 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;
		}


for ex.:
Jass:

JASS:
call Command[&quot;force&quot;].register(someFunction1)
call Command[&quot;kill&quot;].register(someFunction2)



What happens when typing:

"force kill Unit1 Unit2"

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(&quot;1&quot;)
    endfunction
    
    private function test2 takes Args args returns nothing
        call BJDebugMsg(&quot;2&quot;)
    endfunction
    
    private function init takes nothing returns nothing
        call Command[&quot;force&quot;].register(test1)
        call Command[&quot;kill&quot;].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
 

quraji

zap
Reaction score
144
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.

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 ^.^
 

quraji

zap
Reaction score
144
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.
 

quraji

zap
Reaction score
144
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
 

Executor

I see you
Reaction score
57
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.

Well it isn't that hard. You simply have to check if GetEventPlayerChatStringMatched() is at the front of the string.
 

quraji

zap
Reaction score
144
Well it isn't that hard. You simply have to check if GetEventPlayerChatStringMatched() is at the front of the string.

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
 

Executor

I see you
Reaction score
57
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.
 

quraji

zap
Reaction score
144
>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.
 

Jesus4Lyf

Good Idea™
Reaction score
397
>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.
 

quraji

zap
Reaction score
144
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.

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!
 

quraji

zap
Reaction score
144
>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... :))

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:
 

Jesus4Lyf

Good Idea™
Reaction score
397
>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
 

quraji

zap
Reaction score
144
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
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • The Helper The Helper:
    The bots will show up as users online in the forum software but they do not show up in my stats tracking. I am sure there are bots in the stats but the way alot of the bots treat the site do not show up on the stats
  • Varine Varine:
    I want to build a filtration system for my 3d printer, and that shit is so much more complicated than I thought it would be
  • Varine Varine:
    Apparently ABS emits styrene particulates which can be like .2 micrometers, which idk if the VOC detectors I have can even catch that
  • Varine Varine:
    Anyway I need to get some of those sensors and two air pressure sensors installed before an after the filters, which I need to figure out how to calculate the necessary pressure for and I have yet to find anything that tells me how to actually do that, just the cfm ratings
  • Varine Varine:
    And then I have to set up an arduino board to read those sensors, which I also don't know very much about but I have a whole bunch of crash course things for that
  • Varine Varine:
    These sensors are also a lot more than I thought they would be. Like 5 to 10 each, idk why but I assumed they would be like 2 dollars
  • Varine Varine:
    Another issue I'm learning is that a lot of the air quality sensors don't work at very high ambient temperatures. I'm planning on heating this enclosure to like 60C or so, and that's the upper limit of their functionality
  • Varine Varine:
    Although I don't know if I need to actually actively heat it or just let the plate and hotend bring the ambient temp to whatever it will, but even then I need to figure out an exfiltration for hot air. I think I kind of know what to do but it's still fucking confusing
  • The Helper The Helper:
    Maybe you could find some of that information from AC tech - like how they detect freon and such
  • Varine Varine:
    That's mostly what I've been looking at
  • Varine Varine:
    I don't think I'm dealing with quite the same pressures though, at the very least its a significantly smaller system. For the time being I'm just going to put together a quick scrubby box though and hope it works good enough to not make my house toxic
  • Varine Varine:
    I mean I don't use this enough to pose any significant danger I don't think, but I would still rather not be throwing styrene all over the air
  • The Helper The Helper:
    New dessert added to recipes Southern Pecan Praline Cake https://www.thehelper.net/threads/recipe-southern-pecan-praline-cake.193555/
  • The Helper The Helper:
    Another bot invasion 493 members online most of them bots that do not show up on stats
  • Varine Varine:
    I'm looking at a solid 378 guests, but 3 members. Of which two are me and VSNES. The third is unlisted, which makes me think its a ghost.
    +1
  • The Helper The Helper:
    Some members choose invisibility mode
    +1
  • The Helper The Helper:
    I bitch about Xenforo sometimes but it really is full featured you just have to really know what you are doing to get the most out of it.
  • The Helper The Helper:
    It is just not easy to fix styles and customize but it definitely can be done
  • The Helper The Helper:
    I do know this - xenforo dropped the ball by not keeping the vbulletin reputation comments as a feature. The loss of the Reputation comments data when we switched to Xenforo really was the death knell for the site when it came to all the users that left. I know I missed it so much and I got way less interested in the site when that feature was gone and I run the site.
  • Blackveiled Blackveiled:
    People love rep, lol
    +1
  • The Helper The Helper:
    The recipe today is Sloppy Joe Casserole - one of my faves LOL https://www.thehelper.net/threads/sloppy-joe-casserole-with-manwich.193585/
  • The Helper The Helper:
    Decided to put up a healthier type recipe to mix it up - Honey Garlic Shrimp Stir-Fry https://www.thehelper.net/threads/recipe-honey-garlic-shrimp-stir-fry.193595/
  • The Helper The Helper:
    Here is another comfort food favorite - Million Dollar Casserole - https://www.thehelper.net/threads/recipe-million-dollar-casserole.193614/

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top