Tribes Vengeance Tutorial: Making Mods

First Mod with UnrealScript

This Tutorial describes how to make a simple weapon a an universal weapon mod. The mod will replace the weapon, assign it to all inventory stations, add it to every armour class, replace it in loadouts and in loadout-profiles. In the defaultproperties you just have to define the old and new weapon-class and the quantity of ammo.

After you downloaded and set up UCC, you can start making the first mod.
Skills in other programming language are recommended!
UnrealScript is similar in basic design principles to Java, so the syntax is almost like PHP, C, or JavaScript. The compiled script is stored in u-packages, that can contain an unlimited amout of classes.

To make your own package, create a directory named like the package-name in [TVDir]\source\Game (dont use special characters or spaces!). In this Tutorial we name our package 'MyPackage'. So we create a dir named 'MyPackage' and inside this, we create another dir called 'Classes'. Here we can put all our script-files with the extension '.uc'.

Now we need a script-editor. You can use Notepad, but there are better ones like Notepad++ (that I use) or WOTgreal, a integrated development environment for UnrealScript.

Inside the '[TVDir]\source\Game\MyPackage\Classes'-dir create a new file named 'MyMutator.uc', that is our first class.
In the first line of this file write the following code:

class MyMutator extends Gameplay.Mutator;

The keyword 'class' has to be followed by the class name (must be the same like the filename). Then you have to define which class is extended by the new class. In this case it is 'Gameplay.Mutator' because we find the class 'Mutator', that is the basic class for all mutators, in the package 'Gameplay'. For more information about Class Syntax, see http://wiki.beyondunreal.com/wiki/Class_Syntax.

After the definition of the class, global variables used in the class have to be declared:

var string oldWeaponClassName;
var class<Gameplay.Weapon> oldWeaponClass;
var string newWeaponClassName;
var class<Gameplay.Weapon> newWeaponClass;
var int newWeaponAmmoQuantity;

oldWeaponClassName is the name of the old weapon-class, oldWeaponClass is the class-handle for the weapon (the mutator loads the class named like 'oldWeaponClassName' and stores it's handle into oldWeaponClass). Because this is a wepon mod, the class-type is 'Gameplay.Weapon' and we only can use child classes of the Weapon-class. It's the same thing with the following two variables, besides they are for the new weapon. The last variable contains the ammount of ammo the wepon can have. The keywords 'string', 'class' and 'int' defines what type the variables are. See http://wiki.beyondunreal.com/wiki/Variable_Syntax and http://unreal.epicgames.com/UnrealScript.htm for more information.

After the variable declaration we can start writing our own functions ans scripts. I already said, that the two classes have to be loaded and 'stored' into the class-variables (oldWeaponClass, newWeaponClass):

function PreBeginPlay()
{
	oldWeaponClass = class<Gameplay.Weapon>(DynamicLoadObject(oldWeaponClassName, class'Class'));
	newWeaponClass = class<Gameplay.Weapon>(DynamicLoadObject(newWeaponClassName, class'Class'));
	Super.PreBeginPlay();
}


All code inside the function PreBeginPlay() is executed before the game starts. So we load both classes, convert them to the right type (class'Gameplay.Weapon') and 'save' them in the two variables. After this we execute the PreBeginPlay-function of the super class. (This is important because functions in classes overwrites functions in super classes.)

We want the weapon to be in our quick-loadouts, so lets make a function to do that:

function ModifyPlayerProfile(Character c, string oldW, string newW)
{
	local int i, j;
	local TribesGui.PlayerProfile pp;
	
	pp = TribesGui.TribesGUIController(PlayerController(c.Controller).Player.GUIController).profileManager.GetActiveProfile();
	if(pp == None || oldW ~= "" || newW ~= "") return;

	pp.bReadOnly = true;
	for(i = 0; i < pp.loadoutSlots.length; i++)
		for(j = 0; j < pp.loadoutSlots[i].weaponClassNameList.length; j++)
			if(pp.loadoutSlots[i].weaponClassNameList[j] == oldW)
				pp.loadoutSlots[i].weaponClassNameList[j] = newW;
}
This function replacese all weapons of the class 'oldW' with the one of 'newW' in character's ('c') profile. First we declare some variables, 'i' and 'j' are for the two iterations, the 'pp' in for the 'PlayerProfile'. We get this in the next line, where the 'profileManager' of the 'GUIController' of the 'Player' of the 'Controller' of the 'Character' 'c' gets the avtive profile for us ;-). Then we check if everything is all right, or better, we check if something is wrong. If 'pp' does not exists or 'oldW'/'newW' is empty we return nothing, so we stop the function from being executed. Because we dont want to save the changed Profiles with our new weapon (I dont think that anyone wants the server to modify his profile) we set 'bReadOnly' = true. We seach in all 'loadoutSlots' for the matching weapon and replace it with the new one.

Now we add the replace-funtion that replaces the weapon and assign it to all inventory stations:

function Actor ReplaceActor(Actor Other)
{
	local int i;

	if (Other.IsA(Name(oldWeaponClassName)))
	{
		Other.Destroy();
		return ReplaceWith(Other, newWeaponClassName);
	}

	if (Other.IsA('InventoryStationAccess'))
	{
		for(i = 0; i < InventoryStationAccess(Other).weapons.length; i++)
			if(InventoryStationAccess(Other).weapons[i].weaponClass == oldWeaponClass)
				InventoryStationAccess(Other).weapons[i].weaponClass = newWeaponClass;
	}

	return Super.ReplaceActor(Other);
}

This function is executed for each actor on the map. So we have to check if the current actor's class-name is the same like oldWeaponClassName. With the function IsA we can check if this is true, but we first have to convert oldWeaponClassName from a string to the datatype name (the funtion wants a name, not a string). So if the current actor is the weapon to replace, we destroy it and replace it with the new weapon-class.
If the actor's class-name is 'InventoryStationAccess' then its the access-class for all kind of inventory stations. With the for-loop we serach in the weapon-array for the oldWeaponClass and replace it with newWeaponClass.

To the loadouts:

function string MutateSpawnLoadoutClass(Character c)
{
	local int i, j;
	
	for(i = 0; i < c.team().combatRoleData.length; i++)
		for(j = 0; j < c.team().combatRoleData[i].role.default.defaultLoadout.default.weaponList.length; j++)
			if(c.team().combatRoleData[i].role.default.defaultLoadout.default.weaponList[j].weaponClass == oldWeaponClass)
				c.team().combatRoleData[i].role.default.defaultLoadout.default.weaponList[j].weaponClass = newWeaponClass;
	
	ModifyPlayerProfile(c,oldWeaponClassName,newWeaponClassName);	
	
	return Super.MutateSpawnLoadoutClass(c);
}

It is the same like in the invo-access: we search for the old wepon and replace it. In addition we modify the player's profile of the current character.

The final step is to allow weapons for the armors:

function string MutateSpawnCombatRoleClass(Character c)
{
	local int i, j;
	
	for(i = 0; i < c.team().combatRoleData.length; i++)
		for(j = 0; j < c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons.length; j++)
			if(c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons[j].typeClass == oldWeaponClass)
			{
				c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons[j].typeClass = newWeaponClass;
				c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons[j].quantity = newWeaponAmmoQuantity;
			}
	
	ModifyPlayerProfile(c,oldWeaponClassName,newWeaponClassName);
	
	return Super.MutateSpawnCombatRoleClass(c);
}

The MutateSpawnCombatRoleClass-function is called for character-modifications. Here we have two local variables that have to be declared at the beginning of the function for the two for-loops. The first loop go through all armor-types, the second through all weapons. If a weapon-class is the same like oldWeaponClass, it is replaced with newWeaponClass and its quantity (ammunition) is set to newWeaponAmmoQuantity.

To make sure that all player-profiles are modfied, we call our 'ModifyPlayerProfile'-function again.

Finally we give some values to the variables:

defaultproperties
{
	oldWeaponClassName	= "EquipmentClasses.WeaponSpinfusor"
	newWeaponClassName 	= "MyPackage.MySpinfusor"
	newWeaponAmmoQuantity	= 32
}

In the defaultproperties-section you can give all declared variable a (default)value. Here we dont use a semicolon to seperate the properties.

Now we have a nice weapon mutator that replaces all kinds of weapon. The file MyMutator.uc (download here) should now look like this:

class MyMutator extends Gameplay.Mutator;

var string oldWeaponClassName;
var class<Gameplay.Weapon> oldWeaponClass;

var string newWeaponClassName;
var class<Gameplay.Weapon> newWeaponClass;
var int newWeaponAmmoQuantity;


function PreBeginPlay()
{
	oldWeaponClass = class<Gameplay.Weapon>(DynamicLoadObject(oldWeaponClassName, class'Class'));
	newWeaponClass = class<Gameplay.Weapon>(DynamicLoadObject(newWeaponClassName, class'Class'));
	Super.PreBeginPlay();
}


function ModifyPlayerProfile(Character c, string oldW, string newW)
{
	local int i, j;
	local TribesGui.PlayerProfile pp;
	
	pp = TribesGui.TribesGUIController(PlayerController(c.Controller).Player.GUIController).profileManager.GetActiveProfile();
	if(pp == None || oldW ~= "" || newW ~= "") return;

	pp.bReadOnly = true;
	for(i = 0; i < pp.loadoutSlots.length; i++)
		for(j = 0; j < pp.loadoutSlots[i].weaponClassNameList.length; j++)
			if(pp.loadoutSlots[i].weaponClassNameList[j] == oldW)
				pp.loadoutSlots[i].weaponClassNameList[j] = newW;
}

function Actor ReplaceActor(Actor Other)
{
	local int i;

	if (Other.IsA(Name(oldWeaponClassName)))
	{
		Other.Destroy();
		return ReplaceWith(Other, newWeaponClassName);
	}

	if (Other.IsA('InventoryStationAccess'))
	{
		for(i = 0; i < InventoryStationAccess(Other).weapons.length; i++)
			if(InventoryStationAccess(Other).weapons[i].weaponClass == oldWeaponClass)
				InventoryStationAccess(Other).weapons[i].weaponClass = newWeaponClass;
	}

	return Super.ReplaceActor(Other);
}

function string MutateSpawnLoadoutClass(Character c)
{
	local int i, j;
	
	for(i = 0; i < c.team().combatRoleData.length; i++)
		for(j = 0; j < c.team().combatRoleData[i].role.default.defaultLoadout.default.weaponList.length; j++)
			if(c.team().combatRoleData[i].role.default.defaultLoadout.default.weaponList[j].weaponClass == oldWeaponClass)
				c.team().combatRoleData[i].role.default.defaultLoadout.default.weaponList[j].weaponClass = newWeaponClass;
	
	ModifyPlayerProfile(c,oldWeaponClassName,newWeaponClassName);	
	
	return Super.MutateSpawnLoadoutClass(c);
}

function string MutateSpawnCombatRoleClass(Character c)
{
	local int i, j;
	
	for(i = 0; i < c.team().combatRoleData.length; i++)
		for(j = 0; j < c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons.length; j++)
			if(c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons[j].typeClass == oldWeaponClass)
			{
				c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons[j].typeClass = newWeaponClass;
				c.team().combatRoleData[i].role.default.armorClass.default.AllowedWeapons[j].quantity = newWeaponAmmoQuantity;
			}
	
	ModifyPlayerProfile(c,oldWeaponClassName,newWeaponClassName);
	
	return Super.MutateSpawnCombatRoleClass(c);
}

defaultproperties
{
	oldWeaponClassName	= "EquipmentClasses.WeaponSpinfusor"
	newWeaponClassName 	= "MyPackage.MySpinfusor"
	newWeaponAmmoQuantity	= 32
}

Without a weapon the weapon mutator is useless. So we create a new file named 'MySpinfusor.uc' and put the following code inside:

class MySpinfusor extends Gameplay.Spinfusor;

class MySpinfusor extends Gameplay.Spinfusor;

defaultproperties
{
	localizedName = "Super Spinfusor"
	infoString = "This is my new Spinfusor."
	
	roundsPerSecond = 3
	ammoCount = 64
	ammoUsage = 1

	projectileClass = Class'EquipmentClasses.ProjectileSpinfusor'
	projectileVelocity = 1500
	projectileInheritedVelFactor = 1	
	

	// this properties are taken from 'EquipmentClasses.WeaponSpinfusor'
	
	attentionFXMaterial = Shader'FX.ScreenFindmeShader'
	emptyMaterials(1) = Shader'weapons.SpinfusorDialEmptyShader'
	fireAnimSubString = "large"
	firstPersonAltMesh = SkeletalMesh'weapons.HeavySpinfusor'
	firstPersonAltOffset = (X=-26,Y=22,Z=-18)
	firstPersonAltTraceExtent = (X=10,Y=20,Z=10)
	firstPersonAltTraceLength = 150
	
	firstPersonTraceExtent = (X=10,Y=20,Z=10)
	firstPersonTraceLength = 125
	
	thirdPersonAltStaticMesh = StaticMesh'weapons.HeavySpinfusorHeld'
	thirdPersonAttachmentOffset = (X=25,Y=-2,Z=5)
	thirdPersonStaticMesh = StaticMesh'weapons.spinfusorheld'
    
	pickupRadius = 60
	
	CollisionHeight = 12
	CollisionRadius = 35	
}


This class is very simple: it just extends the Spinfusor and changes the default-properties. 'localizedName' is the name of the weapon, 'infoString' is shown in invo-hud when you want to add the weapon to your equipment. 'roundsPerSecond' defines how often the weapon can be fired in a second. 'ammoCount' is the amount of ammon and 'ammoUsage' the usage of ammo per shot. projectileVelocity is self-explanatory. 'projectileInheritedVelFactor' is the factor the character's velocity is multiplied with, before adding it to the projectileVelocity.
(If you want to make a weapon with new functions, take a look in the weapon classes in '[TVDir]\source\Game\Gameplay\Classes\Equipment\Weapon'.)

After saving both files in the '[TVDir]\source\Game\MyPackage\Classes'-folder its time to compile. Open '[TVDir]\Program\Bin_dev\UCC.ini' in your text-editor and search for the lines starting with 'EditPackages='. Under these lines, add 'EditPackages=MyPackage', so that it looks like this:

EditPackages=Core
EditPackages=Engine
EditPackages=IGEffectsSystem
EditPackages=IGVisualEffectsSubsystem
EditPackages=IGSoundEffectsSubsystem
EditPackages=Editor
EditPackages=UWindow
EditPackages=GUI
EditPackages=TVEd
EditPackages=IpDrv
EditPackages=UWeb
EditPackages=UDebugMenu

; Tribes specific packages
EditPackages=MojoCore
EditPackages=MojoActions
EditPackages=PathFinding
EditPackages=Scripting
EditPackages=AICommon
EditPackages=Movement
EditPackages=Gameplay
;EditPackages=TribesGui
EditPackages=Tyrion
EditPackages=Physics
EditPackages=TribesAdmin
EditPackages=TribesWebAdmin
EditPackages=TribesVoting
EditPackages=TribesTVClient
EditPackages=TribesTVServer

; My packages
EditPackages=MyPackage

Save and close the file and run UCC, after changing to the right directory ('cd %PROGRAMFILES%\VUGames\Tribes Vengeance\Program\Bin_dev') :
'ucc make -nobind'

ucc make -nobind

If everything is fine, you should see something like that:

ucc make -nobind

Now, carefully take a look into '[TVDir]\Program\Bin_dev', where you should find the file 'MyPackage.u'. This is our compiled code! Copy the file to '[TVDir]\Program\Bin'.

Coding is finished, let's test what we did. Just type the following in the commandline: "cd ..\Bin" and "TV_CD_DVD.exe mp-emerald?mutator=MyPackage.MyMutator" and press enter:

running tribes