Left 4 Dead 2

Left 4 Dead 2

34 ratings
A guide to VScript Talkers
By ChimiChamo
A guide to creating your own talker scripts entirely using VScript! So no conflicting files with other talker scripts!
   
Award
Favorite
Favorited
Unfavorite
What even is this?
Talker scripts are the name of a particular group of txt files located in the scripts/talker directory of L4D2. These files house every bit of dialogue survivors speak and when they speak them. Without it, survivors would be completely silent. They are comprised of response tables, which house the names of the scenes played when a survivor speaks the response concept and a rule table, which houses the criteria and concept name.

^^
Above is the response and rule table for Nick's "PlayerMoveOn" concept, the one that is called when you use "Let's Go" in your vocaliser.

I won't talk about the regular talker scripts too much here as this guide already gives a good description of what these are.

This guide instead focuses on VScript talkers.
They have (almost) the same features as regular talker scripts, plus more, and most importantly they don't conflict with other talker scripts, so you can have as many as you like!
Getting Started
To begin, make sure you have L4D2 Authoring Tools installed (you can find them in the tools tab of your library).
Next create a new folder in your Left 4 Dead 2\left4dead2\addons\ directory and name it whatever you want.
Inside the folder make a new folder name "scripts" and then inside that scripts folder make another new folder named "vscripts".
Now make a new file named "director_base_addon.nut".

Now you are ready to begin!
The Basic Script
At the top of the script, paste in:
IncludeScript("response_testbed", this)
If you wish you an also add
printl("Your message here")
so you know the script had loaded.
Following this, add:
local newrules = [ ] g_rr.rr_ProcessRules( newrules );

Now inside the [] brackets you can begin creating your custom concepts, below I have the template for one:
{ name = "CanBeAnything", //A name, doesn't actually do anything ingame criteria = [ ["concept", "CanBeAnythingAswell"], //The name of the concept that must be fired for the responses to play ["who", "Gambler"], //Who can use this concept ], responses = //The scenes that will be randomly picked when the concept is fired [ { scenename = "scenes/Gambler/MoveOn01.vcd", } //Look at ya. What are we waiting for? Let's go. { scenename = "scenes/Gambler/MoveOn02.vcd", } //Let's roll! ], group_params = g_rr.RGroupParams({}) }
You can have as many scenes as you like in one concept.

This should be enough for most purposes, you can test this script by dragging the whole folder you made in the addons folder to bin/vpk.exe in your main game directory.

The survivor script names are as follows:
Nick - Gambler
Rochelle - Producer
Coach - Coach
Ellis - Mechanic

Bill - NamVet
Zoey - TeenGirl
Louis - Manager
Francis - Biker
Firing the Concepts
To test whether your concept works in game, you have to trigger it, there are several ways to do this.

sv_cheats Method

Load into a map by typing "map xyz" in console, replace xyz with an actual map obviously. Once you load in, type "sv_cheats 1" in the console. Now you can test your concept. You can do the command "ent_fire !self speakresponseconcept ConceptName" to have your character speak the concept you want. Replace "ConceptName" with the actual concept you want to test. You can test it on other survivors by replacing !self with !survivorname so for example !ellis. This is most likely the method you'll use most often to test your concepts.

Mapping Method

If you plan to use these custom concepts in a custom campaign, you'll want to use this method for the campaign.

Triggers

You can use a trigger entity to fire a concept to whoever walks into it, just add this output to your trigger:

It's red but it still works, you can use a filter entity to target only survivors or only 1 specific survivor. Again replace ConceptName with the concept you want.

Random Survivor

You can have a concept be fired to a random survivor by using the info_director entity, set up the output as so:
You can have any entity that has outputs do this, replace "director" with the targetname of your info_director entity.
Followups
If you want to create a whole survivor conversation you'll have to have a way for concepts to be strung together. This can be done by using followups.

Here is an example of a response that uses a followup:
{ scenename = "scenes/Gambler/DLC1_C6M2_SafeRoomConvo08.vcd", followup = RThen( "coach", "WhateverConcept", {additionalcontext="null"}, 0.1 ) }
Here, when Nick finishes his scene, Coach fires the "WhateverConcept" concept 0.1 seconds after Nick is done.

Here is what happens in game:

You can also set the target as:
Any - Which will pick a random survivor to play the concept
Self - Which will play from the same survivor who started the followup
All - Which will make all survivors speak the concept minus the one who started the followup

The additionalcontext bit is a context that will be sent and used by whoever the followup target is for that specific context. For example if you have the additionalcontext bit be {isstupid ="yes"}, then the survivor that speaks the concept after the first survivor finishes will have one of their contexts be ["isstupid", "yes"]
Criteria
The criteria of a concept are a set of checks that must be made before the responses are played, if any of the checks fail, no response is played. These are placed in the criteria section of the concept.
For example:
["map", "c1m3_mall"],
^^
is a criterion that checks what map it is, if the map is c1m3_mall (Dead Center map 3) then the response is fired. If it is a different map, no response is played. This will allow you to replace concepts such as the one for pouring a gas can and have it only affect the custom map you are making(the concept for that is PlayerPourStarted).

The names for all the criteria in the game is stored in the "terror_player.txt" file found in the talker folder. You need GCFScape to extract it.[nemstools.github.io] Here's how to use this file:
Say you wanted to know the name of the criteria for the "NotInCombat" criteria you find in the regular talker files. Searching for this string in the txt file returns:
criterion "NotInCombat" "InCombat" "0" required
What this tells us is that the name of the criteria is "InCombat" and the value has to be 0 to recreate NotInCombat so the VScript criterion would look like this:
["incombat ", 0],

For something that has a range such as "IsSomeoneDied" you'll have to write the criterion differently than the txt file, the txt file shows:
criterion "IsSomeoneDied" "NumberOfTeamDead" ">=1" required
Typing >=1 won't work, instead you'll have to write a range like this:
["numberofteamdead", 1, 99],

You can replace the numbers with the range you need.

It is recommended to have the criteria name be all lowercase, that way it won't conflict with other things. You'll notice that doesn't apply to anything else here or my addons because I'm lazy :)

For criteria that have "!=" in them then you'll have to take a slightly more complex approach. It will be touched on later in the guide.
Applying Context
Context is essentially something that modifies the criteria of a survivor, for example to prevent other lines being said or everyone speaking at once. You can add these to a script by modifying the response.
applycontext = {context = "TalkGambler", value = 1, duration = 15}
^^
This sets the "TalkGambler" context to 1 for the speaker for 15 seconds. If you put -1 for the duration it makes it infinite.

To have multiple contexts, you would do:
applycontext = {context1 = {context = "TalkGambler", value = 1, duration = 15}, context2 = {context = "SaidLeavingSafeArea", value = 1, duration = 24}}
You can have as many contexts as you want.

You would add these next to the line in the response like this:
{ scenename = "scenes/Gambler/Taunt04.vcd", applycontext = {context = "TalkGambler", value = 1, duration = 15} }

To apply "world" criteria you would have the same "applycontext" section as well as:
applycontexttoworld = true
^^
This makes the contexts in applycontext apply to worldspawn. All criteria inside the applycontext section will automatically have "world" appended to the start of it so something like "SaidLeavingSafeArea" would become "worldSaidLeavingSafeArea"

You must have a scene or function in the response for the context to work, otherwise the script file itself doesn't load. You can set the scenename to "" if you don't want any lines to be said.
Functions
A large bonus of VScript talkers is that it allows for VScript function to be called as a response. To do this, you want to alter your response to look like this:
{ func = FunctionNameHere }
This will call the FunctionNameHere function every time that response is picked, here's a quick demo function:
function FunctionNameHere(speaker, query) { speaker.TakeDamage(5, 0, null) }
This function will make the survivor who speaks the concept take 5 damage. You can also combine these with regular scene responses like this:
{ scenename = "scenes/Gambler/DLC1_C6M2_SafeRoomConvo08.vcd", func = FunctionNameHere }

If you have a followup that triggers a concept that has a function as a response, you can have that function be called when the survivor finishes speaking, here's a demo of that:
RGroupParams
The RGroupParams table located at the bottom of your concept can alter two things about all the responses in a concept.

NoRepeat
To enable this write:
g_rr.RGroupParams({norepeat = true})
in the designated section.
If enabled, each response will only play once and cannot repeat.

Sequential
To enable this write:
g_rr.RGroupParams({sequential = true})
in the designated section.
If enabled, the responses will play in a set order. They will play top to bottom from your response table.

MatchOnce
To enable this write:
g_rr.RGroupParams({matchonce = true})
in the designated section.
If enabled, only one response will ever be picked, then the concept is disabled. This happens regardless of how many responses are present.
Extra Response Options
There are a couple extra things you can add on top of a response:

speakonce - Makes it so the response it's attached to is only played once

To apply it you would do:
{ scenename = "scenes/Gambler/DLC1_C6M2_SafeRoomConvo07.vcd", speakonce = true }

odds - A number 0-100. It makes it so there is a chance that if the response is picked nothing will play.
Custom Criteria
You can create custom criteria for your response rules in case you can't find one that you specifically need.

To do this create a function where one of the parameters is "query" like so:
function WhateverName(query) { }
For the response to be accepted, the function must return true, if it doesn't the response doesn't play. For example, this here function checks whether the map is Dead Center Hotel and if it is, disallows the response from playing:
function IsNotHotel(query) { if(Director.GetMapName() == "c1m1_hotel") { return false } else { return true } }
To call this, you want to add it to your criteria like this:
[IsNotHotel],
Query and != Criteria
The "query" of a criterion houses all the contexts that the player speaking the responses has like what player they are, how close they are to other survivors, whether they're speaking, etc. You can use this to check certain contexts for a specific player rather than all of them.

MASSIVE NOTE:
Capitalisation is VERY important. If just one letter from the query is lowercase when it should be uppercase or vice versa, it could cause the whole function to simply not work. And the worst part is, there's no easy way to know what should be capitalised or not without just printing out the whole query. However, what we can do (which I just figured out like 10 minutes before writing) is convert every part of query to lowercase like this:

function LowQuery(query) { local newquery = {} //creates a new table foreach(key,val in query) { newquery.rawset(key.tolower(),val) //sets the keys to lowercase versions } }

This will then allow us to check for whether certain world criteria exist or not. Here is a snippet:

function IsNotSaidchargerpound(query) { local newquery = {} foreach(key,val in query) { newquery.rawset(key.tolower(),val) } if("saidchargerpound" in newquery) //is saidchargerpound found in the query { if(newquery.saidchargerpound != "1") //does saidchargerpound NOT equal to 1 { return true //if so allow the response } else { return false //otherwise dont } } else { return true //if something is not present in the query, that essentially means it hasnt been set and so has no value } }

You can use this to check whether certain "world" criteria that is usually shared across survivors doesn't equal to 1 for example.

You add this to your criteria array just like custom criteria.
Custom Voice Lines
Since you are able to use functions with VScript talkers, it allows you to create custom voice lines...kinda. They are very limited, you need a separate function for each concept, there will be no face animation and the sounds won't stop if the survivor speaks again but hey, it's better than nothing?

MP3 sound files play normally, WAV files require a sound cache to be built. You must also precache each one you are going to use at the start of the script like so:

PrecacheSound("bruh/BillDefib01.mp3")

This is only and example sound.

Here is a script that gives Bill custom voice lines when defibbed:
function BillFibLines(speaker, query) { local BillLines = [ "bruh/BillDefib01.mp3", "bruh/BillDefib02.mp3", "bruh/BillDefib03.mp3" ] //defines which sounds we want to play local WhichLine = RandomInt(0,BillLines.len()-1) //Picks a random sound from the above table speaker.SetContext("TalkNamVet", "1", GetSoundDuration(BillLines[WhichLine],null)) //Prevents bill from using most voice lines for the duration of the sound speaker.SetContext("who", "null", GetSoundDuration(BillLines[WhichLine],null)) //Prevents bill from using ALL voice lines for the duration of the sound speaker.PlayScene("scenes/Namvet/blank.vcd", 0.0) //Ends the current scene and voice line Bill is currently speaking g_rr.rr_PlaySoundFile(speaker, 0, BillLines[WhichLine], 0, 0, 1.0, 0) //Plays the sound we selected on Bill } local newrules = [ { name = "BillDefibNew", criteria = [ ["concept", "RevivedByDefibrillatorDelayed"], ["who", "Namvet"], ["Coughing", 0], ], responses = [ { func = BillFibLines } ], group_params = g_rr.RGroupParams({}) } ] g_rr.rr_ProcessRules( newrules );

Here's a video of it in action, yes the voice lines might sound a little off but I picked whatever random sounds I had on my PC at the time.
Drawbacks
So far, this guide has made it seem that VScript talkers are near-perfect and that regular talker scripts are basically obsolete. This is wrong.

The implementation of VScript talkers are unfinished and there are many aspects that simply have not been done and are left as a "TODO".

For example: setting "norepeat = true" in the group parameters does nothing. Why? Looking at rulescriptbase.nut, an official Valve file, shows that the function that disables the response looks like this:
function Disable() { printl( "TODO: rule " + rulename + " wants to disable itself." ) }

Other things are missing as well such as most things related to responses such as predelays and weight.

So you won't be able to recreate the regular talker scripts with VScript but you can damn well get close!
So yeah, that's about it
Hopefully some of you have found some parts of this helpful, if you didn't don't care.

Here I've linked an addon that contains a script showcasing pretty much everything here, you need to extract the files tho

Feel free to ask question in the comments for this, there's very little documentation on this.
12 Comments
ion Mar 31 @ 11:58am 
So if you use for example "WHO", "Coach" in criteria, you'll end up with query table having WHO = "Coach" . And if you call query.who after that anywhere, it will fail.
ion Mar 31 @ 10:05am 
It would be nice if you recommended writing criteria names in lower-case, like this: "incombat", 0 or "numberofteamdead", 1, 99 . This is because criteria can change the query table where names are written in lower-case.
senzawa Jan 24 @ 6:54pm 
Note that to do world contexts you DON'T use applyworldcontext instead of applycontext.
You SHOULD use applycontext normally and then set applyworldcontext = true
Since Valve only checks if applyworldcontext in an if statement to determine to set it to world spawn or firing entity

Cite:
In rr_ProcessResponse:


local applycontext = null
local applycontexttoworld = false
if ( "applycontext" in resp )
{
applycontext = resp.applycontext
}
if ( "applycontexttoworld" in resp )
{
applycontexttoworld = resp.applycontexttoworld
}
[---snip---]
if ( "scenename" in resp )
{
scene = resp.scenename

local Func = func
if ( applycontext )
func = @( speaker, query ) g_rr.rr_ApplyContext( speaker, query, applycontext, applycontexttoworld, Func )
}
senzawa Jan 24 @ 6:54pm 
in rr_ApplyContext:

if ( ( "context" in contextData ) && ( typeof contextData.context != "table" ) )
{
local duration = contextData.duration
if ( duration == 0 )
duration = -1
if ( contexttoworld )
{
local world = Entities.FindByClassname( null, "worldspawn" )
if ( world )
world.SetContext( contextData.context, contextData.value.tostring(), duration )
}
else
speaker.SetContext( contextData.context, contextData.value.tostring(), duration )
}

I spent 8 hours of my life figuring this out, I hope nobody will make the same mistakes as me again.
kat Dec 3, 2023 @ 8:24am 
I could kiss you. Thanks for this.
Kyle H. McCloud Oct 30, 2023 @ 3:21pm 
Just chiming in again to say that I'm finally getting around to implementing vscript dialogue in my next custom campaign and it's working perfectly so far. This is an excellent tutorial! Thank you so much for making it. I followed every step and my dialogue worked on the first try.

Friendship ended with logic_choreographed_scene, now director_base_addon.nut is my new best friend :emofdr:
ChimiChamo  [author] Sep 12, 2023 @ 1:27pm 
Yeah, there's no need for any other entities for VScript talkers to work.
Kyle H. McCloud Sep 12, 2023 @ 1:24pm 
Ooh, this is a helpful guide. For custom campaigns to trigger these talker concepts, do all the lines still need to be loaded in the level as logic_choreographed_scene entities? Or can talker vscripts be used to keep the entity count down by eliminating the need for logic_choreographed_scenes?
ChimiChamo  [author] Sep 4, 2023 @ 11:10am 
If you mean to create custom responses for the vocaliser then yes, the actual radial menus are a different file
Nicholas "Nick" Overbeck Sep 4, 2023 @ 10:30am 
Question, Does this Work for Vocalizer's aswell?