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
Sporadia
Corporal
Corporal
Posts: 151
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


team
Each team has a unique team number.

teamSize
The number of players on a given team.

teamMem
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.

charIdx
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.

classNum
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.

classStr
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.

charUnit
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.

entityMatrix
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.

vehObj
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
ExitVehicle(charIdx)

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)
		end
	end
end
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
		end
	end
	return  nil
end

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")
	else
		print("Failed to spawn clone trooper")
	end
else
	print("Failed to find a free charIdx on team 3")
end

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
CreateTimer("timer1")
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
OnTimerElapse(
	function(timer)
		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)
			StartTimer(timer)
			
		else -- this runs after the match
			DestroyTimer(timer)
		end
	end,
	"timer1"
)
DestroyTimer("timer1")
-- end of timer initialization

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

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(
	function(charIdx)
		
		-- put your code here
		
		-- this eventObj must contain the OnCharacterSpawnClass, see the first line
		ReleaseCharacterSpawn(eventObj)
	end,
	"rep_inf_ep3_rifleman"
)

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.


Oddities

(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.
MileHighGuy
Jedi
Jedi
Posts: 1194
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
Sporadia
Corporal
Corporal
Posts: 151
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.

EDIT: This is deprecated code. If anyone finds this and wants to use it, I recommend they just wait a while instead, and I'll add the updated version when it's done.
Hidden/Spoiler:
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:

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
kenobi:autoRespawn("Always")
maul:spawnOnce()
maul:autoRespawn("Timer", 15)

OnCharacterSpawnClass(
function(charIdx)
maul:setLiveProperty("AddHealth", 100)
end,
maul.name
)

kenobi:arrow(true, REP)
kenobi:arrow(true, CIS)
maul:arrow(true, REP)
maul:arrow(true, CIS)
end

function ScriptInit()
-- ...
heroes:init(kenobi)
villains:init(maul)
-- this is instead of the AddTeamAsFriend and AddTeamAsEnemy commands
GroupTeams({REP, CIS}, {REP, heroes.team}, {CIS, villains.team})
-- ...
end
Last edited by Sporadia on Wed Feb 28, 2024 4:30 am, edited 1 time in total.
iuxeayor
Posts: 4
Joined: Tue Apr 25, 2023 4:06 pm
Projects :: No Mod project currently.
Games I'm Playing :: Battlefront original
xbox live or psn: No gamertag set

Re: Lua reference for manipulating characters

Post by iuxeayor »

This is awesome! I'm glad you posted this and I hope you keep adding to it. Out of curiosity, how did you find a lot of those functions? I didn't notice them in the documentation.
MileHighGuy
Jedi
Jedi
Posts: 1194
Joined: Fri Dec 19, 2008 7:58 pm

Re: Lua reference for manipulating characters

Post by MileHighGuy »

I think this would be a good thread to post some functions I've made recently.

Some functions dealing with the angle of objects and characters.

Code: Select all

    -- get object rotation with respect to world origin
    -- 0 to 360 degrees
    function getObjWorldRotation(object)
        -- y is height here
        local x, y, z = GetWorldPosition(object)
        local phi = math.atan2(z, x)
        if phi < 0 then
            phi = phi + (2 * math.pi)
        end
        local degrees = phi * (180/math.pi)
        return degrees
    end

    function getAngleBetween(object1, object2)
        -- y is height here
        local x1, y1, z1 = GetWorldPosition(object1)
        local x2, y2, z2 = GetWorldPosition(object2)
        local zDiff = z1 - z2
        local xDiff = x1 - x2
        local phi = math.atan2(zDiff, xDiff)
        if phi < 0 then
            phi = phi + (2 * math.pi)
        end
        local degrees = phi * (180/math.pi)
        return degrees
    end

    -- which angle the object is facing
    function getFacingAngle(object)
        --if you want to use this for a unit it must first be in GetCharacterUnit(object)
        
        -- y is height here
        local x1, y1, z1 = GetWorldPosition(object)
        local objLocation = GetEntityMatrix(object)
        -- move object a little bit temporarily so we can calc some rotations
        -- this creates a vector
        SetEntityMatrix(object, CreateMatrix(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01, objLocation))
        local x2, y2, z2 = GetWorldPosition(object)
        -- move it back to original position
        SetEntityMatrix(object, objLocation)

        --calculate angle (y-axis rotation)
        local xDiff = x2 - x1
        local zDiff = z2 - z1
        local phi = math.atan2(zDiff, xDiff)
        if phi < 0 then
            phi = phi + (2 * math.pi)
        end
        -- phi is in radians
        -- note that the CreateMatrix and SetEntityMatrix use radians, not degrees
        local degrees = phi * (180/math.pi)
        -- return angle in degrees
        return degrees
    end
    
        function getHeightAngleBetween(object1, object2)
        -- y is height here
        local x1, y1, z1 = GetWorldPosition(object1)
        local x2, y2, z2 = GetWorldPosition(object2)
        local zDiff = z1 - z2
        local yDiff = y1 - y2
        local xDiff = x1 - x2
        local hypotenuse = math.sqrt(xDiff^2 + zDiff^2)
        local phi = math.atan2(yDiff, hypotenuse)
        -- it might make more sense not to correct the angle for this case. up to you
        if phi < 0 then
            phi = phi + (2 * math.pi)
        end
        local degrees = phi * (180/math.pi)
        return degrees
    end
    
A repeating timer where you can add functions as its running. Useful if you are running up against the timer limit (which I think is like 60 or something? I don't know). You can add to a list of functions called by a repeating timer. Below is just one example function it uses (it prints even numbers, activated when you dispense health). Instead of instantiating a bunch of different timers you can use this as a sort of "Game Loop Timer". You can adjust the speed of the timer to be 60 fps and use modulo to call some functions once every 60 seconds and one 60 times per second for example

Code: Select all

    local timeInterval = 1 -- one second
    local repeatTimer = CreateTimer("repeatTimer")
    local count = 0
    SetTimerValue(repeatTimer, timeInterval) -- in seconds

    local functionList = {}
    local repeatTimerFunc = nil
    repeatTimerFunc = OnTimerElapse(function(timer)
        count = count + 1
        print("timer hit " .. count)

        for functionString, func in functionList do
            --execute the function
            functionList[functionString]()
        end

        SetTimerValue(timer, timeInterval)
        StartTimer(timer)
    end
    , repeatTimer)

    StartTimer(repeatTimer)

    local tempCount = nil
    local funcTickDuration = 10

    local evenFunction = function()
        print("my NEW function")
        if ( math.mod(count, 2) == 0) then
            print("count is even " .. tostring(count))
        end
        
        if tempCount == nil then
            tempCount = count
        end
        -- only last for 10 ticks
        if count >= tempCount + funcTickDuration then
            print("removing the function")
            functionList["evenFunction"] = nil
            tempCount = nil
        end

    end

    local onDispense = OnCharacterDispensePowerup(
            function(player, powerup)
                --print("dispensed something " .. tostring(powerup))
                if IsCharacterHuman(player) and
                        GetEntityClass(powerup) == FindEntityClass("com_item_powerup_dual") then

                    -- insert the function into the list of functions that the timer executes every tick
                    functionList["evenFunction"] = evenFunction

                end

            end)
Post Reply