Galaxy++ editor by Beier

10.6k Downloads

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.