Arranging llDialog buttons

Okay, back in Second Life! And/or OpenSim.

A purely Scripting-geekery post today, on a really niche topic.

When you get one of those blue dropdowns (or pop-ups, or drop-ins, depending on your viewer) with a number (up to twelve!) of buttons in them (when you touch a danceball for instance and it asks you what dance you want to do), the piece of LSL (Liden Scripting Language) that shows you that thing with those buttons is a built-in function called “llDialog“.

(All of the built-in functions start with “ll” which presumably stands for “Linden Lab”, but which has made many beginning LSL scripters, including Yers Truly, wonder how an identifier can start with eleven at all.)

One of the arguments to llDialog is, naturally enough, a list of the strings to put on the buttons. And the order in which the strings are copied from the list onto the buttons is… perhaps counterintuitive. For instance, if the list looks like:

[ "one", "two", "three", "four" ]

then the dialog as displayed will look like:

which might not be the first thing one would have guessed.

It basically starts at the bottom, filling rows upward as it goes, until it runs out of strings.

I have sometimes fiddled around to make things come out how I want them to manually, and more often have ignored the problem entirely and just not cared that the buttons were in a stupid order.

But for some reason today I got tired of it and decided to fix it; so here:

list arrange(list l) {
list outl = [];
integer n = llGetListLength(l);
do {
if (n<3) return outl + l;
n = n - 3;
outl = outl + llList2List(l, -3, -1);
if (n==0) return outl;
l = llList2List(l, 0, -4);
} while (TRUE);
return []; // UNREACHABLE
}

This will take a list in an order, and return a list in a different order such that if you pass the new list to llDialog, the labels on the buttons will be in the order that one might expect, left to right and up to down. As in for instance:

llDialog(avatar_id, prompt, arrange(button_list), channel);

It basically just divides the input list into pieces of size three, and then reverses them, with complications in case the input list isn’t equally divisible by three. And because if you ask LSL to copy the first through fourth-to-last (inclusive) elements of a three-element list, it silently copies the entire list rather than (as one might have expected) copying nothing. And because it doesn’t realize that a “do while(TRUE)” will never exit and without the unreachable line you get an error saying that not all code paths return a value. And because llGetListLength is said to be computationally expensive. And because LSL does let you use negative indices (roughly) on lists!

This works in both Second Life and OpenSim, because the scripting languages are extremely compatible for obvious reasons.

Many, many, implementations of this same algorithm no doubt exist all over the place :) but this is mine, for the moment and for what it’s worth!

Gotta ride it like you find it

(That’s actually about the Rock Island Line, and this is about a different line, but it seems apposite.)

Train crossing a bridge

That picture is from Karima Hoisan’s new Kitely sim “The Hudson Line”, which she has just announced over in her (much more frequently updated) weblog.

It’s a wild dreamlike re-interpretation of the RL commuter rail line of the same name; in one of those great SL synergies it started in casual conversations about the differences between various people’s living and working environments, and grew into this lovely impressionistic world. I helped with my usual small scripting contributions, and this time even some of the building!

It was wonderful fun to watch it emerging out of nothing (what an age we live in), and while you don’t get to have that fun, you can visit the announcement, and the sim itself (Kitely account or something required I guess?), and ride the train, and watch the movies, and find out what’s down there in the tunnel at the end (and it’s not Grand Central Station, muhahaha).

(Scripting note: llSetKeyframedMotion works very nicely in Kitely these days, and therefore I assume probably also in OpenSim; I do advise not entirely trusting that the moved object will arrive exactly where you expect it to, especially the first time after a sim reset, and setting at least a backup timer as well as the usual at_target() event. But it’s running the trains and cars and trucks and tugboats in the Hudson Line sim just fine!)

Moving Trains

That title is a pun, since these particular trains that move scriptily down the track are in a video that is itself quite moving in the emotional sort of way.

As the official announcement from Karima Hoisan says, Karima and Natascha Randt’s “Window on a Train” is now up for general viewing an’ admiration.

Here it is on the “You Tube”:

and note that per Director Randt “Watching our film without being in full-screen and HD (720p) is Prohibited!” :)

I don’t want to say anything specific about the movie itself, because I think it has the most impact when viewed with the least preconceptions. Do prepare to be touched…

Machinima in general is so cool. I am not a visual person myself, so generally I can only sort of gawk (or make pictures with math), but I am pleased to say that in this case I was able to contribute, in the form of some scripts to provide smooth and mostly consistent motion in three objects (an engine and two cars) that were far too primmy to make physical, and also far too primmy to link together.

If I can gather up the energy and time :) I will post about it (the one-word giveaway is LLSetKeyframedMotion, which having been added to LSL later than 2007, is for me a Strange New Thing in the world, which proved Just Right for this particular use-case).

But anyway! Watch the movie! :) It is good!

Automatic walking (and the invisible stranger)

So very poetic friend Karima flatteringly chose my avatar following script as some context for a new poem of hers lauding scripters (she even used some of the code in the poem, and although this sounds like it couldn’t possibly work it certainly does, and you should go read it).

This reminded me of a related script that I have been too lazy to work on forever and ever, and I now have a rough draft. It may stay a rough draft for some time :) since LSL makes it hard to do in a really polished way, but in any meantime here it is. When you’re using it, once you start walking you keep walking, more or less forward unless you use the side-arrow keys to turn, until you tap backward or etc to stop. (Works best on or over flat ground; see the page for details!)

If I ever like it enough I’ll put it into the Library.

Here is a picture of me walking with the automatic walker. Note that this is a hard sort of picture to get in general, because you have to walk and cam around to looking at yourself from the front at the same time, but the walker makes it relatively simple!

(Well, okay, and without any special scripts you can also cheat by just playing a walking anim and not actually walking. :) But this would let you make a video of yourself walking! A bit, anyway…)

In other news, I noticed that for a long time (like, weeks) there had been a green dot over in an adjacent parcel on the Rise, and whenever I went over to look it was the same AV name in the title floating there, but with no visible person, and they never answered chat or IMs.

Feeling whimsical the other day, I’d nudged the invisible person (who turned out to be nudgeable) over into my land, and built a little protective envelope of colorful devices around them. I figured I’d keep them around as a decoration until my conscience got the better of me, and then send them home (since they were on my land, and I could now do Teleport Agent Home).

But then not long after that I logged into the Rise, and there they were again, standing invisible and unresponsive at the same place in the empty field next door. (And yeah I’ve checked high and low and Highlight Transparent; I suppose they are probably wearing a full-transparent body alpha, or something subtler.)

Most puzzling! What process moved them back there again, to stand again invisible and unresponsive?

But that’s why we love the mainland. :)

Update: the llSetForce() version of this seems to work just awesomely! I am now playing with a version where you stop by pressing Back, and each time you press Forward it speeds you up a bit. Enormous fun so far!

Dale meets Dale

An age and an age ago, I made a picture with both Dales in it (or, strictly speaking, both Dale-bottoms), for some pictures (of bottoms) event that has since slipped my mind.

(Oh, right, looking at the text, it was one of them there weblog memes, notable for involving bottoms.)

It was comparatively easy, since I had the two of me almost entirely not touching, so there was just one tiny piece of foot to fiddle with carefully in Photoshop, and otherwise the only hard part was holding the camera absolutely still while I took one picture, stood up, changed shape and skin and hair, sat down again on the other sit-place, and took another picture.

For some reason the other day I wanted to take some more-overlapping Dale and Dale pictures, and eventually I did:

Narcissus 1

Nice but her face doesn’t show at all. (And there’s that little oops just under his near hand, oops.)

Narcissus 2

Quite nice, really. :)

This time I was working in gimp rather than Photoshop (for financial and sundry reasons), and it turns out that you can do this sort of thing pretty easily. I’m not going to do a detailed tutorial unless someone wants that, but basically you take two pictures from exactly the same place, layer one over the other, add a layer mask to the top one, and then draw on that layer mask in black or white, depending on which layer you want to show at any given point. It’s a nice forgiving process, doesn’t require an exceptionally steady hand, and so on.

This time I didn’t want to have to hold the camera absolutely still to take the pictures, so I wrote a script. Here it is!

integer SAVE_FACE = 0;
integer RESTORE_FACE = 1;
vector saved_camera_pos = ZERO_VECTOR;
rotation saved_camera_rot = ZERO_ROTATION;
integer have_saved = FALSE;

default {
    state_entry() {
        have_saved = FALSE;
    }
    attach(key id) {
        if (id!=NULL_KEY)
            llRequestPermissions(id,PERMISSION_CONTROL_CAMERA | 
                                    PERMISSION_TRACK_CAMERA);
    }
    run_time_permissions(integer p) {
        llOwnerSay("Touch that one face to save camera position,"+
                   " that other face to put it back.");
    }
    touch_start(integer total_number) {
        if (llDetectedTouchFace(0)==SAVE_FACE) {
            have_saved = TRUE;
            saved_camera_pos = llGetCameraPos();
            saved_camera_rot = llGetCameraRot();
            llOwnerSay("Camera position saved.");
        } else if (llDetectedTouchFace(0)==RESTORE_FACE) {
            if (have_saved) {
                llSetCameraParams([
                    CAMERA_ACTIVE,TRUE,
                    CAMERA_POSITION,saved_camera_pos,
                    CAMERA_POSITION_LOCKED,TRUE,
                    CAMERA_FOCUS,saved_camera_pos+*saved_camera_rot,
                    CAMERA_FOCUS_LOCKED,TRUE
                    ]);            
            } else {
                llOwnerSay("No position is saved yet.");
            }
        }
    }
}

So basically you put that into some prim, you color face 0 some color, color face 1 some other color, attach it to your HUD somewhere, fiddle it around so that you can see both of those faces, put your camera where you want it, touch face 0, take a picture, do whatever you want to, touch face 1, possibly press ESC to make sure you’re on the default camera, and now your camera is back where it was before for taking your second picture.

I could have added a third face case with like llReleaseCamera() or whatever it is on it, but it’s easiest to just take the HUD off when you’re done to free up your default camera. :)

Isn’t nature wonderful! I may get up the energy to put up a real page about it like my famous ExplodingObjects and AvatarFollower pages, but for now I am impressed with myself just for having slapped it up here.

Now I will go back to admiring my pictures; aren’t I a cute couple? :)

Combat System Scripting V: our first combat meter

I have suddenly remembered this here series of combat scripting posts that I was doing the other Geological Epoch!

So I will do another one. A pretty simple, but important one; our first Combat Meter.

Now as I didn’t explain in the first post but probably should have, when it comes to people (AVs) taking “damage” in Second Life, there are two distinctly different ways that can work. There is a notion of “health” and “damage” that are built directly into the world (but as far as I can tell very seldom used, because they’re inflexible and kind of silly), and then there’s “health” and “damage” as enforced by scripts in things that you wear.

(In a way this is like the difference between vendors that let you buy using “buy”, and those that let you buy using “pay”, as explained in my extremely thorough post on the subject. “Buy” is built into the world whereas “Pay” enables scripting, just like Linden Damage is build into the world whereas Scripted Damage is scripted. Not that there is anything all that significant about the analogy. :) )

Many, even most, Residents probably don’t even know that every AV is always at some state of Health, and that if that Health goes to zero, we “die”, in the sense that we are suddenly teleported Home, just as though we’d hit control-shift-h or whatever.

The reason that people don’t know this is that the vast majority of SL land has the “damage enabled” flag turned off, so you’re always at 100% health, and the viewer doesn’t normally even bother displaying the health meter in that case.

But if you stop by my Park in the center of Hughes Rise, or various other places, and then look carefully, you’ll be able to find a little heart on the screen, probably with a “100%” next to it. It you fly or teleport a few dozen meters into the sky and let yourself fall, or fly at high speed into a tree, or other amusing things, the number may go down from 100% (and then rather quickly go back up again). If you lose enough health fast enough, you may even hear your AV make a little supposedly gender-appropriate pained-grunt sort of noise.

And if you get the little number next to the heart down to 0%, you will suddenly be teleported home.

Now that sounds like a useful basis for a combat system, but it really isn’t, because it’s relatively easy to script up an object that instantly kills anyone it’s pointed at, and being teleported home isn’t a very flexible way to implement defeat (for instance it’s tough to script a scoreboard of “kills” or anything, just for one example), and healing is always exactly the same over time (no way to make “medikits” or “health potions” or anything), and it doesn’t apply to non-AVs (targets, monsters, robots, etc) and so on and so on.

So basically no one uses the built-in health and damage system for actual combat. What they use instead is Scripted Damage, which works by having the combatants wear some sort of attachment or other, that somehow finds out when they are “damaged” by “weapons”, or “healed” by whatever, and does whatever the system designer thought appropriate when they “die”.

The things that you wear that keep track of your damage generally have some sort of indicator of how healthy you are right now, in Scripted Damage terms, and are therefore generically called Damage Meters.

In today’s posting here, we will make a Damage Meter. Or, actually, we will discover that we’ve Already Got One!

(“Oh, yes, it’s very nice.“)

Already got one with a few modifications, that is; because it turns out that our self-healing target, from the self-healing target post, makes a very nice basis for an AV damage meter.

This is because of this Very Important Fact: when something hits your AV, all of your attachments see the collision event.

(I understand that this may not be true in OpenSim, or in some OpenSims, in which case damage meters there will have to work in some entirely other way of which I amn’t aware; I may explore that at some point too.)

So for instance if you were to take our self-healing target into inventory, and attach it to the center of your HUD say, and stand in front of the auto-popgun, you would see the health value above it go gradually down; and then if you step aside out of the stream of pellets, it will go slowly up again.

The only problem with it is that when it gets down to zero nothing useful happens. The script does an “llDie()” at that point, which you might think would cause the attachment to Cease To Be, or to detach or something, but in fact (another Interesting Fact) llDie() does nothing at all in attachments, so nothing actually happens.

This is easy to change, though. For now let’s just have the meter say something amusing when you “die”, to demonstrate that it is working. We will just change the process_collision routine so that it does something slightly different when health is zero, as in:

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"Arg! "+llKey2Name(llGetOwner())+
                    " has been defeated!");
        }
        llSetTimerEvent(HEAL_SECONDS);
    }
}

(We also took out the “pow” and “clunk”, because we’re confident enough that things are generally working that we don’t need them anymore.)

So modify the target script as above, put it into a prim, wear that prim somewhere appropriate on your HUD, and stand in front of the auto-popgun or have a friend shoot at you, and you will find yourself being “damaged” and eventually being “defeated”.

Combat! Shazam!

Some other stuff one might want to do:

  • Support “medikits” or “health potions” or whatever, that heal you when, say, you walk (run) across them (I have code for that!),
  • Cause you to, say, fall down and stop running around when you “die” (I have code for that too!),
  • Remove your ability to use your own weapons while you are “dead” (I don’t have code for that yet, but I might talk about various approaches).

(The other thing I’m currently playing with, which I haven’t quite scripted up yet, is to have the targets, the “monsters”, be “lootable”, so that once you’ve defeated one you can touch it or something and get I dunno some kind of reward. If anyone knows of a sim or a system of scripts that does this sort of WoW-like lootable-monsters thing, let me know; I’d like to take a look!)

So that’s all for this time. Pew pew! :)

Wind and Rotations

So I must apologize for (A) having gotten distracted from the combat-system scripting by thinking about the details of sailboat physics, and (B) posting some stuff here that is really just for me, but I thought it might someday be useful to someone else to, and besides this was a handy place to jot it down.

There are an annoying number of ways to write down rotations and directions and things in LSL, and different parts of the word want different ones. (And this is just, for the moment, talking about rotations in two dimensions!)

For instance, llWind() returns a vector representing the wind direction. Wind is (I believe!) always flat (parallel to the ground) in SL, so we can ignore Z. But what exactly are X and Y?

Well, Rule 1 (which I always forget and have to try out again) is that X is East and Y is North. So if for instance I am standing at 117, 119, 117 in Hughes Rise, on the walkway in the Park, and I walk one meter due North, I’ll be at 117, 120, 117. See how Y got bigger? And if I walk one meter due West from there, toward the stairs, I’ll be at 116, 120, 117; X got smaller.

So if llWind returns < 5, 0, 0 > for instance, that means the wind is blowing at five meters per second toward the East (and therefore is called a West wind, ’cause winds are called by where they are coming from, just to be confusing).

Now say you have a sail, and you want to know the angle between the sail and the wind. Well, the sail’s trim is represented as a rotation, which can be represented in LSL either as an actual rotation object (which has four components, corresponding to the four components of the corresponding quaternion, where a quaternion is of course an element of a four-dimensional associative normed division algebra over the real numbers), which is mathematically wonderful but not too useful for actually thinking about, or as a three-element vector, each element representing a rotation about the corresponding axis.

So for instance if your sail is flat in the X dimension, it will be facing head-on to winds coming from the east or west. If you then rotate it ninety degrees (which is a rotation of pi/2, or PI_BY_TWO, in radians) about the Z axis (the up-and-down one), it will be facing head-on to winds coming from the north or south.

To represent a rotation of pi/2 radians as a vector in LSL, we write < 0, 0, PI_BY_TWO >. Now the fun part! If I know the X and Y parts of the wind speed from llWind, and I have the rotation of my sail as a rotation about the Z axis, how do I figure out the angle at which the wind is hitting my sail?

Now I pause to think and experiment a little, so as to have an answer to write down in the next paragraphs. :)

Well, first of all notice that by using a symmetrical sail and a rotation of pi/2, I’ve sort of cheated; in particular, I didn’t have to figure out whether rotations are clockwise or counterclockwise (anticlockwise). This is probably obvious to smart people of various kinds, but I can never remember it (some Right Hand Rule or something is probably involved) so I have done the experiment and will write down Rule 2: Rotations are counterclockwise, or, to be precise and therefore meaningful, if you’re looking down at something that is not rotated, and you rotate it positively about the Z axis, it moves around anticlockwise.

This surprised me. :)

It makes sense, though, if we think back to like Trig 101 or whatever that was; X is East (i.e. to the right on the map), and Y is North (upward on the map), which is just how the graphs were all done on the board, and if you start out with a line running from the origin to < 1,0 >, say, and “rotate it” by some small angle theta, it goes up a bit into th’ ol’ First Quadrant there, which means it’s moved anticlockwise.

(Whoever designed clocks must have skipped that class or something.)

So we can represent wind direction as a rotation-about-Z by drawing it on the board there using the X and Y that we got from llWind(), and straightforwardly figure out what angle we would have to rotate that origin-to-1,0 line through to get it there, and it turns out to be (unless I’m wrong) llAtan2(Y,X); that is, the angle whose tangent is y over x.

So we have two rotation amounts, the amount that our sail’s been rotated and the amount that the wind’s been “rotated”, and we can just subtract them from each other to get the angle between them. If they’ve been rotated the same, the difference is zero, and our sail is getting (basically) all of the benefit of the wind. If they are ninety degrees apart, the sail is edge-on to the wind, and getting no benefit at all. Because we suspect there is probably something trigonometric going on here, we will note that llCos(0) is 1.0, and llCos(PI/2) is 0.0, and decide that we can do, say:


vector wind = llWind(ZERO_VECTOR); // The wind right here
float windrotz = llAtan2(wind.y,wind.x);
vector sailrot = llRot2Euler(llGetLocalRot());
float sailrotz = sailrot.z;
float rot_difference = windrotz - sailrotz;
float wind_effect_on_sail = llCos(rot_difference);

and so wind_effect_on_sail will be 1 if the wind’s blowing right onto the sale, will be 0 if the wind is blowing edge-on onto the sail (luffing!), and will be inbetween in between. (It will also be negative, I think, if the wind is blowing on the wrong side of the sail, but I haven’t thought about that yet.)

And some similar calculation can be used to get the angle between the sail and the hull (or, really, the keel), to figure out how much of the wind’s effect on the sail then contributes to the forward motion of the boat. But that’s a little later. :)

Combat System Scripting, interlude: OpenSim

So on our last post in this thread someone asked if this stuff would work in OpenSim.

I did a little playing around, and the answer seems to be “it depends”.

Unlike Second Life, which is both (arguably) a bunch of software, and a particular virtual world that uses that software, OpenSim is just a bunch of software (and an Open Source one at that, which means that there are all various forks and versions and levels and patches around out there). So what you get when you log into one virtual world that’s running (some level of) OpenSim may be different from what you get in another.

What I found when I tried out the Scripts So Far in the first OpenSim-based grid that came to hand (running perhaps “OpenSim 0.7.2 ReactionGrid”, although it wasn’t ReactionGrid) was:

There’s one simple thing in the self-healing target script that this OpenSim didn’t like; this line gets a syntax error:

integer health = MAX_HEALTH;

because apparently this OpenSim is even more silly than Second Life is, about global initializers not having any hard stuff like math or variables in them. So I changed that to just:

integer health;

and added a

    health = MAX_HEALTH;

down in the state_entry() handler, and that got rid of the compilation error.

The autopopgun script worked fine except that (as one of the commentors anticipated) I had to create my own bullet for it because there was no popgun in the library to cheat with. :) I won’t post the bullet script and other bullet details here right now ’cause it would be a distraction, but it worked.

Then I pointed the autoshooter at the target, and the target said “Clunk”.

Repeatedly.

As you may recall, this means that it thinks it’s being hit by something that’s not moving fast enough to cause damage.

A little poking around revealed that in this particular OpenSim, llDetectedVel() was always returning 0.000, making it not very useful for our purposes!

As a temporary test, I changed the

    if (llVecMag(llDetectedVel(index))>15) {

in the target script to the obvious

    if (llVecMag(llDetectedVel(index))>=0) {

so that instead of “is the thing that hit us moving at least fifteen meters per second?”, it was instead asking “is the thing that hit us moving at all, or for that matter not moving at all?”. To which the answer must always be Yes! :)

With that change the target worked fine; its health count went down while the shooter was shooting at it, went back up again when I moved the shooter away, and then went down and turned yellow and then red and then the target Ceased to Be when I left the shooter pointed at it long enough.

There may be OpenSim-based grids where llDetectedVel() works (this list of LSL functions that work in OpenSim seems to claim that it is implemented), but it didn’t seem to in mine. But other than that, and the one tiny syntax change mentioned above, everything we’ve done so far seems to work in OpenSim!

In case anyone else was wondering. :)

Combat System Scripting IV: the auto-popgun

Before we continue in our series from targets to personal combat-meters, it will be convenient to have something that shoots.

So far we’ve been using whatever projectile weapons happen to be in inventory to shoot at targets, but as soon as we want something to shoot back, we’re going to need either a patient and/or interested friend (but those aren’t always available at short notice), or something that will shoot on its own.

And to avoid the complexities of bullets and all, we’re going to cheat. :)

Here’s what you do to get our project for today, the auto-popgun:

  1. Search inventory for “popgun”,
  2. Find the last one that shows up; it should be in the library rather than your personal inventory, and be called something like “Popgun (drag onto yourself)”,
  3. Don’t drag it onto yourself, but just onto the ground.
  4. Edit it, rename it to say “autopopgun”, set the rotations to 354, 0, and 0 (so it’s pointing down a little), and raise it up a bit off the ground.
  5. Go into Contents, open the Popgun script in there, and replace all the text with the script below.

It should then sit there shooting a little ball every second or so, and the little balls should vanish on their own, just as if you were shooting it yourself.

You can rez one of the targets we made earlier in its path, or rez a target and then rotate and move the gun so it’s pointing at it, and observe how the target satisfyingly gets damaged and vanishes, just like you’d expect.

Here’s the autopopgun script:

float SPEED         = 18.0;   //  Speed of bullet in meters/sec
integer LIFETIME    = 7;      //  How many seconds will bullets live 
float DELAY         = 1.0;    //  Delay between shots

default
{
    state_entry() {
        llSetTimerEvent(DELAY); 
    }
    
    on_rez(integer param) {
        llSetTimerEvent(DELAY);
    }
    
    timer() {
        string bullet_name = llGetInventoryName(INVENTORY_OBJECT,0);
        vector vel = <0,SPEED,0>*llGetRootRotation();
        vector pos = llGetPos()+vel/SPEED;      
        llRezObject(bullet_name, pos, vel, ZERO_ROTATION, LIFETIME); 
    }
  
}

Pretty simple script, boiled down from the original popgun script by taking out all of the user-interaction stuff. Basically, once a second, in the timer() event, it figures out the name of the first object in inventory, figures out which way it (the gun) is pointing, and rezzes one of those objects moving in that direction at the given speed SPEED.

It passes the LIFETIME number along to the objects that it rezzes, and as it happens the bullets in the popgun read that number and delete themselves after that many seconds. But we don’t have to worry about any of that, because we cheated and didn’t have to do the bullet scripting ourselves! We are so lazy… :)

Next time, or some time after, or something: our first personal combat meter, so we can stand in the way of the popgun and be shot and take damage!

Combat System Scripting III: target, heal theyself

Okay, short one! No picture! :)

Starting with the final script from last time, as usual, the simplest way to allow our targets to gradually heal themselves over time, is to just stick in a little timer that will increase health back up to maximum:

  timer() {
    if (health<MAX_HEALTH) {
      health += 1;
      show_health(health,MAX_HEALTH);
    }
  }

and add something to, say, state_entry(), that starts the timer, so that state_entry() would now look like this:

    state_entry() {
        show_health(health,MAX_HEALTH);
        llSetTimerEvent(HEAL_SECONDS);
    }

with HEAL_SECONDS set to like 2.0 or something at the top.

And if you do that and play with it, you’ll find that it works. You may also notice that once in awhile when you shoot the target, it will instantly heal again, which is annoying.

To fix that, we can make sure that the timer is running only when there is actually damage to heal, by moving the “start the timer” line to the place where the target notices damage, and adding a “stop the timer” line to the timer itself, whenever it’s at full health.

And so tonight’s final result looks like this:

integer MAX_HEALTH = 20;
integer health = MAX_HEALTH;
float HEAL_SECONDS = 2.0;

vector color_for(integer h,integer max) {
    float ratio = (float)h/(float)max;
    if (ratio>0.666) return <0,1,0>;   // Green
    if (ratio>0.333) return <1,1,0>;   // Yellow
    return <1,0,0>;  // Red
}

show_health(integer h,integer max) {
    llSetText((string)h+"/"+(string)max,color_for(h,max),1);
}

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
        llSetTimerEvent(HEAL_SECONDS);
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        show_health(health,MAX_HEALTH);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
    timer() {
        if (health<MAX_HEALTH) {
            health += 1;
            show_health(health,MAX_HEALTH);
        } 
        if (health>=MAX_HEALTH) llSetTimerEvent(0.0);
    }
}

We make such progress! :)

Combat System Scripting II: slightly buffer target

Ah, now I remember! Before going on to other things besides the basic target, I was first going to make the basic target a little less basic.

In this posting, we’ll make the targets a little more robust, so it takes more than one collision to make them Cease To Be (or, in more violent language, more than one shot to kill them).

We’ll also, by the end, stick some nice color-changing floaty text over them, so you can tell how many collisions away from Ceasing to Be they are. (I’m sort of swerving between showing how a combat system might work, and giving little lectures on programming style, here, but it’s my weblog and I can do what I want! Feedback welcome if you’d like to see it go faster or slower or whatever, although I don’t promise to necessarily do what any feedback might suggest.)

The nice simple target script from last time was this:

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        llSay(0,"boom");
        llDie();
    } else {
        llSay(0,"clunk");
    }
}
default {
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

To make the target a little harder to kill, we’ll add a “health” variable to keep track of just how many more hits it has left, and update process_collision to count that variable down, and do the Cease to Be thing only when it reaches zero. Other hits will just make some I dunno “pow” noise. So:

integer health = 20;

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}

The rest of the script (the collision_start() handler in the default state there) stays the same.

So play with that a little. It’s very nice, but it’s lacking some indication of how damaged the target is. We’ll add the floaty-text for that. It’ll require a new event handler in the default state to initialize the floaty text in the first place, and then some new code in process_collision to update the text when it’s hit.

That gets us to something like this:

integer health = 20;

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        llSetText((string)health+"/20",<1,1,1>,1);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        llSetText("20/20",<1,1,1>,1);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

This works nicely, but it is Evil because very similar llSetText() calls appear in two different places, and the “20” appears in four different places, which means that if I decided to change it to only take 10 hits, say, I would be sure to miss at least one of them.

With the maintainability improved, it does exactly the same thing, but looks nicer:

integer MAX_HEALTH = 20;
integer health = MAX_HEALTH;

show_health(integer h,integer max) {
    llSetText((string)h+"/"+(string)max,<1,1,1>,1);
}

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        show_health(health,MAX_HEALTH);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

Much better.

And for one final touch, we can make the floaty text be green or yellow or red depending on how damaged the thing is, by just changing the <1,1,1> in show_health(), which causes it to always appear white, to a call to yet another function that will calculate the right color for us.

The final form for today, then:

integer MAX_HEALTH = 20;
integer health = MAX_HEALTH;

vector color_for(integer h,integer max) {
    float ratio = (float)h/(float)max;
    if (ratio>0.666) return <0,1,0>;   // Green
    if (ratio>0.333) return <1,1,0>;   // Yellow
    return <1,0,0>;  // Red
}

show_health(integer h,integer max) {
    llSetText((string)h+"/"+(string)max,color_for(h,max),1);
}

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        show_health(health,MAX_HEALTH);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

Isn’t Nature Wonderful?

Next time, maybe: target, heal thyself!

Combat System Scripting I: intro and our first target

As foreshadowed previously, we will now begin a series of posts about the Combat System Scripting we have been doing, because (a) it will be fun, and (b) it might be interesting.

What I’ve done so far, and hope to get to in this series, includes:

  • Stationary targets to shoot at,
  • Moving targets that shoot back,
  • A combat HUD that keeps track of your “damage” and makes you fall down when you take too much,
  • A “medikit” that makes you be less damaged when you walk through it.

Which is enough to have fun battles with your friends and/or Daleks. :) We’ll talk about just that first one in this episode.

A couple of notes:

  • If you just want to have a working combat/RP system to play with, and don’t want to learn scripting of it from first principles, take a look at Myriad Lite (Preview 4) on the Second Life wiki; lots of neat features (and a certain amount of complexity) there.
  • I’m going to assume here that you know how to rez a cube, and how to get a script into one. Not much more than that, though!
  • Loose design goals of the project will include compatibility with various existing things (e.g. it would be good to be able to shoot at stuff with any normal sort of SL weapon), and a preference for less inter-object communication over more, where that’s possible (to reduce lag and complexity).
  • I’m not going to worry too much about preventing ‘cheating’ of various kinds; this is mostly to play with, not to organize serious combat RP with untrusted others around.

Okay, off we go!

The first thing we’ll make is a simple target. Here’s probably the simplest possible target script:

default {
    collision_start(integer n) {
        llSay(0,"clunk");
    }
}

This says, basically, “whenever something collides with you, say ‘clunk'”. (You can change that line to ‘llOwnerSay(“clunk”);’ if you don’t want anyone else to hear the object talking for whatever reason.) Technically the stuff inside “collision_start” is an event-handler, and in this case the event is something hitting the prim that the script is in.

So make a cube, put that script into it, and walk into the cube. It will say “clunk”. Amazing!

Of course walking into things is sort of boring, so let’s get out a weapon and shoot at the cube bang bang. Any weapon that fires actual projectiles will do; if you don’t have one, or aren’t sure of what you have, you can use the simple popgun that is in the standard SL library (the lower part of your inventory, where many people never go; there’s some pretty good stuff in there).

Find the popgun, wear it, go into Mouselook (via the Mouselook button, or pressing M, or whatever), mouse so the little crosshair is on the box (the popgun has weightless projectiles, so you don’t need to compensate for any drop), and click. A thing will emerge and hit your cube (with some amusing colors and sounds and bubbles), and the cube will say “clunk”; huzzah!

Just saying “clunk” is good to be able to tell that you’ve actually hit it, but if we’re going to do Combat we want the target to explode into a flaming ball of gas. Well, or at least to Cease To Be, since I’m not going to figure out a particle-system for a Flaming Ball of Gas today.

So here’s another script:

default {
    collision_start(integer n) {
        llSay(0,"boom");
        llDie();
    }
}

Replace the original one with this one, make a copy of the prim, and then walk into it or shoot at it, and it will say “boom” (which sounds more final than “clunk”), and Cease To Be (quite thoroughly Cease To Be, not in your trash or Lost and Found or anything, which is why I suggested you make a copy of the prim).

You can use shift-drag (or just rez a bunch of copies) to make a whole row of these, and either run along it or go into mouselook and pop them one at a time, boom boom boom boom.

Not bad for a six-line script!

Now it’d be better if the target didn’t actually Cease To Be if someone just casually bumped into it. There are various ways to try to tell casual bumps from official Weapon Projectiles; we’ll use one of the simplest: speed. If somehthing is moving more than, say, fifteen meters per second (which is about as fast as it’s possible for a lone AV to move in normal circumstances), we’ll assume it’s a Weapon Projectile (or maybe a car; getting hit by a speeding car counts); but if it’s moving slower than that, it isn’t. So:

default {
    collision_start(integer n) {
        if (llVecMag(llDetectedVel(0))>15) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"clunk");
        }
    }
}

The only new complexity here is the “llVecMag(llDetectedVel(0))” bit, which just means “the scalar magnitude of the velocity vector of the thing that hit us” which is to say, how fast it was going.

The (0) means “the first of the N things that hit us”, where N is almost always 1 (so it looks at the first and only thing that hit it), but in some situations, especially when there are lots of bullets flying about, two or more things can collide with the cube so quickly that they’ll both get reported in the same collision event.

If we were just saying “clunk” that wouldn’t matter too much, but if (say) a slow bump happened at the same time as a fast projectile impact, we wouldn’t want to look at just the slow bump. So we’ll add a loop that looks at each of the things that hit us, in case there’s more than one.

default {
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) {
            if (llVecMag(llDetectedVel(i))>15) {
                llSay(0,"boom");
                llDie();
            } else {
                llSay(0,"clunk");
            }    
        }
    }
}

Relatively straightforward.

And then, and finally for this episode, just because I like to keep blocks of code simple wherever possible, we’ll pull out the bit of code that gets done for each colliding thing in to a subroutine of its own, and call it from the event handler:

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        llSay(0,"boom");
        llDie();
    } else {
        llSay(0,"clunk");
    }    
}
    
default {
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

And there you have it! A nice target that can be bumped into with impunity, but that goes away with a boom when shot (or when run into at high speed in a Chevy, I expect, although I haven’t tested that… yet).

Next time, hm, I dunno… maybe the Combat HUD or something…

Battling Bots

The Sinister Inventor contemplates his latest Evil Creation.

Contemplating improvements

“I believe the lower edge is dragging on the ground; perhaps if I boosted the hoverheight a bit…”

Well, that worked

“Well, that worked.”

Removing the combat HUD leads to instant resurrection.

Dale's Killbots

“Ah, my faithful lethal servants! What shall I call you? Dale’s Killbots, perhaps. ‘Daleks’ for short!

“Nah, that’s a silly name…”

(The combat scripting proceeds apace; the shooting robots there can be damaged and ultimately destroyed using anything that shoots projectiles, pretty much, although new ones get rezzed as long as the system is on. The combat HUD tosses one rudely to the ground after a certain number of hits by any projectile. I also made a full-auto infinite-ammo version of the standard Linden popgun, which is pretty awesome, but probably banned in most RP sims. I am vaguely thinking of doing a series of weblog posts, tutorial-style, on how it all works. Unless I do something else instead…)

The secret is out! “Placemaker: Plazas” released to worldwide acclaim!

Well, I’m sure there will be worldwide acclaim soon, anyway. Probably!

Placemaker: Plazas / publicity shot

There is the publicity shot for Placemaker: Plazas (Basic Edition), just released onto the SL Marketplace.

For a mere 400 Linden-dollars, you can own a device capable of producing literally a whole lot of different and sometimes even interesting (and in any case full-perm) plaza-like structures!

I have no doubt if that price-point makes any sense :) but I thought I’d give it a try.

So buy it if it sounds interesting! Let me know if it works for you, if the instructions were confusing, or anything else. If I continue to find the general idea interesting, maybe someday there will be a “Placemaker: Houses”. That was actually my original thought, but that turned out to be a little grandiose to code from scratch, so I started smaller…

SL Merchants: stop with the cruft!

… and in this case I don’t mean the mediocre-quality 2007 freebies on sale for 99L.

The cruft I’m referring to here is (are) the unnecessary scripts that many of your vendor devices are delivering to customers’ inventories, with names like “Floating Text” or “rotation script” or “vendor script – delete me”.

(Or most adorably of all, “New Script”.)

Vendors all too often deliver this stuff along with the stuff that the customer actually wanted (and sometimes annoying non-script cruft like pose stands, multiple landmarks, and so on), and they (you) ought to stop. Not only because it’s annoying to customers, but because it makes you look sort of clueless and unprofessional, and because annoyed customers are that much less likely to be repeat customers.

Why does this cruft come up in the first place? A little background: there are two different basic ways a vendor can work:

  • Vendors that you buy things from with the “Buy” option on the menu, where you get a little popup that says “Buy a copy of Whatever and its contents [list of contents] from Whoever Resident for 235L?” or “Buy contents [list of contents] from…”; we’ll call these “Buy vendors”, and
  • Vendors that you buy things from with the “Pay” option on the menu, and you get a little popup asking how much you want to pay, usually with just a single button with the price on it, but no list of exactly what you’re getting; we’ll call these “Pay vendors”.

A vendor can be set up so you don’t need to select either Buy or Pay, but you just need to touch it, and it acts as though you’d selected Buy (or Pay). You can still tell a Buy vendor from a Pay vendor, though, by the kind of popup you get after you touch it: either the list of contents that you are buying, or just an amount to pay.

(Not being much of a Viewer 2 or Viewer 3 user, I haven’t verified the exact contents of these popups on those viewers, but I think they are similar; feel free to leave comments if things are significantly different there.)

But anyway!

The interesting difference between the vendors for our purposes is this:

  • a Buy vendor always delivers everything in the prim to the buyer, but it doesn’t need to contain any scripts at all, whereas
  • a Pay vendor does have to contain scripts, but it has total control over what it delivers to the customer (so in particular it doesn’t have to deliver any unwanted scripts to anyone).

So the two primary sources of cruft are:

  • Buy vendors that contain scripts that they don’t need mixed in with the goods, and
  • Pay vendors whose scripts are poorly written, and deliver stuff (often including themselves!) that shouldn’t be delivered.

So how do we fix these things?

Remove the scripts! (Buy vendors)

So Buy vendors don’t need to contain any scripts at all in order to sell things (they just need to have the appropriate “For Sale” boxes filled in in the Edit window). Then why do we so often end up with useless scripts in our inventories when buying from Buy vendors? Because sellers put scripts into the Buy vendors in order to get various effects (floating text, spinning, etc), and then they leave the scripts there, even though they don’t actually need to.

If a box that is a Buy vendor has a Floating Text script in it whose only function is to make the box say “Awesome Blue Hat 240L” in floating text above it, the merchant should remove that script; the floating text will stay (it’s like, after you paint a wall, you don’t have to keep the paintbrush around for the wall to stay painted).

If a box has a rotation script in it whose only function is to make the box rotate around in an eye-catching fashion, the merchant should remove that script; the rotation will continue. (This is true for the common ways that people make sale boxes rotate; there are some kinds of rotation that will actually stop if you remove the script, but your sale box is unlikely to be using any of those.)

If a box has that ubiquitous “anim SMOOTH” script in it that makes the textures on the box rotate around, the merchant should remove that script; the cheesy attractive texture animation will continue.

If a box has a “New Script” in it whose only function is to make the box shoot pretty colored sparks out the top, the merchant should remove that script; the particle display will continue. (Again this is true for the common kinds of particle-displays. More elaborate kinds may stop if the script is removed, but in that case you should consider switching to a Pay vendor instead, or moving the particle script to a different prim, or otherwise avoiding delivering it to your customers.)

The result of removing these scripts is that when your customer buys one of your products from your Buy vendor, they get only the product, and none of those baffling and annoying extra scripts. They get a more satisfying shopping experience, and you get a higher customer retention rate. Goodness all around!

Don’t deliver the scripts! (Pay vendors)

Okay, so for Buy vendors the important thing is to just take out all of those visual effects scripts that don’t need to be in there anymore. What about Pay vendors?

In a Pay vendor, there’s always at least one script involved, with the job of noticing when someone has paid money to the vendor, and figuring out what to do about that. (For Buy vendors, SL itself takes care of seeing the buy action and delivering the goods.)

The script in the Pay vendor can do basically anything it likes; it can deliver copies of the items that are in the prim’s contents, it can deliver just the single boxed item that was selected at the time, it can contact some central server that will handle the actual delivery to the buyer. (That last kind is what you are using when you use a Pay vendor to buy something, and the actual item is sent to you by “Fred’s Central Vendor Server” or whatever.)

So if the script in a Pay vendor can control exactly what is delivered, why do these vendors also end up delivering unwanted cruft? Because so many Pay vendor scripts are incredibly simple and basically just deliver everything found in a particular prim. So this can include both all of the kinds of unnecessary scripts that Buy vendors tend to deliver (Floating Text and family), and also the vendor script itself!

This is easy to fix. Typically your vendor shouldn’t be delivering any scripts at all, so you or your friend who scripts can just look in the vendor script, find the place where it’s making a list of prim contents to deliver, and be sure not to add any scripts to that list.

So for instance if the script has a place that says:

    integer n = llGetInventoryNumber(INVENTORY_ALL);
    for(i=0; i<n; i++) {
        items += [ llGetInventoryName(INVENTORY_ALL, i) ];
    }

that is building up the list of things to deliver, you can change it to:

    string item_name;
    integer n = llGetInventoryNumber(INVENTORY_ALL);
    for(i=0; i<n; i++) {
        item_name = llGetInventoryName(INVENTORY_ALL, i);
        if (llGetInventoryType(item_name)!=INVENTORY_SCRIPT)
            items += [ item_name ];
    }

which leaves any scripts (including itself!) out of that list.

(If you do need to deliver scripts, you can use llGetInventoryName() to find out the name of the vendor script, and at least not deliver that. More sophisticated vendors have a configuration notecard or something like that, so you can explicitly tell them what to deliver; those don’t generally have the problem of delivering extra script cruft in the first place.)

Waring: llAllowInventoryDrop (PSA for all)

As an Extra Bonus for those who have read (or even just skimmed) this far, here’s a Public Service Announcement and a warning.

There are a bunch of legacy “copied from person to person for years” scripts out there that contain the line:

    llAllowInventoryDrop(TRUE);

I’ve seen this line in at least some Floating Text scripts; it may well occur in some other kinds of scripts as well.

While I’m sure that whoever put it into the original script had some good reason to do it given what that particular script was intended for, it’s definitely not something that you want in a random vendor.

If you have a script in a vendor that contains this llAllowInventoryDrop TRUE line, you may be opening your customers (and therefore yourself) up to being pranked and griefed in ways that might range from amusing to very harmful, depending on the prankster and the point of view.

(I’m not going to go into detail here on the exact vulnerability to avoid encouraging pranksters and griefers; a few minutes of web searching will find all the necessary information quickly enough.)

And if you do have one or more of these it’s not enough to remove the script or to remove the offending line from the script. If you find one or more vendors of yours have this line in a script, and you don’t know exactly what it’s doing there, you should change the TRUE to FALSE and re-save the script. So instead of the line above, it should say:

    llAllowInventoryDrop(FALSE);

You should also check the contents of any vendors that you find the dangerous line in, to make sure that they don’t contain anything that you didn’t put there.

And about those posing stands…

Okay, that’s all for the “avoiding script cruft” issue. But while we’re on the general subject, I doubt I’m the only one that’s annoyed when Every Single Item I buy at a store comes in a box that also includes a posing stand with the store’s logo, and two or three landmarks for the store and its various branches (and the owner’s boyfriend’s Rock club, and…). I mean sure, have a little vendor that gives away free logo posing stands to people who want them, and by all means have a landmark-giver in the doorway, but if I buy five items from your store, I don’t want five copies of your posing stand, ‘kay?

So could y’all please leave those out, too? :)

The Secret Project proceeds apace

The Sekrit Project proceeds apace

MU hahaha!

New Secret Project!

A tantalizing screenshot:

New Secret Project!

Misleading Hint: you can never have too many flamingos!
:)

Thingmaker news, and another video!

Funnest part first (I recommend one of the HD versions, if you got the bandwidth):

That’s a video from the special Burn2 version of the Thingmaker, currently experiencable (experienceable?) only at Ten Thousand Things Camp, my Burn2 build.

(The kinda neat one-prim-at-a-time pace of the action is thanks to Burn2 lag; I think Time Dilation was somewhere around 0.8 when I took this.)

Once Burn2 is over and I find some time somewhere, I will add various of the new features of this version of the Thingmaker to the commercial (“commercial”) version, and update it in my SL Marketplace store, to which I don’t think I’ve ever linked before because of the whole confusingness of XL Street L or whatever it was going away, and stuff being automatically copied over to the SL Marketplace, which at least I can spell.

If you buy the Thingmaker in its current version from the SL Marketplace, I will send you the new post-Burn2 version as soon as it comes out.

Unless I forget, in which case I will do so as soon as you remind me. :)

Why all those profile-image display things broke

Here is a fact about LSL, the Linden Scripting Language that drives so many clever and wonderful (and also boring and annoying) things in second Life:

There is no LSL function to allow a script to get or display the profile picture associated with a random Resident’s profile.

But, but, I hear you say, what about all those profile-pic boards in stores and clubs and things that display the profile pics of random vistors (sometimes after asking you for your permission, sometimes not). What about them?

Ah, I reply, those work by some clever hacks, using LSL’s ability to read web pages in order to read some stuff from pages off the web and, in a way that usually happens to work, infer from that the ID of the texture that is the profile pic of a given Resident, which can then be displayed on the face of a prim.

And note the “usually happens to work” in the above. As is clear from a recent JIRA entry, it doesn’t always work, and in fact it has recently stopped working, mostly or somewhat, in various ways.

The Hack

First, I will natter on a bit about the clever trick that’s used to display profile images. I was amazed when I first saw one, because I knew very well there was no direct LSL “get me the profile image for resident XYZ” function (although it’d be nice if there were).

On the other hand, there is an afaik little-known Web copy of Resident profiles and various other things, on the host world.secondlife.com. I don’t know how you get to them via the Web normally, from say the main SL website, but I do know that if you stick the ID of any Resident who hasn’t turned off “show in search” on their profile, after the URL stem “http://world.secondlife.com/resident/&#8221;, you’ll find the Web copy of their profile.

For instance, my SL ID is 8a123731-b0cb-47e7-90c0-6ed49aeaff58, and so if you follow this link you will find the Web copy of my profile.

On that Web copy of my profile there is a Web copy of my profile pic (aren’t I attractive?). If you look at the HTML for the page, or otherwise ask your browser nicely, it will reveal that the URL of the image is something like:

http://secondlife.com/app/image/48cc0f84-7f35-3013-c05d-eb05237942f7/1

Now that “48cc” thing there looks like an ID itself, and in fact it is, and if you stick a script into a prim that says:

llSetTexture(“48cc0f84-7f35-3013-c05d-eb05237942f7”,ALL_SIDES);

that fortunate prim will be covered by my profile pic.

And now, since LSL does contain a function for reading web pages (well, to be strictly accurate, for reading the first 2048 characters of web pages), it seems that once a script has a Resident’s key (which is easy to get if the Resident is anywhere nearby), it can fetch the Resident’s Web profile page, read that to find the image URL of their profile pic, extract just the key part, and stick it on one or more faces of a prim for display. (Assuming that the image key is always in a recognizable place, and always in the first 2048 characters of the page.)

And that is in fact exactly how they work!

And that “assuming” there is exactly why they recently stopped working. :)

The Breakage

There were actually two reasons these scripts stopped working: first the Lindens did something wrong in their firewall rules or something, and scripts couldn’t get to world.secondlife.com at all to load the profile pages. That seems to have been mostly fixed now, although it’s still happening here and there.

But more fatally, the Lindens also added lotsa juicy shiny fancy eyecandy HTML to the Web profile pages, and that pushed the image URLs out past the 2048 limit, so that scripts couldn’t get them anymore. Whoever made the change seems to have realized that it might cause problems, or just generally got an attack of neatness, because they also added a brand-new line, near the top of the HTML, explicitly giving the key for the Resident’s profile pic, as in:

<meta name=”imageid” content=”48cc0f84-7f35-3013-c05d-eb05237942f7″ />

This is easier to find and read than the messier HTML down where the image is actually loaded, but still it is different, and while it’s easy to change a profile-pic script to read the new one instead of the old one if you happen to be a scripter with some HTML knowledge and the ability to change the script, there are still dozens upon thousands of already-purchased and already-deployed devices out there whose scripts are now broken.

Is this a Horrible Linden Crime? I don’t think so, really. Anyone who wrote a script that depended on this trick would have known they were using an unsupported interface, and that it might fail at any time. As I wrote on the Jira:

For the people who are complaining that they bought things that have stopped working, you should probably complain to the people you bought them from, rather than to the Lindens. And to the people who are complaining that they sold something that then broke, you knew that could happen when you coded to a completely undocumented and unsupported interface.

It’s nice of the Lab to be working on fixing it, but you know very well that they don’t have to.

If you write a script that only works as long as there’s a “q” in column 17 of line 147 of http://www.secondlife.com/, you have only yourself to blame when someday that q changes…

I do hope and expect that the Lab will fix this, and it’s certainly good customer service for them to do so. On the other hand, people who are getting all angry at the lab and acting as though some actually supported interface was needlessly broken… Well, get a grip. :)

And if that seems a little snooty, I also posted a code snippet showing how to fix at least one popular script, and suggested to the Lindens a way that they could extend that “meta” tag a bit to restore at least many of the existing scripts that look for the full image URL to working order again.

’cause I am awesome that way. :)

So that is the current inneresting little story of the broken profile-pic displayers. And if you happen to have a broken profile-pic script (that you can see and edit the code inside of!), and you are one of the first N people to ask me (where N is the point at which I become overwhelmed and run away), I’d be glad to suggest how you might fix it. Just drop me an IM!

Update: According to a previous JIRA, the Lab did do pretty much exactly my backward-compatibility suggestion (before I’d even made it!), back in December. But apparently someone undid it recently. So, hem hem, I’m sure it’ll be fixed any minute now…

More things!

I’ve been neglecting the Thingmaker lately, out of frustration about how it kept hitting the Grey Goo Fence (i.e. getting “rapid or recursive rez” errors, even though it does only slow and non-recursive rezzing), but then the other night it dawned on me how to modify the script and the object so that it needs do no rezzing at all.

The new Thingmaker is faster, and doesn’t hit the fence. So here are some of its creations that I liked the most so far…

Thingmaker v2, #1Thingmaker v2, #2
Thingmaker v2, #3
Thingmaker v2, #4Thingmaker v2, #7Thingmaker v2, #6Thingmaker v2, #5