for i = 1, #self.MyArray do
MyValue = self.MyArray(i)
end
Lua Function Library For GameMode and Mutator Modding
Revision date: 15 July 2024 (game version 1034.5)
New or modified game modes can now be ‘modded into’ GROUND BRANCH. This is a guide to the functions and coding conventions you will need to know in order to start modding your own game modes into GROUND BRANCH. You will also probably want to look at the GROUND BRANCH Mission Editor guide.
Game modes are now implemented using Lua script, which is an interpreted language that is executed substantially in real time (as opposed to languages like C++ which must be compiled before being executed). This has some advantages, such as being able to debug and modify Lua code while the game is running (and indeed, within the game itself). This guide assumes an at least basic knowledge of Lua. Typically only a few basic features of Lua are needed to implement the game modes. For more information, see http://www.lua.org/.
Notwithstanding the above, some important aspects of Lua to understand include:
Lua variables are fairly weakly typed, and do not need to be defined prior to use.
They are converted into whatever format is appropriate for the use that is made of them.
A variable foo
may contain a number, for example.
If that variable is supplied as a string parameter, it will automatically be converted into string representation.
If a number type includes a decimal point (e.g. “3.5”) then it is a float.
If no decimal point is specified, then it is an integer.
There is no real distinction between integers and floats in Lua.
The basic data types in Lua are nil, Boolean, number, string, userdata, function, table and string. Userdata is a container to hold non-native forms of data (and is used for GROUND BRANCH data types and/or internal pointers). Variables can be set to nil
. If you refer to a variable (or function) that has not yet been defined, you will get the value nil
. If you reference an array element by the wrong key/index, the result is nil
. No error will be generated.
This is a potentially large source of unexpected/unwanted behaviour in your game mode.
If you access an invalid userdata type (for example because the underlying data has been destroyed or garbage-collected), you will get an error. There is no test (so far as I am aware) for an invalid userdata reference. So you’d better get rid of any userdata types that are about to become invalid via the LogOut call (for example)…
Lua’s most distinctive feature, and arguably its strength, is its handling of arrays (called tables in Lua, but they are referred to frequently as arrays in this document, because the author is a lazy, confused squirrel). Tables can be used in the normal C++/Blueprint sense, but essentially they just map a number of keys to values (and both the keys and the values, as explained above, can represent any type of thing, including further tables). In their native form, tables are defined as a series of key-value pairs, e.g.
NewArray = { {"Row10", true}, {"Row11", true}, … }
and items can be read from the table using the key, e.g. IsRowEnabled = NewArray("Row11")
. Empty arrays are defined using the form MyArray = {}
Tables can be defined just as a sequence of values (like traditional arrays), e.g. PriorityTags = {"AISpawn_1", "AISpawn_"”, … }
, but numeric index keys will be implicitly defined and stored, starting at a predefined number and incrementing by 1 each time.
So the above table declaration is actually treated as if it was written as PriorityTags = { {"1", "AISpawn_1"}, {"2", "AISpawn_2"}, … }
. When a table is referred to as an ‘array’, that usually implies the use of an ordered sequence of indices like this.
As you can see above, the default start index for array elements is 1 (not 0!). This may be another source of errors in your game mode. You can brute force things to start at index 0, but really it’s better to go with the flow.
The statement a.x
is functionally equivalent in Lua to a["x"]
. This is especially relevant to calling functions (see below).
One quirk of Lua is a statement of the form Foo = Foo or 4
, which has the actual meaning of <if Foo
is not defined, set it to 4>. This format doesn’t work as intended if Foo
is false. Author’s comment: not my favourite feature of lua but I’m on my own here it seems.
Putting # at the beginning of an expression returns the size of the sequence of in a table (which is nearly always equal to the size of the table, unless it is not an ‘array’ - see above). A for loop iterating through all entries in an array may be written as:
for i = 1, #self.MyArray do
MyValue = self.MyArray(i)
end
An alternative form of for loop for arrays uses the ipairs
keyword, and is of the form:
for MyKey, MyValue in ipairs(self.MyArray) do
-- ...
end
In this case the loop reads off each pair of key and value from the table in turn. It can be a quicker way to access the values MyValue, but it only works if you are using continuous numeric indices in your table (e.g. 1, 2, 3, 4 …).
Usually the index is not of great interest, so it is replaced by a dummy variable, and often that dummy variable is an underscore (‘_’):
for _, MyValue in pairs(self.MyArray) do
-- ...
end
If you have arbitrary indices in your table, you need to use the ‘pairs’ keyword instead:
for MyKey, MyValue in pairs(self.MyArray) do
-- ...
end
As mentioned above, Lua has a function data type which is treated as any other type. Lua functions are defined as function data elements in a container table. You may recall from above that a.x is treated as a[“x”]. So functions in Lua can have the surface appearance of C++ object oriented shenanigans, but they are not really the same. So you will see game mode scripts begin with something of the form…
local uplink = {
UseReadyRoom = true,
UseRounds = true,
StringTables = { "Uplink" },
MissionTypeDescription = "[PvP] Defenders guard an intel device in one of several possible locations, as Attackers must locate and hack it to win.",
PlayerTeams = {
-- ...
},
Settings = {
-- ...
},
DefenderInsertionPoints = {},
DefenderInsertionPointNames = {},
RandomDefenderInsertionPoint = nil,
AttackerInsertionPoints = {},
GroupedLaptops = {},
DefendingTeam = {},
AttackingTeam = {},
RandomLaptop = nil,
SpawnProtectionVolumes = {},
ShowAutoSwapMessage = false,
LaptopObjectiveMarkerName = "",
DefenderInsertionPointModifiers = {},
NumberOfSearchLocations = 2,
MissionLocationMarkers = {},
LaptopLocationNameList = {},
AllInsertionPointNames = {},
CompletedARound = true,
DebugMode = false,
}
…which defines the table (which we would regard as a class) uplink
and some of the table elements contained within it (which we would regard as member variables in a C++ context, though these are usually called globals in lua). So function definitions such as function uplink:PostInit() { ... }
are actually just inserting a function element into the container table, and references to uplink.PostInit()
are actually references to uplink[“PostInit”]
, and so on.
You can see that game mode scripts refer to their ‘member variables’ (defined in this section at the top of the file) using the self.variable
notation.
There are some built in Lua libraries available to use, such as math
, which includes functions such as:
math.sin() / math.cos() -- trig operations are in radians
math.deg() / math.rad()
math.pi()
math.min() / math.max()
math.abs()
math.floor()
math.ceil()
math.modf()
There are also some useful functions in the string
and table
library, amongst others, which you can look up at your leisure (see, for example, table.insert
). See also umath.random()
.
The require()
keyword is approximately equivalent to the #include keyword in C/C++, and loads a shared library if it has not already been loaded.
See for example the use of the ValidationFunctions library in game mode validators:
local validationfunctions = require("ValidationFunctions")
function ... ()
----- carry out generic validation functions using new function library
ErrorsFound = validationfunctions:PerformGenericValidations()
end
There are usually several ways to do a simple thing in lua.
Lua scripts in GROUND BRANCH are universally of the form (where <modulename>
is a game mode name, for example):
local <modulename> = {
-- 'global' variables for module
...
}
require ("<libraryname>")
-- bring in shared lua library functions
function <modulename>.Function1()
end
function <modulename>.Function2()
end
...
return <modulename>
Lua scripts are run (thereby defining all the necessary functions and variables), and the return value from them (a reference to the module) is stored for future calls and callbacks.
When a lua script is first run, the package.path
global variable is set so that the lua virtual machine will search for file references in specific defined places (for example when using a ‘require()’ command). Specifically, the lua VM will look in GroundBranch/GameMode
and GroundBranch/Lua
folders in the base game, and the same subfolders within any mod that is hosting the currently executing lua script (if applicable). In addition, the lua VM will try to match <filename>.lua
and <filename>\init.lua
, in accordance with normal custom.
Each game mode is provided in the form of a single Lua script stored in the GroundBranch/GameMode folder within the GROUND BRANCH content directory:
Game mode scripts need to implement a number of standard functions that are called by the main GROUND BRANCH program at certain times, in order to set up the game mode functionality.
An example of such a function is PostInit()
. There are also now optional Validation functions for each game mode, called <GameMode>Validate.lua
. These are called by the mission editor when validating a level.
GROUND BRANCH provides libraries of functions which can be called from the game mode script.
These libraries are described below.
The game mode script calls certain functions to indicate a win/lose condition for the game mode.
Other functions can be provided in the game mode script to specific game events such as players entering triggers, or a timer expiring.
Between being called in Postinit()
and returning a win/loss condition, the game mode script can essentially do whatever it likes to deliver the necessary game mode experience.
Also please bear in mind that everything/anything here may be out of date and may change without warning. We cannot accept responsibilty for any harm arising from relying on information presented here. That said, if you can somehow use this information to cause harm, bravo! That is the GROUND BRANCH spirit.
The GROUND BRANCH Lua libraries will be described below. First, the structure of the game mode scripts will be described.
A key concept for game modes is the round stage. The normal round stages are as follows:
Round stage | Description | What initiates stage? |
---|---|---|
WaitingForReady |
Players in ready room, selecting loadouts, etc. |
Previous game ends |
ReadyCountdown |
Players in ready room, game is about to begin |
A player selects a spawn location |
PreRoundWait |
Players moved to level, movement is frozen |
Countdown ends |
InProgress |
Players are in the level playing the game |
Pre round wait countdown ends |
PostRoundWait |
Players are all spectating, with post round info displayed on screen |
Game mode determines a win and/or loss condition |
TimeLimitReached |
Players are all spectating, with post round info displayed on screen? |
Time runs out without a win/loss condition being determined |
MatchEnded |
Countdown to new mission after match ends |
Match ended conditions being met |
Round stages can be added by game mode scripts if needed.
The Uplink mode, for example, adds new BlueDefenderSetup
and RedDefenderSetup
round stages.
You can provide custom round stages, with whatever name you wish. However, there is a convention that round names which contain the substring ‘InProgress’ will be treated internally like the normal InProgress round. Thus the DTAS game mode has DTASInProgress and FoxHuntInProgress round stages, and these are treated in the same way as a vanilla InProgress round.
Players have a number of different statuses maintained within GROUND BRANCH. The statuses described below are potentially more relevant for game mode:
Readied-up Status | Meaning |
---|---|
NotReady |
Player is not in the Ops Room |
WaitingToReadyUp |
Player is in the Ops Room but has not clicked on the Ops Board to indicate readiness |
DeclaredReady |
Player has clicked Ops Board and is in Ops Room ready to spawn in |
Players with NotReady status will be left in the Ready Room when a round starts. Players who have a WaitingToReadyUp status will be assigned an insertion point automatically (if appropriate) and pulled into the round when the ready up timer expires.
Ready Room Status | Meaning |
---|---|
Unknown |
Player’s position is temporarily unknown (usually an error state) |
InReadyRoom |
Player is in the ready room (team room or lobby) |
InPlayArea |
Player is in the play area (in the main part of the map, during a round) |
Game modes do not normally deal with these statuses directly, but they are relevant to various functions below. Only players with status of InPlayArea are shown as blips on the map tablet, for example.
This status is true
if a player is spectating a match.
Thus players can have an InPlayArea status but not be playing (if they are spectators).
Another key concept for game modes is game rules. These are essentially internal flags which can be set to true or false by a game mode to tell the core GROUND BRANCH code what standard game play features are required by the game mode. If a game rule is true, the relevant feature is provided. These differ from game settings (see below) in that they may be set as part of a server command line, or set in the admin menu as server defaults.
The current list of available game rules (at the time of writing) is as follows:
Game rule | Meaning | Default |
---|---|---|
UseReadyRoom |
Initially spawn players into the ready room |
true |
UseRounds |
Have discrete rounds rather than continuous play |
true |
AllowCheats |
Allow entry of console commands to enable god mode, etc. |
false* |
SpectateFreeCam |
Allow spectators to move freely rather than be locked to friendly team members |
false* |
SpectateEnemies |
Allow spectators to spectate from the point of view of enemy team members |
false* |
SpectateForceFirstPerson |
Force spectators into first person view instead of allowing third person (or free) movement |
false |
UseTeamRestrictions |
false |
|
AllowDeadChat |
Let live players see chat from dead players |
false |
AllowUnrestrictedVoice |
false |
|
AllowUnrestrictedRadio |
false |
|
AllowEnemyNPCMinimapBlips |
Show AI blips on spectator minimap in PVE |
true |
UseFriendlyNameTags |
Display friendly name tags in-game (up close) |
false |
defaults to true if playing solo or in editor
Game rules are declared as local variables in a game mode. For example, in the defuse game mode:
local defuse = {
UseReadyRoom = true,
UseRounds = true,
-- ...
}
Default server game rules are usually defined in the Server.ini
server config file:
GameRules=(("AllowCheats", True),("AllowDeadChat", True),("AllowUnrestrictedRadio", False),("AllowUnrestrictedVoice", False),("SpectateEnemies", False),("SpectateForceFirstPerson", False),("SpectateFreeCam", True),("UseTeamRestrictions", False))
In hosted games, server specific values will override values declared in the game mode script. If neither the game mode script or the Server.ini file specify values for a particular game rule, a default setting will be applied (which can be adjusted via the admin server settings menu).
Game settings are similar to game rules, but are defined within, and specific to, particular game modes. They are displayed when selecting a mission in the Lone Wolf or Host Server screen, and are displayed on the Ops Board in the ready room. Online, server admins can change the settings to vary the game experience.
Game settings are defined within a special table in the game mode globals section (the top of the .lua file), for example from the Terrorist Hunt game mode (TerroristHunt.lua
):
Settings = {
OpForCount = {
Min = 1,
Max = 50,
Value = 15,
AdvancedSetting = false,
},
Difficulty = {
Min = 0,
Max = 4,
Value = 2,
AdvancedSetting = false,
},
RoundTime = {
Min = 3,
Max = 60,
Value = 60,
AdvancedSetting = false,
},
ShowRemaining = {
Min = 0,
Max = 50,
Value = 10,
AdvancedSetting = true,
},
The sub-table name (e.g. OpForCount, Difficulty, …) is used as the mission setting name. the Min and Max properties define the minimum and maximum values of the setting, and Value gives the default value. All settings are numeric, but can be mapped to text options using localisation features (via the game mode .csv file - see below). The AdvancedSetting option is optional, and defines whether or not the game setting is initially hidden in the Lone Wolf and Host Game mission selection screens.
Some standard game settings which are used include opforcount
, difficulty
and roundtime
. It is good to use these standard settings if possible, rather than custom settings, not just for consistency but also because various default strings are defined for these settings and common parameter values for them.
The timeofday
setting is provided by default and does not need to be added. As of version 1034.5, various time of day properties can now be read by the game mode script via gamemode.GetTimeOfDay(). See section 6.2.55.
The game will do its best to turn mission setting names into proper English text (for example by inserting spaces before capital letters) but this is not consistently done, and it is best always to create localisations/look-ups for the setting names anyway (see section 2.6 below). Mission setting names must not contain underscores (_
) because this will interfere with the localisation/look-ups.
A number of standard global variables are expected to be presented by the game mode Lua script.
The global variables are the variables defined in the local GameModeName = { ... }
section at the top of the game mode script.
Game Rules and Game Settings are discussed above in section 2.3 and section 2.4 above. Other standard game mode variables are:
Any number of comma-separated string tables can be (and usually should be) specified by the game mode.
The convention is to use the game mode name.
This tells the game what files to look at to find localisation text for the game mode, within the location \Content\Localization\GroundBranch\en
(see section 2.6 below for more information) or other codes besides en
for other languages/locales. If you do not specify a string table, your text localisation / mission setting name look-ups will fail.
Here is an example from the Intel Retrieval game mode:
StringTables = { "IntelRetrieval" },
Each game mode may provide a description of itself for display on the Lone Wolf and Host Game mission selection screens.
The current convention is to have a prefix of [Solo/Co-op]
for PvE/co-op modes, and [PvP]
for PvP modes.
This text is now located in the game mode string table (see above), using the keyword gamemode_description_<GameModeName>
, e.g. in GameMode.csv:
"gamemode_deathmatch","Deathmatch",""
"gamemode_description_deathmatch","[PvP] Fight for the most kills in a free-for-all battle with unlimited respawns.",""
"gamemode_dtas","Dynamic Take and Secure",""
"gamemode_description_dtas","[PvP] Two teams spawn in completely random spots on the map.
Once Defenders have placed the flag, Attackers must locate and capture the flag area.",""
Here is an example of a PvP mode team definition:
PlayerTeams = {
Blue = {
TeamId = 1,
Loadout = "Blue",
},
Red = {
TeamId = 2,
Loadout = "Red",
},
},
Here is an example of a PvE (co-op) mode team definition:
PlayerTeams = {
BluFor = {
TeamId = 1,
Loadout = "NoTeam",
},
},
It is highly recommended to stick to these conventions of TeamIds, team names and loadout names.
There is a dummy TeamId of 0 which you should avoid using. TeamId of 255 usually (but not always) is a wildcard for any TeamId, so you should avoid this also. The property is used as a byte internally within the game, so values below 0 or over 255 are invalid.
The game mode author can now (as of v1033) be specified using the GameModeAuthor global variable:
GameModeAuthor = "(c) BlackFoot Studios, 2021-2022",
Prior to v1033, game mode types were deduced based on characteristics of the game mode. Now it is explicitly declared by each game mode. Options are “PVE” (one team vs AI), “PVP” (two teams, with team vs team), “FFA” or “PVPFFA” (free-for-all, one team with player vs player), or “Training”. Different match conditions can be configured for each game mode type.
GameModeType = "PVE",
As of v1034, game modes can declare whether volunteering is allowed. The implementation of volunteers is down to the game mode and has no specific meaning to GROUND BRANCH. Declaring that volunteering is allowed causes the volunteer icon (hand up / hand down) to be displayed in the server roster and allows players and admins to toggle a volunteering status. DTAS uses this feature to allow players to volunteer to be flag carriers or assets (in fox hunt); Hostage Rescue uses this feature to allow players to volunteer to be a hostage, and so on. See section 6.2.49 to section 6.2.52 below for calls relating to the volunteer feature.
VolunteersAllowed = true,
It is desired that all game mode text (in-game messages, option names, option settings, and so on) be localisable into different languages and locales/dialects.
Thus, nearly every bit of text used in a game mode script is looked up in a corresponding game mode localisation table (.csv file), stored (originally) in the location \Content\Localization\GroundBranch\en
(or other codes in place of en
for other languages/locales).
As noted above, game modes must declare their localisation files with the StringTables global variable (see section 2.5.1 above). Furthermore, text keys for localisation must generally not contain underscores (_
), as this interferes with the look-up system.
Here is an example .csv file for the Uplink game mode:
Key,SourceString,Comment
Uplink,Uplink,
objective_DefendObjective,Defend the laptop.,Opsboard
objective_CaptureObjective,Locate and hack the laptop.,Opsboard
summary_DefendObjective,Information was kept safe.,AAR
summary_CaptureObjective,Information was extracted from laptop.,AAR
summary_BlueEliminated,All members of BLUE TEAM were eliminated.,AAR
summary_RedEliminated,All members of RED TEAM were eliminated.,AAR
summary_BothEliminated,Both teams were wiped out.,AAR
roundstage_BlueDefenderSetup_1,Prepare to defend the laptop.,Opsboard
roundstage_BlueDefenderSetup_2,Defenders are setting up.\r\nPrepare to locate and hack the laptop.,Opsboard
roundstage_RedDefenderSetup_1,Defenders are setting up.\r\nPrepare to locate and hack the laptop.,Opsboard
roundstage_RedDefenderSetup_2,Prepare to defend the laptop.,Opsboard
gamemessage_SwapAttacking,Teams have been swapped.\r\nYou are now attacking.,
gamemessage_SwapDefending,Teams have been swapped.\r\nYou are now defending.,
missionsetting_autoswap_0,No,
missionsetting_autoswap_1,Yes,
missionsetting_autoswap,Auto-swap teams,
missionsetting_defendersetuptime,Defender setup time (seconds),Opsboard
missionsetting_capturetime,Capture time (seconds),Opsboard
Three columns are provided, corresponding to the localization key (text to be substituted) and the localization text (the text in the appropriate language to replace the key), and a comment column (not used).
In this case, the game mode name has a look-up (“Uplink” / “Uplink”) based on the Lua package name.
There are some additional conventions to help provide unique game mode customisation:
Key syntax | Encodes… |
---|---|
“objective_” + Objective Name |
Objective name |
“summary_” + Summary Name |
Summary text |
“roundstage_” + Round stage name + “_” + Team number |
Text displayed at start of new round stage <Round stage name> to team <Team number> |
“gamemessage_” + Game message text |
Any message output to gamemode.BroadcastPlayerMessage or player.ShowGameMessage and the like |
“missionsetting_” + Mission setting name (LOWER CASE!) |
Mission setting name displayed on Ops Board and the like |
“missionsetting_” + Mission setting name (LOWER CASE!) + “_” + Mission setting number |
The text displayed for mission setting entry <Mission setting number> |
“gamemode_” + Game mode name (lower case) |
The full name of the gamemode, e.g. DTAS → “Dynamic Take And Secure” |
“gamemode_description_” + Game mode name (lower case) |
A description of the game mode for display in mission selection screens |
Besides the combat info given in the After Action Report (AAR) after the end of a round, more detailed, custom player scores can be awarded by a game mode. To enable this, you will need to provide a player scoring table in something like the following form in the globals section of your game mode (the top bit):
-- player score types includes score types for both attacking and defending players
PlayerScoreTypes = {
SurvivedRound = {
Score = 1,
OneOff = true,
Description = "Survived round",
},
WonRound = {
Score = 1,
OneOff = true,
Description = "Team won the round",
},
DiedInRange = {
Score = 1,
OneOff = true,
Description = "Died within range of flag",
},
SurvivedInRange = {
Score = 1,
OneOff = true,
Description = "Within range of flag at round end",
},
Killed = {
Score = 1,
OneOff = false,
Description = "Kills",
},
LastKill = {
Score = 1,
OneOff = true,
Description = "Got last kill of the round",
},
InRangeOfKill = {
Score = 1,
OneOff = false,
Description = "In proximity of someone who killed",
},
TeamKill = {
Score = -4,
OneOff = false,
Description = "Team killed!",
},
},
Each entry in the score table is the name of the score token (“SurvivedRound”, “WonRound”, and so on); the Score
field is the score awarded each time; if OneOff
is true
then a score is awarded only once per round (or until the scores are reset); and Description
is the text displayed in the After Action Report (Player Scores tab). This text will at some point be localized, likely in the form of “scores_<Description>”.
Player scores are declared using the gamemode.SetPlayerScoreTypes()
function described below in section 6.2.35. They are usually reset at the beginning of the round (for example at the start of the PreRoundWait round stage) using the gamemode.ResetPlayerScores()
function described below in section 6.2.37.
Player scores are awarded using the player.AwardPlayerScore()
function described below in section 6.5.21. Scores can be negative (for example for team kills).
Team scores work exactly the same as player scores, but are awarded to teams using the gamemode.AwardTeamScore()
function described below in section 6.2.38, and displayed in the Team Scores tab of the After Action Report (AAR). They are set up with the gamemode.SetTeamScoreTypes()
function described below in section 6.2.34 and reset with the gamemode.ResetTeamScores()
function described below in section 6.2.36.
An example team score table might include the following:
-- team score types includes scores for both attackers and defenders
TeamScoreTypes = {
WonRound = {
Score = 2,
OneOff = true,
Description = "Team won the round",
},
DefenderTimeout = {
Score = 6,
OneOff = true,
Description = "Defenders held out until end of time limit",
},
DiedInRange = {
Score = 2,
OneOff = true,
Description = "At least one team member died in flag range",
},
SurvivedInRange = {
Score = 1,
OneOff = true,
Description = "At least one team member survived in flag range",
},
TeamKill = {
Score = -4,
OneOff = false,
Description = "Team kills",
},
CapturedFlag = {
Score = 10,
OneOff = true,
Description = "Team captured the flag",
},
PreventedCapture = {
Score = 2,
OneOff = true,
Description = "Team prevented a flag capture",
},
DefenderOutsideRange = {
Score = -3,
OneOff = true,
Description = "A defender was outside range of flag when captured",
},
},
The purpose of the team (and player) scores is to encourage teamwork and provide a more interesting breakdown of a game. In PvP modes, it is technically possible for a team to lose most rounds and win a match on points. Whether you want this to happen is up to you.
The watch worn by the player has several different modes, which can be selected by game modes. Currently there are three modes:
Time of day / Compass orientation)
This watch mode is selected by default.
Range / Height difference / Bearing / Compass orientation / In-range indicators
(time of day / compass orientation / proximity alert)
The watch modes are configured using the gamemode.SetWatchMode() function (see section 6.2.29 bellow) and SetCaptureZone() function (see section 6.2.31 below).
In ObjectiveFinder and IntelRetrieval modes, an objective location is set via gamemode.SetObjectiveLocation() (see section 6.2.32 below), and a capture state (capturing/not capturing) can be set via gamemode.SetCaptureState() (see section 6.2.33 below).
Everything else is handled client-side by the watch, which displays an alert status, alert message and plays an alert sound as dictated by the defined capture zone and capture state.
In the ObjectiveFinder mode, defenders (defined by the DefenderTeamId in SetCaptureZone() ) get a green alert if they are in range of the objective, and attackers get either an amber alert or a red alert depending on the prevailing capture state.
Either a cylindrical (DTAS) or spherical (Intel Retrieval) capture/alert zone may be defined as desired. Setting a capture radius of 0 will disable all in-range events.
As of v1032, players are assigned to one of four ‘elements’ (Alpha, Bravo, Charlie, Delta), for the purpose of grouping and distinguishing groups of players on the in-game map (players with different elements are indicated with different colours on the map). Players are assigned to element Alpha by default. Elements can be changed via the server roster in the in-game escape menu. There is currently no way to manually set a player element via lua script, but see section 5.4.4 below for assigning default elements on server join.
As of v1033, various items of clothing and gear (tops and vests) and headgear (caps and helmets) may have designated positions for displaying patches, divided into six regions (head left, centre, right and body left, centre and right). Patches are selected in the character customisation screen and are stored as part of the player loadout. Some limited manipulation of player patches may be possible via the inventory system (see section 4 below).
There are a number of functions in game mode scripts that are referenced (and called) directly from the GROUND BRANCH code. This is perhaps the most opaque part of game mode modding in GROUND BRANCH, as there is no central list of these functions maintained anywhere (except possibly here); any blueprint or C++ routine in GROUND BRANCH is able to call any part of the current game mode script as it pleases.
Typically you will need to use a fair few of these functions, but none are strictly mandatory:
This function is called when the game mode script is loaded, giving the game mode a chance to find particular actors and update settings based on these before actors are replicated via the game state. If you’re reading that and not sure what it means, you should probably just use PostInit().
This function is called after PreInit() has been called and after some further initialisation of the game mode.
In the context of a game mode, it is essentially an initialisation function approximately equivalent to a C++ constructor.
The purpose of PostInit()
is to set up the properties of the game mode and inform the GROUND BRANCH code of the same.
Activities like spawning enemies and setting up the level for a new round occurs later, typically via the OnRoundStageSet()
function (see below).
Here is an example of a PostInit()
function from the Uplink game mode Lua script:
function uplink:PostInit()
-- Set initial defending & attacking teams.
self.DefendingTeam = self.PlayerTeams.Red
self.AttackingTeam = self.PlayerTeams.Blue
gamemode.SetPlayerTeamRole(self.DefendingTeam.TeamId, "Defending")
gamemode.SetPlayerTeamRole(self.AttackingTeam.TeamId, "Attacking")
end
In this example, team roles (attacking / defending) are set up at the start of the game.
Do not spawn enemy AI or reset the level in PostInit(), because PostInit() is not called each round, but only when the level and game mode load up the first time.
You need to do your level initialisation in OnRoundStageSet() (see below), usually at the start of the PreRoundWait
stage (when players are spawned into level but play is frozen for a few seconds).
You will recall the default set of round stages listed in section 2.1 above. This function is called whenever a new round stage is set (usually by another part of the game mode itself, but it is probably best not to assume that only the game mode can change the round stage). This allows appropriate initialisation to be undertaken for the specific round stage.
Here is an example of an OnRoundStageSet()
function from the Intel Retrieval game mode:
function intelretrieval:OnRoundStageSet(RoundStage)
if RoundStage == "WaitingForReady" then
timer.ClearAll()
ai.CleanUp(self.OpForTeamTag)
self.TeamExfilWarning = false
if self.CompletedARound then
self:RandomiseObjectives()
end
self.CompletedARound = false
elseif RoundStage == "PreRoundWait" then
self:SpawnOpFor()
gamemode.SetDefaultRoundStageTime("InProgress", self.Settings.RoundTime.Value)
-- need to update this as ops board setting may have changed
-- have to do this before RoundStage InProgress to be effective
-- set up watch stuff
if self.Settings.ProximityAlert.Value == 1 and self.RandomLaptopIndex ~= nil then
--print("Setting up watch proximity alert data")
gamemode.SetWatchMode( "IntelRetrieval", false, false, false, false )
gamemode.ResetWatch()
gamemode.SetCaptureZone( self.LaptopProximityAlertRadius, 0, 255, true )
-- cap radius, cap height, team ID, spherical zone (ignore height)
local NewLaptopLocation = actor.GetLocation( self.Laptops[self.RandomLaptopIndex] )
gamemode.SetObjectiveLocation( NewLaptopLocation )
end
-- watch is set up to create a proximity alert when within
-- <LaptopProximityAlertRadius> m of the laptop
elseif RoundStage == "PostRoundWait" then
self.CompletedARound = true
end
end
Here you can see that some additional processing is undertaken when the round stage is set to WaitingForReady
. The list of available laptops is compiled in the xxxInit()
functions, but a random laptop is picked in RandomiseObjectives()
when the WaitingForReady
round stage is reached (corresponding to the beginning of a game, or everyone being sent back to the ready room). Why is this put here and not in PostInit()
? Because if your game mode is round-based (as most are), the game will flip back to WaitingForReady
at the end of the round, but PreInit()
and PostInit()
will not be called again, and some re-initialisation needs to happen at the start of a new round (including, here, making sure the AI is fully cleaned up/deleted).
Some additional processing is done in PreRoundWait
(when players are spawned into the map, but there is a delay of a few seconds to make sure everyone is in and replicated ok) to ensure the main round time is set ok and to set up the objective-based watch mode that is used to track proximity to intel targets.
(Also it is possible for the round stage to move to ReadyCountdown
and then back to WaitingForReady
, for example if all players cancel their spawn or leave the ops room, and so on.
To avoid all the mission objectives resetting, the self.CompletedARound
variable is used, as can be seen in the WaitingForReady
and PostRoundWait
sections)
This function is called when a round stage timer has elapsed.
If this function is not present, the default behaviour will be applied (if the round timer ends in the InProgress
stage, the round times out and the round stage progresses to PreRoundWait
, and so on).
In this example, the OnRoundStageTimeElapsed() function is used to intercept the end of the PreRoundWait stage so as to insert the new custom Round Stage BlueDefenderSetup or RedDefenderSetup, and to intercept the end of those stages to progress to the normal InProgress stage:
function uplink:OnRoundStageTimeElapsed(RoundStage)
if RoundStage == "PreRoundWait" then
if self.DefendingTeamId == self.BlueTeamId then
gamemode.SetRoundStage("BlueDefenderSetup")
else
gamemode.SetRoundStage("RedDefenderSetup")
end
return true
elseif RoundStage == "BlueDefenderSetup"
or RoundStage == "RedDefenderSetup" then
gamemode.SetRoundStage("InProgress")
return true
end
return false
end
This function is called when a player selects or changes an insertion point on the ops board.
The insertion point InsertionPoint is set to nil
if the insertion point has been de-selected.
The InsertionPoint variable is not directly usable but can be passed to the GetInsertionPointName() function mentioned in section 6.2.28 below to extract the name of the insertion point.
The following code is usually executed for typical game modes:
function MyGameMode:PlayerInsertionPointChanged(PlayerState, InsertionPoint)
if InsertionPoint == nil then
timer.Set(self, "CheckReadyDownTimer", 0.1, false)
else
timer.Set(self, "CheckReadyUpTimer", 0.25, false)
end
end
It has been noted that GetInsertionPoint() doesn’t work until a player has been spawned in, so this can provide a way to find a player’s spawn point before then.
This function is called when a player’s readied-up status changes
function intelretrieval:PlayerReadyStatusChanged(PlayerState, ReadyStatus)
if ReadyStatus ~= "DeclaredReady" then
timer.Set("CheckReadyDown", self, self.CheckReadyDownTimer, 0.1, false)
end
if ReadyStatus == "WaitingToReadyUp"
and gamemode.GetRoundStage() == "PreRoundWait"
and gamemode.PrepLatecomer(PlayerState) then
gamemode.EnterPlayArea(PlayerState)
end
end
Though PlayerReadyStatusChanged() and PlayerInsertionPointChanged() do more or less the same thing, they may be combined as you see fit. They are typically used to provide the standard game mode behaviour is to start a countdown timer when the first player selects an insertion point, and to stand down the countdown if all players have deselected the insertion point. The functions are mostly redundant, but only mostly. The easiest thing to do is just copy the appropriate one of the following code fragments (but make sure you have defined all relevant team info in the Lua script globals):
Currently the countdown length is not controllable by the lua script or via UI, but can be set as a command line or map list parameter (?readycountdowntime=45 for 45 seconds, and so on).
PvE (Co-op game) mode (one player team):
function intelretrieval:PlayerInsertionPointChanged(PlayerState, InsertionPoint)
if InsertionPoint == nil then
timer.Set("CheckReadyDown", self, self.CheckReadyDownTimer, 0.1, false)
else
timer.Set("CheckReadyUp", self, self.CheckReadyUpTimer, 0.25, false)
end
end
function intelretrieval:PlayerReadyStatusChanged(PlayerState, ReadyStatus)
if ReadyStatus ~= "DeclaredReady" then
timer.Set("CheckReadyDown", self, self.CheckReadyDownTimer, 0.1, false)
end
if ReadyStatus == "WaitingToReadyUp"
and gamemode.GetRoundStage() == "PreRoundWait"
and gamemode.PrepLatecomer(PlayerState) then
gamemode.EnterPlayArea(PlayerState)
end
end
function intelretrieval:CheckReadyUpTimer()
if gamemode.GetRoundStage() == "WaitingForReady" or gamemode.GetRoundStage() == "ReadyCountdown" then
local ReadyPlayerTeamCounts = gamemode.GetReadyPlayerTeamCounts(true)
local BluForReady = ReadyPlayerTeamCounts[self.PlayerTeams.BluFor.TeamId]
if BluForReady >= gamemode.GetPlayerCount(true) then
gamemode.SetRoundStage("PreRoundWait")
elseif BluForReady > 0 then
gamemode.SetRoundStage("ReadyCountdown")
end
end
end
function intelretrieval:CheckReadyDownTimer()
if gamemode.GetRoundStage() == "ReadyCountdown" then
local ReadyPlayerTeamCounts = gamemode.GetReadyPlayerTeamCounts(true)
if ReadyPlayerTeamCounts[self.PlayerTeams.BluFor.TeamId] < 1 then
gamemode.SetRoundStage("WaitingForReady")
end
end
end
PvP (adversarial) game mode (multiple player teams):
function teamelimination:PlayerInsertionPointChanged(PlayerState, InsertionPoint)
if InsertionPoint == nil then
timer.Set("CheckReadyDown", self, self.CheckReadyDownTimer, 0.1, false);
else
timer.Set("CheckReadyUp", self, self.CheckReadyUpTimer, 0.25, false);
end
end
function teamelimination:PlayerReadyStatusChanged(PlayerState, ReadyStatus)
if ReadyStatus ~= "DeclaredReady" then
timer.Set("CheckReadyDown", self, self.CheckReadyDownTimer, 0.1, false)
end
if ReadyStatus == "WaitingToReadyUp"
and gamemode.GetRoundStage() == "PreRoundWait"
and gamemode.PrepLatecomer(PlayerState) then
gamemode.EnterPlayArea(PlayerState)
end
end
function teamelimination:CheckReadyUpTimer()
if gamemode.GetRoundStage() == "WaitingForReady" or gamemode.GetRoundStage() == "ReadyCountdown" then
local ReadyPlayerTeamCounts = gamemode.GetReadyPlayerTeamCounts(true)
local BlueReady = ReadyPlayerTeamCounts[self.PlayerTeams.Blue.TeamId]
local RedReady = ReadyPlayerTeamCounts[self.PlayerTeams.Red.TeamId]
if (BlueReady > 0 and RedReady > 0) then
if BlueReady + RedReady >= gamemode.GetPlayerCount(true) then
gamemode.SetRoundStage("PreRoundWait")
else
gamemode.SetRoundStage("ReadyCountdown")
end
end
end
end
function teamelimination:CheckReadyDownTimer()
if gamemode.GetRoundStage() == "ReadyCountdown" then
local ReadyPlayerTeamCounts = gamemode.GetReadyPlayerTeamCounts(true)
local BlueReady = ReadyPlayerTeamCounts[self.PlayerTeams.Blue.TeamId]
local RedReady = ReadyPlayerTeamCounts[self.PlayerTeams.Red.TeamId]
if BlueReady < 1 or RedReady < 1
gamemode.SetRoundStage("WaitingForReady")
end
end
end
This function is called to determine if the game should check for team kills.
It should return true
for yes, false
for no.
An example is given here:
function intel:ShouldCheckForTeamKills()
if gamemode.GetRoundStage() == "InProgress" then
return true
end
return false
end
Current behaviour is that team kills have no consequence in the final seconds of the game (in the PostRoundWait
stage). When the PostRoundWait round stage is started, player use of weapons is now disabled, so TKs should no longer be possible after the round end.
This is a very specialist function which is called with Request name ‘join’ when a player clicks on the Ops Board for a deathmatch-style game (with a “Mission Area: Click To Deploy”) message on it. The default behaviour is to send the player to the play area immediately.
function deathmatch:PlayerGameModeRequest(PlayerState, Request)
if PlayerState ~= nil then
if Request == "join" then
gamemode.EnterPlayArea(PlayerState)
end
end
end
This function is called to determine if a player can enter the play area.
It is normally used to determine whether players can spectate or otherwise be sent to the play area to play (as an admin command). The function should return true
for yes, and false
for no.
An example is given here for the Intel Retrieval game mode:
function intelretrieval:PlayerCanEnterPlayArea(PlayerState)
if player.GetInsertionPoint(PlayerState) ~= nil then
return true
end
return false
end
This ensures that a player will have a valid insertion point before being sent to the play area.
This function is called when the game is looking for a player start for a player, to spawn the player into the play area at the start of a round (or on a respawn, if appropriate). If this function is provided, a custom spawn location can be supplied to override the normal process of selecting a player start corresponding to an insertion point selected by the player.
Providing this function is mandatory for game modes like Deathmatch, which have the AllowLateJoiners
property set to true
.
The GetSpawnInfo() function returns either (a) a reference to a player start object (as returned by gameplaystatics.GetAllActorsOfClass(),
for example – see section 6.1.1), or (b) a table containing two fields: a Location
table (in turn having fields x
, y
, z
) and a Rotation
table (in turn having fields yaw
, pitch
and roll
). Either the player start or the manually specified location and rotation will be used to attempt a player spawn.
With manually-specified location and rotation, there is of course a risk that a player will not be able to spawn into the level.
It is best to make some kind of preparation for this contingency.
Example of GetSpawnInfo()
in the Deathmatch game mode:
function deathmatch:GetSpawnInfo(PlayerState)
return self:GetBestSpawn()
end
function deathmatch:GetBestSpawn()
local StartsToConsider = {}
local BestStart = nil
for i, PlayerStart in ipairs(self.PlayerStarts) do
if not self:WasRecentlyUsed(PlayerStart) then
table.insert(StartsToConsider, PlayerStart)
end
end
local BestScore = 0
for i = 1, #StartsToConsider do
local Score = self:RateStart(StartsToConsider[i])
if Score > BestScore then
BestScore = Score
BestStart = StartsToConsider[i]
end
end
if BestStart == nil then
BestStart = StartsToConsider[umath.random(#StartsToConsider)]
end
if BestStart ~= nil then
table.insert(self.RecentlyUsedPlayerStarts, BestStart)
if #self.RecentlyUsedPlayerStarts > self.MaxRecentlyUsedPlayerStarts then
table.remove(self.RecentlyUsedPlayerStarts, 1)
end
end
return BestStart
end
This function is called when a player enters a play area.
An example is given in the Uplink game mode:
function uplink:PlayerEnteredPlayArea(PlayerState)
if actor.GetTeamId(PlayerState) == self.AttackingTeamId then
local FreezeTime = self.DefenderSetupTime + gamemode.GetRoundStageTime()
player.FreezePlayer(PlayerState, FreezeTime)
elseif actor.GetTeamId(PlayerState) == self.DefendingTeamId then
local LaptopLocation = actor.GetLocation(self.RandomLaptop)
player.ShowWorldPrompt(PlayerState, LaptopLocation, "DefendTarget", self.DefenderSetupTime - 2)
end
end
This function is called whenever a character dies (human or AI). Typically you might use this to determine game mode win/lose conditions.
Here is an example of an OnCharacterDied()
function from the Uplink game mode.
By default characters only have one life, but here the death routine will function appropriately if lives are set elsewhere to greater than 1 (so the game mode should play more nicely with other mods/mutators):
function uplink:OnCharacterDied(Character, CharacterController, KillerController)
if gamemode.GetRoundStage() == "PreRoundWait"
or gamemode.GetRoundStage() == "InProgress"
or gamemode.GetRoundStage() == "BlueDefenderSetup"
or gamemode.GetRoundStage() == "RedDefenderSetup" then
if CharacterController ~= nil then
player.SetLives(CharacterController, player.GetLives(CharacterController) - 1)
local PlayersWithLives = gamemode.GetPlayerListByLives(255, 1, false)
if #PlayersWithLives == 0 then
self:CheckEndRoundTimer()
else
timer.Set("CheckEndRound", self, self.CheckEndRoundTimer, 1.0, false);
end
end
end
end
In this case, as is typical, the actual checks for round end are deferred with a timer (to make sure conditions where players ‘trade’ deaths are detected correctly and fairly):
function uplink:CheckEndRoundTimer()
local AttackersWithLives = gamemode.GetPlayerListByLives(self.AttackingTeam.TeamId, 1, false)
if #AttackersWithLives == 0 then
local DefendersWithLives = gamemode.GetPlayerListByLives(self.DefendingTeam.TeamId, 1, false)
if #DefendersWithLives > 0 then
gamemode.AddGameStat("Result=Team" .. tostring(self.DefendingTeam.TeamId))
if self.DefendingTeam == self.PlayerTeams.Blue then
gamemode.AddGameStat("Summary=RedEliminated")
else
gamemode.AddGameStat("Summary=BlueEliminated")
end
gamemode.AddGameStat("CompleteObjectives=DefendObjective")
gamemode.SetRoundStage("PostRoundWait")
else
gamemode.AddGameStat("Result=None")
gamemode.AddGameStat("Summary=BothEliminated")
gamemode.SetRoundStage("PostRoundWait")
end
end
end
This function is called whenever a character (player or AI) enters a trigger area, as defined in the mission editor (or otherwise). For this feature to work, the TeamId for the trigger needs to be set correctly in the mission editor (or conceivably via actor.SetTeamId(), see section 6.3.7 below), and the trigger needs to be set active (via actor.SetActive(), see section 6.3.13 below) in order for it to be triggerable by a player.
Here is an example of an OnGameTriggerBeginOverlap()
function from the Intel Retrieval game mode, checking to see if a player has brought the laptop into the zone.
If the (exfiltrate as team) flag is set, further tests are made.
Otherwise, the round ends there and then:
function intel:OnGameTriggerBeginOverlap(GameTrigger, Character)
if player.HasItemWithTag(Character, self.LaptopTag) == true then
if self.TeamExfil then
timer.Set(self, "CheckOpForExfilTimer", 1.0, true)
else
gamemode.AddGameStat("Result=Team1")
gamemode.AddGameStat("Summary=IntelRetrieved")
gamemode.AddGameStat("CompleteObjectives=RetrieveIntel,ExfiltrateBluFor")
gamemode.SetRoundStage("PostRoundWait")
end
end
end
This function is called whenever a character (player or AI) leaves a trigger area. This is the companion function to OnGameTriggerBeginOverlap() mentioned above.
This function is called whenever a capturable laptop or similar has been captured. It is usually used to set a win/loss state. It is in fact called from either the UplinkTarget.lua or IntelTarget.lua scripts, but we will treat this as a standard function for the purposes of this guide.
Example from Uplink game mode:
function uplink:TargetCaptured()
gamemode.AddGameStat("Summary=CaptureObjective")
gamemode.AddGameStat("CompleteObjectives=CaptureObjective")
if self.AttackingTeamId == self.RedTeamId then
gamemode.AddGameStat("Result=Team2")
else
gamemode.AddGameStat("Result=Team1")
end
gamemode.SetRoundStage("PostRoundWait")
end
This function is called when the laptop is picked up, including after the laptop is dropped (not just when first picked up). It is experimental and may not work correctly. Usually it suffices to use the OnTargetCaptured() function.
This function is called when a setting on the Ops Board is changed. It replaces the old OnMissionSettingChanged() function. It allows mission data to be re-randomised if a relevant mission setting has been changed, for example. This must be used with extreme caution - if a mission setting is updated as a result of this call, the game will be placed into an infinite loop (=bad).
This function works ok for settings selected by combo box (drop down menu). If you are checking for changes to other settings, which can vary quickly and repeatedly, it is advisable to use a timer (say, 0.5 seconds) to delay taking action on any changes. You may also want to include logic so the settings cannot be changed in the middle of the round.
If the table has a mission index corresponding to the name of a mission setting, it indicates that that setting has changed.
You can test for this with if ChangedSettingsTable[<MissionSetting>] ~= nil then …
.
function intelretrieval:OnMissionSettingsChanged(ChangedSettingsTable)
if ChangedSettingsTable['DisplaySearchLocations'] ~= nil then self:RandomiseObjectives()
end
end
This function is called when a player clicks the new randomise objectives button on the Ops Board, and requests that the game mode re-roll the random settings (such as intel locations, team spawns, and so on).
This function returns TRUE if the randomise objectives feature is supported, and FALSE if it is not. If FALSE, then the randomise objectives button (circular arrow) will be hidden.
This function is called whenever a game setting is changed, so that a game mode can allow randomisation of objectives in certain circumstances only, for example.
function terroristhunt:CanRandomiseObjectives()
-- allow randomisation if we have a variety of hotspots and the use of hotspots is enabled
return (#self.AllAIHotspots > 1) and (self.Settings.UseAIHotspots.Value == 1)
end
This function is called when a player is logging out of a game. It allows any necessary clean-up to be undertaken. Typically you may want to check for round end conditions (due to the exit of the player) and remove any userdata data relating to the leaving player from any tables (otherwise you will have unavoidable errors when accessing that userdata data later). For example, if you keep track of any player states (advisable not to if you can avoid it) then you should purge this data when the player exits. Here is an example:
function intel:LogOut(Exiting)
if gamemode.GetRoundStage() == "PreRoundWait" or gamemode.GetRoundStage() == "InProgress" then
timer.Set(self, "CheckBluForCountTimer", 1.0, false);
end
end
This is a special function provided by a <GameMode>Validate.Lua file. It is called when a user selects ‘Validate Level’ in the mission editor menu. It returns a table (which may be empty, indicating no errors detected) with a list of strings corresponding to feedback on errors in the level.
function intelretrievalvalidate:ValidateLevel()
-- new feature to help mission editor validate levels
local ErrorsFound = {}
local AllSpawns = gameplaystatics.GetAllActorsOfClass('GroundBranch.GBAISpawnPoint')
if #AllSpawns == 0 then
table.insert(ErrorsFound, "No AI spawns found")
end
-- ...
return ErrorsFound
end
This function is called when a player has created or updated the loadout of name LoadoutName. The Hostage Rescue game mode uses this as a cue to create a Hostage variant of the loadout, if it doesn’t already exist, for example. See section 4 and section 6.9 below for more information.
This function is called whenever a player is spawned into the level or into the ready room.
In response the game mode is able to return a custom loadout to apply to the player (as might be set up, for example, by inventory.CreateLoadoutFromTable()
– see section 6.9.6 below). The function either returns the loadout name to apply, or nil
to proceed with the default loadout.
This is used by the Hostage Rescue mode to selectively apply a hostage loadout to the selected hostage player:
function hostagerescue:GetPlayerLoadoutName(PlayerState)
if self.CurrentHostage ~= nil and PlayerState == self.CurrentHostage and self.ApplyHostageLoadout then
-- use loadout name 'hostage'
self.ApplyHostageLoadout = false
return "Hostage"
end
-- use team based loadout
return nil
end
Player inventory, weapon builds, kit builds, and player loadouts and the like are stored and manipulated in JSON mark-up format (a simplified version of XML). Stored player loadouts may reference stored item builds, and these are loaded in when a loadout is loaded.
A default loadout (stored in My Documents / GroundBranch / Loadouts) might look something like this (NoTeam.kit
):
"Ver": 11,
"Data": [
{
"Type": "Profile",
"Data": [
{
"Type": "Head",
"Item": "Head:BP_Head_Male03"
},
{
"Type": "Patch",
"Item": "Patch:BP_Patch_CallSign"
},
{
"Type": "Patch",
"Item": "Patch:BP_Patch_HeadRight",
"PatchPath": "/Game/GroundBranch/Patches/BloodType/(BlackfootStudios)BloodA+"
},
// ...
]
},
{
"Type": "Weapons",
"Data": [
{
"Type": "PrimaryFirearm",
"Item": "PrimaryFirearm:BP_416_CQB"
},
{
"Type": "Sidearm",
"Item": "Sidearm:BP_Mk25"
}
]
},
{
"Type": "Gear",
"Data": [
{
"Type": "Platform",
"Item": "Platform:BP_Platform_PlateCarrier_MPC",
"Skin": "OCP"
},
{
"Type": "Belt",
"Item": "Belt:BP_Battlebelt_CB",
"Skin": "OD"
},
{
"Type": "Holster",
"Item": "Holster:BP_Holster_Handgun",
"Skin": "CoyoteBrown"
}
]
},
{
"Type": "Outfit",
"Data": [
{
"Type": "EyeWear",
"Item": "EyeWear:BP_Eyeshield_Clear",
"Skin": "Black"
},
{
"Type": "FaceWear",
"Item": "FaceWear:BP_Mask_Shemagh_Neck",
"Skin": "Black"
},
{
"Type": "Shirt",
"Item": "Shirt:BP_Shirt_ACU_Rolled",
"Skin": "TigerStripe_Desert"
},
{
"Type": "Pants",
"Item": "Pants:BP_Pants_Jeans",
"Skin": "Black"
},
{
"Type": "Gloves",
"Item": "Gloves:BP_Gloves_Tactical",
"Skin": "CoyoteBrown"
},
{
"Type": "Footwear",
"Item": "Footwear:BP_Footwear_HikingShoes",
"Skin": "Tan"
}
]
}
]
}
“Ver”: Each loadout has a top-level “Ver” field. This is the version number of the loadout file. This may be incremented in subsequent GB versions. If a stored loadout file is of a lower version than the current game version, it will be deleted or ignored (? Kris to confirm).
“Type”: There are top-level “type” fields and sub-type “type” fields contained within them:
Profile “Head”, “Patch”
Weapons “PrimaryFirearm”, “Sidearm”
Gear “HeadGear”, “Platform”, "`Belt,`"Holster”
Outfit “Eyewear”, “Shirt”, “Pants”, “Gloves”, “Footwear”
Each sub-type has either an “item” or an “itembuild” field associated with it, and optionally a “skin” field, e.g.
{
"Type": "Platform",
"ItemBuild": "MPC_frags_smokes_rangefinder",
"Skin": "Black"
},
And
{
"Type": "Gloves",
"Item": "Gloves:BP_Gloves_Assault",
"Skin": "Khaki"
},
Other custom fields are possible (see patch items for examples, e.g. “`+PatchPath+`”)
The “Item” field is the actual asset name (in the asset registry, as seen within the UE4 editor), with the asset type as prefix (e.g. “`+Footwear:BP_Footwear_HikingShoes+`”)
Item builds
Item builds are effectively mini-loadout files for specific items of kit. Item builds are stored in (My Documents) / GroundBranch / ItemBuilds and in a subdirectory corresponding to the type of item in question (e.g. Belt / Firearm / HeadGear / Platform / PrimaryFirearm / Sidearm).
A fully equipped platform (vest) of type “Platform” and the specific instance of that type “BP_Platform_PlateCarrier_MPC” might have an item build as shown in Appendix B below.
The top level fields used in ItemBuild files are:
“BuildName” a user-specified name of the item build.
“Item” the specific item of the item type (e.g. “`+Platform:BP_Platform_PlateCarrier_MPC+`”
“Children” a list of attached items, which may themselves have further children
The children items have the fields:
“Item” the specific attached item with type prefix, e.g. “`+Pouch:BP_Pouch_PrimaryAmmo+`”
“Comp” the component name, e.g. “PlatformMeshComponent0”
“Socket” the socket name on the item mesh, e.g. “POUCH_1_2”
“Children” any further sub-items to attach
Children of children only have an “Item” field and are not attached in the same way, e.g. “`+Item+” = “
+Magazine:BP_MP5_9mm_Magazine+`”
Ammo types for primary ammo or secondary ammo pouches are updated to match the primary gun type on being equipped.
At certain points, loadouts in the general form above are converted into ‘monolithic’ kit lists, which are just a flat list of items in the loadout, with all custom builds decoded into constituent parts. This doesn’t affect inventory handling except that you cannot rely on the top-level type fields (Profile, Weapon, Gear and Outfit) being present.
As of v1033, loadouts for players and custom kit lists can now be converted into lua tables and back again into named loadouts for specific players. This can allow a degree of manipulation and customisation of inventories by game modes and mutators. However, because of the peculiarities of the systems in GROUND BRANCH, and the difficulty of maintaining loadout coherence in multiplayer, there are some significant restrictions on how and when these manipulations can be done.
In one place, inventories can be manipulated when they are applied/created, so as to create a temporary version lacking particular items, in the OnPreLoadoutChanged()
callback for mutators (see section 5.4.6 below). Otherwise, however, you need to pre-create a modified loadout and vary which loadout is applied for a particular player when they spawn in (see the calls in section 6.9 below).
A new feature in v1033 is a mutator lua script. This works in a similar fashion to game mode scripts, and has a lot of the same access points and potential behaviours, but it is loaded when the game loads, and persists across different missions and play sessions. Some mutators operate client-side and some operate server-side, for example to allow greater customisation of server behaviour by the server operator.
Visit the Mods / Mutators menu from the game main menu to see currently installed mutators and to view and edit their options (see below).
For the avoidance of doubt, all of the lua library functions and call-back functions in this section apply to mutators only, and not game mode scripts.
In v1033, there are three mutators provided which provide base functionality for the calls in section 5.4 below, that other mutators in mods can override if desired.
Mutators are stored in the Content/GroundBranch/Mutators
folder:
InventoryManagement.lua
: Allows customisation of loadout naming, and provides an inventory dump function
ServerManagement.lua
: Allows the customisation of server policies relating to player names and callsigns
WeaponRestriction.lua
: Allows the restriction of various bits of kit for all players on a server
local servermanagement = {
MutatorAuthor = "(c) BlackFoot Studios, 2022",
MutatorName = "Server management",
MutatorType = "Server",
-- MutatorType not used at present
ServerOnly = false,
-- will be loaded on dedicated servers and listen servers, but not on standalone clients or server clients
ServerAuthoritative = true,
-- server mutator settings will be replicated to client for the duration of the server connection, and can't be changed
Mutator global variables are specified in the same way as gamemode global variables (see section 2.5 above). They include:
This specifies the author of the mutator
This provides the short/internal name of the mutator. It is (or will be) looked up in a string table like the game mode names.
This is an author-supplied description of the mutator type. There are not currently hard/limited categories, but may be in future.
If true
, the mutator will only be loaded on a server (dedicated or host).
If true, the settings of the mutator will be replicated to clients and (temporarily) override client settings while the mutator is running on a server.
Mutators can have options in the same way that game modes can have mission settings, and they are treated very similarly.
Settings are saved to Modding.ini
and can be overridden using parameters in map lists (Maplist.ini
in the ServerConfig
folder), e.g. ?RemovePrimary=1
like with game mode settings.
Mutator options can only be changed between game sessions.
In later versions of GROUND BRANCH, mutator settings may in some cases be temporarily replicated from server to clients during a game session.
The SortOrder parameter is intended to manually specify a display order of the options, but is currently inoperable.
MutatorOptions = {
RemovePrimary = {
-- 0 = no change
-- 1 = remove equipped primaries (e.g.
rifles, shotguns, submachine guns)
Min = 0,
Max = 1,
Value = 0,
SortOrder = 1,
},
RemoveSidearm = {
-- 0 = no change
-- 1 = remove equipped sidearms (e.g.
pistols)
Min = 0,
Max = 1,
Value = 0,
SortOrder = 2,
},
--- ...
The following functions are provided for use by mutators only (they are not called in game modes):
This function is called when the player enters a new player name, and provides a suggested list of call signs for that name. Currently a random entry is picked. PlayerName may potentially be nil (this is an error state). The function returns a table of suggested (3 letter) call signs (each one a string), or nil to pass and let the base function decide.
This function is called on servers to ensure everyone’s callsign is appropriate for that server and to avoid clashes. PlayerPreferredCallSign
is a custom three letter call sign provided by the player (string
type), or nil
if one is not specified. Player
is a player state identifier, which could be nil (if we’re at the main menu and the player info is not yet properly defined). PlayerElement
is a string
type indicating the player’s current element (“A” - “D”, could be nil
if not yet defined). PlayerElementNumber
is a (theoretically) guaranteed unique index of the player within that element (starting at 1), and could also be nil
. If bUseElementCallSign
is true, the player element and element number should always be used as the basis for the callsign.
The precise formatting of the callsign is up to the mutator.
The function should return a callsign string
type of ideally no more than 4 letters (it will probably be capped at 4 or 5 characters regardless). If the function returns nil
, the default callsign will be used.
This function is not usually of interest except where the name is blank or generic. PlayerName
is a string
type.
Returning a string
will override the name with the suggestion, otherwise return nil
for no action.
The player name may be nil
.
This function returns the default team element for the player (generally Alpha). This is only called when a player joins a server or starts a game (may be currently inoperational). Return a string
type “`+A+”, “
+B+”, “
+C+”, “
+D+”, or return `nil
to pass on this opportunity.
This function is called when a player has changed a mutator option - take care not to set any new mutator options here.
This function allows the modification of a player loadout before it is applied.
The loadout is passed as USERDATA
encapsulating an array of JSON objects corresponding to the ‘monolithic’ JSON kit list (see section 4.1.2 above). This is a good place for pistols only mutators and suchlike to remove things.
A different approach is required if you want to change inventory more dynamically, for example before every new round.
In that case, you need to define fallback loadouts in advance and switch between them and the normal loadout as appropriate.
See section 6.9 and the inbuilt WeaponRestriction mutator for more details.
This function is called by the character editor when editing an item build, either from scratch or editing an existing build. ItemType
is a string
containing item type, “e.g. +PrimaryFirearm+
”. ItemBuildTable
is a lua table containing a parallel structure to the loadout Json, but contains only key fields from it: e.g. TypeName
and TypeValue
(expanded from the original Type
field) and Children
. JsonHash
is a hash string
type made from the original loadout Json that can be used to create unique build names (a string
is used rather than number
because it is a very large number).
Return a string
type with the build name, or return nil
to leave the build name unmodified.
GROUND BRANCH provides a number of utility functions, hooks, and so on to game modes (and any other modding Lua scripts). These will be described below. These libraries may be changed or added to at any time. Proceed with caution.
This Lua library clones various functions in UGameplayStatics relating to the UE4 world. These functions typically reproduce various Blueprint nodes in UE4.
Function list:
Returns an array of pointers to actors (AActor*
) of class Class.
Class should be a string of the form ‘GroundBranch.GBInsertionPoint’ (for C++-originating classes) or of the form '/Game/GroundBranch/Props/Electronics/MilitaryLaptop/BP_Laptop_Usable.BP_Laptop_Usable_C'
for UE4 blueprint classes and other UE4 assets.
Example:
local AllInsertionPoints = gameplaystatics.GetAllActorsOfClass('GroundBranch.GBInsertionPoint')
Returns an array of pointers to actors (AActor*
) having a tag equal to Tag.
Tags may be of the form “Defenders” or “Attackers”, for example. They are often used to label/identify particular spawns or other game objects within the same class of object.
Returns an array of pointers to actors (AActor*
) of class Class and having a tag equal to Tag.
This is essentially a combination of GetAllActorsOfClass() and GetAllActorsWithTag() – see above.
This function tries to find a validated spawn location based on a proposed spawn location SpawnLocation. It returns a table with two fields: bValid
(true
if a valid location was found, false
otherwise) and ValidatedSpawnLocation
with the location for the spawn.
This validated location can be used in conjunction with the GetSpawnInfo()
function (see section 3.10 above).
This function places an item of class ItemClass at the specified location and rotation. Location
is expected to be a table containing fields x
, y
, z
, and Rotation
is expected to be a table containing fields yaw
, pitch
and roll
. This function is really a special case for placing the flag at the end of the initial flag placement round in the DTAS game mode.
Otherwise there does not exist a mechanism for tracking and removing any items placed with this function, so it is of limited/no current use for other game modes. Certainly, some actions/items currently work using this function, but it is not advised and may not remain backwards-compatible.
This function does a simple visibility trace from one point to another.
It returns a USERDATA reference to any actor that was hit (if the trace fails), or nil
if nothing was hit.
It is currently used by game mode mission editor validation functions.
You should take care not to run traces too often, or performance may be significantly impacted.
This function returns a Table of USERDATA references to patrol route actors that are linked to by the specified patrol route actor. It is used by game mode validators to check visibility between adjacent patrol routes and suchlike.
This function draws a debug sphere with the specified radius at the specified location for the specified duration (in seconds). It is used to debug game modes. It is not intended to be used in release versions of game modes.
This function draws a debug line from the specified start location to the specified end location for the specified duration.
This library handles interactions between the game mode script and the GROUND BRANCH code.
Function list:
Returns a reference to the current game mode script (which is a Lua table type). This is typically used in scripts for game items that may be present in a game mode (but are not part of it), such as capturable laptops.
Example:
if actor.HasTag(self.Object, gamemode.GetScript().LaptopTag) then
Result.Equip = true
Returns a string describing the current round stage. See [roundStages] above for more information on round stages.
Example:
if gamemode.GetRoundStage() == "WaitingForReady" or gamemode.GetRoundStage() == "ReadyCountdown" then
local ReadyPlayerTeamCounts = gamemode.GetReadyPlayerTeamCounts(false)
Sets the current round stage to the supplied string. Game modes are responsible for changing the game stage. So the PostInit() function will normally use this call to set the game stage to the WaitingForReady stage, and so on.
Example:
gamemode.SetRoundStage("WaitingForReady")
Returns a (float) time equal to the number of seconds remaining in a round stage. This timer is typically used for the ready up countdown after a player selects a spawn point (typically 60 seconds), and also for the round timer (typically many minutes) during the game proper.
Sets the round stage time to the supplied number of seconds, and begins the timer.
This works for custom round stages, but for standard round stages (in particular PreRoundWait
and InProgress
stages) you will need to use the SetDefaultRoundStageTime() function below, before the start of the relevant round stage.
This function sets the default length of the specified round stage (for standard round stages). All of the stages have limits specified in seconds, except for the InProgress stage which has a time set in minutes. This function must be called before the round stage in question begins.
In this following extract from the DTAS game mode, when the custom round stage DTASSetup
is entered (after PreRoundWait
), the current round stage time is set at that point (because it is a custom round, that works), and the default round stage time is set for the next round stage DTASInProgress
(which is treated like a standard InProgress
round stage, because the round stage name contains the text “InProgress”, so that is the round stage name supplied to the function):
elseif RoundStage == "DTASSetup" then
self:SetupRoundDTAS()
-- ...
gamemode.SetRoundStageTime(self.Settings.FlagPlacementTime.Value + 2.0)
-- add a bit to the time as a bit gets eaten up
gamemode.SetDefaultRoundStageTime("InProgress", self.Settings.RoundTime.Value)
This function resets the round stage time to zero, and prevents the OnRoundStageTimerElapsed() function from being called. The timer can be restarted (with a specified new time) using the SetRoundStageTime() function.
This function sets the attitude of one (AI) team Team towards another team OtherTeam (e.g. a player team). The attitude parameter Attitude is a string selected from Friendly
, Neutral
and Hostile
. AI characters and AI teams default to hostile towards individual players and player teams.
Reportedly the function is not case sensitive and these exact strings must be used.
This only takes effect before AI is spawned; it does not (it is believed) currently affect existing AI.
It may not work terribly well in any case - it is not really used by official modes and is not well tested.
This function sends the message GameMessageId to every human player alive in the play area. The message is displayed on screen at a location defined by display type Type for Duration seconds. Messages will normally be queued. Alternatively, specifying a negative duration will cause all current messages to be flushed. Specifying a duration of 0 will display the message indefinitely (until another message flushes the display, or the round stage ends).
Possible Types (corresponding to screen locations) are: Engine
(top left, small orange text), Upper
, Centre
, Lower
.
Player.ShowGameMessage()
can be used to send messages to individual players (see section 6.5.13 below).
This function does what it says on the tin. It is not usually called, because the default game mode handling will send everyone to the ready room at the end of the round after the After Action Report.
This function makes all players enter spectate mode. It is only currently used for game modes which do not use a ready room, which is to say none of them (officially anyway).
This function sends all players who have a DeclaredReady or WaitingToReadyUp status (see section 2.2.1 above) to the play area (typically, to start a round). It is not normally required to be called, because this happens during the normal game mode processing, when the PreRoundWait
round stage is initiated.
This function sends a specified player to the Ready Room. The target Target is a player state. A player state can be obtained from player.GetPlayerState() (see section 6.5.7 below).
This function sends a specified player to the play area. The target Target is a player state. A player state can be obtained from player.GetPlayerState() (see section 6.5.7 below).
Example from the Uplink game mode:
function uplink:PlayerReadyStatusChanged(PlayerState, ReadyStatus)
-- ...
if ReadyStatus == "WaitingToReadyUp" and gamemode.GetRoundStage() == "PreRoundWait" then
if actor.GetTeamId(PlayerState) == self.DefendingTeam.TeamId then
if self.RandomDefenderInsertionPoint ~= nil then
player.SetInsertionPoint(PlayerState, self.RandomDefenderInsertionPoint)
gamemode.EnterPlayArea(PlayerState)
end
elseif gamemode.PrepLatecomer(PlayerState) then
gamemode.EnterPlayArea(PlayerState)
end
end
end
This function returns an integer equal to the number of players on the server (excluding bots or not, in dependence on the Boolean ExcludeBots). It is typically used to determine when all players have readied up (in which case the countdown is aborted and everyone proceeds directly to the round proper). Please note that bots are not currently used (except via console command) and AI (enemies) are distinct from bots and not included in this count. Example from the Uplink game mode:
if DefendersReady > 0 and AttackersReady > 0 then
if DefendersReady + AttackersReady >= gamemode.GetPlayerCount(true) then
gamemode.SetRoundStage("PreRoundWait")
else
gamemode.SetRoundStage("ReadyCountdown")
end
end
This function returns a table (array) including totals of ready players in each team excluding bots or not in dependence on the Boolean ExcludeBots. Typically if all team entries are non-zero (that is, at least one person from each team has selected an insertion point), the pre-round countdown will begin.
Example from the Uplink game mode:
local ReadyPlayerTeamCounts = gamemode.GetReadyPlayerTeamCounts(false)
local DefendersReady = ReadyPlayerTeamCounts[self.DefendingTeamId]
local AttackersReady = ReadyPlayerTeamCounts[self.AttackingTeamId]
This function returns a table (array) of players (corresponding to C++ type AGBPlayerState*
) matching the criteria of team Id TeamId and human or not ExcludeBots. This selects all players on a team, whether or not in the play area (excepting spectators). This is not usually what you want in game modes.
To select all players actually in play, see the function gamemode.GetPlayerListWithLives() below.
Example from the Uplink game mode, sending all players an appropriate message about swapping roles:
if self.ShowAutoSwapMessage == true then
self.ShowAutoSwapMessage = false
local Attackers = gamemode.GetPlayerList(self.AttackingTeam.TeamId, false)
for i = 1, #Attackers do
player.ShowGameMessage(Attackers[i], "SwapAttacking", "Center", 10.0)
end
local Defenders = gamemode.GetPlayerList(self.DefendingTeam.TeamId, false)
for i = 1, #Defenders do
player.ShowGameMessage(Defenders[i], "SwapDefending", "Center", 10.0)
end
end
This function returns a table (array) of players (corresponding to C++ type AGBPlayerState*
) matching the criteria of team Id TeamId and human or not ExcludeBots, and having a minimum number of lives MinLives. This selects all players on a team, also filtering out to select only players with a Readied-up status of DeclaredReady
, only players with Ready Room status of InPlayArea
, and only non-spectators.
This is usually the starting point of processing players in play.
If you need to get a list of AI in a level, you have to use AI.GetControllers() instead (see section 6.4.5 below).
Example from the Intel Retrieval game mode of using GetPlayerListByLives() to determine if any players remain alive (otherwise it’s round over):
function intelretrieval:CheckBluForCountTimer()
local PlayersWithLives = gamemode.GetPlayerListByLives(self.PlayerTeams.BluFor.TeamId, 1, false)
if #PlayersWithLives == 0 then
gamemode.AddGameStat("Result=None")
gamemode.AddGameStat("Summary=BluForEliminated")
gamemode.SetRoundStage("PostRoundWait")
end
end
Returns the insertion point (equivalent to C++ type AGBInsertionPoint*
) that is most appropriate for a late-joining player.
Typically players can only join a round in progress during the initial seconds in the PreRoundWait
period.
This function should be called before sending a late-joining player into the play area. It carries out all necessary initialisation of the player character.
Returns true
if preparation was successful, otherwise false
.
Example from Intel Retrieval game mode:
function intelretrieval:PlayerReadyStatusChanged(PlayerState, ReadyStatus)
if ReadyStatus ~= "DeclaredReady" then
timer.Set("CheckReadyDown", self, self.CheckReadyDownTimer, 0.1, false)
end
if ReadyStatus == "WaitingToReadyUp"
and gamemode.GetRoundStage() == "PreRoundWait"
and gamemode.PrepLatecomer(PlayerState) then
gamemode.EnterPlayArea(PlayerState)
end
end
This function adds a game objective having description Name (typically looked up in the string table) for the team specified with numeric Id TeamId. The type Type is set to 1 if the objective is a primary objective. Otherwise, it will be treated as a secondary objective. The convention (which you should please adhere to in setting your victory conditions) is that primary objectives must be completed in order to achieve a win condition. Secondary objectives can be completed for the purpose of bragging rights, scoring and perfectionism.
As also explained in section 2.6 above, the objective description is looked up in the string table using the format “objective_”
+ Name. The Uplink game mode, for example, has objectives DefendObjective and CaptureObjective, which are stored as follows in the Uplink.csv
string table:
The uplink:SetupRound()
function includes the following code:
gamemode.AddGameObjective(self.DefendingTeamId, "DefendObjective", 1)
gamemode.AddGameObjective(self.AttackingTeamId, "CaptureObjective", 1)
The Intel Retrieval game mode, meanwhile, sets up the following objectives:
gamemode.AddGameObjective(self.PlayerTeams.BluFor.TeamId, "RetrieveIntel", 1)
gamemode.AddGameObjective(self.PlayerTeams.BluFor.TeamId, "ExfiltrateBluFor", 1)
These objectives have the following entries in the intel.csv
string table:
This function clears all current game objectives. The uplink game mode, for example, calls this function before each round, as the game objectives swap round each round for each team.
This function adds a text search location (e.g. “Red House”, “Deck 3”), for modes similar to Intel Retrieval, to be displayed typically in conjunction with graphical search markers set using gamemode.AddObjectiveMarker below. The search location text is supplied as Name and the Type is 1 for primary, or 2 for secondary.
This function clears all currently set text search locations.
This function creates an objective marker, which has no physical presence in the map but which marks a location for use with other functions such as player.ShowWorldPrompt()
(see section 6.5.10 below). The location Location is a vector (a Lua table type containing fields x
, y
and z
). The team Id TeamId identifies the team that the marker is intended for (for example as an exfiltration marker), though all teams will see the marker.
The marker is given the name Name. The marker is turned on or off in dependence on the Boolean Active.
The function returns a reference to the objective marker, which can be stored for later use (for example to make it active). Current available types of objective marker are Extraction
(green exfil markers), MissionLocation
(translucent red circles to indicate intel search areas and the like) or Hotspot
(red rectangle corresponding to an AI hotspot volume). See the TerrroristHunt.lua game mode script to see usage relating to hotspots.
The hotspot markers are special cases where the game’s UI searches for hotspots matching the given Name and ignores the specified location.
To avoid replication problems, it is recommended you activate or deactivate objective markers in a single pass, rather than change the state of markers twice in a row (for example, deactivate all then activate some).
Example from the Intel game mode, setting up the markers for all of the extraction points, in the PreInit()
function (and setting each marker to be inactive):
self.ExtractionPoints = gameplaystatics.GetAllActorsOfClass('/Game/GroundBranch/Props/GameMode/BP_ExtractionPoint.BP_ExtractionPoint_C')
for i = 1, #self.ExtractionPoints do
local Location = actor.GetLocation(self.ExtractionPoints[i])
local ExtractionMarkerName = self:GetModifierTextForObjective( self.ExtractionPoints[i] ) .. "EXTRACTION"
-- allow the possibility of down chevrons, up chevrons, level numbers, etc
self.ExtractionPointMarkers[i] = gamemode.AddObjectiveMarker(Location, self.PlayerTeams.BluFor.TeamId, ExtractionMarkerName, "Extraction", false)
end
Later on in the Intel game mode, a random extraction marker is set active:
self.ExtractionPointIndex = umath.random(#self.ExtractionPoints)
for i = 1, #self.ExtractionPoints do
local bActive = (i == self.ExtractionPointIndex)
actor.SetActive(self.ExtractionPoints[i], bActive)
actor.SetActive(self.ExtractionPointMarkers[i], bActive)
end
The extraction marker name can be marked up with a special prefix to cause a special symbol to be displayed on the marker.
Currently up and down arrows can be added with the prefixes (U)
and (D)
. Up and down staircase icons can be added with (u)
and (d)
, and floor/deck numbers can be added with (0)
to (9)
and special characters (-)
and (=)
for floor/decks -1 and -2.
This function adds a statistic for the After Action Report displayed at the end of a round. You should only call this function once for each game stat, otherwise the result is at best undefined.
Example from the Intel game mode, on achieving a loss by having the whole team wiped out:
function intel:CheckBluForCountTimer()
local BluForPlayers = gamemode.GetPlayerList("Lives", self.BluForTeamId, true, 1, false)
if #BluForPlayers == 0 then
gamemode.AddGameStat("Result=None")
gamemode.AddGameStat("Summary=BluForEliminated")
gamemode.SetRoundStage("PostRoundWait")
end
end
By contrast, an example from the Uplink game mode, when the attackers have captured the laptop:
function uplink:TargetCaptured()
gamemode.AddGameStat("Summary=CaptureObjective")
gamemode.AddGameStat("CompleteObjectives=CaptureObjective")
if self.AttackingTeamId == self.RedTeamId then
gamemode.AddGameStat("Result=Team2")
else
gamemode.AddGameStat("Result=Team1")
end
gamemode.SetRoundStage("PostRoundWait")
end
This function clears all set game statistics. It is not normally called by a game mode script as it is normally handled automatically.
Returns the name of the specified insertion point.
Example from the Uplink game mode, which first selects a group of laptops defined by the mission designer/mapper as being associated with a particular insertion point name (due to their proximity), and then selects a random laptop from that group:
local InsertionPointName =
gamemode.GetInsertionPointName(self.DefenderInsertionPoints[self.DefenderIndex])
local PossibleLaptops = self.GroupedLaptops[InsertionPointName]
self.RandomLaptop = PossibleLaptops[umath.random(#PossibleLaptops)]
See section 2.9 above for an overview of the different watch modes.
The tactical watch is set to the mode WatchMode (currently one of: Time
, ObjectiveFinder
and IntelRetrieval
). If DisplayBearing is true, a bearing is displayed to the current objective location (if set). If DisplayDistance is true, an approximate distance is displayed to the current objective location (if set). If DisplayUpDown is true, an indicator is given if the current objective location (if set) is above or below the player.
If Measure2D is set, the displayed distance is calculated only in a horizontal direction and ignores height differences.
Typically this function is called only once when a game mode initialises.
Here is an example of setting up a watch mode for the Fox Hunt variant of DTAS:
gamemode.SetWatchMode( "ObjectiveFinder", not self.FoxDisableBearing, true, false, true )
-- watch mode, show bearing, show distance, display up/down, measure 2D distance
gamemode.SetCaptureZone( 0, 0, 0, false )
-- no alerts please
In this game mode, an approximate distance to the asset (“Fox”) is shown, but no indication of whether the asset is higher or lower than the player, and no compass bearing is shown. The watch will have a blank display until the game mode starts providing intermittent asset locations using gamemode.SetObjectiveLocation() (see below at 6.2.32).
See section 2.9 above for an overview of the different watch modes. This function clears the current objective location (if appropriate) and clears any current alert level.
This function sets the properties of the capture zone which is used by tactical watch to display in-range alerts. A CaptureRadius of 0 will disable any in-range alerts. If ZoneIsSpherical is true, an in-range event will be generated if the player is within CaptureRadius of the centre of the zone as defined by the current objective location (see below). Otherwise, a cylindrical zone is defined of horizontal radius CaptureRadius and height CaptureHeight from top to bottom, centred around the current objective location.
Any players having a Team Id equal to DefenderTeamId will get a green alert when in-range. Otherwise amber alerts are generated for in-range events. Setting DefenderTeamId to an invalid team number will prevent anyone getting a green alert.
Here is an example from the Intel Retrieval mode of setting up a watch mode and capture zone to register a proximity alert when within LaptopProximityAlertRadius
metres from the laptop (currently 5m):
if self.Settings.ProximityAlert.Value == 1 and self.RandomLaptopIndex ~= nil then
gamemode.SetWatchMode( "IntelRetrieval", false, false, false, false )
gamemode.ResetWatch()
gamemode.SetCaptureZone( self.LaptopProximityAlertRadius, 0, 255, true )
-- cap radius, cap height, team ID, spherical zone (ignore height)
local NewLaptopLocation = actor.GetLocation( self.Laptops[self.RandomLaptopIndex] )
gamemode.SetObjectiveLocation( NewLaptopLocation )
end
ObjectiveLocation is a vector (a Lua table type containing fields x
, y
and z
) which defines the location of an arbitrary thing (such as a static object, or a player, in which case repeated calls to SetObjectiveLocation will be required). This location is used by the tactical watch to determine ranges, bearings, and whether in-range alerts should be generated.
Any updates to the objective location should be done sparingly (typically on a generous timer) so as not to generate too much network traffic keeping player watches updated.
IsCapturing lets the tactical watch know whether any players who are in-range of the objective location (as set with gamemode.SetObjectiveLocation() above) should be given a Capturing alert (yes if true
, no if false
).
This function sets up scoring for teams, based on the supplied table. See section 2.8 for an illustration of the team score table format (see also the DTAS game mode). This must be done when a game mode initialises, and before any team scores are awarded. A game mode can have team scores without player scores, or vice versa. Appropriate tabs on the After Action Report will be displayed only if relevant scores exist.
This function sets up scoring for players, based on the supplied table. See section 2.7 for an illustration of the player score table format (see also the DTAS game mode). This must be done when a game mode initialises, and before any player scores are awarded.
This function sets all team scores to zero (but does not erase the score structure set by gamemode.SetTeamScoreTypes() ).
This function sets all player scores to zero (but does not erase the score structure set by gamemode.SetPlayerScoreTypes() ).
This function awards ScoreMultiple times the score type ScoreName to the team with Id TeamId. The ScoreMultiple parameter is ignored if the OneOff
property for the specified score type is true
.
Here is a complicated example from the DTAS game mode of awarding team and player scores:
if KillerTeam ~= KilledTeam then
self:AwardPlayerScore( KillerPlayerState, "Killed" )
self:AwardTeamScore( KillerTeam, "Killed" )
-- award score to everyone in proximity of killer
local KillerTeamList = gamemode.GetPlayerListByLives(KillerTeam, 1, true)
-- list of player states
local SomeoneWasInRange = false
for _, Player in ipairs(KillerTeamList) do
if Player ~= KillerPlayerState then
if self:GetDistanceBetweenPlayers(Player, KillerPlayerState, false) <= self.ScoringKillProximity then
self:AwardPlayerScore( Player, "InRangeOfKill" )
SomeoneWasInRange = true
end
end
end
if SomeoneWasInRange then
self:AwardTeamScore( KillerTeam, "InRangeOfKill" )
end
self.LastKiller = KillerPlayerState
else
-- suicides count as TKs
self:AwardPlayerScore( KillerPlayerState, "TeamKill" )
self:AwardTeamScore( KillerTeam, "TeamKill" )
end
This function is a special case that informs the game of a change in the current game mode name. This is not normally required, but is used by DTAS to update the Ops Board when the sub-game mode switches between DTAS and Fox Hunt.
self.CurrentDTASGameMode = self:DetermineRoundType()
if self.Settings.ForceDTAS.Value == 1 then
self.CurrentDTASGameMode = 'DTAS'
print("Overriding to DTAS mode")
elseif self.Settings.ForceDTAS.Value == 2 then
self.CurrentDTASGameMode = 'FoxHunt'
print("Overriding to Fox Hunt mode")
end
-- self.CurrentDTASGameMode is now definitive
gamemode.SetGameModeName(self.CurrentDTASGameMode)
In this case the game mode name is set either to DTAS
or FoxHunt
, and the localisation file (see section 2.6 above) expands that to the proper full game mode name.
This function is used to inform the game of the role of the specified team. This is to allow the game to provide more useful descriptions of teams (to display in the After Action Report, for example).
Here is an example from the DTAS game mode, carrying out a swap of the team roles after a round is completed:
if self.CompletedARound and self.Settings.AutoSwap.Value ~= 0 then
if self.DefendingTeam == self.PlayerTeams.Blue then
self.DefendingTeam = self.PlayerTeams.Red
self.AttackingTeam = self.PlayerTeams.Blue
else
self.DefendingTeam = self.PlayerTeams.Blue
self.AttackingTeam = self.PlayerTeams.Red
end
gamemode.SetPlayerTeamRole(self.DefendingTeam.TeamId, "Defending")
gamemode.SetPlayerTeamRole(self.AttackingTeam.TeamId, "Attacking")
-- ...
This is another special case function which allows a game mode to specify that the current round is a ‘temporary’ game mode and that the match stats for the ‘normal’ game mode should not be updated as a result of the round (instead a simple win/loss is displayed on the After Action Report). This is used to allow DTAS to intersperse DTAS rounds with Fox Hunt rounds when numbers are low without affecting the match overall.
The FormatString() function provides a temporary way to create customised/dynamic text using the string table localisation. It works in lone wolf, and if client and server are in the same locale. Further refinements are needed and will be introduced in later versions, to ensure correct localisation always at the client.
The FormatTable
table must include a FormatString
field, which is usually a token that will be looked up in the relevant localisation string table (the .csv file), in the form “format_<FormatString>”.
For example, the “XKilledY” format string is defined in the Deathmatch.csv string table:
"format_XKilledY","{killername} killed {killedname}",formatted string
The XKilledY
token is then referenced in the DeathMatch.lua game mode script, when reporting one player killing another:
local FormatTable = {}
FormatTable.FormatString = "XKilledY"
FormatTable.killername = KillerName
FormatTable.killedname = KilledName
local KilledText = gamemode.FormatString(FormatTable)
gamemode.BroadcastGameMessage(KilledText, "Lower", 3.0)
This function adds the number
type Count of bots to the play area (with total number of bots currently capped at 16) and places them in team TeamId. By default, bots are friendly towards members of their own team, and hostile towards others.
The available bots are defined within the mission editor.
Note the distinction between AI players (using AI spawns and not having player states associated with them) and Bots (essentially computer-controlled replicas of player pawns, which show up in player team lists and so on).
This function removes the number
type Count of bots from team TeamId, if possible.
This function freezes all bots in team TeamId.
This function unfreezes all bots in team TeamId.
This function centralises and expands the team balancing function previously included in the DTAS game mode. Players are automatically moved from attacker team to defender team, or vice versa, to pursue the ideal team size difference IdealTeamSizeDifference with an aggression defined by BalancingAggression. For symmetric modes like Team Elimination, the ideal size difference would be 0, for example. For DTAS, the ideal size difference is 1 (that is, ideally there is one more attacker than defender). For Hostage Rescue, the ideal difference is 2 (since one attacker is taken out of the attack as a hostage), and so on.
The balancing aggression is a number
(integer) type corresponding to one of the following:
0: No team balancing (do nothing)
1: Light touch (balance very large imbalances only)
2: Aggressive (don’t allow imbalances of more than 1, but a difference of 1 is ok in either direction)
3: Always
Players and teams are sent appropriate messages if players have been moved to other teams. Localisation tokens ‘MovedToAttackTeam’, ‘MovedToDefendTeam’, ‘PlayersMovedIntoTeamN’, ‘PlayersMovedOutOfTeamN’ are used, and defined in GameMode.csv.
Player movements between teams are tracked so that the algorithm will attempt to select players to move who have not recently been moved.
This function should be called before using the BalanceTeams() function (see above) to clear the past movements table, and set a couple of parameters that the balancing algorithm will use.
The NewNumberOfMovementsToTrack
number
(integer) type specifies how many player movements between teams will be tracked, in order to avoid moving the same player in quick succession.
The NewAutoBalanceLightTouchSetting
number
(float) type specifies the maximum permitted ratio of team sizes before light touch autobalance will kick in (ish).
Default values of 6
and 0.19
are recommended for the parameters.
This call clears all volunteer status for all players. Volunteer statuses can be set by players or admins in the player roster (accessed by the Escape key) by clicking on the hand up/hand down icon. See section for an overview of the volunteering function.
This call returns a list (can be zero length) of PlayerIDs of players who have volunteered, e.g. to be flag carrier. It checks all players on team TeamId (a TeamId of 255 will check all teams). This call doesn’t care about lives or whether the player is in the play area, because it is typically called before round start. Bots can be volunteered by admins.
This is an analogue of GetPlayerListByLives() (see section 6.2.18) for volunteers.
This function returns a list of players of team TeamId (or 255 for all teams) having the specified ready status. See section 6.5.18 for a list of possible ready statuses.
This function returns the current mission tags (e.g. ‘PVP’, ‘Coop’) for the currently loaded mission, as set in the mission editor. This is currently used for validating missions in the editor. It returns a Table including items of type String, e.g. \{ “Pvp”, “Coop” }
This is an experimental function that was being used for the Defuse bomb effects, amongst other things. It will spawn the named effect class at the specified location.
This function returns a table containing the following entries relating to the current time of day:
Hours
(Integer): Real world hour (0-23). See TimeElapsed for limitations.
Minutes
(Integer): Real world minute (0-59). See TimeElapsed for limitations.
Seconds
(Integer): Real world seconds (0-59). Not very reliable.
Year
(Integer): Year (e.g. 2014, …)
Month
(Integer): Month (1-12)
Day
(Integer): Day (1-31)
Time
(Number): Real world hour (0.0 to 23.99). Take care that 5pm at the Arctic Circle in Winter looks quite different to 5pm at the Equator, for example.
InitialTime
(Number): Real world hour (0.0 to 23.99) when the mission started.
StandardTime
(Number): Standard Time hour (0.0 to 23.99…). In Standard Time, the actual time is remapped such that Sunrise is at 6am (6.0) and Sunset is at 6pm (18.0). This allows events to occur at consistent astronomical times of day without having to make adjustments for latitude and date, and so on.
TimeSpan
(String): one of { "Night", "Dawn", "Day", "Dusk" }
SunlightStrength
(Number): a floating point Number between 0 and 1 indicating relative strength of sunlight (1 = midday, 0 = no sun)
MoonlightStrength
(Number): a floating point Number between 0 and 1 indicating relative strength of moonlight, taking moon phase into account (1 = full moon illumination, 0 = no moon)
This function will (somewhat unusually) return a nil value if Sky Actor cannot be found, or if there is any other error, so you should check for a nil return value.
This function will not return reliable time of day results (except possibly for InitialTime) until play has begun, or after play has finished. You should not rely on this when players are still all in the Ready Room. |
local TODTable = gamemode.GetTimeOfDay()
if TODTable ~= nil then
print("The time is " .. TODTable.Hours .. ":" .. TODTable.Minutes .. ":" .. TODTable.Seconds .. "(" .. TODTable.TimeSpan .. ")")
print("The date is " .. TODTable.Day .. "/" .. TODTable.Month .. "/" .. TODTable.Year)
print("Real world hour = " .. TODTable.Time .. ", Standard hour = " .. TODTable.StandardTime .. ", Mission started at hour " .. TODTable.InitialTime)
print("Sun strength = " .. TODTable.SunlightStrength .. ", Moonlight strength = " .. TODTable.MoonlightStrength)
end
This library provides a wrapper around/interface to a UE4 Actor (essentially anything within the game level is an actor). It should be noted that syntactically Function(Actor, Parameter1, Parameter2, …)
is equivalent in Lua to Actor.Function(Parameter1, Parameter2, …)
Function list:
Returns a Boolean that is true if the specified actor Actor has the specified tag Tag, false otherwise.
Example:
if actor.HasTag(InsertionPoint, "Defenders") then
table.insert(self.DefenderInsertionPoints, InsertionPoint)
table.insert(DefenderInsertionPointNames,
gamemode.GetInsertionPointName(InsertionPoint))
elseif actor.HasTag(InsertionPoint, "Attackers") then
table.insert(self.AttackerInsertionPoints, InsertionPoint)
end
Adds the tag Tag to the actor Actor.
Example:
local RandomLaptopIndex = umath.random(#self.Laptops);
for i = 1, #self.Laptops do
actor.SetActive(self.Laptops[i], true)
if (i == RandomLaptopIndex) then
actor.AddTag(self.Laptops[i], self.LaptopTag)
else
actor.RemoveTag(self.Laptops[i], self.LaptopTag)
end
end
Removes the tag Tag from the actor Actor. Does nothing if tag not present. See AddTag() above for an example.
Returns a table (array) of strings corresponding to all of the gameplay tags associated with actor Actor.
Returns the Indexth gameplay tag associated with actor Actor. The return result is nil
if the index is invalid.
At this time, it appears that the mission editor starts indexing tags for some types of actors at index 0, and for other types of actors at index 1. It is hoped to fix this in due course, but for now please check how the tags are indexed for particular types of actors.
Returns the Team ID associated with the actor Actor as a number in the range 0-255. By convention, AI is assigned Team ID 100. Team ID 255 is a wildcard (that is: any query based on Team ID will match all teams if Team ID is set to 255).
Example:
function uplink:PlayerEnteredPlayArea(PlayerState)
if actor.GetTeamId(PlayerState) == self.AttackingTeamId then
Sets the team Id for actor Actor to TeamId. Actors other than players can have a Team Id associated with them, such as spawn protection volumes. In a PVP mode, both teams may switch roles (such as attacker/defender) and therefore may switch spawns. Therefore the Team Id of spawn protection volumes may need to be changed manually.
Example:
for i, SpawnProtectionVolume in ipairs(self.SpawnProtectionVolumes) do
actor.SetTeamId(SpawnProtectionVolume, self.AttackingTeamId)
actor.SetActive(SpawnProtectionVolume, true)
end
Returns a vector (a Lua table type having fields x
, y
, z
) corresponding to the specified actor Actor’s world location.
Example from the Deathmatch game mode, scoring a player start based on how close it is to current players:
for i = 1, #LivingPlayers do
local PlayerCharacter = player.GetCharacter(LivingPlayers[i])
local PlayerLocation = actor.GetLocation(PlayerCharacter)
local DistSq = vector.SizeSq(StartLocation - PlayerLocation)
if DistSq < self.TooCloseSq then
return -10.0
end
if DistSq < ClosestDistSq then
ClosestDistSq = DistSq
end
end
Returns a rotator (a Lua table type having fields yaw
, pitch
and roll)
corresponding to the specified actor Actor’s world rotation.
Sets the actor Actor to be hidden or not depending on the Boolean value Hidden. Spectators are normally hidden, for example, as well as having collision turned off. Game objects may also need to be hidden or unhidden during the course of a round.
This function enables or disables player collisions depending on the value of Enabled (for example to allow free spectate, or spectate bounded by geometry).
Returns Boolean to indicate whether the specified actor Actor is active.
GROUND BRANCH objects have an active
property which can be read via this function.
Certain objects implement certain behaviours depending on whether or not they are active.
Extraction point markers, for example, will spawn smoke (only) when active.
This function sets the active property of a GROUND BRANCH game object actor Actor to Boolean value Active.
Example:
self.ExtractionPointIndex = umath.random(#self.ExtractionPoints)
for i = 1, #self.ExtractionPoints do
local bActive = (i == self.ExtractionPointIndex)
actor.SetActive(self.ExtractionPoints[i], bActive)
actor.SetActive(self.ExtractionPointMarkers[i], bActive)
end
Returns a Boolean value indicating whether or not the first actor Actor is overlapping the second actor OtherActor.
Returns a table (array) of all actors of class Class that are overlapped by actor Actor.
local Overlaps = actor.GetOverlaps(self.ExtractionPoints[self.ExtractionPointIndex], 'GroundBranch.GBCharacter')
-- ...
for j = 1, #Overlaps do
if Overlaps[j] == LivingCharacter then
-- ...
Returns a string corresponding to the actor’s name in the level (e.g. SomeClass_3).
An example from the Terrorist Hunt validation script (TerroristHuntValidate.lua):
for _,GuardPoint in ipairs(AllGuardPoints) do
GuardPointName = ai.GetGuardPointName(GuardPoint)
if GuardPointName == 'None' then
table.insert(ErrorsFound, "AI guard point '" .. actor.GetName(GuardPoint) .. "' has group name set to None")
else
if GuardPointNames[GuardPointName] == nil then
GuardPointNames[GuardPointName] = false
GuardPointCount = GuardPointCount + 1
end
end
end
This call performs a box trace (volumetric trace) to determine if the specified actor is colliding with anything else in the level. Tests against normal sorts of things (other objects, players, and so on) but may also register collisions with odd things like UI grab handles in the editor. Experimental. Used by game mode validators.
This library provides a set of functions for controlling the GROUND BRANCH AI.
Function list:
This function deletes all AI having the AI controller tag aiControllerTag. Typically this function is called on entry to the WaitingForReady
round stage, which is typically the first round stage reached after the end of a previous round.
Example:
function intel:OnRoundStageSet(RoundStage)
if RoundStage == "WaitingForReady" then
-- ...
ai.CleanUp(self.OpForTeamTag)
This function spawns an AI character at the spawnpoint SpawnPoint with the attached AI controller tag aiControllerTag and with an initial freeze time of FreezeTime.
This function spawns a number Count of AI characters at the spawn points OrderedSpawnPoints over the course of the duration Duration seconds. The OrderedSpawnPoints table is cycled through in order, and is cycled repeatedly if the spawn count is larger than the table size.
Example:
function intel:SpawnOpFor()
local OrderedSpawns = {}
for Key, Group in ipairs(self.PriorityGroupedSpawns) do
for i = #Group, 1, -1 do
local j = umath.random(i)
Group[i], Group[j] = Group[j], Group[i]
table.insert(OrderedSpawns, Group[i])
end
end
ai.CreateOverDuration(4.0, self.OpForCount, OrderedSpawns, self.OpForTeamTag)
end
This is a variant of AI.Create which uses a virtual AI spawn point to provide the properties of the AI (such as loadout, orders, and so on) but spawns the AI at the specified location and rotation in SpawnTransform. To use this function, a number of prototype AI spawns need to be provided. The location of these spawns is unimportant.
Known issue: in v1033, this function can ignore changes made to the team ID of the actor before they spawn in.
An example from the revised Deathmatch game mode, which now spawns AI in with players if the total player count is below a preset minimum:
if TotalPlayers < self.Settings.MinimumPlayerCount.Value and self.NumAIToSpawn>0 then
local BestPlayerStart = self:GetBestSpawn()
local SpawnTransform = {}
SpawnTransform.Location = actor.GetLocation( BestPlayerStart )
SpawnTransform.Rotation = actor.GetRotation( BestPlayerStart )
local VirtualAISpawnPoint = self.AISpawnPoints[ self.NextSpawnIndex ]
self.NextSpawnIndex = self.NextSpawnIndex + 1
if self.NextSpawnIndex > #self.AISpawnPoints then
self.NextSpawnIndex = 1
end
actor.SetTeamId( VirtualAISpawnPoint, 0 )
ai.CreateWithTransform( VirtualAISpawnPoint, SpawnTransform, self.BotTag, 2.0 );
-- transform, ai tag, freeze time
self.NumAIToSpawn = self.NumAIToSpawn - 1
if self.NumAIToSpawn > 0 then
timer.Set("SpawnAI", self, self.SpawnAITimer, self.AIRespawnTime, false)
end
else
self.NumAIToSpawn = 0
end
Returns a table (array) of AI controller object pointers (of type AGBAIController*
) matching the specified AI controller class Class, AI controller tag Tag, Team Id TeamId and Squad Id SquadId.
Returns a number equal to the max AI count, which is currently set on the ops board (number of enemies, or similar). This is typically a number in the range 1 to 30.
Example:
for j, SpawnPoint in ipairs(AllSpawns) do
if actor.HasTag(SpawnPoint, PriorityTag) then
-- ...
-- Ensures we can't spawn more AI then this map can handle.
self.MaxOpforCount = self.MaxOpforCount + 1
-- ...
end
end
-- ...
self.MaxOpforCount = math.min(ai.GetMaxCount(), self.MaxOpforCount)
This function tests whether vector EndLocation is reachable from vector StartLocation via the navmesh in the level.
The function returns true
if the location is reachable, and false
otherwise.
The IsPartialOk parameter has been removed in v1034 as it is not meaningful after the update to Kythera AI middleware.
This function is used by DTAS to determine if a candidate spawn location is reachable from a regular spawn point (if not, this is suggestive that it is out of bounds of the level in some way). This function should be used sparingly as it can take a while to run a query.
This function is essentially a wrapper for the UE4 function which finds a random point on the navmesh within Radius distance (measured in cm) from the vector Origin (a Lua table type having fields x
, y
, z)
. This is used for finding locations close to team mates in Team Elimination respawn modes, for example.
This is another specialised function used by DTAS, which projects a point in space to the nearest bit of navmesh.
This function allows a specific AI controller to be killed on demand.
This function returns information about a specified AI spawn point, primarily for the purpose of validating a level.
Currently it returns a table containing two fields: SquadId
corresponding to the AI squad Id, and SquadOrders
containing a text string corresponding to the squad orders (currently one of Guard
, Patrol
and Idle
).
This function returns the name property of the specified guard point. Primarily for the purpose of validating levels.
This function returns the current squad number for the specified AIController
reference, as set in the mission editor.
This function returns the current squad orders for the specified AIController
. As of v1033, these orders should be one of Guard
, Idle
, Patrol
or Search
. Orders may also be None
or (nil
) if none are set.
When an AI is in search mode, they will move to and look around the location defined as the search target (see below). Other orders will be known from the mission editor.
This function sets the squad orders (one of Guard
, Idle
, Patrol
and Search
) for the squad SquadId in team TeamId (usually 100 for AI) for AI controllers of class Class (usually GroundBranch.GBAIController
) with the AI controller tag Tag (nil
for all/any).
This function sets the squad orders for the single AI controller identified by the aiController reference.
This function sets the current search target for the AI controller aiController to the location TargetLocation, for a search time of SearchTime seconds.
This will only take effect if the AI controller has Search
orders, for example set by AI.SetSquadOrders() and so on.
If the AI is not near the search region, it will move towards it, subject to any navigation pathing issues, and so on.
This function sets the search target for an AI squad identified by Class, Tag and SquadId.
This function returns true
if the AI spawn point referenced by aiSpawnPoint is within the AI hotspot references by aiHotspot, and false
otherwise.
Returns nil
if there is an error with the parameters.
This function returns the name of the AI hotspot referenced by aiHotspot, or nil
if not found.
Returns true
if the specified controller is an AI controller in the current play session, optionally also checking that the AI entity has the specified AI controller tag (if the tag is not nil
).
This library provides a set of functions for manipulating and getting information on the player pawns and controllers.
The Player
reference encodes an AGBCharacter class (native to GROUND BRANCH). It cannot be directly manipulated within the Lua script, but can be received and passed to the library functions.
A list of current players can be obtained for example using the gamemode.GetPlayerList()
call (see section 6.5.17 above).
Function list:
Returns the number of lives a player has (in normal GROUND BRANCH, this will be one or zero).
Sets the number of lives that a player has.
If less than one, the player is deemed to be dead.
If killing a player, it is better to call SetLives()
with a number equal to GetLives() - 1
instead of calling SetLives(0)
, in order to ensure maximum compatibility with other mods and mutators.
Example from Uplink game mode:
if gamemode.GetRoundStage() == "PreRoundWait"
or gamemode.GetRoundStage() == "InProgress"
or gamemode.GetRoundStage() == "BlueDefenderSetup"
or gamemode.GetRoundStage() == "RedDefenderSetup" then
if CharacterController ~= nil then
player.SetLives(CharacterController, player.GetLives(CharacterController) - 1)
timer.Set(self, "CheckEndRoundTimer", 1.0, false);
end
end
This function sets whether or not a player is allowed to restart a round (after leaving an active round and returning to the Ready Room).
This function returns the insertion point selected by the player, as a userdata object that can be passed to other library functions as needed (but not directly accessed within Lua).
This function sets a new insertion point for the player using the given Insertion Point object NewInsertionPoint.
This function freezes the player in place for Duration seconds.
Usually this is called just after players are spawned into the play area at the start of the PreRoundWait
game round.
Returns a GB Player State object (which cannot be directly manipulated in the Lua script but can be passed as a parameter to further calls).
Returns a reference to a character pawn corresponding to the specified player. Again, the returned reference cannot be directly manipulated but can be passed to other library functions.
Example from Intel game mode:
local LivingPlayers = gamemode.GetPlayerListByLives(self.BluForTeamId, 1, false)
for i = 1, #LivingPlayers do
local LivingCharacter = player.GetCharacter(LivingPlayers[i])
--- ...
for j = 1, #Overlaps do
if Overlaps[j] == LivingCharacter then
--- ...
end
end
--- ...
end
Returns a Boolean which is true
if the player is alive, false
if dead (or spectating).
This function causes an object to become visible for Duration seconds, with the supplied text tag (which will be looked up in the string table in the usual way) displayed on screen in the appropriate place (regardless of whether the location is visible from the player’s position). This is used in the Uplink game mode, for example, to
show the location of the laptop to the Defenders at the start of a round:
elseif actor.GetTeamId(PlayerState) == self.DefendingTeamId then
local LaptopLocation = actor.GetLocation(self.RandomLaptop)
player.ShowWorldPrompt(PlayerState, LaptopLocation, "DefendTarget", self.DefenderSetupTime - 2)
Returns a table of inventory items. The items cannot be directly manipulated within Lua but can be processed using other library functions.
Returns a Boolean which is true
if the player Player possesses an item with the gameplay tag Tag, and false
otherwise.
Example from the Intel game mode:
if player.HasItemWithTag(Character, self.LaptopTag) == true then
if self.TeamExfil then
timer.Set(self, "CheckOpForExfilTimer", 1.0, true)
else
gamemode.AddGameStat("Result=Team1")
gamemode.AddGameStat("Summary=IntelRetrieved")
gamemode.AddGameStat("CompleteObjectives=RetrieveIntel,ExfiltrateBluFor")
gamemode.SetRoundStage("PostRoundWait")
end
end
This function displays a message on the player Player’s screen, with the specified message Message, for the specified duration Duration. The message Message is looked up in the string table(s) using the format “gamemessage_”
+ Message.
For example, the message “TeamExfil” in the Intel game mode matches the following entry in the Intel.csv string table:
As used in this part of the Intel game mode script:
elseif not self.TeamExfilWarning then
self.TeamExfilWarning = true
player.ShowGameMessage(Character, "TeamExfil", 5.0)
end
See gamemode.BroadcastGameMessage (see section 6.2.9 above) for more info on the Type and Duration parameters.
This is a specialist function for giving a player a flag in DTAS.
This call prevents a user using weapons for a given reason Reason that is an identifier used to allow multiple overlapping restrictions if need be. This is used by DTAS to prevent either team shooting (but not moving/running) during the flag placement phase at the start of a round.
This call removes the restriction for reason Reason imposed by player.AddIgnoreUseInputReason().
This call removes all current use restrictions on a player.
This function returns a player’s Readied-up status (see section 2.2.1).
This function sets a player’s Readied-up status (see section 2.2.1). The possible statuses (in name/string format) are DeclaredReady
, WaitingToReadyUp
, and NotReady
.
This function returns the current player name as a string. There may be issues with special characters, if used.
This function awards the specified score type ScoreName a multiple ScoreMultiple of times (unless the relevant score type has a OneOff property of true
). Player scores must have been set up via gamemode.SetPlayerScoreTypes() first (see section 6.2.35 above).
This function zeroes all player scores but keeps the scoring structure intact.
This function returns the current value of the player stat Key.
An example from the Deathmatch game mode, which tracks how many kills a player has:
local NumberKills = player.GetPlayerStat(KillerPlayerState, "Kills")
if NumberKills ~= nil then
local ActualFragLimit = self.FragLimitValues[ (self.Settings.FragLimit.Value)+1 ]
if NumberKills >= ActualFragLimit then
timer.Clear("DisplayFrags")
self:DisplayFragsTimer()
-- update with final kill score
gamemode.AddGameStat("Result=Team" .. self.PlayerTeams.Deathmatch.TeamId)
gamemode.AddGameStat("Summary=ReachedFragLimit")
gamemode.AddGameStat("CompleteObjectives=KillEveryone")
gamemode.SetRoundStage("PostRoundWait")
player.AwardPlayerScore( KillerPlayerState, "WonRound", 1 )
end
end
(This is an internal call for now, as specific widgets need to be created for specific hints. It is planned to make a more general version of this function.)
This function returns true
if the player reference is still valid (i.e. the player is still in the session), otherwise false
. Since v1033, it is much less fatal to retain invalid references to players (they are now number
type rather than userdata
), but you may still want to clean out old references from time to time.
Returns a string
type containing the referenced player’s (usually 3 letter) callsign.
Returns true
if the supplied CallSign string
type is considered to correspond to a profanity (or thereabouts).
Returns true
if the referenced player is a bot (as opposed to AI).
An experimental feature that may be broken. Use with caution.
Returns true
if the referenced player has the specified TagName gameplay tag.
Used in Hostage Rescue currently.
The gameplay tag is not the same thing as an actor tag.
It is an internal token that can indicate various player statuses.
Returns true
if the specified player has an active volunteer status (hand up), false
otherwise.
See section 2.5.6 for an overview of volunteering.
Sets the volunteer status of the specified player to true
or false
.
This function will kill the specified player (if alive), bypassing any and all damage reduction/prevention. The call will fail if the player is in the Ready Room.
This function will damage the specified player by the specified DamageAmount of DamageType damage. This will not bypass damage reduction/prevention; for example, players who are frozen at the start of a round will take no damage from this call. Players usually start with 100 health. Damage type is not currently supported and can take any String value.
This function is experimental, and will spawn an effect of the specified effect class locally to the player. See also gamemode.SpawnEffectAtLocation() (see section 6.2.54).
This library provides a compact set of functions for setting and clearing the UE4 timer. The timer is typically used to monitor the game state intermittently (instead of continuously running code to test the game state).
Function list:
This function sets up a timer with handle InTimerHandle to call the function InFunction in table (module/class) InTable after InRate seconds have elapsed. If Boolean InLoop is true, then the timer repeats every InRate seconds. InTable is usually a reference to the game mode package/name (i.e. self).
If a timer already exists for the same handle, it will be reset with the new values and the old timer will effectively be cancelled. If you try to set a timer with the same handle but a different function… don’t.
Example from Intel Rerieval game mode:
if self.Settings.TeamExfil.Value == 1 then
timer.Set("CheckOpForExfil", self, self.CheckOpForExfilTimer, 1.0, true)
This function clears any timer associated with the handle InTimerHandle.
Example from Intel Retrieval game mode:
if bLaptopSecure then
if bExfiltrated then
timer.Clear(self, "CheckOpForExfil")
gamemode.AddGameStat("Result=Team1")
gamemode.AddGameStat("Summary=IntelRetrieved")
gamemode.AddGameStat("CompleteObjectives=RetrieveIntel,ExfiltrateBluFor")
gamemode.SetRoundStage("PostRoundWait")
This function clears all currently set timers (not just for the game mode).
Example from Intel game mode:
if RoundStage == "WaitingForReady" then
timer.ClearAll()
-- ...
end
This library provides a couple of functions to provide random numbers using the UE4 random number system.
Function list:
If Max is an integer, returns a random integer between 1 and Max.
If Max is a float, returns a random float between 1.0f and Max.
Example from Uplink game mode, which uses umath.random()
to pick a random insertion point:
if #self.DefenderInsertionPoints > 1 then
local NewDefenderIndex = self.DefenderIndex
while (NewDefenderIndex == self.DefenderIndex) do
NewDefenderIndex = umath.random(#self.DefenderInsertionPoints)
end
self.DefenderIndex = NewDefenderIndex
else
self.DefenderIndex = 1
end
If Max is an integer, returns a random integer between Min and Max.
If Max is a float, returns a random float between Min and Max.
This library provides a set of functions for handling (geometric) vectors, and implements various mathematical operators. The library implements vector addition, subtraction, multiplation and division, and tests for equality. In addition, a number of additional functions are defined. Vectors have X, Y and Z coordinates, and may represent 3D or 2D (ignores Z) vectors.
Function list:
Returns the vector size (length) in 3D, which is equivalent to the square root of X^2 + Y^2 + Z^2.
Returns the vector size squared in 3D, i.e. X^2 + Y^2 + Z^2.
Returns the vector size (length) in 2D, which is equivalent to the square root of X^2 + Y^2.
Returns the vector size squared in 2D, i.e. X^2 + Y^2.
This library provides a set of functions for handling inventory (player loadouts, custom builds, and so on). See section 4 for more information on the inventory system.
Please note: this set of functions is intended for use with players only. It may work to a degree with AI, but since AI does not have associated player states, it is unlikely.
This section of the Hostage Rescue game mode script illustrates a few relevant functions described below:
-- remove all combat items and other inappropriate things:
local ItemsToRemove = { "PrimaryFirearm", "Sidearm", "FaceWear", "Platform", "Belt", "HeadGear", "Holster", "EyeWear", "Gloves" }
inventory.RemoveItemTypesFromLoadoutTable(ItemsToRemove, Loadout, SplitItemField)
-- ItemsToRemove argument can be a single string instead of table to remove just one thing
-- set hostage pants, boots and shirt from custom kit - replace kit defaults
local ClothingKit = inventory.GetCustomKitAsTable("hostage", SplitItemField)
-- this searches game CustomKit folder in current mod then base game
if ClothingKit.Data ~= nil then
local MoreItemsToRemove = { "Pants", "Shirt", "Footwear" }
inventory.RemoveItemTypesFromLoadoutTable(MoreItemsToRemove, Loadout, SplitItemField)
inventory.AddCustomKitTableToLoadoutTable(ClothingKit, Loadout, SplitItemField)
end
-- now add flexcuffs:
local Cuffs = { ItemType = "Equipment", ItemValue = "BP_Restraints_FlexCuffs" }
table.insert(Loadout.Data, Cuffs)
local Sack = { ItemType = "HeadGear", ItemValue = "BP_HostageSack" }
table.insert(Loadout.Data, Sack)
inventory.CreateLoadoutFromTable(PlayerState, "Hostage", Loadout, SplitItemField)
Function list:
This function retrieves the previously stored/set up player loadout in lua table
format, with sub-tables where appropriate corresponding to child
items.
Sub-tables are stored at indices 1, 2, 3, and so on in the usual way.
So ReturnedLoadoutTable[1]
corresponds to the first Child item, and so on.
See the functions in the InventoryManagement mutator for examples of usage.
If SplitItemField
is true
, then Item
fields are split into ItemType
and ItemValue
fields in the lua table.
For example, if an Item field in JSON is (for example):
"Item": "Magazine:BP_PMAG556_Magazine"
then the function will return table entries [ItemType]
= "Magazine"
and [ItemValue]
= "BP_PMAG556_Magazine"
. This simplifies matters, because lua does not have a straightforward inbuilt process for splitting strings at a particular character.
After a loadout has been manipulated in lua form, it can be converted back to JSON and re-imported into the game using the CreateLoadoutFromTable() call (see section 6.9.6 below).
The function attempts to load the file with name KitFileName from the GroundBranch/CustomKit folder, and returns the result as a lua table encoding the stored kit, in the same format as the returned table in GetPlayerLoadoutAsTable (section 6.9.1 above). If SplitItemField
is true, then Item
fields will be returned as separate ItemType
and ItemValue
fields, as above.
Custom kit types should be monolithic kit lists with any references to custom item builds replaced with the constituent parts expanded out.
The purpose of custom kits is to allow the loading in of loadout fragments or whole loadouts, to simplify modifications of player inventories. For example, in the Hostage Rescue mode, a hostage loadout is loaded in and merged with other aspects of a player loadout. In practice, this applies player head types and patches and so on to the stored hostage kit.
This function returns as string
type the display name for the referenced asset. ItemType is the asset type, corresponding to the ItemType field returned by GetPlayerLoadoutAsTable() and the like (e.g. “PrimaryFirearm”, “Sidearm”, “Scope”, etc). The ItemAssetPath field is misleading named, as it is the asset file name (not full path), which is usually the internal name of the relevant blueprint (i.e. “BP_PMAG556_Magazine”). So a call with ItemType of “`+Magazine+” and ItemAssetPath of “
+BP_PMAG556_Magazine+” would return the string “
+PMAG 30rd [5.56 NATO]+`”. The display names are defined internally and cannot be modified externally.
This function is essentially a shortcut to remove particular item types from a lua table version of a loadout. NewItemTypesToRemove may be a table of strings, and each of those strings corresponds to an item type that is removed. NewItemTypesToRemove may alternatively be a string
type, and that single item type is then removed.
The value for SplitItemField should be the same as that used to generate the table in the first place.
This function takes a custom kit list, such as would be returned by GetCustomKitAsTable() (see section 6.9.2 above), and merges it with the lua loadout table referenced by LoadoutTable. If an item type in the custom kit list already exists in the loadout, the custom kit item will be added rather than replace the loadout version. This will probably break most things.
This function carries out the reverse process of GetPlayerLoadoutAsTable() (see section 6.9.1 above) and creates a JSON version of the kit list in the LoadoutTable lua table, and then stores that as a new or amended loadout for the player Player, using loadout name LoadoutName. The value of SplitItemField should be the same as that used originally to generate the lua table.
The default loadout names are “NoTeam” for PvE, and “Blue” and “Red” for PvP. The Hostage Rescue game mode creates a new loadout with name “Hostage”, for example, to hold the hostage version of the loadout for all players.
This function returns true
if a loadout by the name LoadoutName exists already for the player Player, and false
otherwise.
The next two functions operate on Loadout Reference Objects which are USERDATA references that are unique parameters for the OnPreLoadoutChanged() function described in section 5.4.6 above. In essence, these objects are edited on the C++ side rather than converted into tables and edited on the lua side (to avoid lots of inventory conversions that are rarely needed):
The present function clears out any items of type ItemType from the selected loadout. This function is used by the WeaponRestriction mutator, for example, to remove primaries or secondaries from loadouts (i.e. for ‘pistols only’ rounds).
This function cycles through the loadout referenced by LoadoutReferenceObject (as passed to OnPreLoadoutChanged() ) and removes as many items as necessary to comply with the specified item count.
If a parameter is set to 0 or above, that is the limit for those items (0
to completely remove). If a parameter is set to -1
, the items are left unchanged.
So, for example, calling the function with a FragsLimit of 3 and all other parameters set to -1 will remove any grenades beyond the 3rd from the loadout, and leave all other items unchanged.
This is used by the WeaponRestriction mutator to limit supplies.
Where Bomb
is a reference to a Bomb Mission Actor (which can be dropped into the level in the mission editor), the bomb Lua component can be accessed with the GetLuaComp() function, and the following functionality is provided:
Set bomb team
GetLuaComp(Bomb).SetTeam(TeamId)
Set bomb detonation time:
GetLuaComp(Bomb).SetDetonationTime(DetonationTime)
Explode bomb:
GetLuaComp(Bomb).Explode()
Defuse bomb:
GetLuaComp(Bomb).SetDefused(true)
This is covered also in the accompanying GROUND BRANCH Mission Editor guide, but the mission editor adds certain standard actor tags to various things.
Every mission actor has the tag MissionActor
to identify it as a mission item.
In addition, AI spawn points will always have a tag corresponding to the selected spawn priority, which is selected from the group of tags: AISpawn_1
, AISpawn_2
, AISpawn_3
, AISpawn_4
, AISpawn_5
, AISpawn_6_10
, AISpawn_11_20
, AISpawn_21_30
, AISpawn_31_40
, and AISpawn_41_50
.
These are most easily manipulated using the priority drop-down box in the mission editor. It is highly recommended not to modify or delete either of these sets of tags.
Please consult the mission editor guide for guidance on how to set up AI priorities. Game modes do not have to attribute any significance to particular priorities (and can ignore them entirely if desired) but see the Terrorist Hunt game mode, for example, for a rather complex system of spawning AI using the priorities while ensuring as much randomisation as possible (within plausible limits).