Player guide

Console scripting

Four additions turn binds and configs from fixed strings into something you can branch on, substitute into, and schedule. They are most useful together, so they are documented together.

Everything here came from NewJK and NewMod, and their documentation is the original account of ifCvar and strSub. This page describes what the TaystJK client actually does, which is not identical. See what does not work.

The console reference has the catalogue entries: ifCvar, strSub, delay, waitf, delaycancel and waitfcancel.

Quoting and nested quotes

Stock Jedi Academy ends a quoted string at the next ", so a bind cannot contain a bind. TaystJK tracks quote depth instead, and the rule that makes it work is worth stating directly:

Each time a command is executed, exactly one layer of quotes is removed.

So this is a single bind that, when pressed, creates another bind:

bind a "bind b "set c "d ; e" ; say "hello there"""

Pressing A peels the outer quotes and runs bind b "set c "d ; e" ; say "hello there"". Pressing B peels the next layer. Count your closing quotes at the end; three in a row is not a typo.

A " counts as opening a level only when the character after it is not a space, a ; or another ", and the character before it is not a " (IsOpeningQuote). That heuristic is what lets the parser tell say "hi" from a stray quote, and it is why "" is treated as an empty argument rather than as one level of nesting.

Three places count depth the same way, which is why the feature holds up across all the routes a command can take: the command buffer, so a ; inside quotes no longer splits the line (Cbuf_Execute); the tokenizer that splits a line into arguments (Cmd_TokenizeStringNestedQuotes, used by Cmd_ExecuteString); and bind parsing, so a bound string containing ; inside quotes stays one command (CL_ParseBinding).

There is no backslash escape. \" is not special; depth counting is the only mechanism.

Substituting cvar values: strSub

strSub runs the command that follows it, first replacing every $cvarname$ with that cvar’s current value (Com_StrSub_f).

strSub say "Hello, I am $name$"

The name is delimited on both sides by $. Write $$ for a literal dollar sign (common.cpp:454). Substitution happens per argument, so a value containing spaces is re-quoted as one argument before the command runs.

This is the difference between say My fps cap is $com_maxfps$ under strSub, which sends the number, and vstr, which executes a cvar’s contents as a command. Use strSub when you want a value inside a string; use vstr when the cvar is the command.

Branching on a cvar: ifCvar

ifCvar reads one cvar, tests it against conditions in order, and runs the command belonging to the first match (Com_IfCvar_f). Nothing runs if no condition matches.

ifCvar <cvar> <setting> <count> <command...> [<setting> <count> <command...> ...]

The simplest form compares the value as text:

ifCvar cg_myCvar 0 2 say_team hi 1 2 say_team bye

If cg_myCvar is 0 it says hi; if it is 1 it says bye. The 2 before each command is the argument count, explained below.

Modifiers

A setting may start with one of these. Without one, the comparison is a case-insensitive string match.

Modifier Test
$= Numeric equality
$!= Numeric inequality
$> Numerically greater than
$< Numerically less than
$>= Numerically greater than or equal
$<= Numerically less than or equal
$contains The value contains this text anywhere
$beginswith The value starts with this text
$startswith Alias for $beginswith
$endswith The value ends with this text
$else Always true

$startswith is accepted alongside $beginswith (common.cpp:394); the NewMod documentation lists only $beginswith.

The text follows the modifier with no space: $containsbeer, $>=50, $else. Put a $ in front of the comparison value to read it from another cvar instead of using it literally, for example $>=$cg_someOtherCvar. That indirection works for the six numeric operators and for plain equality; it does not work for $contains, $beginswith, $startswith or $endswith, for the reasons in what does not work.

$else should be last. It always matches, so anything after it is unreachable.

Counting arguments

The number before each command is how many arguments that command occupies, including the command’s own name. This is the part that trips people up.

Command to run Count Why
quit 1 The command name only.
say_team hi 2 Name plus one word.
set model desann 3 Name plus two words.
bind x say_team "hello there" 4 "hello there" is quoted, so it is one argument.

A quoted phrase counts once, and the client re-adds the quotes when it runs the command (common.cpp:417). Get the count wrong and ifCvar reads the following condition from the middle of your command; the count must be between 1 and 1023 or it refuses and prints why (common.cpp:410).

Putting it together:

ifCvar cg_myCvar $>=$cg_someOtherCvar 1 quit $containsbeer 3 set model desann $else 4 bind x say_team "hello there"

Greater than or equal to cg_someOtherCvar quits; otherwise a value containing beer switches model; otherwise the bind is set.

Timed commands: delay and waitf

Stock wait stalls the entire command buffer. delay and waitf do not: they set the rest of the line aside and let everything else keep running. delay counts milliseconds, waitf counts frames.

say darth;delay 1000;say vader

Two things about the syntax are not obvious, and both follow from these being intercepted in the command buffer rather than run as ordinary commands (Cbuf_Execute):

Omitting the number entirely is the same as 1, but a space must remain between the command and the separator: delay ;say vader works, while delay;say vader does not (cmd.cpp:314).

delaycancel and waitfcancel drop pending entries whose text contains the argument, so delaycancel vader cancels the example above. delaycancel "" cancels every pending delay. Each cancels only its own kind.

What does not work

Verified against the source this reference is generated from. These are defects in the client, not deliberate limits, so they may be fixed in a build newer than this page. Compare the dates.

Cvar indirection is broken for the three text operators. $contains$myCvar, $beginswith$myCvar, $startswith$myCvar and $endswith$myCvar all read the wrong position when resolving the cvar name ($contains, $beginswith, $endswith). The lookup returns an empty string, and an empty string matches anything, so the condition silently becomes always true rather than failing visibly. Compare against literal text with these operators.

$endswith needs at least three characters. $endswithab falls through to plain string equality instead of testing the suffix (common.cpp:398).

Do not pass an empty setting. ifCvar someCvar "" 2 say hi reaches a loop that does not advance (common.cpp:363). Use $else when you want a condition that always matches.

strSub needs the closing $. $name without a trailing $ is read to the end of the argument (common.cpp:461), which is rarely the cvar you meant.

Report anything else you hit at https://github.com/taysta/TaystJK/issues.

Last changed History Edit this page on GitHub