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:
testInteger0would be0testInteger1would still be1testInteger2would be2testDouble1would still be1.0
Supported Control Flow Elements
The following control flow elements are supported within the scripting language:
if,else if,elsestatementsforloopsforeachloopswhileloopsswitchstatements
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:
void SendPublicChatMessage(string message): sendsmessageto twitch chat, as the bot.void SendChatWhisper(string username, string message): whispersmessageto twitch userusername. Note: Twitch will frequently block or refuse to forward whispers from bots. Generally you should be cautious using this.void SendDebugMessage(string message): Sendsmessageto your debug console and web interface as a standard informational message.void SendWarningMessage(string message): Sendsmessageto your debug console and web interface as a warning message, highlighted yellow.void SendErrorMessage(string message): Sendsmessageto your debug console and web interface as an error message, highlighted red.
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:
bool HasDatum(string key): returns whether the user has a datum stored under thekey.T GetDatum<T>(string key): returns the user datum stored underkeyas typeT. The object is deserialized as json.void SetDatum<T>(string key, T value): savesvalueof typeTunderkeyas a datum for this user. The value is serialized as json.
IScriptHelper features
To make use of the IScriptHelper, add "extern IScriptHelper scriptHelper;" to your script.
IScriptHelper Methods:
bool HasGlobalDatum(string key): returns whether a global datum is stored under thekey.T GetGlobalDatum<T>(string key): returns the global datum stored underkeyas typeT. The object is deserialized as json.void SetGlobalDatum<T>(string key, T value): savesvalueof typeTunderkeyas a global datum. The value is serialized as json.User GetUserByTwitchLogin(string userName): returns theUserdata associated with this TwitchuserName.User GetUserByTwitchId(string userId): returns theUserdata associated with this TwitchuserId.List<User> GetAllUsersWithDatum(string key): returns data of allUsers who have a datum stored underkey.
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:
int Length: The size of the array.
T[] Indexer:
T [n] { get; set; }: accesses thenth item in the array.
List<T>
A List is a flexible collection of data. It supports collection initialization syntax.
List<T> Properties:
int Count: The number of items in the container.
List<T> Indexer:
T [n] { get; set; }: accesses thenth item in the container.
List<T> Methods:
void Add(x): appends itemxto the end of the container.void Insert(n, x): inserts itemxinto the container such that it is thenth item, shifting following items down.bool Remove(x): removes the first appearance of itemxfrom the container, and returns whether the operation removed an item.void RemoveAt(n): remove thenth item in the container.bool Contains(x): returns whether the container contains itemx.int IndexOf(x): returns the first index where itemxappears, or-1if it does not.void Clear(): empties the container.
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:
int Count: The number of items in the container.
Queue<T> Methods:
void Enqueue(x): appends itemxto the end of the container.T Dequeue(): returns the item from the front of the container, and removes it from the container.T Peek(): returns the item from the front of the container, but leaves it in the container.bool Contains(x): returns whether the container contains itemx.void Clear(): empties the container.
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:
int Count: The number of items in the container.
Stack<T> Methods:
void Push(x): Adds itemxto the top of the container.T Pop(): returns the item from the top of the container, and removes it from the container.T Peek(): returns the item from the top of the container, but leaves it in the container.bool Contains(x): returns whether the container contains itemx.void Clear(): empties the container.
RingBuffer<T>
RingBuffers have limited capacity. Adding items past this capacity bumps the oldest value out of the RingBuffer.
RingBuffer<T> Properties:
int Count: The number of items in the container.int Size: The total capacity of the container.T Head: The item most recently added to the container.T Tail: The item oldest item in the container.
RingBuffer<T> Indexer:
T [n] { get; set; }: accesses thenth item in the container.
RingBuffer<T> Methods:
void Add(x): Adds itemxto the head of the container.void Push(x): Adds itemxto the head of the container.T Pop(): returns the item from the head of the container, and removes it from the container.T PopBack(): returns the item from the tail of the container, and removes it from the container.T PeekHead(): returns the item from the top of the container, but leaves it in the container.T PeekTail(): returns the item from the tail of the container, but leaves it in the container.bool Contains(x): returns whether the container contains itemx.int GetIndex(x): returns the first index where the itemxappears in the container.bool Remove(x): removes the first appearance of itemxfrom the container if it exists, and returns whether the operation removed an item.void RemoveAt(n): removes the item at indexnfrom the container.void Resize(n): resizes the container to have a total capacity ofn.int CountElement(x): returns the number of times itemxappears in the container.void Clear(): empties the container.
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:
int Count: The number of active items in the container.int TotalCount: The number of items (active and inactive) in the container.bool AutoRefill { get; set; }: Whether the container marks all items active when all items are depleted and aPopNext()is attempted.
DepletableList<T> Methods:
T PopNext(): returns the next active item in the container, and sets it as inactive.void Add(x): appends itemxto the end of the container, as active.bool Remove(x): removes the first appearance of itemxfrom the container if it exists, and returns whether the operation removed an item.bool Contains(x): returns whether the container contains itemx, whether active or inactive.bool DepleteValue(x): marks the first active appearance of itemxin the container as inactive if it exists, and returns whether the operation succeeded.bool DepleteAllValue(x): marks all active appearances of itemxin the container as inactive if any exist, and returns whether the operation succeeded.bool RefreshValue(x): marks the first inactive appearance of itemxin the container as active if it exists, and returns whether the operation succeeded.bool RefreshAllValue(x): marks all inactive appearances of itemxin the container as active if any exist, and returns whether the operation succeeded.void Reset(): marks all items as active.void Clear(): empties the container.
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:
int Count: The number of active items in the container.int TotalCount: The number of items (active and inactive) in the container.bool AutoRefill { get; set; }: Whether the container marks all items active when all items are depleted and aPopNext()is attempted.
DepletableBag<T> Methods:
T PopNext(): returns a random active item from the container, and sets it as inactive.void Add(x): adds itemxto the container, as active.bool Remove(x): removes the first appearance of itemxfrom the container if it exists, and returns whether the operation removed an item.bool Contains(x): returns whether the container contains itemx, whether active or inactive.bool DepleteValue(x): marks the first active appearance of itemxin the container as inactive if it exists, and returns whether the operation succeeded.bool DepleteAllValue(x): marks all active appearances of itemxin the container as inactive if any exist, and returns whether the operation succeeded.bool RefreshValue(x): marks the first inactive appearance of itemxin the container as active if it exists, and returns whether the operation succeeded.bool RefreshAllValue(x): marks all inactive appearances of itemxin the container as active if any exist, and returns whether the operation succeeded.void Reset(): marks all items as active.void Clear(): empties the container.
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:
int Count: The number of items in the container.
HashSet<T> Methods:
bool Add(x): adds itemxto the container if it did not already exist, and returns whether the operation added an item.bool Remove(x): removes itemxfrom the container if it exists, and returns whether the operation removed an item.bool Contains(x): returns whether the container contains itemx.void Clear(): empties the container.
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:
int Count: The number of items in the container.IEnumerable<TKey> Keys: An enumeration of all of the keys.IEnumerable<TValue> Values: An enumeration of all of the values.
Dictionary<T> Indexer:
TValue [x] { get; set; }: accesses the value stored under keyx.
Dictionary<T> Methods:
bool Add(x, y): adds itemyto the container under keyx.bool Remove(x): removes item stored under keyxfrom the container if it exists, and returns whether the operation removed an item.bool ContainsKey(x): returns whether the container contains an item stored under keyx.bool ContainsValue(x): returns whether the container contains an item with valuex.void Clear(): empties the container.
Unsupported Elements
The following are unfortunately unsupported at this time:
asyncandawaitmethods and invocationsout,ref, andparamsmethod arguments- reference nullability checking and primitive nullable types
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");
}