TBAF guide

Multi-language server and extension for vscode-based editors. Supports various Infinity Engine and Fallout syntaxes.
Locked
User avatar
Magus
Site Admin
Posts: 516
Joined: Mon Nov 21, 2016 9:13 am
Contact:

TBAF guide

Post by Magus »

MLS 2.2.0 supports a new file extension: .tbaf. TBAF stands for Typed BAF. Essentialy, it's a Typescript file, which, upon running "Compile" command, will be converted to BAF.

The goal of TBAF is to provide saner IE scripting experience. To that end, TBAF supports a limited selection of TS language features like loops, nested conditions, functions. Although that's not much, it's still considerably better than BAF's only IF-THEN clause.
Of course, there's already SSL which has more or less the same goal. However, Typescript is much more mainstream, and I think, easier to get started with. And it allows to take advantage of the far superior Typescript tooling.

At this moment, it's released as feature preview, so could be a little rough around the edges, and syntax may evolve. Make sure to check the generated BAF.
 

General

TBAF is Typescript. If you're familiar with Typescript and BAF, then you will have no problem writing TBAF. If you're not familiar with Typescript, do not worry. TS is extremely popular, and TBAF only uses a very limited list of TS features, so it's quite easy to understand. To get a feeling of how TBAF looks and why'd you want to use it, take a look at the following example

Code: Select all

/** I am doomed! */
const LVAR_doomed = "doomed"
/** Fires a spell-trigger just to show what a great mage he is */
const LVAR_castSpellTrigger = "SpellTrigger"

// First talk, then fight
if (See(Player1) && Global(LVAR_doomed, LOCALS, 0)) {
    SetGlobal(LVAR_doomed, LOCALS, 1)
    FaceObject(Player1)
    SmallWait(8)
    StartDialog("WM_RHIA", Player1)
}

/** Start fight: spell trigger */

/** Common spell trigger actions */
function actSpellTrigger() {
    DisplayString(Myself, 39968) // ~Spell Trigger - Fired~
    ReallyForceSpellRES("WM_LUCK", Myself)
    SetGlobal(LVAR_castSpellTrigger, LOCALS, 1)
}

if (See(NearestEnemyOf(Myself)) && Global(LVAR_castSpellTrigger, LOCALS, 0)) {
    if (LevelLT(LastSeenBy(Myself), 3)) {
        actSpellTrigger()
        ReallyForceSpell(Myself, WIZARD_ARMOR)
        ReallyForceSpell(Myself, WIZARD_SHIELD)
    }
    if (LevelLT(LastSeenBy(Myself), 8)) {
        actSpellTrigger()
        ReallyForceSpellRES("WM_LIGHT", Myself)
        ReallyForceSpell(Myself, WIZARD_SHIELD)
    }
}
/** End start fight: spell trigger */

Hopefully this self-explaining enough to see both how and why.

In order to code in TBAF you will need to install IElib npm package, which provides BAF definitions for intellisense. Refer to the example repository to see how to do that. Note that at this moment IElib-ts only supports classic BG2 scripting style, EE additions are not included. And even BG2 support is WIP - this is where you come in.

Features


There's a number of caveats to all of these, so make sure to read to the end to understand the limitations.

Variables

Variables are a basic requirement for more advanced language features. Typescript is a strongly typed language, so you will have to declare every variable (and sometimes its type too).

Code: Select all

const LVAR_castFireball = "fireball";
if (See(TenthNearestEnemyOf(Myself)) && Global(LVAR_castFireball, LOCALS, 0)) {
    IncrementGlobal(LVAR_castFireball, LOCALS, 1)
    ReallyForceSpell(LastSeenBy(Myself), WIZARD_FIREBALL)
}

is equivalent to

Code: Select all

if (See(TenthNearestEnemyOf(Myself)) && Global("fireball", LOCALS, 0)) {
    IncrementGlobal("fireball", LOCALS, 1)
    ReallyForceSpell(LastSeenBy(Myself), WIZARD_FIREBALL)
}

Which is transpiled into the following BAF

Code: Select all

IF
    See(TenthNearestEnemyOf(Myself))
    Global("fireball", "LOCALS", 0)
THEN
    RESPONSE #100
        IncrementGlobal("fireball", "LOCALS", 1)
        ReallyForceSpell(LastSeenBy(Myself), WIZARD_FIREBALL)
END

Hopefully, the conversion is obvious enough, so in the following examples BAF will be omitted.

Important: there is no runtime. You cannot change variables. Therefore, TBAF variables are always constant and declared with const (with one exception, see below).

Loops

TBAF supports simple for and for..of loops (examples are contrived, just for illustration):

Code: Select all

for (let rng = 0; rng < 2; rng++) {
    if (See(NearestEnemyOf(Myself)) && Range(LastSeenBy(Myself), rng)) {
        ReallyForceSpell(LastSeenBy(Myself), WIZARD_LARLOCH_MINOR_DRAIN)
    }
}

const players = [Player1, Player2]
for (const player of players) {
    if (See(TenthNearestEnemyOf(player))) {
        ReallyForceSpell(LastSeenBy(player), WIZARD_LARLOCH_MINOR_DRAIN)
    }
}

turns into

Code: Select all

IF
  See(NearestEnemyOf(Myself))
  Range(LastSeenBy(Myself), 0)
THEN
  RESPONSE #100
    ReallyForceSpell(LastSeenBy(Myself), WIZARD_LARLOCH_MINOR_DRAIN)
END

IF
  See(NearestEnemyOf(Myself))
  Range(LastSeenBy(Myself), 1)
THEN
  RESPONSE #100
    ReallyForceSpell(LastSeenBy(Myself), WIZARD_LARLOCH_MINOR_DRAIN)
END

IF
  See(TenthNearestEnemyOf(Player1))
THEN
  RESPONSE #100
    ReallyForceSpell(LastSeenBy(Player1), WIZARD_LARLOCH_MINOR_DRAIN)
END

IF
  See(TenthNearestEnemyOf(Player2))
THEN
  RESPONSE #100
    ReallyForceSpell(LastSeenBy(Player2), WIZARD_LARLOCH_MINOR_DRAIN)
END

Important:
- for loop is the only place where you will use let instead of const in TBAF.
- Only simple iterators and conditions are supported, like in the example.

Nested conditions and Else clause

Nested conditions are flattened. Else statements are inverted into if's and also flattened.

Code: Select all

if (Global(LVAR_contingency, LOCALS, 0) && TookDamage()) {
    // Level 1-14
    if (LevelLT(LastSeenBy(Myself), 15)) {
        DisplayString(Myself, 40252); // ~Contingency - Protection from Magical Weapons~
        ApplySpell(Myself, WIZARD_PROTECTION_FROM_MAGIC_WEAPONS)
        SetGlobal(LVAR_contingency, LOCALS, 1)
    } else {
        // Level 15+
        DisplayString(Myself, 43050); // Chain Contingency - Improved Mantle
        ApplySpell(Myself, WIZARD_IMPROVED_MANTLE)
        SetGlobal(LVAR_contingency, LOCALS, 1)
    }
}

Is equivalent to

Code: Select all

if (Global(LVAR_contingency, LOCALS, 0) && TookDamage() && LevelLT(LastSeenBy(Myself), 15)) {
    DisplayString(Myself, 40252)
    ApplySpell(Myself, WIZARD_PROTECTION_FROM_MAGIC_WEAPONS)
    SetGlobal(LVAR_contingency, LOCALS, 1)
}

if (Global(LVAR_contingency, LOCALS, 0) && TookDamage() && !LevelLT(LastSeenBy(Myself), 15)) {
    DisplayString(Myself, 43050)
    ApplySpell(Myself, WIZARD_IMPROVED_MANTLE)
    SetGlobal(LVAR_contingency, LOCALS, 1)
}

Important: while conditions are merged, action blocks never are. Each action block is final as defined. (But you can use functions to combine multiple actions - see the corresponding section).

Functions

Or, "functions". Functions in TBAF are not real TS functions (again, there is no runtime). They actually are function-like macros. Meaning, every function call is replaced by function body, substituting variables, if any.

There are 3 kinds of functions allowed in TBAF:

  • block - an entire if-then block or a list of blocks
  • action - a list of actions
  • trigger - a list of triggers

Block and action functions may not have a return value (they are of type void). Do not use return statement in these kinds of functions.

Code: Select all

/**
* If random roll is less than rollLimit, create a random magic effect after party has rested.
* Make sure to use this in incrementing order of `rollLimit`, to avoid missing effects.
* @param rollLimit roll limit
* @param headString passed to DisplayStringHead
* @param spellRes spell resource
*/
function randomEffect(rollLimit: number, headString: number, spellRes: SplRef) {
    if (PartyRested() && RandomNumLT(SLEEP_MAX_ROLL, rollLimit)) {
        PlaySound("EFF_M10")
        DisplayString(Myself, $tra(10201)) // "Wild Magic made your dreams come true"
        DisplayStringHead(Myself, headString)
        ApplySpellRES(spellRes, Myself)
    }
}

Trigger functions may only contain a return statement, which must be a valid combination of BAF triggers. They always return boolean.

Code: Select all

/**
* @returns true if current creature has any of the listed wizard spells
*/
function haveWizardSpells(): boolean {
    return HaveSpellRES("WM_ATTR") || HaveSpellRES("WM_RND");
}

Functions can call other functions, naturally.

Includes

Includes are normal Typescripts includes.

Code: Select all

/* lib.ts */
export function randomEffect(rollLimit: number, headString: number, spellRes: SplRef) {
    // body
}

/* script.tbaf */
import { randomEffect } from "./lib" // if lib.ts is in the same directory

// "Pretty Sparkles" -> Wild Magic 30: Pretty Sparkles
randomEffect(15, 397, "SPWM130")

Handy, as you can see. Can share functions, constants, etc between multiple files.

Important: do keep .ts extension for libraries so that TS language server can find them.

Libraries can include other libraries, naturally.

Types

As mentioned earlier, Typescript is a strongly typed language, which means every variable has a type. Most of the time, TS can infer it, but sometimes it has to be declared explicitly. For someone unfamiliar with strongly typed concept, this may seem like an unnecessary hurdle. But rest assured, as you write more complex scripts, you will come to appreciate the type system. In the end, it helps to produce much more robust code.

TBAF transpiler itself doesn't know anything about BAF types. These definitions are provided by IElib-ts.

IElib provides a degree of type safety, although it's not super strict. That's due to various factors - Typescript type system limitations, suboptimal IESDP data presentation, and the fact that learning curve with full type safety would probably too steep for an average IE modder unfamiliar with Typescript. This is subject to change, though, and in general type restrictions will likely tighten as IElib matures.

Generally, most functions will accept strings and numbers as arguments, where intuitively applicable. However, it's recommended to define or declare more specific types:

Code: Select all

function randomEffect(rollLimit: number, headString: number, spellRes: SplRef) {
// function body
}
// "SplRef" means ResRef of SPL type. It's more cosmetic for now, though.

// Recommended usage
const myspell: SplRef = "spwi123";
randomEffect(0, 0, myspell);

// But this will work too
const myspell = "spwi123";
randomEffect(0, 0, myspell);

// And this
const myspell: ResRef = "spwi123";
randomEffect(0, 0, myspell);

// And this, of course
randomEffect(0, 0, "spwi123");

// This, however, will throw a type mismatch error
randomEffect(0, 0, 123);

IElib declares many built-in IDS symbols, but that's a work in progress, and depends on user demand and help. For mod-added symbols, you can define or declare them:

Code: Select all

const MYSPELL = 2225;
HaveSpell(MYSPELL);
// This will be transpiled into "HaveSpell(2225)"

declare const MYSPEL: number;
HaveSpell(MYSPELL);
// This will remain verbatim: "HaveSpell(MYSPELL)"

In this example, if you've added your spell symbols to spell.ids, you will likely want to use the second form. Otherwise, the first one.

Notable peculiarities

$obj function

$obj is a special function provided by IElib. It takes an object specifier string and return an object specifier type, which is subtype of ObjectPtr - game object type.

Use it like this See($obj("[GOODCUTOFF.0.0.0.0.SUMMONED]")). Also can be used with death variables: $obj("Melicamp").

(Why ObjectPtr and not just Object? "Object" is a reserved word in Typescript. "ObjectPtr" stands for "object pointer").

Only needed for object specifiers and death vars, not for objects. See(Myself) is just fine.

$tra function

Similarly, $tra is syntax sugar for tra references. Where in BAF you would say @123, in TBAF you say $tra(123). For example, DisplayString(Myself, $tra(30121)).

Because TS doesn't like "@".

Only one RESPONSE

Multiple RESPONSE blocks for the same IF condition are not supported. Use RandomNum instead. It works better, anyway.

Scope

Functions and variables have global scope. (Or, at least, you can assume that they do). So, diversify the names, no shadowing.
At its core, TBAF transpiler is a simple text processor. Keep that in mind while naming stuff.
Please do not PM or email me about my mods and projects. Use forums. Also, see our talk channels.
User avatar
Magus
Site Admin
Posts: 516
Joined: Mon Nov 21, 2016 9:13 am
Contact:

Re: TBAF guide

Post by Magus »

This topic is locked to avoid making it a dump for every tbaf question. If you have a question, just start a new topic.
The guide will be updated on demand.

To get started with TBAF, see the example repository.
To see a real world application, check out WMA repo.
Please do not PM or email me about my mods and projects. Use forums. Also, see our talk channels.
Locked