About

The SL Developer’s Corner is my place in the virtual world “Second Life”, where i experiment and tinker with the features of this platform. I try to publish my experiences and often also include the source code of the scripts on this site. Other posting related to Second life can be also found on Happy Coding 2nd-life section.

Friday 30 March 2007

The Second Life "Twitter Wristlet"

I have created a little accessory for the Twitter-service in SL. It allows you to move around in the world with the "stylish" possibility to send some updates to twitter. I call it the "Twitter Wristlet", because its a wristlet. ;)

The only differences between the wristlet and my other fixed Twitter-service are the following:

  1. The wristlet doesn't create objects on which the tweets (status-updates) are displayed. I just prints them to screen using llOwnerSay.
  2. Because it can be very disturbing to have half of the screen full of Twitter-updates, you can stop/start the timer which receives the tweets.
Note: When you are on land where running scripts is not allowed, it won't work. Does anyone know how to bypass this? Or is it impossible?

The LSL-code for the wristlet looks like that. I use the Chatchannel 998 for communicating with the wristlet.

key requestId;
list resultList;
string rawList;
float refreshTime = 30.0;

default
{
state_entry()
{
integer i;
rawList = "";
llSetTimerEvent(refreshTime);
llListen(998, "", llGetOwner(), "" );
}

listen(integer channel, string name, key id, string message) {
if(message == "#stop#") {
// stops the automatic refresh of the tweets
llSetTimerEvent(0.0);
llOwnerSay("The automatic refresh has been stopped. Reactivate using #start#");
} else if(message == "#start#") {
llSetTimerEvent(refreshTime);
llOwnerSay("Refresh will taking place every " + (string)refreshTime + " seconds. Stop using #stop#");
} else {
llOwnerSay("trying to send your status message: " + message);
llHTTPRequest("###YOUR RUBY URL###/twitter/post_message",[HTTP_METHOD,"POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"],"msg="+message);
llOwnerSay("ok, status has been sent.");
}
}

timer() {
llOwnerSay("loading tweets...");
requestId = llHTTPRequest("###YOUR RUBY URL###/twitter/get_messages?type=public",[HTTP_METHOD,"GET"],"");
}

http_response(key request_id, integer status, list metadata, string body) {
integer i;

if (request_id == requestId) {
rawList = body;

// create the boxes from the list information
resultList = llParseString2List(body,["\n"],[]);
integer listlength = llGetListLength(resultList);
float boxPosition = 1;

for(i=0;i<listlength;i+=1) {

string statusLineWithName = llList2String(resultList, i);
list statusParts = llParseString2List(statusLineWithName, ["|"], []);
string text = llList2String(statusParts, 0);
string name = llList2String(statusParts, 1);
llOwnerSay(name + ": " + text);
}

} else {
// llSay(0,(string)status+" error");
}
}
}

Sunday 18 March 2007

Second Life Twitter

In my last posting i introduced my Twitter-application in Second Life. Now i want to tell something about the implementation of it.
First of all, there is a little Ruby on Rails server application working in the background, which fulfills two tasks:

  1. Authenticate in Twitter and get the new public updates.
    The idea is to parse the xml-result of Twitter and extract the update-text and the author of this text. And after that to build a compact string seperated by delimiters.
  2. Authenticate in Twitter and post a new personal update
Twitter provides for this task some XML-interfaces at http://twitter.com/help/api.
For XML-processing in Ruby we will use the great REXML-processor.

The following code gets the public-updates.

def get_messages

# get public updates
# doesnt need authentication actually but no problem
# if its in the code. but i integrate it, because i want
# to integrate also private/friends updates later
urlStr = 'http://twitter.com/statuses/public_timeline.xml'

url = URI.parse(urlStr)
req = Net::HTTP::Get.new(url.path)
req.basic_auth ###username###, ###password###

res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }

@content = res.body
resultStr = ""

case res
when Net::HTTPSuccess, Net::HTTPRedirection

# build a dom from the xml-string and parse it
xmlDoc = REXML::Document.new(@content)

# building the delimiter string for the later in-world processing
# every update is line-separated and the name and text in each update
# is delimited with the pipe-character ('|')
xmlDoc.elements.each("statuses/status") do |status|
resultStr += status.elements["text"].get_text.value + "|" + status.elements["user/name"].get_text.value + "\n"
end
render :text => resultStr

else
render :text => 'error'
end

end
###username### and ###password### have to be replaced with your Twitter username and password.

For posting a new update i use the following code.

def post_message

# get the message as parameter
message = params[:msg]

# authenticate using basic-auth and make a post request using
# the 'parameter' for the new message. see twitter API for
# details
url = URI.parse('http://twitter.com/statuses/update.xml')
req = Net::HTTP::Post.new(url.path)
req.basic_auth ###username###, ###password###
req.set_form_data({'status' => message})

res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }

# print out some little message in case of failing
# actual just for debugging purpose, because it wont be used
# in the application later
case res
when Net::HTTPSuccess, Net::HTTPRedirection
render :text => 'ok'
else
render :text => 'error'
end

end

Ok, now with this application on a running RoR-Server we can get the updates from Twitter and are able to send new updates to Twitter.

In Second Life the idea is, to have a little box which creates for every Twitter-update a little sphere which displays the update-text and the author. When the spheres are created they should be updated every n seconds (n=30 in this example).
So we have actually two prims:
  1. The main box
    which handles the connection to the Rails-application, creates the spheres, links them together and sends a message to every sphere if an update occurs.
  2. The sphere
    Listen for messages and updates its text.
For the sphere we have the following code:


integer childNumber;
string text;
string name;

default
{
// if the prin is created print a message and sets the number of the
// child. this is used for later accessing the message parts
on_rez(integer start_param) {
llSetText("creating... ", <1,1,1>, 1.0);
childNumber = start_param;
}

// if a link message is received, get the parts of the delimited string and
// print them above the prim
link_message(integer sender_num, integer num, string str, key id) {
llSetText("updating... ", <1,1,1>, 1.0);
list resultList = llParseString2List(str,["\n"],[]);

string statusLineWithName = llList2String(resultList, childNumber);
list statusParts = llParseString2List(statusLineWithName, ["|"], []);
text = llList2String(statusParts, 0);
name = llList2String(statusParts, 1);
llSetText(name + " is doing the following right now:\n" + text, <1,1,1>, 1.0);
}
}

The main Twitter-prim script-code looks like this:


key requestId;
list resultList;
string rawList;
integer firstRun;

default
{
state_entry()
{
integer i;
rawList = "";
firstRun = 1;
// set permissions so the prim is able to link the objects
llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
// update every 30 seconds
llSetTimerEvent(30.0);
// listen on a channel so that the owner can post updates
llListen(4001, "", llGetOwner(), "" );
}

// if the owner wants to submit an update to Twitter
listen(integer channel, string name, key id, string message) {
llSay(0, "trying to send your status message: " + message);
// post the received chat-message to our server.
// use a special mimetype for submitting post-variables
llHTTPRequest("###YOUR RAILS URL###/post_message",[HTTP_METHOD,"POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"],"msg="+message);
llSay(0, "ok");
}

// request the new updates
timer() {
llSay(0, "loading statuses...");
requestId = llHTTPRequest("###YOUR RAILS URL###/get_messages",[HTTP_METHOD,"GET"],"");
}

http_response(key request_id, integer status, list metadata, string body) {
integer i;

if (request_id == requestId) {
rawList = body;

// create the boxes from the list information
resultList = llParseString2List(body,["\n"],[]);
integer listlength = llGetListLength(resultList);
float boxPosition = 1;

for(i=0;i<listlength;i+=1) {
if(firstRun == 1) {
// rez objects only in the first run
llSay(0, "rez object "+(string)i);
llRezObject("status", llGetPos() + <0,>, ZERO_VECTOR, ZERO_ROTATION, i);
boxPosition+= 0.7;
} else {
// after that only update with link messages
llMessageLinked(LINK_ALL_CHILDREN, 0, rawList, NULL_KEY);
}
}
firstRun = 0;

} else {
// llSay(0,(string)status+" error");
}
}


object_rez(key id) {
llCreateLink(id, 1);
llMessageLinked(LINK_ALL_CHILDREN, 0, rawList, NULL_KEY);
}

}

So, that was the whole code, which works really nice as you can see in the screenshot in the old posting. If you want to see it in in-world-action, please visit me at http://slurl.com/secondlife/Oz/199/242/26.

Friday 16 March 2007

Twitter/Second Life Mashup

I have created a little application for integrating the Twitter service into second life. I named it "twitterer" ;)
As you can see in this screenshot, the twitterer gets the newest messages from the twitter-service and displays them above some spheres.
For every status-message which comes from the twitter-xml-feed the application creates a sphere and than refreshes it every 30 seconds.

And it can also submit messages to twitter, so you are able to write messages in-world to twitter. I like such things. :)
As "backend"-stuff i use some RubyOnRails. I will submit the code this week.

Wednesday 14 March 2007

Presentation screen for Second Life

I have built a small presentation screen with two navigation buttons. The whole communication between the buttons and the screen takes place using the llMessageLinked-function. Each slide have to be placed as texture into the prim of the mainscreen. The textures have to be numbered in the order in which they should be displayed later.

The script of the screen also informs the buttons when the last slide has been reached. So that the “next”-prim is not able to navigate anymore. The “previous” and “next” prim have nearly the same scriptcode, except that the one checks if the first slide has been reached and the other looks out for the last slide.

The lsl-code for the “previous” button looks like this.



integer slideToDisplay;
integer firstSlideReached;
integer currentSlide;

default
{
state_entry() {
firstSlideReached = 1;
slideToDisplay = 1;
currentSlide = 1;
llSetTexture("da79968e-b8b7-8a0a-cc66-2a717e9c3d41", 4);
llRotateTexture(PI, 4);
}

touch_start(integer total_number)
{
if(firstSlideReached == 0) {
slideToDisplay = currentSlide - 1;
llMessageLinked(LINK_SET,slideToDisplay,"slideToDisplay",NULL_KEY);
} else {
llSay(0, "First slide reached.");
}
}

link_message(integer sender_num, integer num, string str, key id) {

if(str == "currentSlide") {
currentSlide = num;
if(currentSlide == 1) {
firstSlideReached = 1;
} else {
firstSlideReached = 0;
}
}
}
}

The “next” prim code looks like this. It also uses a “arrow”-texture which is included via a key in the state_entry()-function. The “previous” button uses the same texture only rotated in another rotation.




integer slideToDisplay;
integer lastSlideReached;
integer currentSlide;
integer maxSlide;

default
{
state_entry() {
lastSlideReached = 0;
slideToDisplay = 1;
currentSlide = 1;
maxSlide = 1000;
llSetTexture("da79968e-b8b7-8a0a-cc66-2a717e9c3d41", 4);
}

touch_start(integer total_number)
{
if(lastSlideReached == 0) {
slideToDisplay = currentSlide + 1;
llMessageLinked(LINK_SET,slideToDisplay,"slideToDisplay",NULL_KEY);
} else {
llSay(0, "Last slide reached.");
}
}

link_message(integer sender_num, integer num, string str, key id) {

if(str == "currentSlide") {
currentSlide = num;
if(currentSlide < maxSlide) {
lastSlideReached = 0;
}
}

if(str == "lastSlideReached" && num == 1) {
lastSlideReached = 1;
maxSlide = currentSlide;
}
}

}


The screen itself checks first if a texture is available. If not it sets an test-texture (in this case: some color-circles i did, identified by the unique-key).




integer lastSlideReached;
integer firstSlideReached;
integer currentSlide;

default {

state_entry() {
// check for first slide and set it when available
if(llGetInventoryKey("1") != NULL_KEY) {
llSetTexture("1", 4);
} else {
llSetTexture("b13e131e-13a4-37ad-e137-4b6f81a0a3fa", 4);
}
}

link_message(integer sender_num, integer num, string str, key id) {

if(str == "slideToDisplay") {
string slideToDisplayStr = (string) num;

if(llGetInventoryKey(slideToDisplayStr) != NULL_KEY) {
llSetTexture((string)num, 4);
llMessageLinked(LINK_SET,num,"currentSlide",NULL_KEY);

// check if next slide exists
integer nextSlideNr = num+1;
string nextSlideStr = (string) nextSlideNr;
//llSay(0, "check for next slide nr " + nextSlideStr);
if(llGetInventoryKey(nextSlideStr) == NULL_KEY) {
lastSlideReached = 1;
llMessageLinked(LINK_SET,1,"lastSlideReached",NULL_KEY);
} else {
lastSlideReached = 0;
}
} else {
llSay(0, "Slide not available");
}
}
}
}

With some text-textures the screen will look like this:

Presentation screen

On the left side is the test-texture i did, if no slides are available.

Display external XML-Resources on objects in Second Life

If have played a little bit with Second Life and its ability to access external resources. Inspired by Matt Biddulph’s “flickr screen”, now i wanted to load xml data from an external source into the SL-world and display it on a nice looking screen. Because it is not possible — from my point of view — to display text directly on a SL-primitive, it was necessary to render the text in a first step to an image. I did this using a little RubyOnRails application which also loads and parses the xml-resource. Ok, now i show how i did this.
I used the REXML processor to work with the xml-data and RMagick to create the image. The xml-source is used is a little xml-file provided by LindenLabs which shows the land sales by resident for the last three month (http://secondlife.com/reports/marketplace_stats/2007-03-01/land_sales_by_residents.xml). The whole parsing is specific to this source.

# get the xml via http and read it into a variable and create a XML-Document after that
@content = Net::HTTP.get(URI.parse("
http://secondlife.com/reports/marketplace_stats/2007-03-01/land_sales_by_residents.xml"))
xmlDoc = REXML::Document.new(@content)

# get some attributes and select them with xpath
elRecords = REXML::XPath.first(xmlDoc, "records")
reportTitle = elRecords.attributes['report']
reportDate = elRecords.attributes['date']

# create a new image in the size you need. in my case it was 400 width and 200 height
f = Image.new(400,200) { self.background_color = "white" }# make some settings for the font
drawable = Magick::Draw.new
drawable.pointsize = 15.0
drawable.font_family = 'Helvetica'
drawable.fill = 'gray'
drawable.gravity = Magick::NorthWestGravity
drawable.font_weight = Magick::BoldWeight
drawable.annotate(f, 0, 0, 5, 4, "Second Life #{reportTitle} (#{reportDate})")
drawable.pointsize = 13.0
drawable.fill = 'black'
drawable.font_weight = Magick::NormalWeight

# set some position for the text i want to print
@startTopPos = 24

# go the records in the xml-document and print the values into the new image
REXML::XPath.each( xmlDoc, "records/record") do |element|
element.elements.each("field") do |field|
name = field.attributes['name']
value = field.attributes['value']
drawable.annotate(f, 0, 0, 5, @startTopPos, name)
drawable.annotate(f, 0, 0, 250, @startTopPos, value)
@startTopPos = @startTopPos + 15
end
@startTopPos = @startTopPos + 15
end

# write the image to the disk. i wasnt able to return it directly without saving. :(
f.write("result.png")
# return the image
createdImg = Magick::Image.read("result.png").first
@response.headers["Content-Type"] = "image/png"
@response.headers['Content-Disposition'] = %q{inline; filename="thepicture.png"}
render :text => createdImg.to_blob

This script returns the following image:

Second Life land sales

To put this image on an primitive i used the following linden script code:

touch_start(integer num_detected) {
llSay(0, "trying...");

if(llGetLandOwnerAt(llGetPos()) != llGetOwner()) {
llSay(0,"Error: Cannot modify parcel media settings.");
}

key video_texture = llList2Key(llParcelMediaQuery( [PARCEL_MEDIA_COMMAND_TEXTURE]), 0);

if(video_texture == NULL_KEY)
{
video_texture = VIDEO_DEFAULT;
llParcelMediaCommandList([PARCEL_MEDIA_COMMAND_TEXTURE, VIDEO_DEFAULT]);
}
llSetTexture(video_texture,ALL_SIDES);
llParcelMediaCommandList([PARCEL_MEDIA_COMMAND_URL,"http://###URL###"]);
llParcelMediaCommandList([PARCEL_MEDIA_COMMAND_PLAY]);
llParcelMediaCommandList([PARCEL_MEDIA_COMMAND_AUTO_ALIGN,TRUE]);
}

###URL### has to be replaced with the URL of the Ruby-Script.

Note: To use this ParcelMedia-feature you have to allow streaming media i think in SL.