Skip to content

Developer Guide

thebigsleepjoe edited this page Jan 1, 2024 · 8 revisions

This is a long guide for developers to create extensions or to contribute to the source code. I recommend you review the built-in index on the right side and select the section you are interested in.

If you are making an extension to this mod, please consider putting in a PR. I will approve most requests as long as a.) the code is well-written and b.) you explain thoroughly what the PR is for. That said, you don't have to be a master to submit one!!!

I assume you have a basic understanding of GLua or can use Google effectively. This guide will not cover fundamental Lua or GLua concepts.

The Basics: What does what?

This is a quick reference for what I mean by various terms referenced in the code and this guide.

Components

A component is an object attached to a bot with a few main functions: :Think, :Initialize and :New. Every tick (typically 5/sec), every bot's component's :Think function is called on itself. This allows for neat management of data. There is currently no officially supported way of creating this in a separate extension, but you probably won't ever need to.

Some built-in components are:

  • Locomotor - An essential component that controls bot movement and all direct interaction with the game world.
  • ObstacleTracker - A component that tracks the locations of breakable and non-breakable obstacles in the level.
  • Chatter - A component that manages chat messages sent through bots. The :On function is handy.
  • Inventory - A component used in managing the bot's inventory, mostly automatically.
  • Memory - Used to allow bots to remember where they saw/heard something. While only sometimes used, this is still crucial.
  • Morality - Dictates when a bot should fight back against someone else. It is indispensable for innocent roles.
  • Personality - Gives the bots a unique "flavor." It impacts a large variety of things that I won't bother to list comprehensively here.

Behaviors

A behavior is a node of the behavior tree with several key functions. If you are familiar with behavior trees, know that these are a little special and are all designed to be interruptible by default. This can be disabled explicitly, but I wouldn't recommend it.

local Bhvr = {}

-- Return true/false if we can start working on this node (or continue if interruptible)
 function Bhvr.Validate(bot) end
-- Return a STATUS enum for our next state
 function Bhvr.OnStart(bot) end
-- Return a STATUS enum for our next state
 function Bhvr.OnRunning(bot) end
-- This is mostly unused in the base code, but you may find it useful. Only called when SUCCESS is last returned.
 function Bhvr.OnSuccess(bot) end
-- This is mostly unused in the base code, but you may find it useful. Only called when FAILURE Is last returned.
 function Bhvr.OnFailure(bot) end
-- This is called every time the behavior exits (e.g., either on FAILURE or SUCCESS)
 function Bhvr.OnEnd(bot) end

You may wonder, "What is a 'status?' Running? What does that mean?" Here is how it's defined in the code:

local STATUS = {
    RUNNING = 1, -- We can call OnRunning the next tick
    SUCCESS = 2, -- We are done and can call OnSuccess and OnEnd
    FAILURE = 3, -- We failed and can call OnFailed and OnEnd
}

In other words, if you return RUNNING, the bot will continue performing this behavior for the next tick until you return anything else, or your Validate check fails. If you return SUCCESS, the tree halts after running your code. On FAILURE (or a failed validation), the tree continues past your node as if you could not validate.

Behavior tree

This is a fundamental data structure that is a table of Behaviors. Here is an example as defined in the base code:

innocent = {
        b.ClearBreakables,
        b.AttackTarget,
        b.Defuse,
        b.InvestigateCorpse,
        b.FindWeapon,
        b.InvestigateNoise,
        b.UseHealthStation,
        b.Follow,
        b.Wander,
    }

A behavior tree will call the nodes from top-to-bottom until it reaches a RUNNING or SUCCESS return code.

Language / Translations

To put it briefly, translations are a pain in the ass. I've tried to make the process as simple as possible for everyone's sanity. There are two important distinctions between various texts in the mod. "Localized chats" are different in this mod to "localized strings."

Localized Chats

A "localized chat" is a string that supports variability for the same event and has built-in support with the Chatter system. This means the mod can randomly select between a range of strings, most commonly used for bots to type in chat. This uses a different system than localized strings, using substitution like {{variable}}. Below is an example of what I mean by this. I use this system as it is far more flexible, considering the dynamic nature of this kind of text. You do not have to include every possible variable in each string.

RegisterCategory(f("Plan.%s", ATTACKANY), P.CRITICAL) -- When a traitor bot is going to attack a player/bot.
Line("I'm going to attack {{player}}.", A.Default)
Line("I've got {{player}}.", A.Default)
Line("I'll take {{player}}.", A.Default)
-- and so on

Please note that Line and RegisterCateogry are locally defined functions I won't show here for brevity. It would be best to look at the proper implementation of these functions (and what they are calling) by looking at the code. It's not crazy complex.

You will notice RegisterCategory -- you must register each event type before you may utilize it in the code. You must also register each chat line to a particular Archetype, a personality category assigned to each bot based on its traits. A bot of the Casual archetype will utilize any casual lines, if any exist, before using any default lines.

Truthfully, the best way to learn how to do this for yourself is to look at how I did it in sh_chats.lua. Same with Localized Strings.

Localized Strings

The other type, "localized strings," are non-variable bits of text that only support lua text substitution (i.e., %s or %d). They are typically used for GUI and various warnings and print statements. These are far simpler than chats. Below is a simple definition of the no.kickname localized string for the French localization:

local function loc(id, content)
    local lang = "fr"
    TTTBots.Locale.AddLocalizedString(id, content, lang)
end

loc("no.kickname", "Vous devez fournir un nom de bot à expulser.")

Unlike localized chat messages, you do not need to register categories for localized strings.

Custom Behaviors

TODO: write this

Custom Behavior Trees

TODO: write this

Custom Role Support

Creating your custom role support can range from extremely easy to exceptionally difficult, depending on the nature of the role. Here is an example of the detective role as of 1/1/24. (Note that the code may have changed drastically since then)

local detective = TTTBots.RoleData.New("detective")
detective:SetDefusesC4(true)
detective:SetCanHaveRadar(true)
detective:SetTeam(TEAM_INNOCENT)
detective:SetBTree(TTTBots.Behaviors.DefaultTrees.detective)
TTTBots.Roles.RegisterRole(detective)

return true

^ This is the entirety of the Detective role definition. This excludes the buyable registration used to purchase health stations and such. Buyables are a topic covered later in the guide.

The default roles all return true/false at the end of the role definitions to signify that they were registered; this is just to notify server admins of what roles have been loaded successfully. If the def returns false, that suggests that the role did not load successfully, typically because it is not supported or not installed. You do not have to do this if your role definition is independent of TTT Bots. This is only needed if you want to do a PR to contribute role compatibility directly to the TTT Bots mod.

Now, I will cover some more advanced concepts related to role support. I'll break it down line-by-line when possible. I will use the Jester code as an example, but please note that it may have changed by the time you read this. (Last updated 1/1/24)

-- Jester.lua
if not TTTBots.Lib.IsTTT2() then return false end -- don't load this module if we are not playing TTT2.
if not ROLE_JESTER then return false end -- Do not load this module if the user does not have Jester installed.

This next bit is where it gets a little more complicated

local allyTeams = {
    [TEAM_JESTER] = true,
    [TEAM_TRAITOR] = true,
}

allyTeams is an important variable to declare here. Not only are we stating that Jesters are on our side, but so are traitor roles. This is important in preventing traitors from attacking us, as alliances go both ways, even if they are defined on one side. You may notice how the TEAM enums are in square brackets.

^ Note that if you are going to mark a team as an ally, you MUST either stub some default text in (like [TEAM_JESTER or 'jesters']) or check that the team you are adding as an ally exists as seen in the prior section. If you reference a TEAM enum not installed on the server, your mod/extension will throw errors.

local Bhvr = TTTBots.Behaviors
local bTree = {
    Bhvr.ClearBreakables,
    Bhvr.AttackTarget,
    Bhvr.UseHealthStation,
    Bhvr.FindWeapon,
    Bhvr.Stalk,
    Bhvr.InvestigateNoise,
    Bhvr.Follow,
    Bhvr.Wander,
}

Now, we are just defining the structure of the behavior tree. You don't always need to do this, as I provide functionality for built-in traitor, innocent, and detective roles. In the case of the Jester, though, we want it to act traitorously intentionally, hence 'Follow' and Stalk. Before setting one of these up yourself, I suggest you look at some of the default trees in sv_tree.lua to see what behaviors are often used and why.

Now for the "role definition" part of the code:

local jester = TTTBots.RoleData.New("jester", TEAM_JESTER)
jester:SetDefusesC4(false)
jester:SetStartsFights(true)
jester:SetTeam(TEAM_JESTER)
jester:SetBTree(bTree)
jester:SetAlliedTeams(allyTeams)
TTTBots.Roles.RegisterRole(jester)

I will write a document on what these :Set operations do later, but you may always find updated explanations in the sv_roledata.lua file.

We start by declaring a new role with TTTBots.RoleData.New(rolename, team). This creates a new role definition but has yet to be added to the registry. That is accomplished in the last line in the above snippet. Since we want the Jester to act traitorous, we disable its ability to defuse and enable its ability to start random fights. We then set its team, btree, and allied teams before properly registering it with the system.

hook.Add("TTTBotsModifySuspicion", "TTTBots.jester.sus", function(bot, target, reason, mult)
    local role = target:GetRoleStringRaw()
    if role == 'jester' then -- Make sure the 
        if TTTBots.Lib.GetConVarBool("cheat_know_jester") then
            return mult * 0.3
        end
    end
end)

This hook is 100% optional but can be helpful. Please take a look at the Hooks section below for more details. For Jesters specifically, we have a cvar that makes them less suspicious of other bots to make innocent bots appear challenging to fool.

return true

Since this is a default role, we return true to signify the module loaded. Again, this isn't required in your add-ons but is helpful for anything under this mod's tttbots2/roles/ folder.

Custom Buyables

TODO: write this

Hooks

TODO: write this