A simple script to handle simple conversation trees. Example conversation tree notecard is below the script.
///////////////////////
// User Variables //
///////////////////////
// The name of the note with the conversation tree.
string note = "conversation";
// The following variable determines if conversations
// Are exclusive to the person who initiates them.
integer exclusive = TRUE;
//////////////////////////
// System Variables //
//////////////////////////
// List layout
// How a line and conversation are arranged
integer STRIDE = 5; // How many parts to a line
integer STATE = 0; // The state of this command
integer NEXTSTATE = 1; // The next state after this command
integer TRIGGER = 2; // The trigger string
integer MESSAGE = 3; // The text message
integer COMMAND = 4; // The link command
// The current section of the notecard.
list currTriggers;
list currMessages;
list currNextStates;
list currCommands;
// The entire contents of the notecard. Could get large.
// state, goto, trigger, message, command
list conversation;
// Others
list tempL;
integer i;
integer j;
integer l;
integer max;
key k;
key user = NULL_KEY;
/////////////////
// Functions //
/////////////////
// Replaces all occurances of find in src with rep
string replace(string src, string find, string rep)
{
// Save our selves some work in an easy double edge case
if (src == find) return rep;
// Prevent infinite loops when find is in rep
else if (llSubStringIndex(rep,find) != -1) return src;
// Look for find in src
l = llSubStringIndex(src,find);
// While there are still find in src
while (l != -1)
{
// It is at the begning of src
if (l == 0)
src = rep + llGetSubString(src,llStringLength(find),- 1);
// It is at the end of src
else if (l == llStringLength(src) - llStringLength(find))
src = llGetSubString(src,0,0 - llStringLength(find) - 1) + rep;
// It must be in the middle of src
else
src = llGetSubString(src,0,l - 1) + rep +
llGetSubString(src,l + llStringLength(find),-1);
// See if there are more
l = llSubStringIndex(src,find);
}
return src;
}
// Set the conversation state.
// Note this is a 'fake' state, not an LSL state.
// Conversation is assumed to be sorted by STATE
integer setState(integer s)
{
// Grab a list of the states
tempL = llList2ListStrided(conversation,0,-1,STRIDE);
// Find the first occurance of the state s
i = llListFindList(tempL,[(string)s]);
// If the state doesn't exist, go to the zero state
if (i == -1)
{
i = 0;
s = 0;
}
// Find the first occurance of the state s + 1
j = llListFindList(tempL,[(string)(s + 1)]);
// If there is no next state, must be at the end of the list
if (j == -1) j = 0;
// We want through the last of s, which is right before s + 1
j = j - 1;
// Convert i and j to locations in conversation
i = i * STRIDE;
j = j * STRIDE;
// Pull the chunks out of conversation into their own sections.
currTriggers = llList2ListStrided(
llList2List(conversation,
i + TRIGGER,
j + TRIGGER),
0,-1,STRIDE);
currNextStates = llList2ListStrided(
llList2List(conversation,
i + NEXTSTATE,
j + NEXTSTATE),
0,-1,STRIDE);
currMessages = llList2ListStrided(
llList2List(conversation,
i + MESSAGE,
j + MESSAGE),
0,-1,STRIDE);
currCommands= llList2ListStrided(
llList2List(conversation,
i + COMMAND,
j + COMMAND),
0,-1,STRIDE);
return s;
}
//////////////
// States //
/////////////
default
{
state_entry()
{
// We listen to everything
llListen(0,"","","");
llWhisper(0,"...setting up system...");
i = 0;
conversation = [];
k = llGetNotecardLine(note,i++);
}
listen(integer chan, string name, key id, string mes)
{
if ( (exclusive && ( user == NULL_KEY || user == id)) || !exclusive)
{
// Cycle through every current trigger
max = llGetListLength(currTriggers);
for (i=0;i<max;i++)
{
// Look for any occurance of trigger in the line said
j = llSubStringIndex(mes,llList2String(currTriggers,i));
// We found one!
if (j != -1)
{
// Whisper the response, replacing -@- with players name.
llWhisper(0,replace(llList2String(currMessages,i),"-@-",name));
// If there is a link message command, give it.
if(llList2String(currCommands,i) != "NULL")
{
llMessageLinked(LINK_SET,
llList2Integer(currNextStates,i), // Next State
llList2String(currCommands,i), // Command
id); // Speaker
}
// Set the next state and user.
if ( setState((integer)llList2String(currNextStates,i)) ) user = id;
else user = NULL_KEY;
// Exit early since we found a response.
return;
}
}
}
}
// Detect a change to trigger a reset of the conversation
// This could be replaced by another reset mechanism
changed(integer change)
{
if (change & CHANGED_INVENTORY)
{
llResetScript();
}
}
// We stuff every line in the notecard into
// conversation with the exception of comments
dataserver(key query, string data)
{
if(query == k)
{
k = NULL_KEY;
// Done reading the card
if (data == EOF)
{
// Sort in chunks of STRIDE by STATE
llListSort(conversation,STRIDE,TRUE);
// All conversations start in state 0
setState(0);
// Yay, setup is done.
llWhisper(0,"...setup complete...[" +
(string)(llGetListLength(conversation) / STRIDE) +
" lines :: " + (string)llGetFreeMemory() +
"b free]");
}
// Ignore blank lines and comments
else if (llGetSubString(data,0,1) == "//" || data == "")
{
k = llGetNotecardLine(note,i++);
}
// A 'real' line
else
{
// Parse the line to a list
tempL = llParseString2List(data,["-=-"],[]);
// If this line is the correct length
if (llGetListLength(tempL) == STRIDE)
{
//llWhisper(0," -=- " + llList2CSV(tempL));
conversation = conversation + tempL;
k = llGetNotecardLine(note,i++);
}
// If it is short, we assume no link message
else if (llGetListLength(tempL) == STRIDE - 1) // No Link Message
{
conversation = conversation + tempL + ["NULL"] ;
k = llGetNotecardLine(note,i++);
}
// The line is not formatted right.
else
{
llWhisper(0,"ERROR: line " +
(string)(i - 1) + ", [" + data + "]");
}
}
}
}
}
Here is an example conversation notecard. It goes on a notecard named "conversation" in the same object as the above script. This note will cause a link message to be sent at the begning and end of the conversation - for simple conversations this is totally unnecesary.
// State, Goto State, Trigger, Message
// This is a response that will never leave the 0 level, you can ask this as much as you want
0-=-0-=-Are you a computer?-=-Yes, I am!
// This starts an actual conversation, and moves to the next conversation level
0-=-1-=-Hello-=-Hi, -@-!-=-StartingConversation
// This is the next level of conversation
// The AI will only respond to these after someone says 'Hello'
1-=-1-=-What color is the sky?-=-Why, -@-, do you know, I think it's blue!
// This will end the conversation by going back to the 0 level
1-=-0-=-Bye-=-Ok, -@-!-=-EndingConversation
Another approach to scripting natural language AI and natural language interaction with commands is through an AIML like markup language. An example of such a markup would be lindenAIML (see
LibraryLindenAIML).
Script Library |
Examples