Random AV Profile Projector

From Second Life Wiki
Revision as of 09:17, 24 January 2011 by Cerise Resident (talk | contribs)
Jump to navigation Jump to search

~ RANDOM AV PROFILE PICTURE PROJECTOR v5.4.5 by Debbie Trilling ~

Set-up, configuration and operation Help: http://wiki.secondlife.com/wiki/Talk:Random_AV_Profile_Projector

Profile: http://wiki.secondlife.com/wiki/User:Debbie_Trilling

KBcaution.png Important: This script relies on data scraped from world.secondlife.com. That site, and the page format it currently serves, will probably be replaced by my.secondlife.com pages in the coming months. The new pages use a different format and have different access controls, so expect this script to stop working if and when the change is made. See WEB-3510 for details.

<lsl> // ~ RANDOM PROFILE PROJECTOR v5.4.5.20091126 by Debbie Trilling ~

// *** This script randomly selects an AV from a crowd & then displays their // profile picture as texture on a prim and/or a 'holographic' image projected above the prim***

// Free to use as you wish by under condition that the title and this introduction remain in place, // and that due credit continues to be given to Moriash Moreau, Coder Kas and // Debbie Trilling.

// 2009-11-24 VD - mod to use <meta> tag, to work around WEB-1383 // 2009-11-26 VD - add fallback to <img> tag in case the profile changes are reverted

// TOUCH to switch ON and OFF

// ONLINE HELP AT: http://wiki.secondlife.com/wiki/Talk:Random_AV_Profile_Projector

// ** PARAMETERS THAT YOU CAN CHANGE **


//************************ // GENERAL PARAMETERS //************************

// listen channel for Owner integer OwnerChannel = 54321;

// set the maximum number of entries in the 'Exclude List' at any one time integer ExcludeListSize = 30;

// the number of seconds that a listen remains open before timing-out & automatically closing float DialogTimeout = 20.00;

// how often in seconds the sensor fires float RepeatTime = 35.00;

// sensor range in meters. Maximum 96m but in practice 10 to 30m because of particle draw distance float Range = 25.00;

// sets the number of consecutive times that the scanner is allowed to operate without having located an AV within range // eg: if RepeatTime = 60.0 seconds and TotalNoScansAllowed = 30, then the toy will operate for 1800 seconds (60x30, or 30 minutes) without locating // anyone before it automatically powers down. Set to '0' to disable the auto-off function integer TotalNoScansAllowed = 20;

// texture palette of UUID's. One will be randomly selected for display when an AV without a profile pic is selected list DefaultTexturePalette = ["8fb9ad84-4183-51df-f566-9b21c3a610fe", "1201c3de-d022-c0a5-56bd-bc49ab971726", "c3eebd9e-ee92-a16f-f906-bc275928df86"];

// sets whether the DefaultTexturePalette will be used to texture/project when the toy is switched OFF. 'TRUE' to texture/project; 'FALSE' to have no texturing/projection when off integer EmployDefaultTexture = TRUE;


string profile_key_prefix = "<meta name=\"imageid\" content=\""; string profile_img_prefix = "<img alt=\"profile image\" src=\"http://secondlife.com/app/image/"; integer profile_key_prefix_length; // calculated from profile_key_prefix in InitialiseObject() integer profile_img_prefix_length; // calculated from profile_img_prefix in InitialiseObject()

//************************ // PARTICLE EMISSION PARAMETERS //************************

// set to TRUE to display the profile picture as a particle 'holographic' image above the prim integer DisplayBanner = TRUE;

// width size in meters of the projected image (max 4.00) float Size = 2.50;

// height above object the centre of projected image will be (theoretical max. 50.0, in practice 2.0 to 10.0)) float Height = 2.50;


//************************ // PRIM TEXTURE PARAMETERS //************************

// set to TRUE to texture the prim with the profile picture on ALL_SIDES integer TexturePrim = TRUE;

// the following 'Prim*' parameters effect the prim only if ("EmployDefaultTexture = FALSE") OR ("EmployDefaultTexture = TRUE" and "TexturePrim=FALSE") // if TexturePrim = TRUE then the prim is automatically set to solid blank white no shiny with full bright as this is usually the best surface to display the profile picture

// texture to use for the prim when toy is OFF key PrimUUIDTexture = "5748decc-f629-461c-9a36-a35a221fe21f";

// set to TRUE to turn on Full Bright on ALL_SIDES when the toy is OFF integer PrimFullBright = FALSE;

// vector for the prim colour when toy is OFF vector PrimColour = <0.0, 0.0, 0.0>;

// set the alpha of the prim from 0.0 (clear) to 1.0 (solid) for when toy is OFF float PrimAlpha = 1.00;

// set the degree of 'shininess' to apply to the prim "0" = None, "1" = Low, "2" = medium, "3" = high integer PrimShine = 3;


//************************ // FLOATING TEXT PARAMETERS //************************ // set to TRUE for floating text above the prim; FALSE to disable the floating text integer ApplyFloatingText = TRUE;

// set the text to be displayed string FloatingText = "'Touch' for more information";

// set the text colour vector FloatingTextColour = <1.0,1.0,0.0>;

// set the text alpha float FloatingTextAlpha = 0.8;


//************************ // ROTATION PARAMETERS //************************

// if TRUE, applies a slow rotation to the prim when the toy is swtiched ON and TexturePrim = TRUE; FALSE to disable rotation integer ApplyRotation = FALSE;


//************************ // SHOUTOUT PARAMETERS //************************

// set to 'TRUE' to give a 'ShoutOut' to the AV once they have been selected; 'FALSE' for no 'ShoutOut' integer ShoutOut = TRUE;

// text to 'ShoutOut' when an AV's profile is projected. Text will be preceeded by their name, eg: "<AV Name>'s face is up in lights!" string ShoutOutText = "'s face is up in lights!";



//************************ // ** DO NOT CHANGE BELOW THIS LINE ** //************************



integer Power = FALSE; integer ListenChannel; integer OwnerListenChannel; integer NoSensorCounter = 0; key AVKey = ""; key DetectedUser = ""; key ObjectOwner = ""; key LastTexture = ""; string OwnerName = ""; string ObjectName = "Profile Projector"; string Author = "Debbie Trilling"; string Supplier = "The Particle Crucible"; string Version = " v5.4.5.20091126"; string OwnerListenText = "OpenListen"; string SelfExcludedSuffix = "**"; string PowerText = "On"; list ExcludeListing = [];


GiveShoutOut() {

   // any interaction with selected AV (give Inventory items etc) can safely be done from this function
   // this function will only execute if ShoutOut == TRUE

   //although fondly calling it a 'ShoutOut', it actually makes more sense to keep within the 20m range of llSay
   llSay(0, llKey2Name(AVKey) + ShoutOutText);

}


AnnounceWelcome() {

   llOwnerSay(
       "\nThank you for your interest in this " + ObjectName + " "
           + Version + " created by " + Author + " at " + Supplier + ". \nHelp setting up, configuring and operating the "
       + ObjectName + " can be gotton at http://wiki.secondlife.com/wiki/Talk:Random_AV_Profile_Projector \nTOUCH the " + ObjectName + " to operate it.");

}


InitialiseObject() {

   llParticleSystem([]);
   StopRotation();
   ObjectOwner = llGetOwner();
   OwnerName = llKey2Name(ObjectOwner);
   llSetObjectName(ObjectName + Version);
   llSetObjectDesc("Supplied free by " + Author + "'s " + Supplier);
   CloseAllListens();
   profile_key_prefix_length = llStringLength(profile_key_prefix);
   profile_img_prefix_length = llStringLength(profile_img_prefix);

}


SetFloatingText() {

   if (ApplyFloatingText)
   {
       llSetText(FloatingText, FloatingTextColour, FloatingTextAlpha);
   }
   else
   {
       llSetText("",<1.0,1.0,1.0>,0.0);
   }

}


ProjectTexture() {

   // are we going to use a default texture when toy is OFF?
   if (EmployDefaultTexture)
   {
       if (TexturePrim)
       {
           // using a default texture
           ApplyPrimSurface();
       }
       else
       {
           // not texturing the prim, so apply the prim preferences
           ApplyPrimPrefs();
       }
       ApplyDefaultTexture();
   }
   else
   {
       // we're not doing anything when the toy is OFF; change the prim to user preferences
       llParticleSystem([]);
       ApplyPrimPrefs();
   }

}


ApplyPrimSurface() {

   // putting texture on the prim, let's make sure it is solid white, blank. full bright
   llSetPrimitiveParams([PRIM_FULLBRIGHT, ALL_SIDES, TRUE, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, PRIM_BUMP_SHINY, ALL_SIDES, 0, 0 ]);

}


ApplyPrimPrefs() {

   llSetPrimitiveParams([PRIM_FULLBRIGHT, ALL_SIDES, PrimFullBright,
       PRIM_TEXTURE, ALL_SIDES, PrimUUIDTexture, <1.000000, 1.000000, 0.000000>, <0.000000, 0.000000, 0.000000>, 0.000000, PRIM_COLOR, ALL_SIDES, PrimColour, PrimAlpha, PRIM_BUMP_SHINY, ALL_SIDES, PrimShine, 0 ]);

}


StartUp() {

   Power = TRUE;
   PowerText = "Off";
   llSensorRepeat("",NULL_KEY,AGENT,Range,PI,RepeatTime);
   NoSensorCounter = 0;
   ApplyPrimUpdate();
   llOwnerSay("\nThe " + ObjectName + " is now switched ON. Please wait...");

}


ApplyPrimUpdate() {

   if (TexturePrim)
   {
       ApplyPrimSurface();

       if (ApplyRotation)
       {
           llTargetOmega(<0.0,0.0,1.0>, 0.2, PI);
       }
   }

}

ApplyDefaultTexture() {

   ApplySelectedTexture((key)llList2String(DefaultTexturePalette, (integer)llFrand((float)llGetListLength(DefaultTexturePalette))));

}


string RemainingExcludeSlots() {

   string RemainingSlots = (string)(ExcludeListSize - llGetListLength(ExcludeListing)) + " slots are now available.";
   return RemainingSlots;

}


string DeriveName(string messagecapture) {

   string DerivedName = llGetSubString(llStringTrim(messagecapture,STRING_TRIM),8,llStringLength(llStringTrim(messagecapture,STRING_TRIM)));
   return DerivedName;

}


integer DeriveNamePosition(string messagecapture) {

   integer DerivedNamePosition = llListFindList(ExcludeListing, (list)llGetSubString(llStringTrim(messagecapture,STRING_TRIM),8,llStringLength(llStringTrim(messagecapture,STRING_TRIM))));
   return DerivedNamePosition;

}


ApplySelectedTexture(key texture) {

   if (DisplayBanner)
   {
       // make the particle banner if required
       //core code by Moriash Moreau. Adapted to suit by Debbie Trilling
       llParticleSystem([
           PSYS_PART_FLAGS, 0,
           PSYS_SRC_PATTERN, 4,
           PSYS_PART_START_ALPHA, 0.50,
           PSYS_PART_END_ALPHA, 0.50,
           PSYS_PART_START_COLOR, <1.0,1.0,1.0>,
           PSYS_PART_END_COLOR, <1.0,1.0,1.0>,
           PSYS_PART_START_SCALE, <Size * 1.6 ,Size,0.00>,
           PSYS_PART_END_SCALE, <Size * 1.6,Size,0.00>,
           PSYS_PART_MAX_AGE, 1.20,
           PSYS_SRC_MAX_AGE, 0.00,
           PSYS_SRC_ACCEL, <0.0,0.0,0.0>,
           PSYS_SRC_ANGLE_BEGIN, 0.00,
           PSYS_SRC_ANGLE_END, 0.00,
           PSYS_SRC_BURST_PART_COUNT, 8,
           PSYS_SRC_BURST_RADIUS, Height,
           PSYS_SRC_BURST_RATE, 0.10,
           PSYS_SRC_BURST_SPEED_MIN, 0.00,
           PSYS_SRC_BURST_SPEED_MAX, 0.00,
           PSYS_SRC_OMEGA, <0.00,0.00,0.00>,
           PSYS_SRC_TEXTURE, texture]);
   }

   if (TexturePrim)
   {
       llSetTexture(texture, ALL_SIDES);
   }

}


ShutDown() {

   Power = FALSE;
   PowerText = "On";
   CloseAllListens();
   llSensorRemove();
   StopRotation();
   ProjectTexture();
   llOwnerSay("\nThe " + ObjectName + " is now switched OFF.");

}


StopRotation() {

   llTargetOmega(<0.0,0.0,0.0>, 0.0, PI);

}


CloseAllListens() {

   CloseUserListen();
   llListenRemove(OwnerListenChannel);
   OwnerListenText = "OpenListen";

}


CloseUserListen() {

   llSetTimerEvent(0.0);
   llListenRemove(ListenChannel);

}


default {

   on_rez(integer start_param)
   {
       // reset script on rez
       llResetScript();
   }

   changed( integer change )
   {
       if(change & CHANGED_OWNER )
       {
           // reset script on change of owner
           llResetScript();
       }
   }

   state_entry()
   {
       //initialise system
       InitialiseObject();
       SetFloatingText();
       ProjectTexture();
       AnnounceWelcome();
   }

   touch_start(integer total_number)
   {
       DetectedUser = llDetectedKey(0);
       list MenuItems = ["LearnMore", "GetScript", "Help"];
       string MenuText = "MAIN MENU: Please make a selection within " + (string)llFloor(DialogTimeout)
           + " seconds.\n- LearnMore: Read the forum thread on this product\n- GetScript: Get yourself the latest version of this free script\n- Help: Link to the "
           + ObjectName + " Help page";

       if (DetectedUser == ObjectOwner)
       {
           // touched by Owner

           if (Power)
           {
               MenuItems = [PowerText, OwnerListenText] + MenuItems;
               MenuText = MenuText + "\n- Off: Switch off \n- Open/CloseListen: Opens/Closes Owner listen channel " + (string)OwnerChannel;
           }
           else
           {
               MenuItems = (list)PowerText + MenuItems;
               MenuText = MenuText + "\n- On: Switch on";
           }
       }
       else
       {
           // touched by someone other than Owner. Send them a message & a dialog box of options

           // if they placed their own name on 'Exclude List', so give opportunity to clear their name, else give opportunity to exclude their name
           if (llListFindList(ExcludeListing, (list)(llKey2Name(AVKey) + SelfExcludedSuffix)) != -1 )
           {
               // they placed themself on the 'Exclude List' so are therefore allowed to clear their name
               MenuItems = (list)"IncludeMe" + MenuItems;
               MenuText =  MenuText + "\n- IncludeMe: Have the " + ObjectName + " remove your name from the 'Exclude List'";
           }
           else
           {
               // they have not placed themself on the 'Exclude List', so check whether the Owner has already done it
               if (llListFindList(ExcludeListing, (list)llKey2Name(AVKey)) == -1 )
               {
                   // neither they themself nor the Owner has previously excluded them, so give them the opportunity to exclude themselves now
                   MenuItems = (list)"ExcludeMe" + MenuItems;
                   MenuText =  MenuText + "\n- ExcludeMe: Have your name added to the " + ObjectName + "'s 'Exclude List'";
               }
           }
           llInstantMessage(DetectedUser, "\nThank you for your interest in the " + ObjectName + " created by " + Author + "\nThe dialog menu offers a number of options.");
       }
       // generate the dialog menu
       integer CommChannel = (-200000 - ((integer)llFrand(12345) * -1));
       ListenChannel = llListen(CommChannel, "", DetectedUser, "");
       llDialog(DetectedUser, MenuText, MenuItems, CommChannel);
       llSetTimerEvent(DialogTimeout);
   }


   sensor(integer total_number)
   {
       // save the AV key in case it is needed for a 'ShoutOut'
       AVKey = llDetectedKey((integer)llFrand(total_number));
       // core code by Coder Kas. Adapted to suit by Debbie Trilling
       string URL_RESIDENT = "http://world.secondlife.com/resident/";
       llHTTPRequest( URL_RESIDENT + (string)AVKey,[HTTP_METHOD,"GET"],"");
   }


   no_sensor()
   {
       // counts the number of times that the scanner doesn't find anyone in range. If TotalNoScansAllowed is set to greater than zero, automatically powers down the toy
       // when the number of no_sensors exceeds TotalNoScansAllowed. However, this functionality is disabled if TotalNoScansAllowed is set to zero.
       NoSensorCounter++;
       if ((NoSensorCounter > TotalNoScansAllowed) && (TotalNoScansAllowed > 0))
       {
           ShutDown();
           llInstantMessage(ObjectOwner, "\nThe " + ObjectName + " has been automatically switched OFF as no Agents have been detected within the set timeframe.");
       }
       else
       {
           ApplyDefaultTexture();
       }
   }


   http_response(key req,integer stat, list met, string body)
   {
       // core code by Coder Kas. Adapted to suit by Debbie Trilling
       integer s1 = llSubStringIndex(body, profile_key_prefix);
       integer s1l = profile_key_prefix_length;
       if(s1 == -1)
       { // second try with img tag
           s1 = llSubStringIndex(body, profile_img_prefix);
           s1l = profile_img_prefix_length;
       }
       if(s1 == -1)
       {
           // selected AV doesn't have a profile picture, so use the default instead
           ApplyDefaultTexture();
       }
       else
       {
           key NewTexture = (key)llGetSubString(body,s1+s1l,s1+s1l+35);

           //check whether this was the texture used last time
           if (NewTexture == LastTexture || NewTexture == NULL_KEY)
           {
               // same profile pic as last time. so display a random default instead
               ApplyDefaultTexture();
               // clear the last texture out
               LastTexture = "";
           }
           else
           {
               // are they on the ExcludeListing, with or without a suffix? if so, display a random default texture instead
               if ((llListFindList(ExcludeListing, (list)llKey2Name(AVKey)) != -1 ) || (llListFindList(ExcludeListing, (list)(llKey2Name(AVKey) + SelfExcludedSuffix)) != -1))
               {
                   // they are on the 'Exclude List'
                   ApplyDefaultTexture();
               }
               else
               {
                   // different profile from last time & not on ExcludeListing, so display it
                   ApplySelectedTexture(NewTexture);
                   // save the key for comparison purposes next time tho'
                   LastTexture = NewTexture;

                   // give a 'ShoutOut', if set to do so
                   if (ShoutOut)
                   {
                       GiveShoutOut();
                   }
               }
           }
       }
   }


   listen(integer channel, string name, key id, string message)
   {

       if (message == "LearnMore")
       {
           string URL_FORUMTHREAD = "http://forums-archive.secondlife.com/54/1b/225692/1.html";
           llLoadURL(DetectedUser, "Thank you for choosing to learn more about the " + ObjectName + ".This link will take you to the relevant SL forum thread.", URL_FORUMTHREAD);
           CloseUserListen();
       }
       else if (message == "GetScript")
       {
           string URL_WIKIPAGE = "http://wiki.secondlife.com/wiki/User:Debbie_Trilling";
           llLoadURL(DetectedUser, "Thank you for choosing to look at the script for the " + ObjectName + ".This link will allow you to get your own free copy.", URL_WIKIPAGE);
           CloseUserListen();
       }
       else if (message == "Help")
       {
           string URL_HELPPAGE = "http://wiki.secondlife.com/wiki/Talk:Random_AV_Profile_Projector";
           llLoadURL(DetectedUser, "This link will take you to the " + ObjectName + "'s Help page.", URL_HELPPAGE);
           CloseUserListen();
       }
       else if (message == "ExcludeMe")
       {
           // as a suffix to the name, as it is the user adding their own name
           ExcludeListing = (list)(llKey2Name(DetectedUser) + SelfExcludedSuffix) + llList2List( ExcludeListing, 0, (ExcludeListSize - 2));
           llInstantMessage(DetectedUser, "You have been added to the " + ObjectName + "'s 'Exclude List'");
           CloseUserListen();
       }
       else if (message == "IncludeMe")
       {
           integer ExcludeListPosition = llListFindList(ExcludeListing, (list)(llKey2Name(DetectedUser) + SelfExcludedSuffix));
           ExcludeListing = llDeleteSubList(ExcludeListing, ExcludeListPosition, ExcludeListPosition);
           llInstantMessage(DetectedUser, "You have been removed from the " + ObjectName + "'s 'Exclude List'");
           CloseUserListen();
       }
       else if ((message == "On") && (id == ObjectOwner))
       {
           StartUp();
       }
       else if ((message == "Off") && (id == ObjectOwner))
       {
           ShutDown();
       }
       else if ((message == "OpenListen") && (id == ObjectOwner))
       {
           // open the Owner Only channel
           OwnerListenChannel = llListen(OwnerChannel, "", ObjectOwner, "");
           OwnerListenText = "CloseListen";
           llOwnerSay("Owner Only channel " + (string)OwnerChannel + " is now open for you.\n Options are: 'Exclude <AV_NAME>', 'Include <AV_NAME>', 'ClearAll' and 'List'");
       }
       else if ((message == "CloseListen") && (id == ObjectOwner))
       {
           // close the Owner Only channel
           llListenRemove(OwnerListenChannel);
           OwnerListenText = "OpenListen";
           llOwnerSay("Owner Only channel " + (string)OwnerChannel + " is now closed.");
       }
       else if (((llGetSubString(message,0,6) == "Exclude") || (llGetSubString(message,0,6) == "exclude")) && (id == ObjectOwner))
       {
           // before adding them to the 'Exclude List', check if already on it
           // they could be on the list simply as their own name, their name + a suffix, perhaps even both.

           if ((DeriveNamePosition(message) != -1 ) || (DeriveNamePosition(message + SelfExcludedSuffix) != -1))
           {
               llOwnerSay(DeriveName(message) + " already exists on the " + ObjectName + "'s 'Exclude List'.");
           }
           else
           {
               // not on the list, so add them (without a suffix, as it is the Owner doing the adding)
               string NewExcludeName = DeriveName(message);
               ExcludeListing = (list)NewExcludeName + llList2List( ExcludeListing, 0, (ExcludeListSize - 2));
               llOwnerSay(NewExcludeName + " has been added to the "
                   + ObjectName + "'s 'Exclude List'. \nThere are now " + RemainingExcludeSlots());
           }
       }
       else if (((llGetSubString(message,0,6) == "Include") || (llGetSubString(message,0,6) == "include")) && (id == ObjectOwner))
       {
           // they could be on the list simply as their own name, their name + a suffix, perhaps even both. We need to test for all scenerios
           // locate their position within in 'Exclude List', if they do exist
           integer NamePositionTest = DeriveNamePosition(message);
           integer NamePositionSuffixTest = DeriveNamePosition(message + SelfExcludedSuffix);

           // test to see if either are on the 'Exclude List'.
           if ((NamePositionTest != -1) || (NamePositionSuffixTest != -1))
           {
               // well, their name is definately on the 'Exclude list', but is it with or without a suffix? Is it both?
               // would look neater to do the next tests in an IF-IF/ELSE-ELSE, but we can save a lil memory using two IF statement (albeit with a tiny speed overhead)
               if (NamePositionTest != -1)
               {
                   // it's on without a suffix
                   ExcludeListing = llDeleteSubList(ExcludeListing, NamePositionTest, NamePositionTest);
               }

               if (NamePositionSuffixTest != -1)
               {
                   //it's on with a suffix
                   ExcludeListing = llDeleteSubList(ExcludeListing, NamePositionSuffixTest, NamePositionSuffixTest);
               }
               llOwnerSay(DeriveName(message) + " has been removed from the "
                   + ObjectName + "'s 'Exclude List'. \nThere are now " + RemainingExcludeSlots());
           }
           else
           {
               llOwnerSay(DeriveName(message) + " could not located on the " + ObjectName + "'s 'Exclude List'.");
           }
       }
       else if (((llGetSubString(message,0,7) == "ClearAll") || (llGetSubString(message,0,7) == "clearall") || (llGetSubString(message,0,8) == "clear all") || (llGetSubString(message,0,8) == "Clear all")) && (id == ObjectOwner))
       {
           ExcludeListing = [];
           llOwnerSay("The 'Exclude List' has been cleared. All " + RemainingExcludeSlots());

       }
       else if (((llGetSubString(message,0,3) == "List") || (llGetSubString(message,0,3) == "list")) && (id == ObjectOwner))
       {
           if (llGetListLength(ExcludeListing) > 0)
           {
               llOwnerSay("The following " + (string)llGetListLength(ExcludeListing) + " AV's are listed on the 'Exclude List'. Names ending with '" + SelfExcludedSuffix + "' chose to exclude themselves.\n"
                   + llDumpList2String(ExcludeListing, " | "));
           }
           else
           {
               llOwnerSay("No AV's are listed on the 'Exclude List'.");
           }
       }
       else
       {
           llInstantMessage(DetectedUser, "Unrecognised option or selection made from a timed-out menu.");
       }

   }

   timer()
   {
       CloseUserListen();
   }

//default end }

</lsl> Bold text