Got a question that the wiki doesn't answer? Ask on the forum (preferred), or join us on IRC.

BeastNode

CommandHelper/Procedures

From EngineHub.org Wiki
Jump to: navigation, search


Procedures are essentially user defined functions. Creating a procedure will allow you to reuse code you have created, without having to copy-paste everywhere.

Contents

Overview

There are two steps to using a procedure: creating the procedure, and calling the procedure. To create a procedure, use the proc() function. The format of the proc function is:

proc(proc_name, [variables...], proc_code)

Where proc_name is a string, variables are ivars, and proc_code is any code you want to run. proc_name MUST begin with an underscore. This is to distinguish between procedures and functions, so that compile time checking of functions can still occur, and so that there will be no namespace collisions in the future (this limitation will be removed in the future, once namespaces are implemented). To call a procedure, you call it as if it were an actual function:

proc(_myProc,
     msg('I work!')
)

_myProc()

Note that if we called _myProc() above the call to proc(), the procedure would not have been defined yet, and the script would throw an InvalidProcedureException.

Variables

Let's look at a more complicated example with variables

#Adds two numbers and outputs the results (note the name of the procedure is "_addition")
proc(_addition, @i, @j,
    msg(add(@i, @j))
)

_addition(1, 1)
#Outputs 2

_addition(2, 2)
#Outputs 4


Variable Scope

Before we go into more complicated scripts, we have to discuss variable scope. Note that for the purposes of this discussion, "variable" refers to ivars (@ defined variables), not normal variables ($ defined variables). Up to this point, all variables have been defined in global scope; i.e. a variable defined anywhere in the script references the same variable regardless of where it is used elsewhere. Procedures have their own variables scope. So, when you define a procedure, the variables are not set at the point of procedure creation, nor is the code inside of the procedure executed. When you run a procedure, the variables inside of the procedure do not refer to variables of the same name , but only the name of the variables that have been passed in. (You can use import() and export() if you need to grab other values, but typically this should not be done.) Let's look at an example where this comes into effect:

assign(@i, 0)
msg('@i is currently' @i)
proc(_out, @i,
    msg('@i is currently' @i 'and @j is' @j)
    #Remember, default value of variables is an empty string
)
_out('hello')
assign(@j, 'goodbye')
_out('world')

Running this code yields:

@i is currently 0
@i is currently hello and @j is
@i is currently world and @j is

As you can see, @j, even though defined right before the call to _out('world') is not actually accessible inside of the procedure.

Another example:

proc(_a, 
     assign(@a, 1)
)
assign(@a, 2)
_a()
msg(@a)

#Echoes 2, not 1. The proc's variable scope doesn't affect the @a defined outside of the proc.

Default Values

The default value assigned to a variable is an empty string. So, even though a procedure may have 3 variables, if the script calls the procedure without including the 3 values, the script will fill in the extra parameters with a blank.

proc(_myProc, @i, @j, @k,
    msg(@i @j @k)
)

_myProc() #Outputs nothing
_myProc(1) #Outputs 1
_myProc(1, 2) #Outputs 1 2
_myProc(1, 2, 3) #Outputs 1 2 3
_myProc(1, 2, 3, 4) #Outputs 1 2 3

As you can see, the default value of a variable is blank if it is not send in the process of the call. In the fourth example, we are sending more variables than requested, and the script doesn't throw any kind of errors. That brings us to our next section: variable argument procedures.

In addition, you can set your own custom defaults, by using assign(), like this:


proc(_hello, assign(@i, 'world'),
    return(@i)
)
msg(_hello('hello'))
msg(_hello())

This would output the following:

hello
world

Please note that while you can include more complicated code inside of the assign, it may not behave like you would think. The code to define the defaults is only run once, when the procedure is initially defined, which means that if you were to call the function multiple times, the defaults cannot be changed. Let's look at the following code to demonstrate more complex behavior of this phenomenon:

assign(@j, 'world')
proc(_hello, assign(@i, @j),"
     return(@i)
)
assign(@j, 'goodbye')
msg(_hello('hello'))
msg(_hello())

This outputs "hello", then "world", even though we change the value of @j after we defined the procedure. You can think of the defaults as being "locked in" to their initial values. For this reason, you should generally only use unchanging things as the default, generally only strings or numbers.

Variable arguments

Sometimes, you don't know how many arguments will be sent to the function. In cases like this, it is helpful to be able to accept a number of arguments. In order to access all the arguments passed in to a procedure (whether or not they are named variables) you can use the pre-defined @arguments variable. This is an array with all variables passed in to the procedure, as an array.

proc(_myProc, 
    msg(@arguments)
)

_myProc() #Outputs {}
_myProc(1) #Outputs {1}
_myProc(1, 2) #Outputs {1, 2}

Returning from a procedure

There are two ways to exit a procedure, besides getting to the end of the code, or running die() inside the procedure. Note that calling die kills the entire script though, and is usually not what should happen, either you should throw an exception (in the event of something bad happening) or you should return (in the event of normal execution).

return()

By default, a procedure returns void. However, using the return() function, a procedure can return a value up that can be used by the calling script. Since MScript is typeless, any value may be returned. If return() is called without arguments, void is returned, and program flow leaves the procedure.

Exceptions

If a procedure throws an exception, it is passed up the stack, and can be caught like any typical exception.

include()

By themselves, procedures are nearly useless from a code reduction standpoint. In order to use a procedure, you must first create it. If you create useful procedures that could be used by multiple commands, you would still have to copy paste the procedure definition from command to command. To prevent this from being necessary, the include() function exists. The include() function is functionally equivalent to eval(read('filepath')), except there are certain other benefits. Essentially what we are doing is reading in the contents of some external file, and evaluating it as a standalone MScript. The difference is that with include(), optimization is done, and the external files are not read in from the file system each time the script runs, they are compiled and cached. (They may already have been compiled and cached when the script that included it was compiled too, which is even better, because it will have already thrown compile errors.)

Ultimately, the argument to include must be a string, which should be the filepath to the external script to load. A simple example then, is this:

include('includes/custom_functions.ms')

however, more complicated scripts can be written that dynamically decide which files to load. All paths are relative to the file that is running, not the path to wherever the script started running.

If we were to look in custom_functions.ms, we might see something like the following:

proc(_server_name,
     return('The most awesome server ever')
)

proc(_complicated_proc, @a, @b, @c, @d, @e,
     #Super complicated code that uses all the variables
)

proc(_another_proc, @a, @b,
     #Another reusable section of code
)

Now, when we go back to our aliases.msa file, we see this:

/cmd = >>>
     include('includes/custom_functions.ms')
     #Now I can use all the procs defined in custom_functions.ms
     msg(_server_name())
     _complicated_proc('a', 'b', 'c', 'd', 'e')
     _another_proc('a', 'b')
<<<

auto_include.ms

In all scripts, the file auto_include.ms is automatically included. This file can have further includes, so it is possible to never write an include in your commands, and still take full advantage of external files. The file auto_include.ms is automatically created if it doesn't exist, and includes a sample procedure, to demonstrate usage. It is bad practice to include executable code inside of this file, and it is recommended that you only create procedures inside this file, and only include other files that also only create procedures.

Also note that CH automatically creates the includes folder. Though you are not required to use this folder, it can be used to help organize your external scripts.

Other notes

When a function is defined with proc(), it is defined inside the current scope. So, If a procedure is defined from within another procedure, it will not be available outside of that procedure. This can allow for "private" methods. Also to note, it is bad practice to use $variables in procedures, because normal variables are global throughout the entire script. Using them inside of a procedure is not technically restricted however.

Also, if you try to define a procedure twice, it will not cause an exception. The second call will replace the old version. If warnings are set to be displayed, you will however receive a warning.

Examples

Let's look at some more complex examples that demonstrates how to get the same functionality without proc() or include(), which should demonstrate more clearly why you would want to use them.

Example without proc() or include():

/msg1 $player = >>>
   msg(concat(color(red), player(), color(white), ': ', color(green), 'Message sent'))
   tmsg(concat(color(red), player(), color(white), ': ', color(green), 'This is message 1'))
<<<

/msg2 $player = >>>
   msg(concat(color(red), player(), color(white), ': ', color(green), 'Message sent'))
   tmsg(concat(color(red), player(), color(white), ': ', color(green), 'This is message 2'))
<<<

/msg3 $player = >>>
   msg(concat(color(red), player(), color(white), ': ', color(green), 'Message sent'))
   tmsg(concat(color(red), player(), color(white), ': ', color(green), 'This is message 3'))
<<<

This may not seem like much code, but what if we decide to change our color scheme for sending these messages? Now we have to change it in 6 places! So, let's make formatting our colors a procedure.

Example without include():

#In aliases.msa

/msg1 $player = >>>
     #You must define a proc before you can use it
     proc(_format_colors, @name, @msg,
        return(concat(color(red), @name, color(white), ': ', color(green), @msg))
     )
     msg(_format_colors(player(), 'Message sent'))
     tmsg($player, _format_colors(player(), 'This is message 1'))
<<<

/msg2 $player = >>>
     #You must define a proc before you can use it
     proc(_format_colors, @name, @msg,
        return(concat(color(red), @name, color(white), ': ', color(green), @msg))
     )
     msg(_format_colors(player(), 'Message sent'))
     tmsg($player, _format_colors(player(), 'This is message 2'))
<<<

/msg3 $player = >>>
     #You must define a proc before you can use it
     proc(_format_colors, @name, @msg,
        return(concat(color(red), @name, color(white), ': ', color(green), @msg))
     )
     msg(_format_colors(player(), 'Message sent'))
     tmsg($player, _format_colors(player(), 'This is message 3'))
<<< 

As you can see, this example repeats the exact same code 3 times. We were able to use the procedure twice in each command, so now if we want to change our color scheme, we only have to change it in 3 places, but since it is the same code in each, we have still duplicated a lot of effort. Let's see this code now when we move it to an external file:

#In includes/procs.ms
#Note that the whole file here is an mscript, so we don't use exactly the same format as in aliases.msa
proc(_format_colors, @name, @msg,
    return(concat(color(red), @name, color(white), ': ', color(green), @msg))
)

Now, lets look at our config:

#In aliases.msa
/msg1 $player = >>>
     include('includes/procs.ms')
     msg(_format_colors(player(), 'Message sent'))
     tmsg($player, _format_colors(player(), 'This is message 1'))    
<<<

/msg2 $player = >>>
     include('includes/procs.ms')
     msg(_format_colors(player(), 'Message sent'))
     tmsg($player, _format_colors(player(), 'This is message 2'))
<<<

/msg3 $player = >>>
     include('includes/procs.ms')
     msg(_format_colors(player(), 'Message sent'))
     tmsg($player, _format_colors(player(), 'This is message 3'))
<<<

Here you can see that we only had to define _format_colors once, even though we used it multiple times. If _format_colors would have been longer, you can see there is an even greater increase in benefit to moving it outside of the aliases.msa file. Also, if we discover a bug with the _format_colors proc, or if we want to change the color scheme, we only have to fix the code in one place, instead of 3 or 6. Further, we could have reduced this to 1 line each by making our function handle messaging the current player "Message sent", and using auto_include.ms.

#In auto_include.ms
proc(_format_colors, @name, @msg,
    return(concat(color(red), @name, color(white), ': ', color(green), @msg))
)
proc(_send_message, @to, @message,
    msg(_format_colors(player(), 'Message sent'))
    tmsg(@to, _format_colors(player(), @message))
)
#In aliases.msa
/msg1 $player = _send_message($player, 'This is message 1')

/msg2 $player = _send_message($player, 'This is message 2')

/msg3 $player = _send_message($player, 'This is message 3')





Namespaces

Variants
Actions