General Gamerule Scripting Tutorial
Part 1 - Introduction
Welcome to the first general scripting tutorial. The aim of this tutorial is to give people a start in scripting, much like the "Making the Keepers a Playable Race" gives them a start in general modding. Gamerule scripting is really an extension of the modding techniques everyone is familiar with. Simple modding involves modifying and creating new ships, weapons, subsystems, and races (with their associated tech trees). Scripting allows you to achieve so much more that cannot be achieved normally.
First of all, to be able to understand and use this tutorial, you will need to:
1. Have patched your HW2 to v1.1.
2. Have read and understood "Making the Keepers a Playable Race".
3. Have tried out some of the things in that tutorial, like making a ship, a weapon, a new build or research item etc. In other words, have got some experience in modding.
4. Understand how to use the Hw2.log. In other words, not be one of those people who posts their minidump file when asked for the log (we've all done it).
5. Know what Karos Graveyard is, and have a link to it, or, preferably, an offline version.
6. Have some understanding of programming concepts. This can be achieved multiple ways. If you have ever written a program, that should be enough. If you have ever read a book, teaching you how to do that, that should be enough. If you have ever done advanced scripting for a game, that should be enough. Basically, you need to know what an "If" statement is, what a "function" is, and so on.
7. Have some understanding of the LUA scripting language. They have an official site and manual, though you can work out most things just by looking at existing scripts. I still suggest you read the first few chapters of their manual though.
For those who haven't read all of the LUA manual (I haven't) some quick important things. LUA does not have a boolean (true/false) variable type. All numbers evaluate to true (even 0). So, you cannot use "if Player_IsAlive(PlayerIndex) then" alone. You must use "if Player_IsAlive(PlayerIndex) == 1 then". second, you do not have to declare the type of a variable when you declare it. The type is determined by the type of information you assign to the variable.
More can be added to the above paragraph, if anyone can think of something useful.
Ok, ready?
Let us begin.
Gamerule scripts in HW2 are used primarily for deciding when a player wins. They are basically a series of program instructions that can be executed regularly during the game, such as a check for whether a player has any build capable ships left. If they don't, the script can "Kill" them, meaning all their ships die, and they are treated as "dead". If all your enemies are dead, you win.
Also, the same scripting system is used for the Single Player campaign (so this tutorial can help you with that, as well, though it won't be the focus of it). As a result, there are many hard-coded functions, used by the developers to write the campaign, that can be used in a skirmish. This allows you to do many other special things with the scripts, besides determining win conditions.
The only gamerule script in vanilla HW2 is deathmatch.lua. It is located in the Data\leveldata\multiplayer folder, and contains the deathmatch gamerule. This, as you know, is the only one on the list, when you open "Player vs CPU". To add a new game rule, all you need to do is add another file, such as "mynewrule.lua" to that folder. There is another way, using *.grm files, but I don't know how to do that (someone please clarify). To start off, just make a copy of deathmatch.lua, rename it, and place it in the folder. Then start editing. You can make a new game rule. If you like, you can only edit the settings (those that the player can change from the menu, such as starting resources, and unit caps). But first, I suggest you read this tutorial.
One last thing you need to know before we can get into the actual scripts of the game is the "Sob Group".
SobGroups are the means through which HW2 manipulates ships in scripts. SobGroup stands for "Space Object Group", and is an abstract object, with a string name (used to refer to it in the script), that can contain 0 or more ships. It cannot contain ships that do not exist (i.e. have been blown to bits), but it CAN contain ships in hyperspace or that are docking.
To create a SobGroup you use SobGroup_Create("My_New_Sobgroup_Name")
You then use one of the many functions for filling it. (More on this later).
Finally, a useful feature of SobGroups and LUA allows you to create many similar groups, and iterate through them. in lua, you can concatenate two strings, or even a number, using the ".." operator. Thus, if you create a SobGroup for each player, containing their mothership, you can make groups named
"Player1Mothership" through to "Player6Mothership" respectively. To create them, you could refer to them as so: SobGroup_Create("Player"..PlayerIndex.."Mothership"), where PlayerIndex is a number variable that is iterated in a loop which goes through every player.
The game also supports a group for each player that contains, every frame, all of the player's active ships (not docked or in hyperspace). They are called "Player_Ships"..i , where i is the player's number, starting from 0, to a normal max of 5 (6 players).
Now that you know some things about gamerule scripting, let's start by examining an existing script, deathmatch.lua.
Part 2 - deathmatch.lua
First of all, you will need the deathmatch.lua file. You can get the decompiled version from a data.zip somewhere. You can get it from the karos graveyard, which is supposedly a better version. Or you can get my copy (taken form karos) below.
Let's start looking at this file in sections.Code:GUID = { 110, 91, 157, 190, 18, 23, 250, 78, 144, 20, 41, 246, 181, 128, 214, 12, } GameRulesName = "$3203" Description = "$3230" GameSetupOptions = { { name = "resources", locName = "$3240", tooltip = "$3239", default = 1, visible = 1, choices = { "$3241", "0.5", "$3242", "1.0", "$3243", "2.0", }, }, { name = "unitcaps", locName = "$3214", tooltip = "$3234", default = 1, visible = 1, choices = { "$3215", "Small", "$3216", "Normal", "$3217", "Large", }, }, { name = "resstart", locName = "$3205", tooltip = "$3232", default = 0, visible = 1, choices = { "$3206", "1000", "$3207", "3000", "$3208", "10000", "$3209", "0", }, }, { name = "lockteams", locName = "$3220", tooltip = "$3235", default = 0, visible = 1, choices = { "$3221", "yes", "$3222", "no", }, }, { name = "startlocation", locName = "$3225", tooltip = "$3237", default = 0, visible = 1, choices = { "$3226", "random", "$3227", "fixed", }, }, } dofilepath("data:scripts\\scar\\restrict.lua") function OnInit() MPRestrict() -- changed this from 'Rule_Add' to 'RuleAdd_Interval' to speed the game up -Mikail Rule_AddInterval("MainRule", 1) end ------------------------------------------------------------------------------- -- main rule to call for this game type -- function MainRule() local PlayerCount = Universe_PlayerCount() - 1 local numAlive = 0 local numEnemies = 0 local gameOver = 1 -- check to see if ALL of our enemies are dead, then gameOver for playerIndex = 0, PlayerCount do if (Player_IsAlive(playerIndex) == 1) then -- kill the player if the player has no production capability if (Player_HasShipWithBuildQueue(playerIndex) == 0) then Player_Kill(playerIndex) -- else, only process 'alive' players else -- compare this player against all others for otherPlayerIndex = 0, PlayerCount do -- are enemies? if (AreAllied(playerIndex, otherPlayerIndex) == 0) then -- is the enemy alive - if so the game is still on if (Player_IsAlive(otherPlayerIndex) == 1) then gameOver = 0 else numEnemies = numEnemies + 1 end end end numAlive = numAlive + 1 end end end -- special case: if there are no enemies, then never end if ((numEnemies == 0) and (numAlive > 0)) then gameOver = 0 end -- if gameOver flag is still set then the game is OVER if (gameOver == 1) then Rule_Add("waitForEnd") Event_Start("endGame") Rule_Remove("MainRule") end end function waitForEnd() if (Event_IsDone("endGame")) then setGameOver() Rule_Remove("waitForEnd") end end -- EVENTS Create the events table. The name of this table must always be 'Events' because this is what the game looks for. Events = { endGame = { { {"wID = Wait_Start(5)", "Wait_End(wID)"}, }, }, }
1. The start
I don't know what the first value does (someone please clarify), but the other two are simple. They are the name, seen in the menu, and the description, seen when you press the "?" button next to the name, respectively. Here, they are references to a localised string in English.big (If you have an English version of HW2), but you can just write a plain string.Code:GUID = { 110, 91, 157, 190, 18, 23, 250, 78, 144, 20, 41, 246, 181, 128, 214, 12, } GameRulesName = "$3203" Description = "$3230"
2. The options
This, is a table. A table is a LUA type, used to replace arrays and classes, which LUA doesn't have. It is essentially a named variable that can contain other variables, and other tables. The "GameSetupOptions" is the name of the table, and this particular one is responsible for the options that the player sees below their selected gametype. It consists of several other tables, each of which represents an option that the player can choose from a drop-down box. Each of these tables contains several variable required for that option. They are detailed here:Code:GameSetupOptions = { { name = "resources", locName = "$3240", tooltip = "$3239", default = 1, visible = 1, choices = { "$3241", "0.5", "$3242", "1.0", "$3243", "2.0", }, }, { name = "unitcaps", locName = "$3214", tooltip = "$3234", default = 1, visible = 1, choices = { "$3215", "Small", "$3216", "Normal", "$3217", "Large", }, }, { name = "resstart", locName = "$3205", tooltip = "$3232", default = 0, visible = 1, choices = { "$3206", "1000", "$3207", "3000", "$3208", "10000", "$3209", "0", }, }, { name = "lockteams", locName = "$3220", tooltip = "$3235", default = 0, visible = 1, choices = { "$3221", "yes", "$3222", "no", }, }, { name = "startlocation", locName = "$3225", tooltip = "$3237", default = 0, visible = 1, choices = { "$3226", "random", "$3227", "fixed", }, }, }
1. name - this is the internal name of the option. It is never seen by the player, but is needed in the script. some names, specifically all the ones in the file above are hard coded. The game looks for these, and if it finds them, will perform an action on them (such as adjusting starting resources). If you want to add your own options, you will have to write code to enforce them as part of your gamerule.
2. locName - this is a reference to a localised string, or a normal string, that is the name seen by the player of the option.
3. tooltip - this is the tool tip seen in the bottom of the screen, when the mouse hovers over this option.
4. default - appears to be the drop down option that is default, e.g. "Normal" for unitcaps. Is zero-based, meaning the first value is 0, second is 1, and so on.
5. visible - unknown, could this mean whether the player can see it or not? What would be the use in turning it off? Someone please clarify.
7. choices - this is a table containing pairs of variables. The first is the visible name of the drop down option, while the second is the value that is set (not seen by the player). In the hard-coded options, the values are used to set some kind of internal game variable, for example, "resources" is a multiplier applied to the amount of resources in every asteroid. If you make your own, you will have to interpret this value in your own script.
A simple example of an option is to set the frequency and size of an RU injection. Then, every time that amount of time passes, that amount of RU is given to each living player. Those would be two options that both set a number.
3. OnInit()
This really contains two parts - the OnInit function, and a dofilepath statement. dofilepath essentially inserts the contents of a file into this one, at runtime (or is it compile time?). restrict.lua, found at that path, contains a function that prevents players from building and researching single-player specific things, like movers. The function is called MPRestrict().Code:dofilepath("data:scripts\\scar\\restrict.lua") function OnInit() MPRestrict() -- changed this from 'Rule_Add' to 'RuleAdd_Interval' to speed the game up -Mikail Rule_AddInterval("MainRule", 1) end
The OnInit() function must be present in every game rule. It doesn't matter what you call all your other rules, but the game specifically looks for a function called "OnInit". This function is executed, once, at the start of the game. You need to use it to call MPRestrict(), perform any other one-time housekeeping, and initiate all your rules (you can add rules later, too). Some interpretation of your custom options can be done here as well, such as adjusting the startingfleet (the hard coded function isn't the only one that can be used), and setting global variables.
There is one last part in this function. "Rule_AddInterval("MainRule", 1)". The original file had "Rule_Add("MainRule")". These functions create a "Rule". A rule is a function that is executed at regular intervals during the game. Rule_Add plays the function every frame, while AddInterval, once per a number of seconds (can have a decimal point) determined in the second argument. The name is the actual name of the function in your script, but without the brackets.
It does not appear that Rules can have arguments. If you really need to pass an argument to a Rule, set a global variable. Rules can be removed using Rule_Remove("Rule_name").
4. MainRule
This is the main game rule that actually decides when players die, and when the game is over (i.e. only the players of one team remain). I will just go through this line by line.Code:function MainRule() local PlayerCount = Universe_PlayerCount() - 1 local numAlive = 0 local numEnemies = 0 local gameOver = 1 -- check to see if ALL of our enemies are dead, then gameOver for playerIndex = 0, PlayerCount do if (Player_IsAlive(playerIndex) == 1) then -- kill the player if the player has no production capability if (Player_HasShipWithBuildQueue(playerIndex) == 0) then Player_Kill(playerIndex) -- else, only process 'alive' players else -- compare this player against all others for otherPlayerIndex = 0, PlayerCount do -- are enemies? if (AreAllied(playerIndex, otherPlayerIndex) == 0) then -- is the enemy alive - if so the game is still on if (Player_IsAlive(otherPlayerIndex) == 1) then gameOver = 0 else numEnemies = numEnemies + 1 end end end numAlive = numAlive + 1 end end end -- special case: if there are no enemies, then never end if ((numEnemies == 0) and (numAlive > 0)) then gameOver = 0 end -- if gameOver flag is still set then the game is OVER if (gameOver == 1) then Rule_Add("waitForEnd") Event_Start("endGame") Rule_Remove("MainRule") end end
First, some local variables are created. PlayerCount is set to one less than the number of players because Player_IsAlive() takes a zero-based number (0 - 5), while Universe_PlayerCount() returns the actual number of players (1 - 6). The other three variables are used for the second section of the game rule (determining when the game is over) gameOver is set to one, and will be set to 0 if at least one player is found who still has an alive enemy. If it still equals 1, no one has a living enemy, and they have therefore won. Most of what I am saying by the way, can be worked out by reading the comments in the code.
Once the variables are created, we enter a "for" loop. This creates a variable playerIndex, sets it to 0, and will repeat the section below it, increasing playerIndex by 1 each time, until it equals PlayerCount. Every player in the loop is checked for being alive. This function takes the zero-based index of the player, and returns either a 1 or a 0. Remember that all numbers in LUA evaluate to true, so you have to explicitly check if it equals 1. If the player is dead, there is no point performing any checks on him, so the following is done only with living players.
Player_HasShipWithBuildQueue(playerIndex) is then checked. This returns a 1 if a player has at least one build-capable ship. If not, the player is dead, and Player_Kill() removes all his ships from play, and ensures that future Player_IsAlive() checks will mark him as dead.
Otherwise, the player is still alive, and the second part will check if he has any living enemies. If he does not, he wins, and the game is over. Another for loop is created, with another player index. Every other player is then checked against the current player. If they are allied, they are friends, and no action is taken. If not, they are enemies, and the script checks whether this enemy is alive. If he is, the current player still has some work to do, and the game goes on, by setting gameOver to 0. numEnemies and numAlive are also set here, but are only used by the "special case" section.
After that is done, there is a special case. If the game was started with everyone on the same team, the game will not end. Presumably, this was for developer testing purposes, as it is not possible to do that now, the "Start" button is grayed out.
Finally, if gameOver is still set to 1, that means there aren't any more enemies left. This then adds the rule "waitForEnd" which simply checks if the 5 sec waiting period is done (I don't know the purpose of this). It also starts the 5 sec waiting event ("endGame"), and removes the MainRule, as we no longer need to check for living players, since the game is over.
5. Events
This has two sections. The waitForEnd function is simple. it calls setGameOver(), which actually ends the game, once the event "endGame" is done. This event is defined in the next section.Code:function waitForEnd() if (Event_IsDone("endGame")) then setGameOver() Rule_Remove("waitForEnd") end end -- EVENTS Create the events table. The name of this table must always be 'Events' because this is what the game looks for. Events = { endGame = { { {"wID = Wait_Start(5)", "Wait_End(wID)"}, }, }, }
The events table is a table which is always called "Events". It is present both in multiplayer gamerules, and the single player campaign. In essence, calling an event is like calling a function, except the script can continue to run, independently of the event. The event table contains multiple tables, one for each event. This one contains only one - "endGame". THIS table contains another table, which contains a table, containing a sequence of functions to run. The functions for events differ from the functions for the game rule, check out karos for details. For more advanced tables, have a look at campaign missions.
"endGame" does two things. It creates a variable "wID" which gets the value returned by Wait_Start. When 5 seconds have passed Wait_End() will end the event. The Wait_End() function doesn't actually end the event, it simply lets it continue, but it ends because there is nothing after it. You can have multiple clocks like that at the same time, by using more than one variable like wID.
Well, that concludes our analysis of deathmatch.lua. Now, hopefully, you understand some things about how to script in Homeworld 2. I have given you much of the required information, so feel free to try something out yourself. And don't worry if you get something wrong, after all, that's how modding works. You tinker ... a lot. Then you break everything. Then you start again.
However, in case you are having trouble, or need to know how to do a particular thing, I will devote the rest of this to actual tutorials, explaining how to do specific things. This can be added to and expanded, so if you have an idea of something that should be added, feel free to voice it.
Part 3.1 - Tutorial - Resource injection
Let's start off with one of the simplest things to implement into your mod - resource injection. Resource injection is, quite simply, when every player on the map is given a set amount of resources once per a regular period of time. This can be used to make the game a bit easier, so you don't have to rely solely on your resource operations, and can generally build more. The player should also be allowed to choose the size and frequency of a resource injection.
Aim: To add two options to the map/game selection screen: 1. How often a resource injection will occur, 2. The size of the injection. Also, to script all players to get the resource injection at the appropriate time.
Adding a resource injection is easy. There is a function, "Player_SetRu", which allows you to set the amount of Ru's a player has. There is another function, "Player_GetRu", which tells you how many they already have. All you need to do to inject some is to add the amount to what they have, and set the player's RU to the new value. We will start, however, by adding the options that will be manipulated by the player.
Start with a clean deathmatch.lua. That is, get rid of all your other files (unless you are experienced and know what you are doing - that it won't impact anything), make a folder in "Data" with the path "Data\leveldata\multiplayer", and make a new file called "deathmatch.lua". This will now override the old one (For some reason, you might not need to use "-overrideBigFile", but use it anyway). Copy the first code box in this tutorial, and paste it into your newly created file. If you like, change the value of the GameRulesName variable at the top to something different, then try running the game. It should, first of all, not crash, and also have something instead of "Deathmatch" as the gametype when you select it. You can also start a game, to confirm it works as normal.
Now, have a look at the GameSetupOptions table. Notice how it is neatly arranged? We are going to try to keep it that way. Add the following code to the end of it, so it matches the formatting of the rest of the table. Note that the tooltip text is in capitals, in keeping with the other ones.
If you like, try running the game again. It should now have two new options, though they won't actually do anything.Code:{ name = "RuInjectTime", locName = "Ru Injection Time", tooltip = "HOW OFTEN RU'S WILL BE GIVEN TO EVERY PLAYER", default = 0, visible = 1, choices = { "None", 0, "30 seconds", 30, "1 minute", 60, "5 minutes", 300, "10 minutes", 600, }, }, { name = "RuInjectSize", locName = "Ru Injection Size", tooltip = "HOW MANY RU's WILL BE GIVEN EACH TIME", default = 0, visible = 1, choices = { "None", 0, "10", 10, "100", 100, "500", 500, "1000", 1000, "3000", 3000, "5000", 5000, }, },
Now we need to add the code to actually give the player RU's, based on these settings.
Find the function OnInit().
It should look like this:
Change it to this:Code:function OnInit() MPRestrict() -- changed this from 'Rule_Add' to 'RuleAdd_Interval' to speed the game up -Mikail Rule_AddInterval("MainRule", 1) end
In case you missed the comments, here is what we just did:Code:function OnInit() MPRestrict() Rule_AddInterval("MainRule", 1) local ruTime = GetGameSettingAsNumber("RuInjectTime") -- This creates a local variable, equal to the setting chosen ruAmount = GetGameSettingAsNumber("RuInjectSize") -- This creates a GLOBAL variable, that contains the size if (ruTime ~= 0 and ruAmount ~= 0) then -- If either of the settings are 0, we don't need to bother with this Rule_AddInterval("RUInject", ruTime) -- This will go through the RUInject function once every "ruTime" seconds end end
1. We found the OnInit function, which the game always looks for, and executes first.
2. In it, we created a local (using "local") variable, and a global one (not using "local").
3. They were set to the values that the player chose using the settings, via the GetGameSettingAsNumber() function.
4. If neither the time nor the amount were equal to 0 (i.e. the player chose "None"), we added a rule, "RuInject", that would be executed once every "ruTime" number of seconds, which, incidentally, is the exact rate at which we want our RU injections to occur.
One thing we haven't done yet is written the RUInject() function. Since the code won't work until we do that, let's do it now.
Directly underneath the OnInit() function (or above it, or under MainRule - this is a matter of style) add this code:
Once again, the explanation:Code:function RUInject() -- Declares a new function. As this is also a Rule, it should have no arguments. local PlayerCount = Universe_PlayerCount() - 1 -- This is the same system as used in MainRule() to loop through each player for playerIndex = 0, PlayerCount do -- The for loop, look back to part 2 for the explanation if Player_IsAlive(playerIndex) == 1 then -- Only process alive players local currentRU = Player_GetRU(playerIndex) -- This creates a local variable with the player's current RU Player_SetRU(playerIndex, currentRU + ruAmount) -- This sets the player's RU to their old amount, plus the injection size end -- Remember, "ruAmount" is the global variable we created earlier, in OnInit() end end
1. We create a new function. In LUA this is done using the keyword "function", followed by the function's name, and brackets, containing any arguments it needs. A return value is not needed (as in C++, Java and C#, for example). In fact, a LUA function can return multiple values at the same time. This function is a Rule, so it has no arguments. The one argument we need to give it, the size of the RU injection, is implemented via a global variable, "ruAmount".
2. We create a loop to go through each player, one by one. This is exactly the same as in "MainRule".
3. We check if the player is alive (Dead players won't really appreciate the gift...).
4. We find out how many RU's the player has (note the Player_GetRU() function - it takes one argument - the player).
5. Finally, we use Player_SetRU() to add the ruAmount to his current RU. This function takes 2 arguments - the player, and the amount of RU.
Also note that the last 2 steps could have been done in one statement. Observe:
If you want to do it this way, you can.Code:Player_SetRU(playerIndex, Player_GetRU(playerIndex) + ruAmount)
Now, save, and test your new mod. Hopefully it won't crash, or anything like that. Instead, wait the allotted amount of time (don't set it to 10 minutes, please!), and see if you get some RU's. If something goes wrong, whether you don't get any RU's, or the game crashes, check out your Hw2.log. After the Explosions tutorial section, I may write how to work out what is wrong with your code. But for now...
Part 3.2 - Tutorial - Capital Ship Explosions
When I started writing this tutorial, many people wanted to know how to make capital ships cause damage to other ships when they explode. This is a feature that was present in HW1, but not HW2, and it is quite a shame. Also, there were mods, such as Complex, which had managed to do this, but it was still a mystery to much of the community, either because they hadn't found out how, or because they were frightened of scripting in general. I had hoped that by writing the parts of the tutorial I already had would make people more ready to try it, and I would add this section for those that were still unsure, or for whom it didn't work. Then I must admit I left this tutorial for a while, but instead, after trying to make explosions myself, I wrote an alternative script that should be MUCH easier to implement and use than the ones before: http://forums.relicnews.com/showthread.php?t=239228. I hope that this has helped some people, but I will still provide detailed instructions on how to implement it, as well as how it works, here.
Aim:
1. To implement my script, so that you can make ships deal damage when they explode.
2. To learn how to use dofilepath() to split up your code, and create a library
Anyway, the only way to cause explosions is to go through every single example of a ship in play at any time. The game does not have a function to do this. However, there is a custom function, created by Apollyon470, called "SobGroup_SpiltGroup()", which uses proximity to separate a single SobGroup (it is possible to get one containing all the ships of a type) into multiple ones, each containing one ship. I will describe how it works in just a bit.
My code has become quite complicated since I started writing it, it is now capable of causing the explosion at a specific time, and of having multiple damage radii. To use my script though, we must first create the "SobGroup_SplitGroup()" and "SobGroup_SplitGroupReference()". We don't want to paste them into deathmatch.lua for two reasons. First, they will take up a lot of space. Second, we may want to use them in other gamerules, and we don't want to have to copy them each time. This is a perfect opportunity to use dofilepath(). By using it, we will put the functions in one file, and use a single dofilepath() to "copy" them into any gamerule we need them in.
Alongside your deathmatch.lua, and/or your modified RU Injection one, create a new folder called "lib". The actual name of this folder is unimportant, as long as it is not "deathmatch". From now on, we will place all of our scripts (which are not gamerules, but are added to them with dofilepath()) into this folder. We cannot just place them into the same folder as the gamerules, because it will confuse the game.
Create a new file called "SobGroupFunctions.lua" in the lib folder. Put this into it:Your file now has three useful functions. SobGroup_SplitGroup(), which splits one group into individual ships; SobGroup_SplitGroupReference(), which does the same, but uses another SobGroup as a reference point, rather than the one you're splitting; and Update_AllShips(), which creates a group that contains all active ships (not docked or in hyperspace). All three of these are needed for many scripts, and I will describe how they work.Code:function SobGroup_SplitGroup(SobGroupOut, SobGroupToSplit, NumberToSplit) -- function created by Apollyon470 local index = 0 local distance = 0 local bool = 0 local SobNum = 0 SobGroup_Create("TempSobGroup") SobGroup_Clear ("TempSobGroup") SobGroup_Create("TempSobGroup1") SobGroup_Clear ("TempSobGroup1") SobGroup_SobGroupAdd ("TempSobGroup", SobGroupToSplit) if ( SobGroup_Empty (SobGroupToSplit) == 1 ) then return 0 end if ( NumberToSplit > SobGroup_Count(SobGroupToSplit) ) then NumberToSplit = SobGroup_Count(SobGroupToSplit) end while (index < NumberToSplit ) do bool = 0 -- in the interests of resource saving, we start with a search band of 625 interval = 625 while (bool == 0) do distance = distance + interval -- something went wrong. Please tell me, or have a go at fixing it yourself. if (interval > 3000000) then bool = 1 return SobNum end SobGroup_FillProximitySobGroup ("TempSobGroup1", "TempSobGroup", SobGroupToSplit, distance) if (SobGroup_Empty("TempSobGroup1") == 1)then -- get the next interval else if (SobGroup_Count("TempSobGroup1") > 1) then -- too many ships, reduce interval if (interval <= .2) then -- Screw it! chunk 'em all in the same sobgroup SobGroup_Create(SobGroupOut .. tostring(SobNum)) SobGroup_Clear (SobGroupOut .. tostring(SobNum)) SobGroup_SobGroupAdd (SobGroupOut .. tostring(SobNum), "TempSobGroup1") SobGroup_Create("tempsob") SobGroup_FillSubstract("tempsob", "TempSobGroup", SobGroupOut .. tostring(SobNum)) SobGroup_Clear ("TempSobGroup") SobGroup_SobGroupAdd ("TempSobGroup", "tempsob") bool = 1 else distance = distance - interval interval = interval / 5 end else -- we got one! add it to the list! SobGroup_Create(SobGroupOut .. tostring(SobNum)) SobGroup_Clear (SobGroupOut .. tostring(SobNum)) SobGroup_SobGroupAdd (SobGroupOut .. tostring(SobNum), "TempSobGroup1") SobGroup_Create("tempsob") SobGroup_FillSubstract("tempsob", "TempSobGroup", SobGroupOut .. tostring(SobNum)) SobGroup_Clear ("TempSobGroup") SobGroup_SobGroupAdd ("TempSobGroup", "tempsob") bool = 1 end end end index = index + SobGroup_Count(SobGroupOut .. tostring(SobNum)) SobNum = SobNum + 1 end return SobNum end function SobGroup_SplitGroupReference(SobGroupOut, SobGroupToSplit, ReferenceSobGroup, NumberToSplit) -- function created by Apollyon470 local index = 0 local distance = 0 local bool = 0 local SobNum = 0 SobGroup_Create("TempSobGroup") SobGroup_Clear ("TempSobGroup") SobGroup_Create("TempSobGroup1") SobGroup_Clear ("TempSobGroup1") SobGroup_SobGroupAdd ("TempSobGroup", SobGroupToSplit) if ( SobGroup_Empty (SobGroupToSplit) == 1 ) then return 0 end if ( NumberToSplit > SobGroup_Count(SobGroupToSplit) ) then NumberToSplit = SobGroup_Count(SobGroupToSplit) end while (index < NumberToSplit ) do bool = 0 -- in the interests of resource saving, we start with a search band of 625 interval = 625 while (bool == 0) do distance = distance + interval -- something went wrong. Please tell me, or have a go at fixing it yourself. if (interval > 3000000) then bool = 1 return SobNum end SobGroup_FillProximitySobGroup ("TempSobGroup1", "TempSobGroup", ReferenceSobGroup, distance) if (SobGroup_Empty("TempSobGroup1") == 1)then -- get the next interval else if (SobGroup_Count("TempSobGroup1") > 1) then -- too many ships, reduce interval if (interval <= .2) then -- Screw it! chunk 'em all in the same sobgroup SobGroup_Create(SobGroupOut .. tostring(SobNum)) SobGroup_Clear (SobGroupOut .. tostring(SobNum)) SobGroup_SobGroupAdd (SobGroupOut .. tostring(SobNum), "TempSobGroup1") SobGroup_Create("tempsob") SobGroup_FillSubstract("tempsob", "TempSobGroup", SobGroupOut .. tostring(SobNum)) SobGroup_Clear ("TempSobGroup") SobGroup_SobGroupAdd ("TempSobGroup", "tempsob") bool = 1 else distance = distance - interval interval = interval / 5 end else -- we got one! add it to the list! SobGroup_Create(SobGroupOut .. tostring(SobNum)) SobGroup_Clear (SobGroupOut .. tostring(SobNum)) SobGroup_SobGroupAdd (SobGroupOut .. tostring(SobNum), "TempSobGroup1") SobGroup_Create("tempsob") SobGroup_FillSubstract("tempsob", "TempSobGroup", SobGroupOut .. tostring(SobNum)) SobGroup_Clear ("TempSobGroup") SobGroup_SobGroupAdd ("TempSobGroup", "tempsob") bool = 1 end end end index = index + SobGroup_Count(SobGroupOut .. tostring(SobNum)) SobNum = SobNum + 1 end return SobNum end function Update_AllShips() SobGroup_Create("AllShips") SobGroup_Clear("AllShips") for iPlayerIndex = 0, Universe_PlayerCount() - 1 do if (Player_IsAlive(iPlayerIndex) == 1) then SobGroup_SobGroupAdd("AllShips", "Player_Ships"..iPlayerIndex) end end end
SobGroup_SplitGroup() accepts three arguments. SobGroupOut is the base name of the output SobGroups. If this is set to "SplitGroups", then the resulting groups will be called "SplitGroups0", "SplitGroups1" and so on. SobGroupToSplit is the name of the SobGroup you are splitting. NumberToSplit is the maximium number of output SobGroups. (normally set to be higher or equal to the number of ships in SobGroupToSplit, so each output group has one ship)
The first thing the funtion does is create four temporary variables, and initialise them to 0. Then, it creates two temporary SobGroups. Notice how it clears them (using SobGroup_Clear()) before continuing? This is because there may be ships left over from previous times left in those groups, which we don't want. After this, "TempSobGroup" is filled with all the ships in SobGroupToSplit, and is then checked. If there is only 1 ship, the function returns 0. Normally, when writing your code, you should check if there is only 1 ship BEFORE calling SobGroup_SplitGroup(), as it doesn't need to be split. After this, if you set the maximum number of split ships to more than there are, it is reduced to the appropriate value, counting the ships using SobGroup_Count().
Then, a "while" loop starts, and lasts until the end of the function. This will repeat until index is equal to NumberToSplit, which is equal to or less than the number of ships in the SobGroup. Every time a ship is caught, and placed into a separate group, index is increased, and when all of the ships are seperated, the loop will stop. Inside the loop, bool is reset to 0 (as it is set to 1 later in the loop, but needs to be 0 here), and interval to 625. The interval is the difference between the radii that ships will be found in. The functions works by only checking ships a certain distance from the center of the SobGroup, and, assuming no 2 ships are the exact same distance, will be able to separate individual ships. The "width" of the distance is controlled by interval, which will get smaller and smaller, increasing the accuracy, if more than 1 ship is at nearly the same distance.
Another while loop then begins. This one will continue until it has a single ship (when bool is set to 1). Then, the interval is added to the distance. The distance is the actual value used when finding ships. After all of the ships in the first 625 have been found, they will be removed from the SobGroup, and distance will be increased again, catching the next band (also the first one, but it has no ships in it). After this is a quick error check, if interval is large, it is clearly an error. Then, all ships "distance" from the center of our main group are put into "TempSobGroup1". If there are none, we can skip ahead to the next interval, otherwise: if there is only one ship, it is added to a sobGroup, removed from the main group, and bool is set to 1, resulting in index and SobNum being increased, and the search band increased, unless this is all of our ships; if there is more than one, then distance is reduced, and the interval is reduced, thereby narrowing the search band. If this has continued to the minimum band, set here to 0.2 metres, then all the ships are placed in the same SobGroup, and we move on. DON'T FORGET that if there are only 2 ships in a SobGroup, they will ALWAYS be the exact same distance, as the "distance" is from the center, and the center is the average location, and will be exactly between them.
That is the purpose of SplitGroupReference().
SobGroup_SplitGroupReference() is exactly the same as SobGroup_SplitGroup(), except it takes a different SobGroup as the centre, rather than the one you are splitting. It is needed if there are 2 ships in the SobGroup you are trying to split. Don't forget to check for this.
Update_AllShips() is simple. It adds together all the "Player_Ships0", "Player_Ships1" etc. groups, which, if you remember, are maintained by the game as containing all active ships belonging to a player in game at any time. Note that the resulting "AllShips" SobGroup will NOT contain: Neutral ships (belonging to player -1); or Inactive ships (docked or in hyperspace).
Allright, now that you are familiar with some SobGroup functions, and a few of the hardcoded ones (like SobGroup_Count(), SobGroup_Clear() etc. look in Karos Graveyard for the whole list), we can get Explosion Damage working. Create another file in "lib", alongside SobGroupFunctions.lua, called something like "ExplosionDamage.lua". We are doing this for the same reason we did SobGroupFunctions.lua. We may want to use the same explosion damage for different gamerules, so there is no point copying it into each file. then, go back to the folder that you have deathmatch.lua in (possibly also your RU injection gamerule), and make a copy of it. Add two lines above OnInit():Now, the contents of both files will be treated as if you have copied them there. Also, add one line to OnInit(): initExplDamage()dofilepath("data:leveldata\\multiplayer\\lib\\SobGroupFunctions.lua")
dofilepath("data:leveldata\\multiplayer\\lib\\ExplosionDamage.lua")This function, initExplDamage(), will be defined in ExplosionDamage.lua, and will add all the rules needed for our capital ship explosions. Paste this code into the ExplosionsDamage.lua file you created earlier:Code:function OnInit() MPRestrict() initExplDamage() Rule_AddInterval("MainRule", 1) endI am afraid that the function is quite complex, and I can't describe it in detail, but if you want, have a look at it, I have tried to add as many comments as possible, so you should be able to understand it. Briefly, though, here is how it works:Code:ExplosionsTable = { { -- The start of a ship entry in the table name = "Hgn_Mothership", -- The name of the ship numRadii = 0, -- How many radii entries there are - leave this, it is set later fDeathTime = 6.5, -- How long after the ship runs out of health, and starts exploding, should the damage be applied radii = -- The radii table { {fRange = 1000, fDamage = 10000}, -- An entry, containing the radius (fRange), and amount of damage dealt (fDamage) {fRange = 2000, fDamage = 4000 }, }, }, { name = "Vgr_Mothership", -- Another ship entry numRadii = 0, fDeathTime = 6.5, -- Note that this value must be smaller than the ship's sobDieTime, and also, radii = -- The difference should be MORE than the sum of fDeathCheckRate and fTimerCheckRate (below) { {fRange = 1000, fDamage = 8000}, {fRange = 2000, fDamage = 2000}, {fRange = 4000, fDamage = 1000}, }, }, } fDeathCheckRate = 0.1 -- This affects how often the script checks for dying ships, and starts timing them fTimerCheckRate = 0.2 -- This is how often the script checks if the timers are done, and implement explosion damage -- For both of these values, setting them lower will give greater accuracy -- In addition, any ship's (sobDieTime - fDeathTime) MUST be greater than the sum of these two iTotalShips = 0 -- Initiating a global variable, to count ships into. Don't set this manually function initExplDamage() print("In initExplDamage()") iTotalShips = LengthOf(ExplosionsTable) -- Count the number of entries in the table print(" Counting ships: "..iTotalShips.." in total")-- And therefore the number of ships with Explosion Damage for iShipEntry = 1, iTotalShips do -- For each ship, count the number of damage radii ExplosionsTable[iShipEntry].numRadii = LengthOf(ExplosionsTable[iShipEntry].radii) print(" "..ExplosionsTable[iShipEntry].name.." has "..ExplosionsTable[iShipEntry].numRadii.." damage radii, and an fDeathTime of "..ExplosionsTable[iShipEntry].fDeathTime) end print("Initiating Tabular Explosion Damage, with "..iTotalShips.." ships") ExplWaitTimers = {} -- Create a table - this will later contain the timers for iTempCounter = 1, iTotalShips do ExplWaitTimers[iTempCounter] = {} -- Create sub-tables, one for each ship type end -- Each one will later contain a counter for every ship of that type that is exploding Rule_AddInterval("LoopThroughExplosionTable", fDeathCheckRate) -- Add the rule that adds ships to the "Exploding" list, and the timer Rule_AddInterval("CheckTimers", fTimerCheckRate) -- Add the rule that checks the "Exploding" list for ships whose timer has finished end function implementExplDamage(sSobGroup, tDamageTable) -- This function takes the table that lists damage and radii for that ship --print (" A ship/s of type \""..tDamageTable.name.."\" is/are exploding!!") for iPlayerIndex = 0, Universe_PlayerCount() - 1 do -- Go through every player to do damage to if (Player_IsAlive(iPlayerIndex) == 1) then -- If that player is still alive, for iDamageEntry = 1, tDamageTable.numRadii do -- Go through every level of damage, as listed in the table -- And do damage to all player's ships in proximity SobGroup_DoDamageProximitySobGroup(sSobGroup, iPlayerIndex, tDamageTable.radii[iDamageEntry].fDamage, tDamageTable.radii[iDamageEntry].fRange) end end end end function LoopThroughExplosionTable() --print ("In LoopThroughExplosionTable()") Update_AllShips() SobGroup_Create("TempRuntimeSobGroup") SobGroup_Clear("TempRuntimeSobGroup") for iTableEntry = 1, iTotalShips do -- Go through all the ship entries local shipName = ExplosionsTable[iTableEntry].name -- Inititate the ship's name to the appropriate entry in the table local iWaitTimer = FindOrCreateNextEmptySobGroup(shipName) -- Generate a temporary timer index - this will be used if any ships are dying for iPlayerIndex = 0, Universe_PlayerCount() - 1 do -- Go through every player if (Player_IsAlive(iPlayerIndex) == 1) then -- Don't bother for dead players Player_FillShipsByType("TempRuntimeSobGroup", iPlayerIndex, shipName) -- Get all ships of that ype local numShips = SobGroup_Count("TempRuntimeSobGroup") -- Count them --if (numShips > 0) then --print (" Player "..iPlayerIndex.." has "..numShips.." ships of type "..shipName) --end if (numShips == 1) then -- If 1 ship, don't need to split SobGroups CheckShipForDeath("TempRuntimeSobGroup", iWaitTimer, iTableEntry) elseif (numShips == 2) then -- If 2 ships, SplitGroup won't work, only SplitGroupReference local numGroups = SobGroup_SplitGroupReference("sRuntimeGroup2", "TempRuntimeSobGroup", "AllShips", numShips) for iShipIndex = 0, numGroups - 1 do if (SobGroup_Count("sRuntimeGroup2"..iShipIndex) == 1) then CheckShipForDeath("sRuntimeGroup2"..iShipIndex, iWaitTimer, iTableEntry) else print(" [SobGroup Splitting of "..SobGroup_Count("sRuntimeGroup2"..iShipIndex).." of Player "..iPlayerIndex.."'s "..numShips.." "..shipName.."'s failed. Skipping]") end end elseif (numShips > 2) then -- Split groups, if not 0 ships (in which case, skip) local numGroups = SobGroup_SplitGroup("sRuntimeGroup2", "TempRuntimeSobGroup", numShips) for iShipIndex = 0, numGroups - 1 do if (SobGroup_Count("sRuntimeGroup2"..iShipIndex) ~= 1) then -- If it hasn't split completely, try again with SplitGroupReference print(" [Basic Splitting of "..SobGroup_Count("sRuntimeGroup2"..iShipIndex).." of Player "..iPlayerIndex.."'s "..numShips.." "..shipName.."'s failed. Using SplitGroupReference]") local numGroups2 = SobGroup_SplitGroupReference("sRuntimeGroup3", "sRuntimeGroup2"..iShipIndex, "AllShips", numShips) for iShipIndex2 = 0, numGroups2 - 1 do if (SobGroup_Count("sRuntimeGroup3"..iShipIndex2) == 1) then CheckShipForDeath("sRuntimeGroup3"..iShipIndex2, iWaitTimer, iTableEntry) else print(" [Advanced Splitting failed. Skipping]") -- Aaargh! This didn't work either! Oh, well, skip them for now end end else CheckShipForDeath("sRuntimeGroup2"..iShipIndex, iWaitTimer, iTableEntry) -- It split properly the first time end end end end end if (SobGroup_Empty(shipName..iWaitTimer) == 0) then -- This group would have been generated with the temporary timer index and filled with any dying ships ExplWaitTimers[iTableEntry][iWaitTimer] = Wait_Start(ExplosionsTable[iTableEntry].fDeathTime) -- If it's empty, it will be used again next time print ("Initiating wait timer \"ExplWaitTimers["..iTableEntry.."]["..iWaitTimer.."]\"") end -- If it's not empty, a ship has started exploding, and the actual timer for it is initiated, using the temporary index end end function CheckShipForDeath(sSobGroup, iWaitTimer, iShipEntry) -- This is the function that will add dying ships to a the shipName..iWaitTimer group if (SobGroup_HealthPercentage(sSobGroup) <= 0) then -- If the ship is dying, we should add it to the SobGroup, but for iSobIndex = 1, iWaitTimer do -- We should only add the ship if it hasn't been added before if (SobGroup_GroupInGroup(ExplosionsTable[iShipEntry].name..iSobIndex, sSobGroup) == 1) then return -- Otherwise, we will be wasting memory, AND, it will override other ships that start dying later end -- As the function runs several times a second, and the ship has <= 0 health each time end SobGroup_SobGroupAdd(ExplosionsTable[iShipEntry].name..iWaitTimer, sSobGroup) -- If the function has't returned yet, the ship is a new one - add it --print("A ship, "..ExplosionsTable[iShipEntry].name..", ("..sSobGroup..") is dying - adding to SobGroup "..ExplosionsTable[iShipEntry].name..iWaitTimer) end end function LengthOf(tTable) local iTableLength = 0 repeat iTableLength = iTableLength + 1 until (not tTable[iTableLength + 1]) -- Loop through all. When the next one is nil (false), you have the exact number of entries return iTableLength end function FindOrCreateNextEmptySobGroup(sSobGroupBaseName) -- This function goes through all the dying ships SobGroups, and finds the first empty one (or creates a new one) local bFunctionOver = 0 -- When this is true, we are done local iWaitTimer = 1 -- The iterator - the name is standard throughout my script while (bFunctionOver == 0) do SobGroup_Create(sSobGroupBaseName..iWaitTimer) -- This will create, if not existing already if ( SobGroup_Empty(sSobGroupBaseName..iWaitTimer) == 1 ) then bFunctionOver = 1 -- If it's empty, return the current iWaitTimer else iWaitTimer = iWaitTimer + 1 -- Othewise, try again with the next one end end return iWaitTimer -- This value will be used to reference the new SobGroup, and a potentially new Timer variable end function CheckTimers() -- This function goes through all the existing timer variables (in the ExplWaitTimers table) for iShipEntry = 1, iTotalShips do -- And the associated dying ships SobGroups - if the timer is done, cause damage local iWaitTimer = 0 while (ExplWaitTimers[iShipEntry][iWaitTimer + 1]) do -- Go through all timers, until the next one is nil (i.e. you're on the last one) iWaitTimer = iWaitTimer + 1 if ( SobGroup_Empty(ExplosionsTable[iShipEntry].name..iWaitTimer) == 0 ) then -- If this isn't an empty explosion group, if ( Wait_End(ExplWaitTimers[iShipEntry][iWaitTimer]) == 1 ) then -- If the time has passed --print ("Implementing damage for ship "..ExplosionsTable[iShipEntry].name.." ("..ExplosionsTable[iShipEntry].name..iWaitTimer.."), with Timer \"ExplWaitTimers["..iShipEntry.."]["..iWaitTimer.."]\"") implementExplDamage(ExplosionsTable[iShipEntry].name..iWaitTimer, ExplosionsTable[iShipEntry]) SobGroup_Clear(ExplosionsTable[iShipEntry].name..iWaitTimer) -- Cause proximity damage, and clear the dying ships group ExplWaitTimers[iShipEntry][iWaitTimer] = 0 end end end end end
1. initExplDamage() counts the number of ship entries in the table, and the number of damage radii in each, setting appropriate global variables (LengthOf() is my function, idea credited to ajlsunrise, defined lower). It also creates a global table, ExplWaitTimers, with one sub-table for each type of ship. Each sub-table will later contain timer variables, which are returned by Wait_Start() and can be checked with WaitEnd() (You may remember these two from the main deathmatch.lua Events table. wID was the wait timer variable there)
2. initExplDamage() then starts two rules. The first checks for exploding ships, and calls WaitStart(), assigning the returned variable to a fresh entry in ExplWaitTimers. The second uses Wait_End() to check ExplWaitTimers for any that have expired, which means it is time to deal damage.
Now, you should be ready to go. You should have added three lines to your copy of deathmatch.lua (also, don't forget to chenge the displayed name, so you can tall it apart), and created two new files, and filled them. One last thing - you may want to edit the table at the top of ExplosionsDamage.lua, as it is only an example. More detailed instructions on how to edit the table, which contains all the info for how powerfully ships explode, can be found in the original thread of this script: http://forums.relicnews.com/showthread.php?t=239228.
This concludes my tutorial for now.
If there is something that should be added, or you are not sure about, or you want clarified, please discuss it in this thread.




