Scripting

Overview

TASagentBot features a powerful scripting system, designed to look and behave like C#. It is simple in the source to register new classes to be either fully or partially accessible in scripts (with ClassRegistrar.TryRegisterClass<ClassName>()).

Conceptually, scripts can be viewed as the guts of anonymous classes. All functions and members declared at the root level of the script are scoped to that script and have a lifetime of its associated ScriptRuntimeContext. You also have the ability to declare and access global values, which can even be shared across scripts, with the global and extern keywords.

A simple test program which runs through many examples of basic scripts can be found here.

Script Usage

All scripts in the bot exist in contexts with pre-defined entry points. That is, any particular script is going to have at least one specific function it expects to be defined - consider this like an interface it expects your anonymous class to implement. This interface should be sufficiently clear with the provided default implementations, and you will receive exceptions for attempting to submit scripts which do not define the expected function(s).

For example, as of this documentation the default script for a scripted command is:

//Default Command Script
extern ICommunication communication;

void HandleMessage(User user, List<string> remainingCommand)
{
    communication.SendPublicChatMessage($"@{user.TwitchUserName}, Hello World");
}
    

And the default Follow script for the ScriptedActivityProvider is:

//Default Follow Script
extern ICommunication communication;

//Used for preventing notifying on double-follow
HashSet<string> followedUserIds = new HashSet<string>();

IAlert GetAlertData(User follower)
{
    if (followedUserIds.Add(follower.TwitchUserId))
    {
        //Remove the references to the user below to make it anonymous
        communication.SendPublicChatMessage($"Thanks for following, @{follower.TwitchUserName}");

        StandardAlert alertData = new StandardAlert();

        //Show image for 4 seconds
        alertData.Duration = 4.0;

        //Remove the reference to the user to make it anonymous
        alertData.ImageText.Add(new Text("Thanks for following, ", "#FFFFFF"));
        alertData.ImageText.Add(new Text(follower));
        alertData.ImageText.Add(new Text("!", "#FFFFFF"));

        alertData.Audio.Add(new SoundEffect("SMW MessageBlock"));

        return alertData;
    }

    return new NoAlert();
}
    

Global Declarations (global and extern)

Unless otherwise specified, variables declared outside of functions in a script are scoped to only that script. However, you can instead declare a variable in a global scope shared by all scripts using the global keyword. Globally declared variables are not automatically imported into each script. You must declare them with either the global or extern keywords.

Adding global to a variable declaration will declare the attached variable in the global space if it does not already exist, otherwise it will import access to that variable into the script. Any initialization attached to the declaration will only be used if the variable does not yet exist in the global space. This is a very powerful feature, but it's important to understand.

Adding extern to a variable declaration will import the variable from the global space, and throw an exception if it does not already exist. Effectively extern is an assertion that you really expect this variable to already exist. As such, it cannot have an initialization.

//Demo Global Declarations
global int testInteger0;
global int testInteger1 = 5;
global int testInteger2 = 2;
extern double testDouble1;
    

In the above example, if the global space had testInteger1 with a value of 1 and testDouble1 with a value of 1.0, then after this script ran:

Supported Control Flow Elements

The following control flow elements are supported within the scripting language:

Communication and Logging

To log errors or messages, or even send chat messages and whispers, you'll need to make use of ICommunication. To do so, add "extern ICommunication communication;" to your script.

ICommunication Methods:

Persistent Data

It can be very important access and use data with a lifetime longer than one session. Whenever a User object is passed to a script, you can access data stored that's associated with that user through HasDatum, GetDatum, and SetDatum. Further, the a global IScriptHelper reference can be imported for more data operations.

User extensions

User Methods:

IScriptHelper features

To make use of the IScriptHelper, add "extern IScriptHelper scriptHelper;" to your script.

IScriptHelper Methods:

Containers

A number of data containers are supported by default, and the supported types can easily be expanded in the source by registering more with ClassRegistrar.TryRegisterClass(typeof(MyContainer<>)). Here is a non-comprehensive list of the supported data containers and some of their most useful members.

T[] (Array)

An Array is a fixed-size collection of data. It supports collection initialization syntax.

T[] Properties:

T[] Indexer:

List<T>

A List is a flexible collection of data. It supports collection initialization syntax.

List<T> Properties:

List<T> Indexer:

List<T> Methods:

Queue<T>

A Queue is like a List, but it lacks random access to the elements. Instead, you are expected to Enqueue and Dequeue items. The first item Enqueued is the first one Dequeued, in what's called FIFO, or "First In, First Out". In this way, it acts like a real-life Queue.

Queue<T> Properties:

Queue<T> Methods:

Stack<T>

A Stack is like an inverted Queue. Instead, you Push and Pop items. The most recent item to be Pushed is the next one Poped, in what's called LIFO, or "Last In, First Out". In this way, it acts like a real-life stack of cards.

Stack<T> Properties:

Stack<T> Methods:

RingBuffer<T>

RingBuffers have limited capacity. Adding items past this capacity bumps the oldest value out of the RingBuffer.

RingBuffer<T> Properties:

RingBuffer<T> Indexer:

RingBuffer<T> Methods:

DepletableList<T>

DepletableLists are like Queues that get filled with elements, but non-destructively Dequeued via PopNext(). Using PopNext(), each element is returned in sequence, until Reset() is called (or the list is depleted and AutoRefill is set to true when PopNext() is invoked), where the DepletableList resets to its fully populated state.

DepletableList<T> Properties:

DepletableList<T> Methods:

DepletableBag<T>

DepletableBags are like DepletableLists, but are sampled randomly with calls to PopNext(). Using PopNext(), elements are returned in a random sequence, until Reset() is called (or the bag is fully depleted and AutoRefill is set to true when PopNext() is invoked), where the DepletableBag resets to its fully populated state. This is ideal for shuffling items where you want to guarantee all are used before any items repeat.

DepletableBag<T> Properties:

DepletableBag<T> Methods:

HashSet<T>

A HashSet is an unordered collections of values. A value either is or is not in the set, but it is not built for accessing the values as much as it is for testing if a value exists in the set.

HashSet<T> Properties:

HashSet<T> Methods:

Dictionary<TKey,TValue>

Dictionaries store values under keys of the specified type. They are also sometimes known as HashTables. You add elements with Add, but you need to specify both the value and the key. You access elements with the indexer using the appropriate key.

Dictionary<T> Properties:

Dictionary<T> Indexer:

Dictionary<T> Methods:

Unsupported Elements

The following are unfortunately unsupported at this time:

Example Scripts

Below are a few example scripts from the current version of the bot.

Follow Scripts

Follow scripts require a IAlert GetAlertData(User follower) method.

Default Follow Script
//Default Follow Script
extern ICommunication communication;

//Used for preventing notifying on double-follow
HashSet followedUserIds = new HashSet();

IAlert GetAlertData(User follower)
{
    if (followedUserIds.Add(follower.TwitchUserId))
    {
        communication.SendPublicChatMessage($"Thanks for following, @{follower.TwitchUserName}");

        StandardAlert alertData = new StandardAlert();

        //Show image for 4 seconds
        alertData.Duration = 4.0;

        //Remove the reference to the user to make it anonymous
        alertData.ImageText.Add(new Text("Thanks for following, ", "#FFFFFF"));
        alertData.ImageText.Add(new Text(follower));
        alertData.ImageText.Add(new Text("!", "#FFFFFF"));

        alertData.Audio.Add(new SoundEffect("SMW MessageBlock"));

        return alertData;
    }

    return new NoAlert();
}
Anonymizing Follow Script
//Default Follow Script
extern ICommunication communication;

//Used for preventing notifying on double-follow
HashSet followedUserIds = new HashSet();

IAlert GetAlertData(User follower)
{
    if (followedUserIds.Add(follower.TwitchUserId))
    {
        communication.SendPublicChatMessage($"Thanks for following!");

        StandardAlert alertData = new StandardAlert();

        //Show image for 4 seconds
        alertData.Duration = 4.0;

        //Remove the reference to the user to make it anonymous
        alertData.ImageText.Add(new Text("Thanks for following!", "#FFFFFF"));

        alertData.Audio.Add(new SoundEffect("SMW MessageBlock"));

        return alertData;
    }

    return new NoAlert();
}

Cheer Scripts

Cheer scripts require a IAlert GetAlertData(User user, Cheer cheer) method.

Cheer Data Classes
class Cheer
{
    int Quantity;
    string Message;
}
Default Cheer Script
//Default Cheer Script
extern ICommunication communication;

IAlert GetAlertData(User user, Cheer cheer)
{
    StandardAlert alertData = new StandardAlert();

    //Show image for 10 seconds
    alertData.Duration = 10.0;

    alertData.ImageText.Add(new Text(user));
    alertData.ImageText.Add(new Text($" has cheered {cheer.Quantity} {(cheer.Quantity == 1 ? " bit: " : " bits: ")} {cheer.Message}", "#FFFFFF"));

    alertData.Audio.Add(new SoundEffect("FF7 Purchase"));
    alertData.Audio.Add(new Pause(500));
    
    if (!string.IsNullOrEmpty(cheer.Message))
    {
        alertData.Audio.Add(new TTS(user, cheer.Message));

        alertData.MarqueText.Add(new Text(user));
        alertData.MarqueText.Add(new Text($": {cheer.Message}", "#FFFFFF"));
    }

    return alertData;
}

Raid Scripts

Raid scripts require a IAlert GetAlertData(User user, int raiders) method.

Default Raid Script
//Default Raid Script
extern ICommunication communication;

IAlert GetAlertData(User user, int raiders)
{
    StandardAlert alertData = new StandardAlert();

    //Send Chat Message
    communication.SendPublicChatMessage($"Wow! {user.TwitchUserName} has Raided with {raiders} viewers! PogChamp");

    //Show image for 10 seconds
    alertData.Duration = 10.0;

    alertData.ImageText.Add(new Text($"WOW! {(raiders >= 5 ? ($"{raiders} raiders") : ("raiders"))} incoming from ", "#FFFFFF"));
    alertData.ImageText.Add(new Text(user));
    alertData.ImageText.Add(new Text("!", "#FFFFFF"));

    alertData.Audio.Add(new SoundEffect("SMW CastleClear"));

    return alertData;
}

Subscription Scripts

Subscription scripts require a IAlert GetAlertData(User user, Sub sub) method.

Subscription Data Classes
class Sub
{
    int Tier;
    int CumulativeMonths;
    string Message;
}
Default Subscription Script
//Default Sub Script
extern ICommunication communication;

IAlert GetAlertData(User user, Sub sub)
{
    StandardAlert alertData = new StandardAlert();

    communication.SendPublicChatMessage($"How Cow! Thanks for the {GetTierText(sub.Tier)} sub, @{user.TwitchUserName}{GetChatMonthText(sub.CumulativeMonths)}!");

    //Show image for 5 seconds
    alertData.Duration = 5.0;

    alertData.ImageText.Add(new Text(GetImageTextIntro(sub), "#FFFFFF"));
    alertData.ImageText.Add(new Text(user));
    alertData.ImageText.Add(new Text("!", "#FFFFFF"));


    alertData.Audio.Add(new SoundEffect("SMW PowerUp"));
    alertData.Audio.Add(new Pause(300));
    alertData.Audio.Add(new TTS("Brian", "Medium", "Medium", "", $"{user.TwitchUserName} has subbed {GetTTSMonthText(sub.CumulativeMonths)}"));
    alertData.Audio.Add(new Pause(300));
    
    if (!string.IsNullOrEmpty(sub.Message))
    {
        alertData.Audio.Add(new TTS(user, sub.Message));

        alertData.MarqueText.Add(new Text(user));
        alertData.MarqueText.Add(new Text($": {sub.Message}", "#FFFFFF"));
    }

    return alertData;
}


//Helper Functions

string GetTierText(int tier)
{
    switch (tier)
    {
        case 0: return "Twitch Prime ";
        case 1: return "";
        case 2: return "Tier 2 ";
        case 3: return "Tier 3 ";
    }
}

string GetImageTextIntro(Sub sub)
{
    if (sub.CumulativeMonths <= 1)
    {
        switch (sub.Tier)
        {
            case 0: return "Thank you for the brand new Prime Gaming Sub, ";
            case 1: return "Thank you for the brand new Sub, ";
            case 2: return "Thank you for the brand new Tier 2 Sub, ";
            case 3: return "Thank you for the brand new Tier 3 Sub, ";
        }
    }
    else
    {
        switch (sub.Tier)
        {
            case 0: return $"Thank you for subscribing for {sub.CumulativeMonths} months with Prime Gaming, ";
            case 1: return $"Thank you for subscribing for {sub.CumulativeMonths} months, ";
            case 2: return $"Thank you for subscribing at Tier 2 for {sub.CumulativeMonths} months, ";
            case 3: return $"Thank you for subscribing at Tier 3 for {sub.CumulativeMonths} months, ";
        }
    }

    return "Thank you, ";
}

string GetChatMonthText(int cumulativeMonths) => (cumulativeMonths <= 1) ? "" : ($", and for {cumulativeMonths} months");

string GetTTSMonthText(int cumulativeMonths) => (cumulativeMonths <= 1) ? "" : ($" for {cumulativeMonths} months");

GiftSub Scripts

GiftSub scripts require a IAlert GetAlertData(User user, User recipient, GiftSub sub) method for giftsubs and a IAlert GetAnonAlertData(User recipient, GiftSub sub) method for anonymous giftsubs.

GiftSub Data Classes
class GiftSub
{
    int Tier;
    int Months;
}
Default GiftSub Script
//Default GiftSub Script
extern ICommunication communication;

IAlert GetAlertData(User sender, User recipient, GiftSub sub)
{
    StandardAlert alertData = new StandardAlert();

    //Show image for 5 seconds
    alertData.Duration = 5.0;

    alertData.ImageText.Add(new Text("Thank you, ", "#FFFFFF"));
    alertData.ImageText.Add(new Text(sender));
    alertData.ImageText.Add(new Text($" for gifting {GetMessageMiddleSegment(sub)} to ", "#FFFFFF"));
    alertData.ImageText.Add(new Text(recipient));
    alertData.ImageText.Add(new Text("!", "#FFFFFF"));

    alertData.Audio.Add(new SoundEffect("SMW PowerUp"));
    alertData.Audio.Add(new Pause(500));

    return alertData;
}

IAlert GetAnonAlertData(User recipient, GiftSub sub)
{
    StandardAlert alertData = new StandardAlert();

    //Show image for 5 seconds
    alertData.Duration = 5.0;

    alertData.ImageText.Add(new Text("Thank you, ", "#FFFFFF"));
    alertData.ImageText.Add(new Text("Anonymous", "#0000FF"));
    alertData.ImageText.Add(new Text($" for gifting {GetMessageMiddleSegment(sub)} to ", "#FFFFFF"));
    alertData.ImageText.Add(new Text(recipient));
    alertData.ImageText.Add(new Text("!", "#FFFFFF"));

    alertData.Audio.Add(new SoundEffect("SMW PowerUp"));
    alertData.Audio.Add(new Pause(500));
    
    return alertData;
}

string GetMessageMiddleSegment(GiftSub sub)
{
    if (sub.Months <= 1)
    {
        switch (sub.Tier)
        {
            case 0: return "a sub";
            case 1: return "a sub";
            case 2: return "a tier 2 sub";
            case 3: return "a tier 3 sub";
            default: return "a sub";
        }
    }
    else
    {
        switch (sub.Tier)
        {
            case 0: return $"{sub.Months} months";
            case 1: return $"{sub.Months} months";
            case 2: return $"{sub.Months} months of tier 2";
            case 3: return $"{sub.Months} months of tier 3";
            default: return $"{sub.Months} months";
        }
    }
}

Command Scripts

Command scripts require a void HandleMessage(User user, List<string> remainingCommand) method. The List<string> remainingCommand argument contains the chat message that triggered the command, split by whitespace, with the name of the command dropped. The chat message "!testscript Hello script" would trigger the script named "testscript" with the remainingCommand equal to {"Hello", "script"}. To collect the remaining command into one string, use string temp = string.Join(" ", remainingCommand).

Default Command Script
//Default Command Script
extern ICommunication communication;

void HandleMessage(User user, List<string> remainingCommand)
{
    communication.SendPublicChatMessage($"@{user.TwitchUserName}, Hello World");
}