The program is now only released on my ftp server - see the thread linked below. http://forums.sc2mapster.com/resources/third-party-tools/19619-galaxy-editor/#p1
Documentation
General
Galaxy++
is a programming language that extends upon the original galaxy language, adding a lot more features. This means that any galaxy code you have should run exactly the same way when feeding it to the galaxy++
compiler.
Like most other programming languages, comments and whitespace is ignored by the compiler, so they can be placed pretty much anywhere without changing the behavior of the program.
If you feel some of your questions still go unanswered after reading this, or you find errors in the documentation, please pm me or post a reply. I will get back to you, and perhaps add to the documentation.
Compiler options
Although they might not classify as a language feature, I feel it's relevant to highlight some of the options in the compiler.
Obfuscation
There are a couple of tools for obfuscating the code. One is to rename everything to short names such as a, b, c. The other is to obfuscate string literals. What this will do is to "encrypt" every string so it is not readable by humans, and "decrypt" them when starting the map.
Please note that no code obfuscation can completely prevent others from reading, understanding and stealing your code. It just makes it difficult.
Declarations
Namespaces
There are multiple ways of defining namespaces.
namespace a.b.c //This way, everything in the file will be in the namespace a.b.c using a.b.c//The whole file can directly reference declarations in the namespace a.b.c int field1; namespace d.e { //Stuff in here will be in the a.b.c.d.e namespace int field2; } namespace d { int field3; namespace e { //Stuff in here will also be in the a.b.c.d.e namespace int field4; void method() { a.b.c.d.field3 = 1;//Have to make an absolute naming field1 = 2;//Can be referenced directly due to the using declaration. field2 = field4;//Both declarations are in the current namespace } } } void method() { field1 = 2;//You can directly reference declarations in your current namespace d.field3 = 3;//You can reference namespaces in your current namespace directly a.b.c.d.e.field4 = 4;//You can also choose to write the whole namespace }
Initializers and Library definitions
Initializes are basically methods that are run at first, when the map loads. Unless you made your own call to initialize the standard library, this call will be inserted before calling the initializers. You can make a simple initialize with
Initializer { [statements] }
The initializers can also contain data about a library you are defining. There are 4 different library fields: LibraryName, LibraryVersion, SupportedVersions and RequiredLibraries. An example of a library initializer is
Initializer ( LibraryName = "my library" LibraryVersion = "enterprise", SupportedVersions = "trial, basic" RequiredLibraries = "other lib 1:version2, (otherLib2:v1.7.4)" ) { [statements] }
All the library fields are optional, except you need both a name and a version in order for others to add it as a requirement. If your library version is not on the list of supported versions, it will be added. You may not use “:”, “(“, “)” or “,” in the name or versions, and all leading and trailing whitespace is ignored.
The method block is optional. It can be replaced with a semicolon.
If an initializer has defined any required libraries, the initializers of those libraries will be called first. In case of cyclic dependencies, a warning is reported, and as few dependencies as possible are ignored.
Includes
Any custom includes are ignored. Includes will be inserted by the compiler as needed, but all code except for the standard libraries are required in the project.
Methods
Unlike normal galaxy, it is not needed to have defined the method before using it.
Inline methods
Inline methods are marked with the keyword inline. When compiling a call to an inline method, the contents of the method is inserted instead of calling the actual method. This is done in a way so that there is no semantic difference to calling the method instead.
inline int method(...) { ... }
Note that inline methods may not be recursive i.e. they may not be called from themselves.
Trigger methods
The Trigger can be put infront of methods, but it no longer has any effect. It was previously used to identify methods that were triggers, but this is now done by searching for a string in a call to TriggerCreate. The keyword is kept for backward compatibility.
Reference parameters and extra return values
You can mark parameters with the ref or out keywords. Arguments passed to parameters marked with one of these must be variables. The effect of ref is that any changes done to the variable during the method call is reflected back in the calling method after the call. out is similar. Like with ref the changes done to this variable will also be done after the call, but unlike ref, it is required that the called method assigns a value to an out parameter, and he cannot read the parameter before he assigns a value to it. After compilation, an out parameter is not actually passed to the function, it is only returned, so it can be used as extra return values.
string[17] strings; string Next(ref int i, out bool hasNext) { hasNext = i + 1 < strings.length; if (hasNext) return strings[++i]; return ""; }
Passing bulkcopy data
Another thing to not is that it is now possible to pass types that would otherwise result in a bulk copy error between methods. This includes struct types and arrays. From your point of view, they are passed in exactly the same way as normal types. The compiler will send them via the data table instead.
Fields
Fields are pretty much what they are in galaxy, except that you don't have to have defined them above where you use them.
Visibility modifiers
You can mark global methods, fields and properties as public or private. By default, they will be set to public. If you mark them as private, they can only be accessed from inside the current namespace.
For methods, fields and properties inside structs, you can mark them as public, private and protected.
Like above, marking them with public is the same as not marking them at all.
If you set them to private, they can only be used from within the current struct.
If you set them to protected, they can only be used from within the current struct, or some other struct that enherit from it.
namespace myNS private int privateField; struct Str { private int privateProp { get { return 2; } } protected void protectedMethod() { } }
Structs
Like with fields and methods, you don't have to define structs above where you use them.
A new thing in galaxy++
is that structs can now contain methods. Simply place a method inside a struct, and that method will be a struct method. Struct methods will always be called with a specific instance of the struct. It is possible to directly refer to the members of the struct that is being called on by just typing the name of the member.
struct Foo { int[10] elements; int GetSum() { int sum = 0; for (int i = 0; i < elements.length; i++) sum += elements[i]; return sum; } } ... void Method(Foo foo) { if (foo.GetSum() > 9000) ... ... }
Classes
Classes are defined in the same way as structs. They are added to be purely dynamic types.
class Foo { int a; int DoSomething() { return a++; } }
The differences between classes and structs are
- The this keyword can not be used in struct methods (but can be used in constructors).
- It is not possible to make a non pointer variable of a class.
- There are some extra work that needs to be done for structs when calling a method on a pointer type (var->method()). For class types, the pointer is simply sent to the method. See the generated code. In short, if you only use your type in a dynamic context, use classes instead of structs.
Generics
You can make generic structs or classes.
struct Pair<T, G> { T p1; G p2; } Pair<int, bool> field;
In the example above, a copy of Str is made, where all T in the struct are replaced with integer types, and all G are replaced with boolean types.
It's possible to nest the types, but be aware that if you type >> it will be taken as the bit shift operator.
Pair<unit, Pair<int, fixed>> field;//This won't parse Pair<unit, Pair<int, fixed> > field;//This will
The transformation are done before everything is type checked. As a result, you can do stuff like this
struct Str { void foo() { } } struct Gen<T> { void bar(T item) { //This looks fishy - no garuantee that T has a foo method. item.foo(); } } Gen<Str> field1;//Will be okay Gen<int> field2;//Will cause a type checking error
Enums
You can create an enum with the following syntax
enum Days { Monday = 1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
You can define an integer literal value for each of the elements. If you don't, they automatically get an integer value one higher than the previous element. If the first element hasn't been given a value, it will recieve the value 0. Two elements may not have the same value.
You can cast enums to/from int and byte, as well as to string.
Days myDay = Days.Thursday; int dayNr = (int)myDay;//Will get the value 4 dayNr++; myDay = (Days)dayNr;//Will get the element Friday string dayName = (string)myDay;//Will get the string "Friday"
Enrichments
Enrichments is a way to add properties and methods to non struct/class type variables. You can also use it to make a non struct type pointer be handeled in an array. See Array based pointers.
enrich unit { fixed HP { get { return UnitGetPropertyFixed(this, c_unitPropLife, true); } set { UnitSetPropertyFixed(this, c_unitPropLife, value); } } inline void Select(int player) { UnitClearSelection(player); UnitSelect(this, 1, true); } } void foo(unit u) { u.Select(1); u.HP++; }
You can enrich any type except structs or classes. So also pointers and arrays.
Constructors
It is now possible to define constructors in structs and classes.
They are defined like methods, except they have no return type, and they must have the same name as the enclosing struct/class.
class Foo { int A; Foo(int a) { A = a; } } void method() { Foo* foo = new Foo(2); ... }
Deconstructors
In addition to constructors, you can also define deconstructors. Theese will be called just before an object is deleted with the delete statement.
You can define them in both enrichments, structs and classes. In structs and classes, they must have the same name as the enclosing struct or class.
In enrichments, they can have any name.
class Foo { private Bar* b; //This is a deconstructor for Foo ~Foo() { //In this case, when foo is deleted, so is the variable b delete b; } }
You can also mark them as public/private/protected.
Array based pointers
With version 2.2.0, I added struct based pointers. Previously, all pointers was based on the data table, which means that all the values was entries in the data table.
Now you can specify that you want the struct/class pointers to be in a global array instead.
The advantage of having array based pointers is that they are faster than the data table.
On the downside however, you must specify an upper bound on the ammount of pointers you will have allocated at one time. This number will be the dimensions of a global array.
That means that the memory required by the entire array will be reserved all the time. That is a problem, because a map can't use more than 2mb of memory for the script.
So, keep this number as low as you can, while you are sure that you won't ever need more pointers than that.
You can define the bound with the following code.
struct[42] Str { int i; bool b; }
It works the same way for classes and enrichments.
Enheritance
Structs and classes can enherit all fields properties and methods of another struct or class with the following syntax.
struct Super { int i; Super(int a) { this->i = a; } void foo(int a) { i = a; } } struct Sub : Super { int j; Sub(int a, int b) : base(a)//Call to the constructor in Super { this->j = b; } void bar(int a) { foo(a); j = i; } }
You can refference all fields, methods and properties from a supertype in a struct.
It is not possible to cast a type to a type that enherits it, however you can assign a struct type to a variable of a supertype.
void foo() { Sub sub; Super super; super = sub;//Allowed sub = (Sub) super;//Not allowed }
It is also not possible to override anything in the subtype.
Properties
Properties are basically a convinient way of making a getter and setter method, but use them as a single variable.
Like fields, properties can be defined either globally, or as a memeber of a struct/class.
fixed radians; fixed Degrees { get { return radians*180/3.14159; } set { radians = value/(180/3.14159); } }
Both the get block or the set block are optional, and the order in which they apear makes no diffrence.
Note that in the setter, the keyword value is used to refere to the value that is being assigned to the property.
You use the property just as you would use a field.
void m() { for(Degrees = 0; Degrees < 360; Degrees++) { FireProjectileOfDeath(radians); } }
You can also create array properties, which means you can do stuff like this.
enrich unitgroup { unit this[int index] { get { return UnitGroupUnit(this, index + 1); } } int Count { get { return UnitGroupCount(this, c_unitCountAll); } } } Initializer { unitgroup group = UnitGroup(null, c_playerAny, RegionEntireMap(), null, 0); for (int i = 0; i < group.Count; i++) { unit u = group[i]; ... } }
This type of property can only be placed inside structs and enrichments. The type of the index variable can be any type, as long as it doesn't result in a conflict with a normal array index. The property has to have the name this, but the index variable can have any name.
Bank preload statement
The preload bank statement from GUI triggers is not an actual method call in code, so I added extra functionality to call this.
PreloadBank("BankName", 2);
I felt that it would be misleading to place it inside functions, since it is not a statement which is executed there, but rather just a message to Starcraft II to load the bank when loading the map. Therefore it is placed out next to methods and fields. Note that the name and playerNr must be literals. I.e. you can not use any kind of variables or expressions other than what you see above.
Trigger declaration
There is a short way you can choose to specify triggers. Use the following notation
Trigger TriggerName { events { //Add the events the usual way TriggerAddEventMapInit(TriggerName); } conditions { if (...) return false; else if (...) return true; //Here, true is automatically returned } actions { //Add the actions of the trigger the usual way. } }
The events, conditions and actions sections are all optional. The conditions are only tested if testConds are true. The actions are only run if runActions are true, and testConds are false or the conditions return true.
If nothing is return from the conditions, true is assumed.
You can reference testConds and runActions from within the conditions and actions sections.
You can reference the trigger variable with the name of the trigger as done in the above example in events. You can also call the trigger function as you would normally be able to by typing TriggerName(<testConds>, <runActions>);
The return type in the actions section is void.
Operator overloading
You can define your own binary operators.
Initializer { point p1 = Point(1, 2); point p2 = p1 + 2.2;//p2 will then have the value (3.2, 4.2) } point operator +(point p, fixed f) { return Point(PointGetX(p) + f, PointGetY(p) + f); }
You can overload the following binary operators:
+, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, <<, >>
Like methods, you can mark operators as public/private, as well as static.
Statements
A statement is basically the members of functions where it doesn't make sense to talk about it having a type. Examples of statements in galaxy are while statements, if statements and expression statements. In this documentation I will only cover statements that are new or have added functionality in galaxy++
.
If statements
In galaxy you were forced to place a block inside if statements, now you can also place a single statement without the block.
if (i < ar.length) i++;
For statements
I added for statements in galaxy++
.
for ([init]; [test]; [update]) [body]
Basically, what it will do is first execute the init, then for as long as the test evaluates to true, the body and then the update is executed. Like GUI for sentences.
Switch statements
I also added switch statements.
int ammount; bool isRanged = false; switch (GetUnitTypeString()) { case "Zergling": ammount = 100; break; case "Marine": isRanged = true; case "Zealot": ammount = 50; break; case GetSuperUnitName(): ammount = 1; break; default: ammount = 10; break; }
It will test the expression it gets in the first parenthesis against every case in the order they are listed. When one of them equals, it will execute the contents of the case. If no cases were equal to the expression, the contents of default will be executed. Note that it is possible to fall through to the next case if one doesn't write break, like it is done from marine to zealot. Another thing to mention is that the method GetUnitTypeString() will only be called once, no matter how many cases there are. Also, GetSuperUnitName() is only called if none of the above cases matched.
AsyncInvoke statement
The AsyncInvoke statement will call a method in a new thread, and continue its own thread. This is done by running a new trigger, so the operator stack is also reset in the process (in case you are wondering, this is a good thing :)). If you wanted to call a method like
int CallMe(int a, bool b){...}
you could write
InvokeAsync<CallMe>(2, false);
In case CallMe is in another namespace called OtherNS, you can write
InvokeAsync<OtherNS.CallMe>(2, false);
Since the target function is called in a new thread, it is not possible to return a value from it, so any return values are ignored.
Local declarations
I made it possible to make multiple local declarations in one statement as long as they are of the same type.
int a = 2, b, c = a + 1;
Array Resize statement
Dynamic arrays can be resized more quickly than deleting them and recreating them.
int[] array = new int[42](); array->Resize(20);//Will run through and delete the 22 last elements array->Resize(5912730);//Will take constant time
As can be seen from the comments, shrinking takes linear time in the number of elements removed, and extending takes constant time regardless of the new size.
Expressions
Expressions are everything that can have a type. Examples of this is method calls, references to local, global or struct variables, binary operations (+, -, *, /, %), etc. Like with statements, I won't cover the expressions that are the same in galaxy as in galaxy++
.
The ++
and --
expressions
I added support for the ++
and --
expressions. They can be placed before and after a variable, and will increment or decrement the variable. When placed after the variable, the current value of the variable is placed where the expression is, and then the variable is updated. For instance, if you write
int i = 0; ar[i++] = i;
then then you will have set the 0th index of ar to 1. Placing it before the variable will update the variable, and then the updated value is placed where the expression is. E.g.
int i = 0; ar[++i] = i;
will set the 1st index of ar to 1. This is most commonly used as a quick way of writing i = i + 1;
Invoke
Similar to the AsyncInvoke statement, this expression will call the target method via a trigger in order to reset the operator stack. Unlike AsyncInvoke however, this is not done in a separate thread. This means that all return values can still be fetched.
InvokeSync<namespace.methodName>(args);
Assignments
With galaxy++
you can now place assignments inside expressions rather than just in statements. For instance you can write
a = b = c = d = 2;
and all of them will have the value 2. Note that the type of the assignments are the type of the left side. What this means is that for instance if c is of type fixed, and b is an integer, you will get an error since fixed is not assignable to int.
Array length
You can get the length of an array by calling .length on it.
String[2] ar; return ar.length; //returns 2
Casts and implicit casts
I added cast expressions. They are a quick way of converting between types that can be converted between.
fixed f = 2.2; int i = (int) f;
I also made some implicit casts, which means that between some types the cast occurs automatically.
int i = 2; UIDisplayMessage(playergroupAll(), c_messageAreaSubtitle, "i = " + i);
In this example, the i is cast to a string, then the two strings are concatenated, and the whole string is cast to a text.
The this expression
You can use the this expression inside constructors and class methods. It is a pointer to the current class/struct.
class Foo { int* a; ... void Dispose() { delete a; delete this; } }
Expression if's
The syntax for creating an expression if is
<condition> ? <then branch> : <else branch>
If the condition is true, the expression will return the then branch, and otherwise the else branch.
Note that the types of the then and else branches must in some way be assignable to eachother. In other words, either the type of the then branch must be assignable to the type of the else branch, or the type of the else branch must be assignable to the type of the then branch.
int Tester(bool b) { return b ? 1 : 2; }
Pointers
This is placed a little out of place compared to the structure of the rest of the documentation, but I thought I would keep everything regarding pointers together.
Pointer types
You can define a type as a pointer by appending *. For instance
int* i; string** s;
Here i will be a pointer to an int, and s will be a pointer to a pointer to a string.
Dynamic array types
You can define a dynamic array type by not specifying array bounds. For instance
int[] ar1; int*[] ar2;
Here, ar1 is a dynamic array of type int, and ar2 is a dynamic array of pointers to int's.
The * expression
If you have an expression of pointer type, and you would like to get the value of that expression, you prefix it with *. For instance
void foo(string* s) { UIDisplayMessage(PlayerGroupAll(), c_messageAreaSubtitle, (text)(*s)); }
Here, the *is needed to get the actual contents of s. Also note that *s is in a parenthesis. If this was not the case the compiler would have tried to multiply a variable called text with the variable s.
The -> expression
The -> expression is really just a quick way of writing (*foo).bar.
struct Foo { int bar; } void method(Foo* foo) { foo->bar = 2; (*foo).bar = 2; }
Here, the two statements are equivalent. In both cases, bar will be set to 2.
The new expression
To create new pointers or dynamic arrays, use the new expression. For instance
int* i = new int(); int[] ar1 = new int[*i]();
Here, i is initialized to a new integer, and ar1 is initialized to a dynamic array of size *i (which will be 0 in this case).
Note that the new expression allocates a new instance in the data table. To avoid memory leaks, remember to call delete after you are done with the instance.
The delete statement
The delete expression will remove the supplied pointer or dynamic array from the data table. You should make sure that you call delete if you are done with your pointer or dynamic array. Note that the delete statement will only remove the actual pointer you supply to it. So writing
void foo(int*[] intAr) { delete intAr; }
Will remove the array, but will not remove any of the child pointers. If you wish to purge all allocated instances, you can clear the global data table with the method.
DataTableClear(true);
So, note that you shouldn't call this unless you wish to lose all pointers and dynamic arrays.
Null checks
As of 3.0.0, null checks are now reliable. What this means that if a pointer is uninitialized, pointing to a deleted object, or has been assigned null, writing (p == null) will return true.
int* p; Initializer { if (p == null) {//This will be true, since p has not been allocated yet p = new int(); } if (p == null) {//This will be false. ... } Wait(1, 0); if (p == null) {//This will be true if p was deleted during the wait ... } else { p = null;//Any null checks from here on will be true. } }
All of this is done with a constant ammount of operations (no loops).
Delegates (function pointers)
Definition
A delegate is a specification of a method. It is defined like a method, but without a body.
delegate int Comparer(MyStruct s1, MyStruct s2); ... void foo() { Comparer cmp; }
You can assign one type of delegate to another as long as the return types and parameters are explicitly assignable.
Delegate creation expression and invocation
To create a new "instance" of a delegate with the following code
MyDelegate d = delegate<MyDelegate>(MyMethod);
And then, to invoke the delegate, call .Invoke
d.Invoke(arg1, arg2);
If a delegate is called which has not been assigned a value, a runtime error will occur, and execution in that trigger will halt. Delegates are not dynamic types. There is no need to call delete for them.