User:SuzannaLinn Resident/ScriptingClasses/Reading Notecards

From Second Life Wiki
Jump to navigation Jump to search

Reading Notecards

Monday class

How the information is stored in SL

Today we will use something that we haven't seen in my classes yet: asynchronous function calls.

Before explaining what is that, let's see how information is organized in secondlife.

Please rezz the "Object 1a Saying names" that is in the Class Materials and open the "Script 1a saying names".

The script stats with a list of uuid's of avatars. There is a function that loops in this list and says the displaynames of the uuid's, called from the event touch.

Touch the box to see it working.

There is a strange thing here. It's not saying the name of the second uuid in the list, which is the uuid of my alt.

It happens that llGetDisplayName() only works with uuid's of avatars that are in the region. If the avatar is in another region or offline the functions returns "".

The same happens with llGetUsername() and llKey2Name(). Also with llGetAgentSize(), llGetAgentInfo() and llGetObjectDetails().

Let's see why.

There is a central database with all the information about objects, avatars, script, notecards, textures, everything. All is stored there: when we save a script, or a notecard, or build a new object. It's a huge database with all SL in it.

And each region has its own database with the information that the region needs: objects that are in the region and people who is currently in the region.

When we teleport, our avatar, the attachments and HUDs that we are wearing, the scripts in these attachments and HUDs, are deleted from the previous region and loaded into the new region.

The region database is fast to access, and many of the info that we need is there. We can use a function, for instance llGetDisplayName() or llGetObjectDetails(), and we receive the data inmediately.

But the central database is slow to access. And our script would have to wait for several 1/10 of second, which is a long time from a script point of view.


Synchronous and asynchronous functions

Here is where the asynchronous functions appear.

The asynchronous function to get the display name is:

  • llRequestDisplayName( avatarId )

with the uuid of the avatar as parameter.

When we call one of these asynchronous functions, the function doesn't return us the info that we want but sends a request for it.

Internally, our request is sent to the central database. And our script will go on executing the next commands.

But we want our info. Where is it?

We will receive it in the event "dataserver", that will be triggered when the data arrives from the central database.

The parameters of the event are:

  • dataserver(key queryid, string data)

later we will see what is "queryid"

data is the info requested, always comes as a string, we will typecast it to the right type.

Why this thing with the dataserver event and not just sending us the data?

Because meanwhile the data arrives, our script can be doing other things.

The functions that we have been using until now are all "synchronous", which means "same time", because we get the returned value at the same time.

"asynchronous" means "different time", because the value will arrive later.

Imagine that, in RL, we need to ask something to a friend.

We take the phone and call our friend, if the friend answers we get the information immediately. This is synchronous.

If our friend doesn't answer the phone, we send a message. And we don't wait for the answer, we go on with our lives.

After some time, the phone will warn us that we have a message from our friend. This is asynchronous.

We need to do an important rewritting in our script.

Our function to say the names would not work with llRequestDisplayName().

After calling llRequestDisplayName() we don't have any name to say yet.

We need to split the function in two: the part that requests the name (a function that we call getName() ), and the part that says the name (that we call sayName() ).

It works like this:

  • getName() calls llRequestDisplayName() that sends the request to the central database
  • the event dataserver is triggered and there we call sayName()
  • sayName() says the name

And now the variables are global, because we are using them in several functions or events.

We do this process for each uuid in the list of people.

sayName() calls again getName() to repeat the process for the next uuid in the list, until we arrive to the end of list.

We don't send all the requests together because in the event dataserver would be difficult to know which name goes with which uuid.

Because perhaps we will not receive the data in the same order that we have requested it.

Each request, internally, is an internet connexion between the computer that is managing the region and the computers in the central database. It can take more or less depending on traffic. And perhaps our second request could go faster and arrived before our first request.

Please rezz the "Object 1b Saying names with async" that is in the Class Materials and open the "Script 1b saying names with async".

Touch the object to see that now it says all the names.

Let's look at the script.

In lines 4-8 we have the variables that now has to be global because we are using them in getName(), setName() and the event dataserver.

totalPeople is the total of uuids in the list.

currentPerson is the index in the list that we are working on.

personId is the uuid that we are working on.

And displayName to store the name when we get it.

The process starts in touch_start, lines 30-34.

We initialize totalPeople and currentPerson and call getName() to start the process of the first uuid.

getName() is in lines 10-18.

We try to get the display name in the usual way, in case that the avatar is in the region.

If we get the name, we call sayName() to say it, in line 14.

If not, we request the name in line 16.

We could do other things, because the event dataserver with the answer will take some time to arrive.

But we don't have anything else to do, so the scripts stops and gets idle.

After a long time (from the script point of view), about half a second, the event dataserver is triggered.

dataserver is in lines 36-39.

The information is in the parameter "data".

We set displayName and call setName() to say it.

sayName() is in lines 20-26. It's called by getName() when the avatar is in the region, and by dataserver when not.

We say the name, increase the index, and, if there are more uuid's, we call getName() to repeat all the process for the next one.


More on asynchronous functions

Now let's improve our script. We will say the display name and also the username.

The asynchronous function to get the username is llRequestUsername(). We will request for both the display name and the username.

But, how do we know in the event dataserver if we are receiving the display name or we are receiving the username?

The llRequest*** functions returns a value.

We get a value of type "key", a UUID, unique for this call to this function. Each time that we call the a llRequest*** function we will receive a different value. And we will store this value in a variable named, for instance, myRequestQueryId.

In the event dataserver we receive in the parameter queryid this same UUID that the function returned to us when we called it, and that we stored in the variable myRequestQueryId.

We can be sending other requests, where we will get different UUID's as query id's.

And in the event dataserver we will compare our "id" variables with the parameter queryid to know what information we are receiving.

We compare the dataserver queryid with our stored id in the dataserver event:

	dataserver( key queryid, string data ) {
    		if ( queryid == myRequestQueryId) {
        			llSay( 0, data);  // data has the info that we requested, use it wisely
   	 	}
	}

Please rezz the "Object 1c Saying names with two async" that is in the Class Materials and open the "Script 1c saying names with two async".

Touch the object to see that now it says all the names an usernames.

Let's look at the script.

The structure of the script is the same and the calls work in the same way.

In lines 8-9, we have added the global variables displayNameQueryId and usernameQueryId to store the uuid's of the requests.

And in line 10 the variable username to store the username.

In getName(), lines 14-26, we request for the display name and the username, in lines 23-24, storing the request uuid's returned by the functions.

In the event dataserver, lines 44-53, we compare the parameter queryid with our id variables in lines 45 and 47, to store the information.

In line 50 we check if we have got both names to call sayName().

And in sayName(), lines 28-34, we add the username to the message.


Reading a notecard

And it happens that the notecards are stored in the central database. So we will need to use an asynchronous function to read them. And several times, because we can't read a notecard with one function, we have to read it line by line.

Please rezz the "Object 2 Reading notecard" that is in the Class Materials folder and open the "Script 2 reading notecard" that is in the same folder.

In this example we are only reading the notecard. We don't have anything else to do while we wait for the central database to answer. We would prefer to have a function that returns us the info, and to have our script waiting for it. But no way to do it, so let's get asynchronous :)

Our strategy in this example is:

  • We ask for the first line of the notecard, and stop here.
  • We will receive the event dataserver with the first line, we store this line, and we ask for the next one.
  • We will reveive the next line, store, ask for next... until we arrive to the end of the notecard.

Let's see how it looks in the script:

In line 2 there is the variable that we will use to store the ID of the query: key notecardQueryId;

And in line 4 the list where we will keep the notecard text: list notecardText;

The function readNotecard() in lines 7-17, that we call in the event state_entry, looks for the first notecard in the object contents and asks for the first line. If there isn't any notecard it says a message. We have already seen the inventory functions in a previous class.

The function to ask for a line in the notecard is llGetNotecardLine() and its parameters are the name of the notecard and the line number that we want, starting with line 0. We assign the return value, the query id, to notecardQueryId.

We use llGetNotecardLine() in line 13 to get the first line. And nothing else until we receive the event dataserver.

Let's go to line 34 to see the event dataserver.

In line 35 we compare our variable notecardQueryId with the parameter queryid. It wouldn't be necessary in this example. We request info one by one and there is only a query going on. But it's a good practice to do it. We will see an example of two queries in our Info Board.

When we ask for a notecard line that doesn't exist it returns the constant EOF (meaning "End Of File"). So we check for it in line 36.

If it is not EOF we add the line to our list, increase the line counter, and request the next line. When EOF we go to say the notecard on lines 19-26.

As you probably already know, lsl script is not a perfect language.

The issue with notecards is that they can't be read if they have items included, like landmarks, textures... You can try it later, it returns 0 lines.

And it's not possible to write a notecard with a script, only to read them.

Now drop a notecard (without any items included!) in your box and see how it says its text to you. If it's a long notecard it can take several seconds.


Answers to questions

Do we get the information from the llFunctions from a sim server ?

  • All synchonous llFunctions get their information from the sim the script is in. They can't learn anything that the sim itself doesn't know.
  • Communcation with the data server where the database is kept is asynchronous, meaning the script has to send a request to the remote server and wait for a reply that will come some indefinite time later. In the meantime, while it waits, the script can go on to do other stuff.

Are our textures are stored on the central database?

  • Yes, the textures and everything else.

Is because of the copy of attachments and scripts from one region to another that it takes a while for our clothes to rez when we TP?

  • Yes, because our attachments and scripts are loading in the new region.
  • In that case most of the communication is between the sending and receiving sims though, not the central database.

Woulld that be affected by a huge inventory?

  • No, it depends on what we are wearing, not on the size of our inventory

If we delete a texture other people can still use it, Is a texture stored somehow redundant?

  • A texture is stored only once, with its id, in the central database.
  • This id is also in the object that has the texture in, and in the inventories of the people who have the texture.
  • The texture is not deleted while its id is somewhere, in some object, or in some inventory.
  • Our inventory, internally, is a list of id's to the object, texture, everything that is stored in the central database.
    • If we build a new object, and then delete it, also from the trash folder, the object is deleted.
    • But if we have given it to a friend before deleting it, the object is not deleted, because our friend has an id of the object.
    • The object will be deleted if our friend deletes it too.
  • Internally, in the central database, with each object, texture, etc, there is a count of how many active id's to it there are, if this count goes down to 0, the item is deleted.

So each object has its own memory?

  • An object has a list of id's of its contents, not the contents themselves.

So synchronous will come from direct communication with the same region we are in and we don't need anything in between? and asynchronous will request the info that is not immediately available?

  • Yes, right

Will the responses to our requests arrive in the same order?

  • No. That, in general is a feature of asynchonous commmunication. Unless you wait for a response each time, you cannot depend on replies arriving in the order requests were sent. Much of the time they will be in order, but if you depend on it, it will reach out and bite you. For instance SL's dataservier is actually most likely several servers working together, with a load manager queuing requests to the server with the least number of outstanding requests. But some requests may take more time than others, so two requests arriving close together may be queued to different machines and it becomes a race to see which one is handled first.

Will llList2Key() give us the username, and llGetDisplayName() will transform that username to the display name, but both use the personId?

  • No, llList2Key() get one of the uuid from the list of people and stores it in personId, then we use personId with llGetDisplayName().
  • Don't confuse llList2Key() with llKey2Name() that gives us the legacy name.

So the first time you click the box, it may take some time to respond. But after that, it responds immediately.

  • It takes a few time for each avatar in the list people who is not here, about half a second.
  • Changing the list people with a long lists of avatars (who are not in the region), we will see their names being said one every half a second or so.

Where, if there are more uuids, do we repeat the process?

  • We repeat the process in sayName(), lines 22-25.
  • We add one to the index (currentPerson) and check it with the list length (totalPeople).
  • If we are not at the end of the list we call getName() again in line 24.

So if totalPeople in the list is bigger than currentPerson we call getName() over and over?

  • Yes, we repeat the sequence getName - dataserver - sayName for each uuid in the list.

Are all the aynchronous requests processed?

  • Yes, all the requests are answered, the time of answer depends on how busy are the servers in the central database, usually is tenths of second.

Do we have to request the names separately?

  • Yes, llRequestDisplayName() only allows us to ask for the name of one avatar, so we need to request them one by one.

Does other scripts receive the same response in their events dataserver?

  • Since the dataserver event occurs in all scripts in the same prim, if there are unrelated scripts in te prim, it is possible to receive dataserver events that aren't for our script. These can simply be ignored. It is also possible to have the requests made In one or more scripts and have the responses handled in another script working in cooperation with them.

Could we get an answer that should be for another object?

  • No, the event response is to the same prim, not even another link in the same linkset will see our events.

What is &&?

  • && means "and".
  • || means "or".
  • They are logical operators to check several conditions together, for instance:
    • if (displayName != "" && username != "")
  • The "if" is true if both displayname and username are not empty.

Why do we use && instead of &? Do they have different meanings in scripting?

  • Yes, they are different.
  • && is the logical "and", that we use with conditions that are TRUE or FALSE.
  • & is the bitwise "and", that we use with two integers that are bit values.
  • It makes sense if you look at the bits. For the logical operations such as && all that matters is zero and not zero. For the bitwise operations, each bit position is compared independently of the others, so in binary 1 is 01, 2 is 10, and 3 is 11: 01 & 10 == 00, 01 & 11 == 01.

Can you give us an example of &?

  • Let's look at it in the event changed:
changed(integer change) {
        if (change & (CHANGED_OWNER | CHANGED_INVENTORY))
            llResetScript();
}
  • the parameter "change" is a set of bits, each possible change has a different bit assigned:
    • CHANGED_OWNER has a bit value of 0000 0001.
    • CHANGED_INVENTORY is 1000 0000.
  • | is an "or" bit by bit. If one of the two bits is 1 the result is 1. If the two bits are 0 the result is 0.
        0000 0001        CHANGED_OWNER
      | 1000 0000        CHANGED_INVENTORY
-------------------------------------------------------------
        1000 0001        ( CHANGED_OWNER | CHANGED_INVENTORY)
  • & is an "and" bit by bit. If the two bits are 1 the result is 1. If one of the two bits is 0 the result is 0.
        1000 0001        ( CHANGED_OWNER | CHANGED_INVENTORY)
      & 1000 0000        change, if the inventory has changed
---------------------------------------------------------------------
        1000 0000        change & (CHANGED_OWNER | CHANGED_INVENTORY)
    • the if condition is TRUE because the result is a non-zero value.
  • in case of another change, for instance CHANGED_LINK:
        1000 0001        ( CHANGED_OWNER | CHANGED_INVENTORY)
      & 0010 0000        change, if the linked prims have changed
---------------------------------------------------------------------
        0000 0000        change & (CHANGED_OWNER | CHANGED_INVENTORY)
    • the if condition is FALSE because the result is zero.
  • Using && and || all these operations would have a TRUE result, because && and || only check if the integer is 0 (FALSE) or not 0 (TRUE).

Does llGetNotecardLine() return an key?

  • Yes, it's an asynchronous function, and returns a key to be used in the dataserver event to compare with the parameter queryid.

Could we read only parts of the text?

  • We are reading lines from 0 to the end, we can read only some lines if we know what numbers of lines we want.


Wednesday class

Reading a notecard with llGetNotecardLineSync

We studied on Monday that notecards are stored in the central database.

We need to use asynchronous functions to read them, line by line, and we receive the text of the lines in the event dataserver.

This is the traditional way to read notecards, that we have been using since the old times.

Early this year a new function was added to LSL to read notecards faster:

  • llGetNotecardLineSync( notecardName, notecardLine)

It has the same parameters than llGetNotecardLine(), the function that we used on Monday.

But the new function reads the lines from the region database, not from the central database, and we get the text of the line in return, not a request id.

llGetNotecardLineSync() is a synchronous function, as its name says.

But there is a problem... how can llGetNotecardLineSync() read the notecards from the region if they are stored in the central database?

Regions has some memory reserved to store notecards, as a cache memory.

And we need to get our notecard stored in the region before using llGetNotecardLineSync().

To store the notecard in the region we need to call an asynchronous function on notecards, like llGetNumberOfNotecardLines() or llGetNotecardLine().

The memory in the region to store notecards is limited. When more notecards are stored, the older ones are removed from the region.

So, if other objects in the region are reading notecards, our notecard could be removed while we are reading it.

If llGetNotecardLineSync() doesn't find the notecard stored in the region, it returns the constant NAK.

We need to look for NAK in each answer, and if it happens, use the asynchronous llGetNotecardLine() instead. We will get the text of the line in the event dataserver, and the notecards stored in the region again.

Please rezz the "Object 3 Reading Notecards sync" that is in the Class Materials folder.

Let's look to the script.

In touch_start, lines 15-21, we look for the first notecard in the contents.

If there is one we read the first line of the notecard in the asynchronous way, as we did on Monday.

With this we get the first line and we get the notecard stored in the region.

The new function is in the event dataserver, lines 23-39.

First we check if it is the answer to our request, comparing the parameter "request" with our variable "requestLineId", in line 24.

The reading happens in the loop do ... while in lines 25-34.

do ... while is like a loop while, but with the condition at the end. This loop is always executed at list once.

We use this loop because we want to process the data before checking the condition.

We start checking is the data is EOF, meaning that we are trying to read a line after the end of a notecard.

If is not EOF, we have the text of the line in data.

We add the text to the list "notecard" and we increase the number of line.

Now we read the next line using llGetNotecardLineSync() and we get the answer immediately.

If should be the text of the line, but it could happen that in meanwhile our notecard has been removed from the memory of the region.

In line 30, we check is the answer is NAK.

If it is NAK we go back to the asynchronous method, requesting the line with llGetNotecardLine().

This request will trigger another event dataserver, so we will not do anything else in the current event.

Let's look at the while in line 34. It handles different situations.

  • In case of EOF, we have finishing reading the notecard, we finish the loop.
  • In case of NAK, our notecard is not in the region, we can't use the sync function, and we have requested, in line 31, the line with the async function.
  • In case of NAK, we are finishing the loop too, but the event will be triggered again when we receive the requested line, and the loop will go on again, from the line where it was, which is in the global variable notecardLine.
  • If it is not EOF neither NAK, it means that we have read the line with the sync function, and we stay in the loop to read the next line.

If we have finished the do ... while with EOF, lines 35-37, we call list Notecard().

If we have finishied the do ... while with NAK, we do nothing, the loop will restart with the next line when the event is triggered again when we receive the requested line.

listNotecad() is in lines 5-11.

We loop on the lines of the notecard to say them.

Touch the sphere to see it working.


Finding text in a notecard with llFindNotecardTextSync

This is a vey new function, added some weeks ago.

llFindNotecardTextSync() looks for a text in a notecard and returns a list with the different lines and positions where the text appears in the notecard.

As it name says, it's a synchronous function. So it needs that the notecard is stored in the region before using it.

If has five parameters:

  • the name of the notecard
  • the text that we want to look for
  • usually 0, we can use another value to discard some matches, for instance 3 will start with the 4th match.
  • how many matches we want to get, the maximum is 64.
  • and empty list, it's reserved to add other options in the future

So we call the function like this:

  • lFindNotecardTextSync( notecardName, text, 0, 64, [] );

It returns a strided list, with:

  • Row
  • Column
  • Length

If it finds 64 matches, the maximum, the list will have 192 items.

Please rezz the "Object 4 Finding Text Sync" that is in the Class Materials folder.

Let's look at the script.

In lines 2-5, we have contants for the stride and the items in the stride.

This way we don't need to remember them, and the script is more clear using names instead of numbers.

In touch_start, lines 27-30, we call our function findText() with "or", which is a text that is repeated several times in the example notecard.

All the process is done in findText(), lines 7-23.

We have a do ... while here too, lines 11-16.

We start trying the sync function in line 12.

Usually we will get a NAK the first time. We check for it in line 13, and call an asynchronous function to get the notecard stored in the region.

We don't need the number of lines, so we don't store the request id that it returns, and we don't have an event dataserver.

When the answer to llGetNumberOfNotecardLines() arrives, it will be discarded, because there is no event dataserver to trigger.

In line 16, if it has been a NAK, we loop again, and this time we will get the answer.

Using found == [NAK] is "wrong", but it works well.

The operator == with two lists only compares the length of the lists, not their contents.

It works because if it is a NAK, the length is 1, otherwise the length is never 1, but 0, or 3, or 6...

In lines 17-22 we say the information, in what lines and positions "or" has been found.

Touch the sphere to see it working.


Both together: reading poems

Now we are going to see a more practical use of the llFindNotecardTextSync(), combined with llGetNotecardLineSync().

The idea of the function to find text is to allow us to have plenty of read only data stored in notecards, without need to read the full notecards in memory (or in the linkset data, that we will study next week).

For instance, long configuration notecards, where we can look for the section that we want, and read only the lines in this section.

Or a notecard with many usernames, where we can look if a name is there or not, without reading any line.

Or a book, to read only a chapter. Or a dictionary, to read only the definition of a word.

In this example we have a notecard with some poems, and we will read one, chosen by the owner of the object.

Please rezz the "Object 5 Reading Poems" that is in the Class Materials folder.

Touch the sphere to get a list of the poems. Say the number of poem that you want in channel 1, like /1 5 and it says the poem.

Look at the notecard "Poem" to see how it is organized. All poems written by ChatGPT.

First we have a line with <LIST>, the titles of the poems, and another line with <LIST>.

We will look for the text <LIST> to find the titles, which are in the lines between the two lines <LIST>.

Below we have all the poems, starting with its title between <> and ending with again with the title.

To read a poem we will look for <title_of_the_poem>, and again the lines of the poem will be between these two lines.

Let's look at the script.

isList, in line 7, is a TRUE/FALSE variable. We use it to know if we are reading the list of poems or a poem.

isReading, in line, is also a TRUE/FALSE variable. We set it to TRUE when we are reading a notecard, and we check it in the touch_event, to avoid that the owner can touch again while we are reading.

We look for the text in findText(), lines 17-28, as we did in the previous script.

In lines 25-26, we store the first and last line of the poem in global variables.

We read the list of poems in readLines(), lines 30-43, with the same process than the previous script.

notecardLine already has the line to start, and we read until the line to end.

We say the list of poems, or the poem, in notecardReady(), lines 45-61, that is called after finishing to read the notecard.

In touch_start, lines 71-80, we start reading the list of poems.

In listen, 82-96, we start reading the poem chosen by the owner.

And in dataserver, lines 98-110, we receive the asynchronous request and call again readLines() to read in sync, or notecardReady() if we receive an EOF.


Answers to questions

Are the synchronous functions faster because they are coming from the region as compared to the central database?

  • Yes, that is the reason why they are faster.

Does llGetNotecardLineSync() tell the central server to send the notecard to the sim server?

  • No, we have to make the request, using an asynchronous function: llGetNotecardLine() or llGetNumberOfNotecardLines().

Can we rely on an entire notecard being present just because a part of it is?

  • We cannot rely on an entire notecard being present.
  • We need to check for NAK each time that we read a line with llGetNotecardLineSync().

So the safest way would be asynchronous and the fastest synchronous?

  • No, the best way is try Sync and fall back to Async when it fails.
  • Sync is the faster way, and its also safe if we script it properly:
    • When we get a NAK we request the line Async and then the notecard will be stored in the region, and we will go on with Sync.
  • We will be mostly using the Sync version, but we need to know the Async version, because we need to use Async requests too.

What size is the storage for notecards in the region?

  • LL recently changed the caching protocol. For over 14 years, it used to store 45 notecards regardless of size. Now it stores a set size in bytes. (48 * 65536) + 10 or 3145738 bytes, so chances of a notecard getting removed while reading from the cache are significantly reduced, but not impossible.
  • Caching didn't come with the llGetNotecardLineSync() function. It's been around for years, but didn't originally exist when the async functions first came. It was added a little later to prevent them from always contacting the dataserver for every line read.
  • The async version is slower than the sync version only because the request has to go through the request and return event process.

Is the chunk size such that an entire notecard will be removed or retained as a unit?

  • Yes, an entire notecard's data is removed upon cycling, we won't be able to get any line via sync.
  • But we cannot trust that all the notecard is there, because it could be removed while we are reading it.
  • Our code is basically unchanged because it is always possible for the notecard to be decached between reads.

A notecard is stored in the object, the region and the data server?

  • A notecard is stored in the central database, accessible to Async funtion and dataserver.
  • The notecard is temporarily copied to the region for a faster access.
  • In the object is stored the uuid of the notecard, not a copy of it, the notecard's contents are never stored in the object.

Is the object storage part of the sim storage?

  • No, the objects has its own storage, but it only stores uuid's, not the items themselves. The same happens with our inventory, it only has uuid's.
    • Object Inventory exists in the Sim only as a collection of named references / UUIDs that point to the actual item's data in the inventory servers.
    • Nothing in our inventory, or an objects inventory has presence in the sim, until it is rezzed or other reference causes it to be pulled in.

Do we try to get the notecard on the sim server first to check if there are any lines?

  • We request the first line Async, and with this we also get the notecard stored in the region, next lines we will try to read with Sync, if there aren't any lines we will get an EOF.

Do we store the lines in a list?

  • Yes, we store all the notecard in a list in the script.

What does it mean NAK?

  • Negative AcKowledge.
  • ACK and NAK are terms for complementary responses to a request in general. ACK means got the request and will fulfill it. NAK means got the request and won't/can't fulfill it.
  • Only NAK is used in LSL scripts.

Could we combine two or more lines to print out at a time instead of one at time?

  • Yes, up to 1024 bytes, all llSay functions cut the text to 1024 bytes, some characters take more than 1 byte.
    • If a multi-byte char is truncated, it is padded with a "?" per truncated byte.

Is there a function that takes a string and returns the byte usage?

  • No, there is no ll function for it, we can use this user function:
integer UTF8ByteSize(string str) {
    return ((3 * llSubStringIndex(llStringToBase64(str) + "=", "=")) >> 2);
}

Does llSay uses UTF-8?

  • LSL script memory uses UTF-16, all characters take at least 2 bytes, some take 4.
  • llSay functions use UTF-8, all strings in chat/comm are UTF-8, the usual characters take 1 byte, but some characters take 2, 3 or 4 bytes.
  • Linkset Data uses UTF-8.

What is the maximum size of a notecard?

  • The maximum size of a notecard is 64k
  • Lines have a maximum of 1024 characters
  • We could save lines longer than 1024 in a notecard, but then we couldn't read them, reading lines are cut to 1024.

Does it matter if we use capitals or not to look for the text?

  • Yes, it will only find the text if it is written in the same lettercase.
  • llFindNotecardTextSync(), instead of text, can use a regex pattern or regular expression, with wildcards and commands, and this way we can search in a case insensitive way.

What are the parameters in llFindNotecardTextSync()?

  • llFindNotecardTextSync( notecardName, text, 0, 2, [] )
    • The name of the notecard.
    • The text that we are looking for.
    • 0 means starting with the first match.
    • 2 means returning two matches (the two first ones), because we are only looking for the <> label before and after the poem.
    • [] is an empty list for a parameter that is not used, reserved for future versions of the function.

Why we do this?

  • if ( data == "" ) data = " ";
    • When the data is empty "", we change it to a white space " ".
    • We do it because the llSay() functions doesn't say empty lines, if we want them to write a blank line, we need to say a space.
    • The same happens with floating texts, empty lines are ignored.


Saturday class

Finding a text in a notecard and reading the lines where the text is

Please rezz the "Object 6 Find Text Sync with RegEx" that is in the Class Materials folder.

The script listens to the owner in channel 1 for a text to search in the notecard. The object has an example notecard with a variety of things to look for.

We use llFindNotecardTextSync() to find the text in the notecard and llGetNotecardLineSync() to read the lines where the text has been found.

We only read the lines with the text, not all the notecard. We say the text found in the line and don't store the line.

Let's look at the script.

We will follow the script in the order that it is executed, starting with state_entry, lines 73-83.

We look for the first notecard in the contents.

If there is one we start the listener and set isReading to FALSE.

We use isReading to check if we are already processing a text, that has been said by the owner in channel 1.

We will set isReading to TRUE when processing a text, and we will not take more texts in the event listen.

If there isn't a notecard we say a message and set isReading to TRUE, so the script will not take texts to search. Mo way to search, without a notecard.

In tne event listen, lines 85-91, we receive the text from the owner and start the search.

We check if the channel is the CHANNEL_TEXT, which is 1. No need to do it here, because we are only listenen to this channel, but it's a good practice in case that in the future we add more channels.

We also check that we are not reading. The process is fast enough to finish before the owner has time to write another text, but it's good to do it, just in case.

If all is ok, we set isReading to TRUE and the message to the global variable "text".

And we call findText() to look for the text.

Let's go to find text, lines 18-32. Here we look for the matches and store them in the global list "found".

We don't know if the notecard has been stored in the region. The first time will not be there, next times probably yes.

So we start trying with llFindNotecardTextSync() in line 20.

In its parameters, the first 0 is the match where we want to start, the first one.

The second 0 is the maximum of matches that we want to find. 0 means no maximum, so it will return up to 64, which is the limit of the function.

We are not doing it in this script, but we could check if the function returns 64 matches (or 192 items) and call the function again starting with match 65th (index 64 starting with 0).

  • found = found + llFindNotecardTextSync(notecardName, text, 64, 0, []);

If the notecard is not stored in the region we will get a list with one element NAK.

We check if the first element of the list is NAK and we call and asynchronous function to get the notecard copied to the region, llGetNumberOfNotecardLines(). We don't need the number of lines but is the way to get the notecard.

In case of NAK we don't do anything else in this function. We will call findText() again from the event dataserver.

Going to dataserver in lines 93-105.

We have stored the request in requestNotecardId . In line 94 we check if this the request answered and call again findText() , now with the notecard copied to the region.

Back in findText() lines 18-32.

Now we have got our list "found" and the process is in line 24.

We store the totals of items in foundLength which is a global variable to be used in several functions.

And we set first match in foundIndex , another global variable. We will increase this index by the stride of 3 to follow all the list "found".

It could happen that there are no matches. We check for it in line 26.

If there is something found we go to readLine() to read the first line, if not we go to finnish() to say to the owner that the process has finished.

And findText() has finished its task, now it's the turn of readLine().

readLine() is in lines 34-49.

We use a do...while to read the first line and then loop to the next lines until the end of the list "found".

It's very very likely that we have the notecard in the region and that we read all the lines with llGetNotecardLineSync().

But we need to be sure, and in case of a NAK we will call llGetNotecardLine() and get the line in the event dataserver.

We start getting the line in the notecard from the list "found".

And we try to get the line in Sync, line 39. If not, we call the Async function, line 41.

In case of NAK we will not do anything else in this function. We will call it again from the event dataserver to go on reading lines.

So let's go back to dataserver.

Now we are storing the request uuid in requestLineId . We check for it in line 97.

In line 98 we call sayLine() to say the match to the owner. We will see this function in a moment.

And if we haven't arrived to the end of the list we call readLine() to go on reading lines. Otherwise we call finish().

Let's go back to readLine()

If we have got the line we go to sayLine() in line 43.

sayLine() is in lines 51-64.

We have another do...while here. If the text has been found several times in the same line we will say all of them in this loop.

In lines 57-60 we get the line in the notecard, the position of the line, then length of the text found, and the text. And we say this info to the owner.

If we are using a regular expression, the text found will not be the same than the text with the regular expression that we have said.

We increase foundIndex , the index that we are using to follow the list "found", by the stride of 3.

We leave the loop, line 63, if the next match is in another line, or we have arrived to the end of the list "found".

And going back to readLine().

We are at the end of the loop do...while, in line 45.

We leave the loop is we have arrived to the end of the list, or in case of NAK.

In case of NAK we will call the function again in the event dataserver to go on reading lines.

If we have arrived to the end of the list "found", line 46, we call finnish().

finnish() is in lines 66-69.

We say a message to the owner with the total of matches found.

And we set isReading to FALSE to accept more texts in the event listen.


Regular expressions

A regular expression, often shortened to regex, is a way to describe patterns in text. It’s like a search tool that helps us find, match, or manipulate specific pieces of text based on defined rules.

A regex is made up of symbols and characters that describe what you’re looking for, like "any digit," "a space," or "a word."

We can find very specific things in text, like "all email addresses" or "dates in the format MM/DD/YYYY."

The complete "Regular Expression Cheat Sheet" is here: https://wiki.secondlife.com/wiki/LlFindNotecardTextSync#Notes

Let's try some examples to see the different expressions in action.

Match all dollar amounts

  • \$[0-9]+\.[0-9]{2}

Matches a dollar sign (\$) followed by one or more digits ([0-9]+), a dot (\.), and exactly two digits ([0-9]{2}).

The characters that has also a meaning as expressions, like $ or ., has to be escaped with a \.

[ and ] contains a range of characters (separated by -) or a list of possible characters (without -).

{ and } contain the quantity of characters, or an interval of quantities.

+ means 1 or more of the previous characters, * means 0 or more, ? means 0 or 1.

Find order numbers starting with #

  • #\d+-\d+

Matches # followed by one or more digits (\d+), a hyphen (-), and more digits.

\d means a digit, \l is a lower case character,\u is an uppercase, \w is alphanumeric plus underscore, \s is any whitespace, tabs and newlines.

Search for email addresses

  • [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

Matches valid email formats (e.g., support@example.com).

{2,} means 2 or more of the previous character, {2} is 2, {2,5} is from 2 to 5.

Match dates in the format Month DD, YYYY

  • [A-Z][a-z]+ \d{1,2}, \d{4}

Matches a capitalized month name ([A-Z][a-z]+), followed by one or two digits for the day (\d{1,2}), a comma, and a four-digit year (\d{4}).

Find times in the format HH:MM AM/PM

  • \d{1,2}:\d{2} (AM|PM)

Matches one or two digits for the hour (\d{1,2}), a colon, two digits for minutes (\d{2}), and either "AM" or "PM."

( aaaa | bbb ) means either aaa or bbb.

Extract all product model numbers

  • [A-Z]{2}-\d{3,4}

Matches two uppercase letters ([A-Z]{2}), a hyphen, and 3 to 4 digits (\d{3,4}).

Capture name prefixes like "Mr." or "Ms."

  • (Mr|Ms|Mrs|Dr)\.

Matches titles like "Mr.", "Ms.", "Mrs.", or "Dr." by using a group (( )) and the | for alternation.

All words starting with "A" or "a"

  • \b[aA]\w*\b

\b: Matches a word boundary, ensuring the match is at the start or end of a word.

[aA]: Matches an uppercase or lowercase "A".

\w*: Matches zero or more word characters (letters, numbers, or underscores).

\b: Matches the boundary at the end of the word.

Extract all phone numbers

  • \(\d{3}\) \d{3}-\d{4}

Matches phone numbers in the format (555) 123-4567. It uses \(\d{3}\) for the area code and \d{3}-\d{4} for the rest.

Match all prices

  • \$[0-9]+\.[0-9]{2}

Matches lines with a $ (\$) one or more digits ([0-9]+) a point (\.) and two digits (0-9]{2}).

Lines with items ordered

  • ^[[:blank:]]*\d+\.[^$]*\$\d+\.\d+

the double [ and ] contains a named type between :, blank is any space character except a newline. Other named types are "alpha", "digit", "control", etc.

^ at the beginning of the expression means "lines starting with...", $ at the end of the expression means "... is at the end of the line"

[^ and ] means any character except this, [^$] is any character except the $.


Answers to questions

What kind of data is stored on region servers and on global server?

  • On global server is everything, on region server all the data of the avatars and objects that are in the region.

Is all the data that is stored on region server also stored on global server?

  • Yes, global server has all the originals, and region server has a copy of what it needs.

Do region servers have a copy so we can have faster access to information we need?

  • Yes, it is.
  • From our point of view there are basically 3 kinds of servers we deal with, the Simulators, each of which is responsible for all the interactions among objects that are "rezzed in world" in that Sim. This includes running scripts, local communications, and all the physical interactions like movement and collision. Then there are the Data Servers, which is a collection of computers that keep track of more global dynamic data, some of which our scripts are allowed to access--things like who is online (can access), where they are (can't) and so on. Then there are the Asset Servers, the keepers of (possibly) data about everything ever made in SL, specifically all items in our inventories and in the inventory of every object in the world. There are also other servers like the Login Manager and Chat Managers, but our scripts have nothing to do with these.
  • So what happens when we read a notecard? We send a query to the Data Servers, it is queued and eventually a Data Server responds. If the Data Servers don't have that notecard's content in their current dataset, they request the notecard data from the Asset Servers, which pass the data to the Data Servers, and the Data Server responding to us pass it back to the Simulator, which stores it in a buffer and raises an event in our script and we get our notecard line.

If we have the script reading notecards and the notecard located in the same object, does the script still has to read data about the notecard from the server?

  • Yes, because the notecard is not located in the object, the object only has the uuid of the notecard.
  • While in the Object the notecard's data is on the Asset Servers and won't even get as far as the Data Servers until we make that first request. The Simulator's responsibllity is limited to things In World and explicitly exclude things In User or Object Inventories.

Could the script read notecards located in other objects in the same region?

  • No, we cannot access notecards in other objects by name, we would need its uuid.
  • The functions that look in object inventory to find a thing by name, only look in the inventory of the prim they are in.

How can we find notecards uuid?

  • There is a function that we haven' t seen yet, llGetInventoryKey( itemName ) that returns a key with the uuid, only if the item is in the contents and it is full perm.

Could we find the uuid of a notecard located in the other object and then read that notecard using its uuid?

  • No, we can't read the uuid's of items that are in other objects

Is it possible to read the notecard located in the other object if the script has the uuid of the notecard?

  • Yes, if the other object has a script that sends the notecard uuid , or we you do it manually it is possible.
  • But remember that if we edit the notecard its uuid changes. Notecards can't be edited, when we "ediit" them, we create a new notecard.

If the script has a notecard uuid and the notecard is located in my inventory and not in the script's objects folder, can it still be read ? Or it has to be inside a object that is rezzed within the same region as script?

  • Yes, it can read. The notecards are not stored in our inventories, they are stored in the central servers, our inventories have only their uuid's.
  • But only if the notecard has full perms.
  • And if the notecard doesn't have items in it. Notecards that include items, like landmarks, objects, textures, anything, are not readable, in any way.

Why is the function called llRequestNumberOfNotecardLines if it returns an uuid?

  • A more correct name would be llRequestNumberOfNotecardLines, because many asynchronous functions start with llRequest***.
  • If LSL functions had a consistent naming convention, all such functions should be named llRequestSomething and reserve the llGetSomething functions for things that return an immediate value.

Is the uuid of the request a kind of the secret password we have to give the dataserver to receive the answer to our request?

  • No, its not a password, we will receive the answer in the event dataserver without using the uuid, but we need it to check which answer is, in case that we have made more than one request.
  • It's like the label on a package telling us which package it is.

Do we only need to check once if the notecard is cached on the sim server?

  • Yes, this is what is expected, that we need only one check and then the notecard will be there.
  • But it could happen that our notecard is removed from the region storage while we are reading it, so we need to check it every time.
  • There is a fixed space of memory in the region to store notecards. if there are many scripts reading different notecards, the region storage would run out of space. When a new notecard is requested, if there is no space, the first notecards are removed.

When I did a search for the dollar sign character, it returned the whole notecard. How do I get it to recognize the search for actual dollar signs?

  • Some characters have a meaning as command for the regex, like the $. We need to escape them with a \. To look for $ in the notecard use \$.
  • $ is a special character in Reg Ex. To search for special characters precede them with a backslash, so \$.
  • It is safe to precede any non alphanumeric symbol with a backslash so you don't have to remember all the special characters to use it.

Right now, the text to be searched is regarding upper- and lowercase letters. Is there a possibility to turn that off? So we find the text regardless of lower- and uppercase?

  • Yes, for instance with [Pp]rice , characters between [ and ] are alternative characters,, any of them matches the search.
  • For case insensitive searches we can start our search string with (?i) including the parentheses (?i)price .