Jump to content

Script Extensions

From Ironworks Games Wiki

Introduction

When making mods, it's very common that you want to extend the functionality of a class or struct without replacing the script file entirely, since doing so would mean that multiple mods wouldn't be able to work together since only one version of the script file would be loaded at a time. It would also mean that mods that rely on the base game data wouldn't get any script updates or fixes when the base game updates.

extend Keyword

The solution to this is theextend keyword. By extending classes, structs and enums, mods can add or replace functionality in a class without needing to copy the whole thing. When you extend an object, anything that you declare will be added to the end of the existing object, allowing you to add new methods, variables, and even interfaces, all without touching the original files.

todo: The ability to add interfaces to existing classes has not yet been implemented.

Example:

// This could be defined anywhere, even in the base game
class MyClass
{
public:
	enum MyEnum
	{
		Value1,     // 0
		Value2,     // 1
		Value3 = 10 // 10
	}
	
	void MyMethod() { Console.Log("Called Original MyMethod!"); }
}

// The extensions added by your mod
extend class MyClass
{
public:
	// Add some elements to the end of MyEnum,
	// continuing the sequence from the initial declaration.
	extend enum MyEnum
	{
		ExtValue1, // 11
		ExtValue2, // 12
	};
	
	// Replace the original 'MyMethod' with a new one
	replace MyMethod() { Console.Log("Called Replaced MyMethod!"); }
	
	// Add a new method to MyClass
	void AddedMethod() { Console.Log("This method was added in an extend!"); }
	
	// Add a new variable to MyClass
	int SomeValue = 5;
}

// [...]

void Main()
{
	MyClass myClass = new MyClass;
	
	// Prints "Called Replaced MyMethod!"
	myClass.MyMethod();
	
	// Prints "This method was added in an extend!"
	myClass.AddedMethod();
	
	// myClass.SomeValue will now be 7
	myClass.SomeValue += 2;
	
	// Prints "Number of elements in MyEnum: 5", since we added 2 new elements
	Console.Log($"Number of elements in MyEnum: {countof(MyClass.MyEnum)}");
}

replace Keyword

There is also the replace keyword, which allows you to outright replace an existing class, struct, enum or method. However this should be used with care, as there's a chance another mod that the player wants to use relies on the original structure of the object you replaced.

Note that replace completely removes all existing object, method and variable declarations, and if you wish to have them in your replacement, you'll have to re-add them yourself.

Example:

// This could be defined anywhere, even in the base game
class MyClass
{
public:
	void MyMethod() { Console.Log("This will never be called"); }
	void RemovedMethod() { }
}

// The replacement added by your mod
replace class MyClass
{
public:
	void MyMethod() { Console.Log("This is the replacement!"); }
}

// [...]

void Main()
{
	MyClass myClass = new MyClass;
	
	// Will print "This is the replacement!"
	myClass.MyMethod();
	
	// Will throw a compiler error, as the replacement class 
	// doesn't contain RemovedMethod
	myClass.RemovedMethod();
}
warning: Note that when the same Object is replaced multiple times in the same Assembly, the *last* replace for that Object will be the version that ends up in the final Assembly. While this is a deterministic order for now (In order of the files in IncludeScripts.mdf), this may change in the future, so try and avoid multiple replacements if you can!

Replacing an Object within an existing Object

If you want to replace, say, an enum within an existing class, but you don't want to replace the class, you can extend the class and then replace the enum within it. This also works for classes and structs.

Example:

class MyClass
{
public:
	enum ReplaceMe
	{
		Value1,
		Value2,
		Value3
	};
}

extend class MyClass
{
public:
	replace enum ReplaceMe
	{
		ReplacementValue1,
		ReplacementValue2,
	}
	
	// 'ReplaceMe' now only contains the two elements above
}

Method Replacements

Methods cannot use extend, but they can usereplace, however it works slightly differently than it does with objects.

Rather than completely erasing the original method, the original can still be called from your replaced method by using the call_replace_base method, which takes the same arguments and returns the same type as the original method. This is useful if you want to extend the functionality of an existing method, or only call it under certain circumstances.

Note that you can only replace methods when extending an Object.

Example:

class MyClass
{
public:
	void ToReplace(int value) 
	{
		Console.Log($"Called the original with value: {value}");
	}
}

extend class MyClass
{
public:
	bool CallOriginal = false;

	replace void ToReplace(int value)
	{
		if (CallOriginal)
		{
			call_replace_base(value);
		}
		else
		{
			Console.Log($"Called the replacement with value: {value}");
		}
	}
}

// [...]

void Main()
{
	MyClass myClass = new MyClass;
	
	// Prints "Called the replacement with value: 5"
	myClass.ToReplace(5);
	
	// Prints "Called the original with value: 32"
	myClass.CallOriginal = true;
	myClass.ToReplace(32);
}
warning: Like with Object replacements, Method replacements happen in the order they're declared in code in a Last-In-First-Out priority.

Limitations

  • You cannot replace anything that has been externally defined in the engine.
  • You cannot replace namespaces.
  • You cannot use extend to add variables to structs that have been externally defined in the engine, since the engine expects these structs to be of a specific size.
  • If the original class is static, the new class must ALSO be static. This goes for both extend and replace.
  • replaced methods must match the signature of the original, including static and virtual modifiers.