Lua reference for manipulating characters

In this forum you will find and post information regarding the modding of Star Wars Battlefront 2. DO NOT POST MOD IDEAS/REQUESTS.

Moderator: Moderators

Post Reply
Posts: 140
Joined: Thu Jan 24, 2019 11:02 pm
Projects :: No Mod project currently
Games I'm Playing :: None
xbox live or psn: No gamertag set

Lua reference for manipulating characters

Post by Sporadia »

Explanation of variables

Each team has a unique team number.

The number of players on a given team.

I've made up the name for this one because I don't know what the stock missions call it. Each player on a team (human or ai) has a different number starting at 0. That number is teamMem. Say for example team 1 has 15 players and team 2 has 12 players, then team 1 will have team members 0 - 14 and team 2 will have team members 0 - 11. (The numbers overlap.) Note: The largest teamMem on a team is always 1 less than teamSize.

This is a unique number given to every player (human or ai). This also starts at 0. Eg players on team 1 could be numbered 0 - 14, then players on team 2 could be numbered 15 - 26, and players on team 3 could be numbered 27 - 30 and so on. Notice the numbers don't overlap for separate teams, which is what makes charIdx different from teamMem.

This tells you the order that the character's class was added to the character's team. (The order of the AddUnitClass calls). The numbers start at 0, and the numbers do overlap for separate teams. Here's an example:

Code: Select all

AddUnitClass(REP, "rep_inf_ep3_rifleman", 0, 4)
AddUnitClass(REP, "rep_inf_ep3_rocketeer", 0, 4)
AddUnitClass(REP, "rep_inf_ep3_sniper", 0, 4)

AddUnitClass(CIS, "cis_inf_rifleman", 0, 4)
In this case, "rep_inf_ep3_rifleman" is team REP and has classNum 0. "rep_inf_ep3_rocketeer" is team REP and has classNum 1. "rep_inf_ep3_sniper" is team REP and has classNum 2. But then for a different team: "cis_inf_rifleman" is team CIS and has classNum 0 again. If you have a SetHeroClass at the end, that will count too. So in the example above, the hero for team REP would have classNum 3 and the hero for team CIS would have classNum 1 (because it keeps counting after the last AddUnitClass). For most stock missions the hero would be classNum 6. Also be aware that the SetUpTeams function runs the AddUnitClass commands in a specific order: soldier, assault, sniper, engineer, officer, special. Often the sniper and engineer are written the wrong way around in the stock mission lua.

The name of the character's class as a string. eg "rep_hero_kiyadi". You could also call it className. I'm undecided on which I prefer.

This is an object handled by the game engine. When a player is alive, they have a charUnit. That charUnit must have some information about what class a specific character is and where they are etc. If you print charUnit, it will print userdata: then a hexadecimal number. The type userdata lets you know that charUnit is an object written in a different programming language to lua (I think C). Then the hexadecimal number is the memory address for a specific instance of that object. You can't do much with this but you can pass it into functions.

This is an object handled by the game engine. This object is a bit like a set of co-ordinates that the game engine can read but you can't. Like before, you can't directly do much with this but you can pass it into functions.

Another object handled by the game engine. This time it's a vehicle. Whenever I write a variable called object or a variable which ends with Obj, assume it's the same as charUnit.

An incomplete list of useful functions with their arguments

(last updated: 14/03/23)
These are the functions I use a lot. I will add more when I think of more. All of these functions work in ScriptPostLoad().

Code: Select all

GetTeamSize(team) -- returns teamSize
GetTeamMember(team, teamMem) -- returns charIdx (yes it looks backwards with this naming scheme)
GetCharacterTeam(charIdx) -- returns team
GetCharacterClass(charIdx) -- returns classNum
GetCharacterUnit(charIdx) -- returns charUnit if the player is alive, returns nil if the player is dead
IsCharacterHuman(charIdx) -- returns bool

GetWorldPosition(entityMatrix) -- returns x, y and z as numbers
CreateMatrix(rot1, rot2, rot3, rot4, x, y, z, anotherMatrix) -- returns a new entityMatrix
GetPathPoint(string) -- returns a new entityMatrix
GetEntityMatrix(charUnit) -- returns an entityMatrix in the same position as charUnit

SetClassProperty(classStr, ...) -- only affects characters before they spawn, other arguments are ODF parameters (as strings or numbers)
SetProperty(charUnit, ...) -- only affects spawned characters, other arguments are ODF parameters (as strings or numbers)
GetObjectHealth(charUnit) -- returns curHealth, maxHealth, addHealth as numbers
GetObjectShield(charUnit) -- returns curShield, maxShield, addShield as numbers

SelectCharacterClass(charIdx, classNum)
SpawnCharacter(charIdx, entityMatrix)
KillObject(charUnit) -- it will take other game objects too, but this example is for killing characters. It does trigger OnCharacterDeath
AllowAISpawn(team, bool) -- affects SpawnCharacter as well as the automatic AI spawns
GetCharacterVehicle(charIdx) -- returns vehObj

SelectCharacterTeam(charIdx, newTeam) -- changes a player's team before they spawn, crashes if the charIdx was alive
BatchChangeTeams(oldTeam, newTeam, num) -- moves a number of players from oldTeam to newTeam. Crashes if any of oldTeam were alive
SetObjectTeam(charUnit, newTeam) -- affects spawned ai players. Weirdly, their charIdx is technically still on the old team. Don't know what happens to humans

An incomplete list of useful events with their arguments

(last updated 14/03/23)
Note: Verified filters means I've successfully used these filters before, not that they're the only filters which exist.

Code: Select all

OnCharacterDeath(function(deadIdx, killerIdx) end) -- verified filters: Team (the team of deadIdx)
OnCharacterSpawn(function(charIdx) end) -- verified filters: Class, Team
OnFlagPickup(function(flag, charIdx) end) -- verified filters: Class (the class of the character that picked up the flag)
OnTicketCountChange(function(team, count) end) -- verified filters: Team
OnTimerElapse(function(timerName) end, "timerName")

Technique for functions which return multiple variables

GetObjectHealth is an example of this. There are two ways get all the variables from GetObjectHealth. One way is to call GetObjectHealth in a table eg.

Code: Select all

local healthTable = {}
healthTable = {GetObjectHealth(charUnit)}
If you do this then healthTable[1] will be given the unit's current health, healthTable[2] will be given the unit's max health and healthTable[3] will be given the unit's AddHealth value (the health regen).
The other technique is to put multiple variables in the = operation eg.

Code: Select all

local curHealth
local maxHealth
local addHealth
curHealth, maxHealth, addHealth = GetObjectHealth(charUnit)
This will store the current health in curHealth and the max health in maxHealth etc.

Technique for finding a charUnit

I'm putting some examples here to help people see how these functions are used.

Example 1: Find every living unit on team 1 and give them max health

Code: Select all

local teamSize = GetTeamSize(1)
for teamMem = 0, teamSize - 1 do
	local charIdx = GetTeamMember(1, teamMem)
	local charUnit = GetCharacterUnit(charIdx)
	if charUnit then
		-- character is alive
		local curHealth, maxHealth = GetObjectHealth(charUnit)
		if curHealth < maxHealth then
			SetProperty(charUnit, "CurHealth", maxHealth)
Example 2: Spawn a clone trooper on team 3 at co-ordinates (4, 5, 7) from the map's origin

Code: Select all

-- function to find the character index of a unit which has not spawned
local function FindFreeIdx(team)
	local teamSize = GetTeamSize(team)
	for teamMem = 0, teamSize - 1 do
		local charIdx = GetTeamMember(team, teamMem)
		local charUnit = GetCharacterUnit(charIdx)
		if not charUnit then
			-- this charIdx is not spawned
			return charIdx
	return  nil

local classNum = 0 -- let's assume this is a clone trooper
local spawnPt = CreateMatrix(0, 0, 0, 0, 4, 5, 7, nil)

local charIdx = FindFreeIdx(3)
if charIdx then
	SelectCharacterClass(charIdx, classNum)
	SpawnCharacter(charIdx, spawnPt)
	local charUnit = GetCharacterUnit(charIdx)
	if charUnit then
		print("Clone trooper spawned successfully")
		print("Failed to spawn clone trooper")
	print("Failed to find a free charIdx on team 3")

Technique for making a looping timer

If you scroll down enough you'll see that destroying timers is very important. Here's how I make looping timers that destroy themselves after the match:

Code: Select all

-- initialize the OnTimerElapse event
-- I'm now in the habit of always initializing my OnTimerElapse event before using the timer
SetTimerValue("timer1", 2) -- you don't need this line, I'm just being safe
StopTimer("timer1") -- you don't need this line either, I'm just being safe
		if (GetTeamSize(1) or 0) > 0 then -- my logic is that this function will always return a positive number during a game
			-- whatever code you want the timer to perform, write it here
			-- loop the timer
			SetTimerValue(timer, 15)
		else -- this runs after the match
-- end of timer initialization

-- run the looping timer
SetTimerValue("timer1", 15)

Technique for single use events

(last updated: 15/03/23)
You can stop events from running again with Release+EventName(eventObj). Do not put the event filter in the release command. It's easiest to demonstrate with an example:

Code: Select all

-- this event will only run on the first clone trooper which spawns, then it will be destroyed
local eventObj = OnCharacterSpawnClass(
		-- put your code here
		-- this eventObj must contain the OnCharacterSpawnClass, see the first line

Co-ordinates and entity matrices

I haven't used entity matrices much so I will add more when I understand more.

CreateMatrix(rot1, rot2, rot3, rot4, x, y, z, otherMatrix) -- returns entityMatrix
I haven't properly looked at what the 4 rotation values actually do. I know there are other posts on GT explaining them. This is a weird function because you can pass in an entityMatrix at the end (I've called it otherMatrix). Then CreateMatrix will return a new entityMatrix where the position and rotation of that new entityMatrix were all set relative to the position and rotation of otherMatrix. (Imagine it applying the position and rotation values as transformations on otherMatrix, then returning the result). Alternatively, you can pass in nil instead. Then it will create an entityMatrix where the position and rotation values are all relative to the map's origin (by origin, I mean the co-ordinate x = 0, y = 0, z = 0). (I only know otherMatrix = nil works after seeing MileHighGuy do it.)

GetPathPoint(string) -- returns entityMatrix
If you're making a map, then you can place a path point in ZEditor, set it's direction and give it a name, eg "HeroSpawnPt". If you pass that same name into this function, eg GetPathPoint("HeroSpawnPt"), the function will return an entityMatrix with all the same position and rotation values as the path point you placed in ZEditor. (I haven't done any map making myself, but I've seen this in the stock missions.)

GetEntityMatrix(object) -- returns entityMatrix
I only ever use charUnit with this but I think it can take all kinds of stuff. This returns an entityMatrix with the same position and rotation values as the object (ie in the same place).

GetWorldPosition(entityMatrix) -- returns number * 3
This function returns the x, y and z co-ordinates of an entityMatrix relative to the map's origin. If you pass in charUnit, it will do the same for a living character.

SetEntityMatrix(object, entityMatrix)
I haven't put this in the functions list because I'm not 100% sure how it works, and I haven't ever needed it. I think it applies the entityMatrix to the object so the object moves to a new position. Try it with charUnit. I've seen people use it for teleports, idk.


(last updated 20/03/23)
  1. If you don't destroy all your timers, they can eventually crash an instant action playlist. From looking at the objective scripts, I'm starting to think the problem may actually be that you need to stop all your timers, not destroy them. Because, those objective scripts get away with a lot and they never crash.
  2. The OnTimerElapse event will persist after a timer has been destroyed. In other words you can create and destroy the same timer multiple times, but you should run the OnTimerElapse code only once. It's more like you're initializing the event than actually running it. If you initialize the event twice, the event will trigger twice. You can however remove the OnTimerElapse code like any other event using ReleaseTimerElapse.
  3. If a mission lua contains EnableSPScriptedHeroes(), then that function will apply to every mission in an instant action playlist afterwards, (this breaks the heroes in most of the conquest or ctf missions after you play a campaign mission)
  4. I don't think OnFlagCapture is a real event even though it's in the documentation
  5. OnCharacterDeath has no Class filter
  6. AddMapClassMarker can sometimes persist after you restart a mission (In the debug game. I haven't checked this in the base game.)
Last edited by Sporadia on Mon Mar 20, 2023 8:20 am, edited 14 times in total.
Posts: 1182
Joined: Fri Dec 19, 2008 7:58 pm

Re: Lua reference for manipulating characters

Post by MileHighGuy »

Excellent post! Would love to help if you are working on some scripting project
Posts: 140
Joined: Thu Jan 24, 2019 11:02 pm
Projects :: No Mod project currently
Games I'm Playing :: None
xbox live or psn: No gamertag set

Re: Lua reference for manipulating characters

Post by Sporadia »

MileHighGuy wrote:
Tue Mar 14, 2023 12:56 am
Excellent post! Would love to help if you are working on some scripting project
I'm not working on a project really, I'm just tinkering with my own mod of the game as usual. That being said I do actually have a lua file that I'm going to upload as an asset soon. I've put together a file that contains a couple of functions that I find useful, for use in mission lua. The whole file is very specific to what I've just been doing and most of the file is OOP that I use for spawning AI heroes/monsters. But I want to put some example missions together before I upload it in it's own thread because one section in particular is total spaghetti when you look at how I've implemented it, but it's really powerful and easy to use in the mission lua. Then at the beginning of the file there are a few small general use functions which are easier to follow. You can look at it now if you want to see it without the explanation of how it works: Dropbox
Add it to your mission.req under "script". Add it to your mission lua with ScriptCB_DoFile("Utility"). Then throw a mission together with this:

Code: Select all

local heroes = BotTeam:new{
	team = 3,
	teamName = "heroes",
	teamIcon = "rep_icon",
	aiGoal = "Conquest"
local villains = BotTeam:new{
	team = 4,
	teamName = "villains",
	teamIcon = "cis_icon"
	aiGoal = "Conquest"

local kenobi = heroes:addBot("rep_hero_obiwan")
local maul = villains:addBot("cis_hero_darthmaul")

function ScriptPostLoad()
	-- ...
	-- I took these co-ordinates myself for naboo
	-- To find your own spawn points, put the CreateWantPos() function in ScriptPostLoad
	-- Temporarily add your heroes as playable characters
	-- Then find suitable spawn points in game, and open the Fake Console, then open the Code Console and run TriggerWantPos()
	-- Then search the debug log for LKHR
	kenobi.spawnPt = EasyMat(104.55433654785, 19, -35.953212738037)
	maul.spawnPt = EasyMat(-62.232501983643, 9.0098075866699, -49.870269775391)
	kenobi:setClassProperty("AddHealth", 100)
	-- other options are "Always" and "Never"
	-- you can change these settings during the game and as far as I'm aware I've got them transitioning correctly
	-- this is the powerful spaghetti code I mentioned
	maul:autoRespawn("Timer", 15)
			maul:setLiveProperty("AddHealth", 100)
	kenobi:arrow(true, REP)
	kenobi:arrow(true, CIS)
	maul:arrow(true, REP)
	maul:arrow(true, CIS)

function ScriptInit()
	-- ...
	-- this is instead of the AddTeamAsFriend and AddTeamAsEnemy commands
	GroupTeams({REP, CIS}, {REP,}, {CIS,})
	-- ...
Post Reply