LSL 3-Body

Overview
LSL is the native scripting language of [Second Life]. As it is not only an interpreted scripting language, but it is also execute at runtime on the same CPU that is doing all of the server-side computation for the simulator (including potentially many other scripts), it is very much not a high-performance environment. Additionally, it's not all that great a language; most notably missing are real arrays, and the overhead for using its list-processing functions is high. As such, while it's entirely possible to write an N-body code in LSL, for performance reasons we've focused first on writing a 3-body code that unrolls all the loops, so as to avoid list-processing overhead.

As implemented, the code uses a number of different objects:


 * A single "star computer", which is the meat of the simulation. This is where all important data and all calculations are done.  It listens on a channel (default 16384) to receive commands, issues commands to stars on channels 1-3, and issues updates about initial conditions on channel 4096.


 * Three "initial condition" control panels, one for each star. This provides a sort of GUI for setting the initial conditions.  They listen on channel 4096 so they can be synced with the Star Computer, and they talk on channel 16384 to provide values to the star computer.


 * Three "stars", numbered 1 through 3, which listen on channels 1 through 3. The Star Computer tells them where to go and how to orient themselves.


 * A "buttons" console that has buttons for issuing simple commands like 'start', 'stop', and 'reset' to the Star Computer.

Where To Find It In-world


An installation of the 3-Body simulation can be found in the StellaNova region of Second Life. You can play with the simulator there; there are signs you can click to get a notecard with instructions. There is also a box there that you can click on to get a copy of the simulator yourself.

All Scripts

 * LSL Script : Star Computer - 3 Body
 * LSL Script : Star
 * LSL Script : Star Init. Conditions Console


 * LSL Script : SetText Slave
 * LSL Script : SetPos Slave
 * LSL Script : SetRot Slave
 * LSL Script : SetScale Slave

How To Set It Up
Instructions for setting up and running the simulation are included in a notecard that is distributed with the simulator. Basically, you just have to rez out the 3-body simulator, rez out the three stars, and attach the two "control panel" objects to your HUD (right-click on the item in your inventory and select "Wear.")

When you rez out the stars, it doesn't matter exactly where you put them. Just make sure that they're close to the "simulation tank" of the simulator. The simulator will move them to where they need to go. Click "reset" on the HUD control panel a couple of times, and the stars should move into the tank.



Vizualization : Code for the Stars
The stars are the tools used to visualize the results of the simulation. Each star is an object that listens on a channel (the channel of it's "star number", 1 through 3). It only listens to the owner and to the object named "3-Body Simulator"-- it will not take commands from anything else. The star includes an optionally-visible "velocity vector" that shows its speed and direction of motion.

Each star keeps track of a few "configuration" parameters. This includes the center of the visualization (the center of the "star tank"), as well as its size. It also keeps track of a "display scale". It will receive positions in AU and velocities in AU/year. The display scale is a number that converts AU to SL meters (1.0 being a 1:1 conversion). It can also receive commands that tell it whether to show or hide its velocity vector. Finally, each star is told its mass in solar masses. The stars use their mass parameter to change their size and color. (The colors are chosen to more or less match the main sequence colors of stars of appropriate masses.)

The primary commands that the stars receive is position and velocity commands. The position tells it where to go; the velocity tells it where to orient its velocity vector. As currently designed, the stars are non-physical objects. They do not put themselves in motion; they just use "SetPos" to move themselves to the position where they are supposed to be. In principle, they could use be made as physical objects that put themselves in uniform motion based on the velocity information they receive; no changes to the core simulator code would need to be made to drop in this sort of replacement. (However, dealing with scripted physical objects that get position updates a lot is very chancy in Second Life.)

Most of the code in the "star" script is fairly straightforward. The "listen" event handler in the default state is what receives the command and calls other code to do the right thing. The setmass function sets the star color and size based on the mass it is given. setpos scales the position its given by dispscale, clips positions at the limits of the simulation area, and uses a link message to actually position itself. I'll describe two functions in greater detail.

Avoiding Script Delays
Stars position themselves with SetPos, and adjust their velocity vector arrows with both SetPos and SetScale. Unfortunately, LSL includes an 0.2-second script delay after each of these calls. This can cause two problems. First, it limits smooth movement. Second if the text messages come in faster than that, unhandled text messages will start to build up for the star. Eventually, presumably, they will be dropped. However, the star may get out of sync from other stars, and will continue moving even after the simulator stops.

As such, we use the standard "LinkMessage" workaround. Each star includes 10 "slave scripts" that do the position setting. When the star wants to change its position, rather than calling "SetPos" directly, it sends a Link Message to itself. This Link Message is handled by the slave scripts. It is set up so that the handling is rotated between the 10 scripts. As such, while one script is put to sleep for 0.2 seconds after it calls SetPos, there are other non-sleeping scripts that are able to handle other positioning requests and keep the stars moving.

Because of this, the construction of a single star object is as follows:


 * Root prim : spherical star
 * Star Script
 * 10 copies of the SetPos Slave script named "Pos 0" through "Pos 9"
 * 10 copies of the SetRot Slave script named "Rot 10" through "Rot 19"
 * Prim #2: vector arrow shaft
 * 10 copies of the SetPos Slave script named "Pos 0" through "Pos 9"
 * 10 copies of the SetScale Slave script named "Scale 20" through "Scale 29"
 * Prim #3: vector arrow head
 * 10 copies of the SetPos Slave script named "Pos 0" through "Pos 9"
 * 10 copies of the SetScale Slave script named "Scale 20" through "Scale 29"

Figuring out which star I am
The code is set up so that you don't have to edit the script to change which star (1 through 3) a given object is. The script reads the name of the object, looking for "Star N" where N is a number, and makes itself that star. This does mean that the script needs to be reset after its name is changed. You can do this either by editing the object and explicitly resetting the script, or by giving the star the RESET command on whichever channel it is listening. (That would be the channel whose number matched the star's previous number.) The actual function that figures out which number the star is is called from the state_entry function of the default state (which will be called whenever the script is reset), and looks as follows:

figurenumber {   string name = llGetObjectName; list nameparse;

nameparse = llParseString2List(name, [" "], []); num = llList2Integer(nameparse, 1);

llSay(0, "I am star " + (string)num); }

This is a simple function that uses standard LSL string parsing and list functions. llParseString2List separates the given string on any occurance of characters in the list in the second argument. Here, we separate on spaces. This does mean that the name must have one and only one space between "Star" and the number; otherwise, it won't be parsed right. Because LSL does not have arrays, instead of indexing an array, we have to call llList2Integer to pull out the actual number. num is a global variable that is used elsewhere in the script to figure out on which channel it should listen. (Aside: if the word "global variable" makes you feel queasy, think of the entire script as a single class definition, at which point num becomes an instance variable of the Star object.)

Updating Star Velocity
The most fiddily function is the one that figures out how to position the stars vector based on the velocity it receives from the simulator object. Stepping through the code....

setvel(vector invel) {   invel *= dispscale; float shaftlen = llVecMag(invel); vector shaftnorm = llVecNorm(invel); float coslat = llSqrt(shaftnorm.x*shaftnorm.x + shaftnorm.y*shaftnorm.y); float cosphi; if (coslat == 0.) cosphi = 0.; else cosphi = shaftnorm.x/coslat;

This starts by scaling the velocity by dispscale, and then determining the length of the shaft of the vector arrow (which is just the length of the passed velocity vector times dispscale), and creating a normal vector in the direction of the velocity. It converts this normal vector into "latitude" and "phi" values-- where "latitude" is π/2 - θ in the usual spherical coordinates.

Next, to avoid the function calling overhead of going into the LSL trig and inverse trig functions, it constructs the quarternion rotation that will rotate the vector shaft from its default position (along the X-axis, I believe) to the proper orientation. It uses half-angle formulae to get the half angles, and then builds the rotation out of those.

float coslat2 = llSqrt( (1 + coslat) / 2.); float sinlat2 = llSqrt( (1 - coslat) / 2.); if (shaftnorm.z < 0) sinlat2 *= -1.; float cosphi2 = llSqrt( (1 + cosphi) / 2.); if (shaftnorm.y < 0) cosphi2 *= -1; float sinphi2 = llSqrt( (1 - cosphi) / 2.); if (shaftlen < minvecsize) shaftlen = minvecsize; if (shaftlen > maxvecsize) shaftlen = maxvecsize; vector shaftpos = ; vector headpos = ; vector shaftscale = ; rotation rot = <0., -sinlat2, 0., coslat2> * <0., 0., sinphi2, cosphi2>; // rotation rot = llEuler2Rot(<0., llAsin(-shaftnorm.z), 0.>) * //                llEuler2Rot(<0., 0., llAtan2(shaftnorm.y, shaftnorm.x)>);

You can see that the code also determines the position of the shaft and the arrow head of the vector arrow, as well as the size of the vector shaft. (0.1*dispscale is the cross-sectional diameter.) You can see that there is a commented out call using llEuler2Rot and inverse trig functions that builds the quarternion instead of just doing it with trig identities and knowledge of the components of the LSL rotation object.

Now that we've figured out the direction of the vector, we rotate the whole object so that the vector will be pointing in the right direction, set the position of the vector arrow shaft and head relative to the star (the star sphere itself being the root prim of the whole star object), and scale the shaft. This uses the "LinkMessage Workaround" mentioned in the section about avoiding script delays.

llMessageLinked(LINK_THIS, rotator, (string)rot, NULL_KEY); if (++rotator > lastrotator) { rotator = firstrotator; }   llMessageLinked(2, vecpositioner, (string)shaftpos, NULL_KEY); llMessageLinked(3, vecpositioner, (string)headpos, NULL_KEY); if (++vecpositioner >= numvecpositioners) { vecpositioner = 0; }   llMessageLinked(2, scaler, (string)shaftscale, NULL_KEY); if (++scaler > lastscaler) { scaler = firstscaler; } }

W00t!

Limitations
Even with the "LinkMessage" workaround, stars can only keep up with a certain rate of messages. If they are receiving messages much faster than 5 per second or so, they will start to fall behind. (You would think that the 0.2-second delay would mean that the "Link Message" workaround is not needed for 5 updates per second. However, there is more than one SetPos and SetScale call per update.  Additionally, the scripts have other things to do than just wait for the end of the SetPos script delay.)

If you find that the stars are getting out of sync with each other (this can be particularly obvious if there are two stars in a close orbit, and the third star is far away), or if any of the stars keep moving for very long after you stop the simulation, it means that the stars are falling behind in keeping up with the messages telling them where to go. The solution is to send updates to the stars from the simulation more slowly. You can do this by upping the "subiterations" parameter in the simulation script in the 3-Body Simulation object.

Data Input : Code for the Control Panels
<< TO BE WRITTEN >>

Simulation : Code for the Star Comptuer
The Star Computer, also known as the "3-Body Simulator" object, does all of the actual computations. While it might be an appealing architecture to design a bunch of star objects that shout out their positions, listen for the positions of the other stars, and each do their own calculations of the force acting on them and their accelerations, that would probably be a messy problem fraught with lots of synchronization problems. So, the calculations are done as a single, single-threaded process. Ideally, this script knows as little as possible about the details of the visualization, and just published positions and velocities for the stars, who themselves know how to use those positions. In practice, that's not entirely true, because the user interface is entirely through the computer object, so some details of visualization (like the display scale "dispscale") are stored in the simulator object and echoed to the stars.

So, all of the numerical computing is done in this one script. Everything else is user controls and visualization.

The "derivs" function
// **************************************** // Return the time derivative of each param //   in globals dposNdt and dvelNdt

derivs(vector p1, vector v1, vector p2, vector v2,      vector p3, vector v3)

This is the physics meat of the script. It's here that we stick in Newton's gravity. The position and velocity of each of the three stars are passed. Note that we don't use an array of vectors here, but have a whole bunch of individual vector parameters. Reason: LSL doesn't have arrays! This is one of the seriously lacking features of LSL. It does have lists, but extracting and inserting values from and into lists requires calling list functions, which comes with all of the function calling overhead. Lists are not efficient at all, nothing like arrays in a real computer language.

What's more, instead of having return values as would be nice and clean, we return the values in global variables. Ugh! Even if you think about the Star Computer as a class, and the globals instead as instance variables (which aren't as a priori evil as global variables), that's an ugly way to return function values. But, LSL parameters are all passed by value, not by reference, so we can't load up the function call with parameters to set. LSL does not have any sort of "structure" type that would allow us to return a single structure with all the values. Our only option would be to return a List... and then we're stuck again with all of that List function-calling overhead I mentioned before. It seems strange in 2009 to be worrying about function calling overhead when writing code for a 3-body problem of all things, but we're writing a script to run in Second Live, which is not coming anywhere close to having the full computational power of the server on which it is running. So, we have to think about these things, just like back in the good old days.

{   vector fperm;

dpos1dt = v1; dpos2dt = v2; dpos3dt = v3;

That was easy! The derivatives of the positions are the velocities. Now on to actual physics:

dvel1dt = <0., 0., 0.>; dvel2dt = <0., 0., 0.>; dvel3dt = <0., 0., 0.>; float d12 = llVecDist(p1, p2); float d12soft2 = d12*d12 + eps*eps; float d13 = llVecDist(p1, p3); float d13soft2 = d13*d13 + eps*eps; float d23 = llVecDist(p2, p3); float d23soft2 = d23*d23 + eps*eps; fperm = G / llPow(d12soft2, 1.5) * (p1-p2); dvel1dt -= mass2 * fperm; dvel2dt += mass1 * fperm; fperm = G / llPow(d13soft2, 1.5) * (p1-p3); dvel1dt -= mass3 * fperm; dvel3dt += mass1 * fperm; fperm = G / llPow(d23soft2, 1.5) * (p2-p3); dvel2dt -= mass3 * fperm; dvel3dt += mass2 * fperm; }

You will recognize Newton's Universal Law of Gravitation here, almost.... The first thing to note is that we don't divided by distance squared, we divided by distance squared plus the variable eps squared. eps is set by default to 0.05 AU. This is "softening the potential". If stars get too close, in reality we would have to deal with all the nasty hydro code that would be invoked when two stars collide. We're not doing that here. Instead, we're dealing with the fact that we don't even have double-precision variables, and if the stars get too close, the forces will get so big that the calculation becomes unreliable. The "softening distance" prevents the force from blowing up as the distance between two stars approaches zero.

Notice that we raise the denominator to the 1.5 power. Really, we're cubing the distance, but we never took a square root before. That is, d23soft2 is the square of the softened distance between star 2 and star 3. That saves us from having to do both the llSqrt and the llPow to power 3 (or three-times multiplication); instead, we have a single call to llPow. You may also object, um, isn't Newton's gravitation an inverse square law, not an inverse cube law? Sure it is... but we have another factor of the distance in the denominator from the calculation of the unit vector for the force's direction, (p1-p2)/d12 (although again we use the softened distance, so there's no worry of dividing by zero). The key thing with all of this is to make sure that when we write energy diagnostics, we write an energy function consistent with this force. (Originally, I didn't do that, but Steve McMillan pointed out that error to me.)

The "advance" function
This is a straightforward implementation of the 4th order Runge-Kutte algorithm... made ugly by the lack of arrays in LSL.

$$ y~=~(\mathrm{pos1}, \mathrm{vel1}, \mathrm{pos2}, \mathrm{vel2}, \mathrm{pos3}, \mathrm{vel3}) $$

(That is, at the present time step... if you see just y below, it means y(t))

$$ \frac{dy}{dt}~=~\mathrm{derivs}(y) $$

$$ y(t+dt)~=~y(t)~+~\frac{dt}{6}(k_1 + 2k_2 + 2k_3 + k_4) $$

$$ k1~=~\mathrm{derivs}(y) $$

$$ k2~=~\mathrm{derivs}(y + \frac{1}{2}k_1 dt) $$

$$ k3~=~\mathrm{derivs}(y + \frac{1}{2}k_2 dt) $$

$$ k4~=~\mathrm{derivs}(y + k_3 dt) $$

advance {   vector p1 = pos1; vector p2 = pos2; vector p3 = pos3; vector v1 = vel1; vector v2 = vel2; vector v3 = vel3; vector k1p1; vector k1p2; vector k1p3; vector k2p1; vector k2p2; vector k2p3; vector k3p1; vector k3p2; vector k3p3; vector k4p1; vector k4p2; vector k4p3; vector k1v1; vector k1v2; vector k1v3; vector k2v1; vector k2v2; vector k2v3; vector k3v1; vector k3v2; vector k3v3; vector k4v1; vector k4v2; vector k4v3;

Yipers, we have to declare a whole bunch of variables if we don't have arrays! For each of the 6 vector quantities we're tracking (positions for three stars plus velocities for three stars), we have to define a vector for the four "k" coefficients we'll need in Runge-Kutte. Ugh!

We make copies of pos1, vel1, etc. into p1, v1, etc., so that we can update p1, v1, etc., as we go through the parts of the Runge-Kutte equations, while keeping the original values for efficiency. We don't have the equations written out all at once in code form, but work our way through them.

derivs(p1, v1, p2, v2, p3, v3); k1p1 = dpos1dt; k1p2 = dpos2dt; k1p3 = dpos3dt; k1v1 = dvel1dt; k1v2 = dvel2dt; k1v3 = dvel3dt; p1 += 0.5*k1p1*dt; p2 += 0.5*k1p2*dt; p3 += 0.5*k1p3*dt; v1 += 0.5*k1v1*dt; v2 += 0.5*k1v2*dt; v3 += 0.5*k1v3*dt;

We call the "derivs" function to load up the (ugh) global variables "dpos1dt", "dvel1dt", etc., with the time derivatives of the position, velocities and time. We then add those to the positions and times for the first set of the Runge-Kutte algorithm. We keep on...

derivs(p1, v1, p2, v2, p3, v3); k2p1 = dpos1dt; k2p2 = dpos2dt; k2p3 = dpos3dt; k2v1 = dvel1dt; k2v2 = dvel2dt; k2v3 = dvel3dt; p1 = pos1 + 0.5*k2p1*dt; p2 = pos2 + 0.5*k2p2*dt; p3 = pos3 + 0.5*k2p3*dt; v1 = vel1 + 0.5*k2v1*dt; v2 = vel2 + 0.5*k2v2*dt; v3 = vel3 + 0.5*k2v3*dt; derivs(p1, v1, p2, v2, p3, v3); k3p1 = dpos1dt; k3p2 = dpos2dt; k3p3 = dpos3dt; k3v1 = dvel1dt; k3v2 = dvel2dt; k3v3 = dvel3dt; p1 = pos1 + k3p1*dt; p2 = pos2 + k3p2*dt; p3 = pos3 + k3p3*dt; v1 = vel1 + k3v1*dt; v2 = vel2 + k3v2*dt; v3 = vel3 + k3v3*dt; derivs(p1, v1, p2, v2, p3, v3); k4p1 = dpos1dt; k4p2 = dpos2dt; k4p3 = dpos3dt; k4v1 = dvel1dt; k4v2 = dvel2dt; k4v3 = dvel3dt; pos1 += (k1p1 + 2.*k2p1 + 2.*k3p1 + k4p1)/6. * dt; pos2 += (k1p2 + 2.*k2p2 + 2.*k3p2 + k4p2)/6. * dt; pos3 += (k1p3 + 2.*k2p3 + 2.*k3p3 + k4p3)/6. * dt; vel1 += (k1v1 + 2.*k2v1 + 2.*k3v1 + k4v1)/6. * dt; vel2 += (k1v2 + 2.*k2v2 + 2.*k3v2 + k4v2)/6. * dt; vel3 += (k1v3 + 2.*k2v3 + 2.*k3v3 + k4v3)/6. * dt;

t += dt; }

And that's the end of the advance function. By now, pos1, vel1, etc., and dt have all been moved forward one time step.

Checking energy conservation
The one "how are things going" diagnostic that I've coded in is calculating the total energy in the system. (It would be easy enough to add a "total momentum" calculation as well.)

This is the energy conservation calculation:

// ***************************************** // Calculate the total energy in the system

float total_energy {   float e = 0.; float mag; mag = llVecMag(vel1); e += 0.5 * mass1 * mag * mag; mag = llVecMag(vel2); e += 0.5 * mass2 * mag * mag; mag = llVecMag(vel3); e += 0.5 * mass3 * mag * mag; mag = llVecMag(pos1 - pos2); e -= G * mass1 * mass2 / llSqrt(mag*mag + eps*eps); mag = llVecMag(pos1 - pos3); e -= G * mass1 * mass3 / llSqrt(mag*mag + eps*eps); mag = llVecMag(pos2 - pos3); e -= G * mass2 * mass3 / llSqrt(mag*mag + eps*eps); return e; }

First, the kinetic energy of the three stars is added. Next, the potential energy is added. Normally, you'd expect potential energy to just be:

$$ E ~ = ~ \sum_i\sum_{j\neq i} \frac{-G m_i m_j}{d_{ij}} $$

where d ij  is the scalar distance between particle i and particle j.

However, with the smoothing length eps that was added to the force, we want to have an energy function that matches:

$$ E ~ = ~ \sum_i\sum_{j\neq i} \frac{-G m_i m_j}{\sqrt{ {d_{ij}}^2 + eps^2 }} $$

corresponding to a potential for particle i of

$$ \phi_i ~ = ~ \sum_{j\neq i} \frac{-G m_j}{\sqrt{ {d_{ij}}^2 + eps^2 }} $$

Take the gradient of this potential function F i = -m i ∇ϕ i , to get a force on one particle of:

$$ F_i~=~\sum_{j\neq i} \frac{-G m_i m_j}{({d_{ij}}^2 + eps^2)^\frac{3}{2}} d_{ij} ~ \hat{d_{ij}} $$

where $$\hat{d_{ij}}$$ is the unit vector pointing from the position of particle i to the position of particle j. (Or maybe in the other direction... choose the direction so that the equation doesn't have a sign error!)

This is exactly the force we have in the "derivs" function.

Dealing with event-driven programming
One particular challenge of writing this object in Second Life is that LSL scripts are entirely event-driven. There's no concept of a main loop where you can just have code running. You could just write the "state_entry" function of the default state to do all of the computations and keep going. However, because one LSL script is itself single-threaded, that would prevent us from ever receiving events we want to receive, like commands to start and stop and adjust the simulation. In order to create a continuous calculation within a purely event-driven structure, we cheat. We have an event that does a spurt of calculations, and then triggers another event that will call the same function. We use link messages for that, because link messages have relatively low impact on the system (less than standard Listeners):

link_message(integer sender, integer num, string str, key id) {       if (num != 1) return; integer j;       if (running && num == 1) {           for (j=0 ; j<subiterations ; ++j) { advance; }           llMessageLinked(LINK_THIS, 1, "", ""); updatestarpos; }   }

The advance function advances the simulation a single time step. All of the calculations are done in response to the computer receiving a link message with an integer parameter of 1.

The code is fast enough that it would be ridiculous to try to update the star positions with each and every single time step. Not only would that be faster than anybody would really need to watch to keep up, it would also be faster than the stars were able to handle the messages; they would fall behind, and different stars might randomly fall behind by different amounts. As such, during one callback we go through "subiterations" time steps. (This has the added benefit of not requiring the overhead of an event callback for each and every timestep.) By default, subiterations is set to 75. Make it smaller for smoother star motion (although be careful about overwhelming the stars with messages), make it larger if the stars can't keep up with the rate at which they're receiving messages.

After subiterations time steps have been advanced, we call updatestarpos, which deals with shouting out the star position and velocity values for the stars doing the visualization.

You can see here that as long as the global "running" variable (or the instance variable "running", if you don't like the word "global") is set, we keep triggering another link message, so the calculations will keep going. However, because we exit from the event handler, the framework in which the script runs will have the opportunity to call other event handlers in our code, in case any commands were received.

Providing Output for the Visualization
You will notice in the previous section a call to the function "updatestarpos". This is the interface between the script that does all the computations and the objects that do the visualization for us. Here it is:

/ **************************************** // Update the stars with where they are

updatestarpos {   integer i;    vector pos; vector vel; string text = "";

llRegionSay(1, "POS " + (string)pos1); llRegionSay(1, "VEL " + (string)vel1); llRegionSay(2, "POS " + (string)pos2); llRegionSay(2, "VEL " + (string)vel2); llRegionSay(3, "POS " + (string)pos3); llRegionSay(3, "VEL " + (string)vel3); if (energyreport > 0) {       if (++energycounter >= energyreport) {           energycounter = 0; if (doreportenergy) {               llSay(0, "At t=" + (string)t + ", " +                         "Total Energy: " + (string)total_energy); }           updatefloatingtext; }   } }

It uses llRegionSay to make sure that the stars, wherever they are in the region, get the message about where they should be. It's up to them to figure out what to do with it. There is a side effect of this: if more than one 3-body simulator is rezzed out in-region, they will conflict with each other! This is not ideal, and is something that could be designed around, but I have not yet done that. (The easiest and imperfect-but-probably-good-enough way is to recognize that the visualization stars are probably always going to be pretty close to the computer object, and as such just use llSay rather than llRegionSay!)

This function does do a little bit of data output all by itself. First, if the global variable "energyreport" has been set, every so often it does a calculation of the total energy in the system. The non-zero value of "energyreport" says how many link-message callbacks should elapse between reporting the energy. This is set by default to 20, which results in the energy being reported every few seconds. (Remember that there are, by default, 75 iterations of "advance" run with each link-message callback.) (You don't want to do it more often than that, because it would overwhelm anybody watching.)  The total energy is just spat out into text chat with llSay. "updatefloatingtext" updates the floating text over the root prim of the star computer object:

updatefloatingtext {   string text = ""; if (running) { text = "Running...\n"; }   llMessageLinked(LINK_THIS, 10, text + "t = " + (string)t, NULL_KEY); }

This lets us know if the simulation is running and what the current value of t is. Note that we don't call llSetText here directly, because llSetText has a sleep delay built in, and we really don't want that for the script doing the main computations! Instead, we use the same trick that we did with, only here because we won't be updating the floating text but every few seconds, we just have a single slave script in the prim (who won't care about an 0.2 second delay) listening for a link message with floating text to put over the object.

I/O
Finally, we have to be able to control the simulation. This is done via text chat commands set on channel 16384; the control panels and control HUD just send out commands on this channel for the star computer to hear. The "state_entry" callback for the "default" state of the computer sets up a listener on this channel (and also initializes the other variables). Commands are then dealt with in the "listen" event handler of the default state:

// ****************************************

listen(integer chan, string name, key id, string message) {       list params; string com; integer num; integer i;       float val; vector vec;

params = llParseString2List(message, [" "], []); num = llGetListLength(params); if (num == 0) {           return; }       com = llList2String(params, 0);

if (com == "stop") {           stopsim; }       else if (com == "start") {           startsim; }       else if (com == "initialize" || com == "reset") {           initialize; } ...

(The whole function is not echoed here; read the script, linked above, to see the rest.) This listen function is very accepting; it lets anybody and everybody issue it commands. It doesn't try to implement any security. (So, if you see somebody's star simulator sitting out, you can screw around with it! But be nice.)  It uses llParseString2List to separate the command string at spaces, and sticks the first word into the variable "com" with llList2String. Then there's a bit if/else if/else if/else if... block that does different things based on what the command is. It's all very exciting. Read the source code to see what commands the script responds to.

Even though the script is single-threaded, because do the calculations in spurts in link_message events (as described above), the script is still able to deal with chat commands without too much delay. (Well, if the region starts to suffer from server lag, there will be delays, as is always the case in Second Life.)

One other wrinkle I should probably mention is that there is a callback for when the object is moved:

// moving_end also gets called on a rez (unless it's rezzed in the same   //   position, in which case we don't need to call it) moving_end {       initialize; }

The reason for this is that inside the "initialize" function, the star computer tells all of the visualization stars where they should go&mdash; where is the center of the "star tank" they should move around in, and what is the scale of the visualization. If the simulator gets moved, of course the stars need to know about this! A side effect is that it stops and resets the simulation.