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:
testInteger0
would be0
testInteger1
would still be1
testInteger2
would be2
testDouble1
would still be1.0
Supported Control Flow Elements
The following control flow elements are supported within the scripting language:
if
,else if
,else
statementsfor
loopsforeach
loopswhile
loopsswitch
statements
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)
: sendsmessage
to twitch chat, as the bot.void SendChatWhisper(string username, string message)
: whispersmessage
to 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)
: Sendsmessage
to your debug console and web interface as a standard informational message.void SendWarningMessage(string message)
: Sendsmessage
to your debug console and web interface as a warning message, highlighted yellow.void SendErrorMessage(string message)
: Sendsmessage
to 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 underkey
as typeT
. The object is deserialized as json.void SetDatum<T>(string key, T value)
: savesvalue
of typeT
underkey
as 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 underkey
as typeT
. The object is deserialized as json.void SetGlobalDatum<T>(string key, T value)
: savesvalue
of typeT
underkey
as a global datum. The value is serialized as json.User GetUserByTwitchLogin(string userName)
: returns theUser
data associated with this TwitchuserName
.User GetUserByTwitchId(string userId)
: returns theUser
data associated with this TwitchuserId
.List<User> GetAllUsersWithDatum(string key)
: returns data of allUser
s 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 then
th 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 then
th item in the container.
List<T>
Methods:
void Add(x)
: appends itemx
to the end of the container.void Insert(n, x)
: inserts itemx
into the container such that it is then
th item, shifting following items down.bool Remove(x)
: removes the first appearance of itemx
from the container, and returns whether the operation removed an item.void RemoveAt(n)
: remove then
th item in the container.bool Contains(x)
: returns whether the container contains itemx
.int IndexOf(x)
: returns the first index where itemx
appears, or-1
if 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 Enqueue
d is the first one Dequeue
d, 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 itemx
to 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 Push
ed is the next one Pop
ed, 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 itemx
to 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 then
th item in the container.
RingBuffer<T>
Methods:
void Add(x)
: Adds itemx
to the head of the container.void Push(x)
: Adds itemx
to 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 itemx
appears in the container.bool Remove(x)
: removes the first appearance of itemx
from the container if it exists, and returns whether the operation removed an item.void RemoveAt(n)
: removes the item at indexn
from the container.void Resize(n)
: resizes the container to have a total capacity ofn
.int CountElement(x)
: returns the number of times itemx
appears 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 itemx
to the end of the container, as active.bool Remove(x)
: removes the first appearance of itemx
from 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 itemx
in the container as inactive if it exists, and returns whether the operation succeeded.bool DepleteAllValue(x)
: marks all active appearances of itemx
in the container as inactive if any exist, and returns whether the operation succeeded.bool RefreshValue(x)
: marks the first inactive appearance of itemx
in the container as active if it exists, and returns whether the operation succeeded.bool RefreshAllValue(x)
: marks all inactive appearances of itemx
in 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 itemx
to the container, as active.bool Remove(x)
: removes the first appearance of itemx
from 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 itemx
in the container as inactive if it exists, and returns whether the operation succeeded.bool DepleteAllValue(x)
: marks all active appearances of itemx
in the container as inactive if any exist, and returns whether the operation succeeded.bool RefreshValue(x)
: marks the first inactive appearance of itemx
in the container as active if it exists, and returns whether the operation succeeded.bool RefreshAllValue(x)
: marks all inactive appearances of itemx
in 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 itemx
to the container if it did not already exist, and returns whether the operation added an item.bool Remove(x)
: removes itemx
from 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 itemy
to the container under keyx
.bool Remove(x)
: removes item stored under keyx
from 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:
async
andawait
methods and invocationsout
,ref
, andparams
method 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");
}