Table of Contents

ChangeLog

7 August 2013

  • Fixed rotations for LSL version.

6 November 2012

  • Reworked the parser and added deeply-nested REPEAT loops. Please see the latest code.
  • Parser fix.
  • Gyration fix for yaw-left, yaw-right.

27 February 2012

  • Since Wizardry and Steamworks is switching to full C#-LSL scripting, we have added the C# variant of the code.

25 February 2012

  • Cleaned up more, fixed all the translations except the testing ones.
  • Added llAbs for some normalization.
  • Renamed FR → FO to conform to our specification.
  • Added some more friendly output in case a notecard was not found.

Introduction

The LOGO language is a small programming language, originally developed by NASA and meant for educational purposes. The language stems basically from the LISP-category of programming languages and thus sharing the same ancestor with the Linden Scripting Language. This allows us to create a (sweet) fully-recursive parser implementation based on movements. Reading the various references, it seems that the turtle was meant to move in a two-dimensional space, on the XY plane and had no spatial component.

Due to the necessity of educators trying to explain rotations and translations in Second Life, as well as the concepts behind them, we give LOGO a new dimension by implementing a third dimension.

Video

The video on the left illustrates the gyration on the axes for roll, pitch and yaw. The video on the right illustrates the REPEAT command.

Translations and Rotations

Because of the third dimension, we extend the movement possibilities by using aircraft terminology to represent rotations around axes. We simplify the language and create a dialect of acronyms where every movement or rotation is given by the two first letters of the word describing the translation or rotation.

Overview

 The ''Front'' direction represents a translation in the positive sense of the x-axis, the ''Left'' translation represents a translation in the positive sense of the y-axis and an upward movement represents a translation along the positive sense of the z-axis (3-right hand, finger rule).

Key

* FO means front - along the positive sense of the x-axis
* BA means back - along the negative sense of the x-axis
* UP means up - along the positive sense of the z-axis
* DO means down - along the negative sense of the z-axis.
* LE means left - along the positive sense of the y-axis.
* RI means right - along the negative sense of the y-axis.
* RL means roll left - rotation in angles around the x-axis (in the positive sense of the y-axis)
* RR means roll right - rotation in angles around the x-axis (in the negative sense of the y-axis)
* PU means pitch up - rotation in angles around the y-axis (in the positive sense of the z-axis)
* PD means pitch down - rotation in angles around the y-axis (in the negative sense of the z-axis)
* YL means yaw left - rotation in angles around the z-axis (in the positive sense of the y-axis)
* YR means yaw right - rotation in angles around the z-axis (in the negative sense of the y-axis)

Real-Time Parser

Similar to our XML Animation Overrider based on StAX-technology, the LOGO Turtle uses a real-time parser that reads and extracts the commands from the local chat and enqueues them to the stack of already existing commands. Since we are dealing with stacks and queues (please note, that in this case we use them interchangeably: we pop elements off the stack to determine the next command and we enqueue a list of commands at the bottom of the stack), it is easy to write a recursive descent parser that traverses a stack by consuming a list.

Furthermore, all the movements are expressed recursively, in a step-like fashion without using physics. The step-like movement behavior of the LOGO Turtle, contrary to Wanderer, is an intentional feature because we are aiming for compatibility with step-by-step motors and other gyration equipment.

Since listen events are themselves enqueued on the call stack, every time a new set of commands is issued on the local chat, we enqueue the parser on the run-time call-stack by calling the same recursive function again on the new list of commands:

parse(commands)

which, albeit needing appropriate security restrictions (call-stack overflow being a concern for a sufficiently many commands posted on the local chat), we believe it is a very elegant solution. The reason for that, being no doubt, the beautiful LISP-like behavior of both LOGO and LSL.

Features

This list will be updated as we extend the LOGO parser to include and process the rest of the LOGO semantics.

Limitations

Parser Flow

Take an expression such as:

REPEAT 2 [ AB REPEAT 3 [ GL ] ] REPEAT 2 [ TL ]

The expected output is:

AB
GL
GL
GL
AB
GL
GL
GL
TL
TL

We do that using the following algorithm:

  1. Get the next token from input.
  2. If the next token is REPEAT:
    1. Get the number of times to repeat by reading the next token.
    2. Delete the tokens REPEAT and 2 from the input.
    3. Get the next bracketed sub-expression [ AB REPEAT 3 [ GL ] ] and delete it from the input.
    4. Trim the sub-expression for the first and last symbols ([ and ]).
    5. While the number of repeats is equal to 0:
      1. Goto (2.), and recursively parse the sub-expression AB REPEAT 3 [ GL ].
      2. Subtract one from the number of repeats.
    6. Goto (7.).
  3. If the next token is AB, print the token.
  4. If the next token is GL, print the token.
  5. If the next token is TL, print the token.
  6. Delete one token from the input.
  7. Goto (1.).

Usage

Simply create a blank primitive and drop the script inside. Then follow the key-overview sections above and issue commands or write them up in the notecard.

Some example input could be:

LE 5
FR 5
RI 5
BA 5

which is also equivalent to:

LE 5 FR 5 RI 5 BA 5

would make the primitive move on the perimeter of a square.

Latest Code

logo_turtle.lsl
//////////////////////////////////////////////////////////
// Wizardry and Steamworks (c) 2012,  GPLv3             //
// Please see: http://www.gnu.org/licenses/gpl.html     //
// for legal details, rights of fair usage and          //
// the disclaimer and warranty conditions.              //
//////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////
//                      ABOUT                           //
//////////////////////////////////////////////////////////
// This is an implementation of LOGO in 3D.             //
// The original developement page can be found at:      //
// http://grimore.org/secondlife:logo_turtle                 //
//////////////////////////////////////////////////////////
//                     EXAMPLES:                        //
//////////////////////////////////////////////////////////
// LOGOParse("REPEAT 4 [ FO 1 YL 90 ]");                //
//   - draws a square.                                  //
// LOGOParse("REPEAT 3 [ FO 1 YL 120 ]");               //
//   - draws an isosceles triangle                      //
//////////////////////////////////////////////////////////
 
front(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <1,0,0>*llGetRootRotation());
    front(--step);
}
 
back(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <-1,0,0>*llGetRootRotation());
    back(--step);
}
 
left(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,1,0>*llGetRootRotation());
    front(--step);
}
 
right(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,-1,0>*llGetRootRotation());
    back(--step);
}
 
up(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,0,1>*llGetRootRotation());
    up(--step);
}
 
down(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,0,-1>*llGetRootRotation());
    down(--step);
}
 
rollLeft(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<-1,0,0> * DEG_TO_RAD) * llGetRot());
    rollLeft(--degrees);
}
 
rollRight(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<1,0,0> * DEG_TO_RAD) * llGetRot());
    rollRight(--degrees);
}
 
pitchUp(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,-1,0> * DEG_TO_RAD) * llGetRot());
    pitchUp(--degrees);
}
 
pitchDown(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,1,0> * DEG_TO_RAD) * llGetRot());
    pitchDown(--degrees);
}
 
yawLeft(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,0,1> * DEG_TO_RAD) * llGetRot());
    yawLeft(--degrees);
}
 
yawRight(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,0,-1> * DEG_TO_RAD) * llGetRot());
    yawRight(--degrees);
}
 
list bs = [];
string inner(string in) {
    if(llStringLength(in) == 0) return "";
    string c = llGetSubString(in, 0, 0);
    in = llDeleteSubString(in, 0, 0);
    if(c == "[") {
        bs += "[";
        jump next;
    }
    if(c == "]") {
        if(llGetListLength(bs) == 0) {
            return "";
        }
        string peek = llList2String(bs, llGetListLength(bs)-1);
        if(peek == "[") {
            bs = llDeleteSubList(bs, llGetListLength(bs)-1, llGetListLength(bs)-1);
            jump next;
        }
    }
@next;
    return c + inner(in);
}
 
LOGOParse(string input) {
    list split = llParseString2List(input, [" "], [""]);
    do {
        string in = llList2String(split, 0);
        if(in == "REPEAT") {
            // Get the number of times to repeat.
            integer times = llList2Integer(split, 1);
            // Delete the "REPEAT" command and number of times.
            split = llDeleteSubList(split, 0, 0);
            split = llDeleteSubList(split, 0, 0);
            // Clean the sub-expression of brackets.
            string sub = llDumpList2String(split, " ");
            sub = llDeleteSubString(sub, 0, 0);
            sub = llDeleteSubString(sub, llStringLength(sub)-1, llStringLength(sub)-1);
            sub = inner(sub);
            // And now delete the sub-expression from the input 
            // leaving the rest of the input in split for to
            // be processed by the current recursive branch.
            split = llDeleteSubList(split, 0, 0);
            list delete = llParseString2List(sub, [" "], [""]);
            do {
                split = llDeleteSubList(split, 0, 0);
                delete = llDeleteSubList(delete, 0, 0);
            } while(llGetListLength(delete) != 0);
            split = llDeleteSubList(split, 0, 0);
            // Here we go. For the number of times we have to repeat 
            // the sub-expression, go ahead and parse it that number
            // of times.
            do {
                LOGOParse(sub);
            } while(--times);
            // Don't pop off the next REPEAT, we're done here, loop again!
            jump continue;
        }
        if(in == "FO") {
            front(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("FO");
        }
        if(in == "BA") {
            back(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("BA");
        }
        if(in == "LE") {
            left(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("LE");
        }
        if(in == "RI") {
            right(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("RI");
        }
        if(in == "UP") {
            up(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("UP");
        }
        if(in == "DO") {
            down(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("DO");
        }
        if(in == "RR") {
            rollRight(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("RR");
        }
        if(in == "RL") {
            rollLeft(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("RL");
        }
        if(in == "PU") {
            pitchUp(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("PU");
        }
        if(in == "PD") {
            pitchDown(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("PD");
        }
        if(in == "YR") {
            yawRight(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("YR");
        }
        if(in == "YL") {
            yawLeft(llList2Integer(split, 1));
            /* DEBUG */// llOwnerSay("YL");
        }
        split = llDeleteSubList(split, 0, 0);
@continue;
    } while(split);
}
 
default
{
    state_entry() {
        llListen(0, "", llGetOwner(), "");
    }
    listen(integer channel, string name, key id, string message) {
        // Enqueue commands by injecting them into the recursive parser.
        LOGOParse(message); // Enqueue the commands on the call stack.
    }
    on_rez(integer num) {
        llResetScript();
    }
}

Previous Code and C#-LSL Hybrid Version

This is kept here as a reference for the old version of the code.

Code

logo_turtle.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
key nQuery = NULL_KEY;
integer nLine = 0;
string commands = "";
//pragma inline
string nName = "LOGO";
 
front(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <1,0,0>*llGetRootRotation());
    front(--step);
}
 
back(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <-1,0,0>*llGetRootRotation());
    back(--step);
}
 
left(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,1,0>*llGetRootRotation());
    front(--step);
}
 
right(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,-1,0>*llGetRootRotation());
    back(--step);
}
 
up(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,0,1>*llGetRootRotation());
    up(--step);
}
 
down(integer step) {
    if(step == 0) return;
    llSetPos(llGetPos() + <0,0,-1>*llGetRootRotation());
    down(--step);
}
 
rollLeft(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<-1,0,0> * DEG_TO_RAD) * llGetRot());
    rollLeft(--degrees);
}
 
rollRight(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<1,0,0> * DEG_TO_RAD) * llGetRot());
    rollRight(--degrees);
}
 
pitchUp(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,-1,0> * DEG_TO_RAD) * llGetRot());
    pitchUp(--degrees);
}
 
pitchDown(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,1,0> * DEG_TO_RAD) * llGetRot());
    pitchDown(--degrees);
}
 
yawLeft(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,0,1> * DEG_TO_RAD) * llGetRot());
    yawLeft(--degrees);
}
 
yawRight(integer degrees) {
    if(degrees == 0) return;
    llSetRot(llEuler2Rot(<0,0,-1> * DEG_TO_RAD) * llGetRot());
    yawRight(--degrees);
}
 
parse(list command) {
    if(!llGetListLength(command)) return; // Empty list.
 
    // DEBUG: Queue display on every command.
    //llOwnerSay("List: " + llDumpList2String(command, " "));
 
    integer steps = llAbs(llList2Integer(command,1));
    if(llList2String(command,0) == "FO") {
        front(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "BA") {
        back(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "LE") {
        left(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "RI") {
        right(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "UP") {
        up(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "DO") {
        down(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "RR") {
        rollRight(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "RL") {
        rollLeft(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "PU") {
        pitchUp(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "PD") {
        pitchDown(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "YR") {
        yawRight(steps);
        jump next; // break;
    }
    if(llList2String(command,0) == "YL") {
        yawLeft(steps);
        jump next; // break;
    }
@next;
    parse(command = llDeleteSubList(command, 0, 0));
}
 
default
{
    state_entry() {
        llListen(0, "", llGetOwner(), "");
        integer itra;
        for(itra=0, commands="", nLine=0; itra<llGetInventoryNumber(INVENTORY_NOTECARD); ++itra) {
            if(llGetInventoryName(INVENTORY_NOTECARD, itra) == nName)
                jump found_notecard;
        }
        llInstantMessage(llGetOwner(), "Failed to find notecard. Listening just to local chat...");
        return;
@found_notecard;
        nQuery = llGetNotecardLine(nName, nLine);
    }
    listen(integer channel, string name, key id, string message) {
        // Enqueue commands by injecting them into the recursive parser.
        parse(llParseString2List(message, [" "], [])); // Enqueue the commands on the call stack.
    }
 
    touch_start(integer total_number) {
        parse(llParseString2List(commands, [" "], []));    
    }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
 
    dataserver(key id, string data) {
        if(id != nQuery) return;
        if(data == EOF) {
            llOwnerSay("Instructions loaded...");
            return;
        }
        if(data == "") jump next_line;
        commands += data + " ";
@next_line;
        nQuery = llGetNotecardLine(nName, ++nLine);
    }
}

C#-LSL Code

This script was tested and works on OpenSim version 0.7.4!

We have switched the Wizardry and Steamworks hideout to full C#-LSL hybrid scripting. Use this script if you are on OpenSim and prefer C#. This variant in C# does not support REPEAT.

logoturtle_sharp.lsl
//c#
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
LSL_Types.LSLString nQuery = "";
LSL_Types.LSLInteger nLine = 0;
 
string commands = "";
//pragma inline
string nName = "LOGO";
 
LSL_Types.Vector3 _px = new LSL_Types.Vector3(1,0,0);
LSL_Types.Vector3 _nx = new LSL_Types.Vector3(-1,0,0);
LSL_Types.Vector3 _py = new LSL_Types.Vector3(0,1,0);
LSL_Types.Vector3 _ny = new LSL_Types.Vector3(0,-1,0);
LSL_Types.Vector3 _pz = new LSL_Types.Vector3(0,0,1);
LSL_Types.Vector3 _nz = new LSL_Types.Vector3(0,0,-1);
 
LSL_Types.list spc = new LSL_Types.list(" ");
LSL_Types.list emp = new LSL_Types.list("");
 
public void default_event_state_entry()
{
    // Listen for input.
    llListen(0, "", llGetOwner(), "");
    LSL_Types.LSLInteger itra = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
    do {
        if(llGetInventoryName(INVENTORY_NOTECARD, itra) == nName) {
            nQuery = llGetNotecardLine(nName, nLine);
            break;
        }
    } while(--itra>=0);
}
 
public void default_event_listen(LSL_Types.LSLInteger channelIn, LSL_Types.LSLString name, LSL_Types.LSLString id, LSL_Types.LSLString message) {
    parse(llParseString2List(message, spc, emp));
}
 
private void parse(LSL_Types.list commands) {
    if(!llGetListLength(commands)) return; // Empty list.
 
    string command = llList2String(commands, 0);
    LSL_Types.LSLInteger steps = llAbs(llList2Integer(commands,1));
    switch(command) {
        case "FR":
            front(steps);
            break;
        case "BA":
            back(steps);
            break;
        case "LE":
            left(steps);
            break;
        case "RI":
            right(steps);
            break;
        case "UP":
            up(steps);
            break;
        case "DO":
            down(steps);
            break;
        case "RR":
            rollRight(steps);
            break;
        case "RL":
            rollLeft(steps);
            break;
        case "PU":
            pitchUp(steps);
            break;
        case "PD":
            pitchDown(steps);
            break;
        case "YR":
            yawRight(steps);
            break;
        case "YL":
            yawLeft(steps);
            break;
        default:
            break;
    }
    parse(commands = llDeleteSubList(commands, 0, 0));
}
 
private void front(LSL_Types.LSLInteger step) {
    if(step == 0) return;
    llSetPos(llGetPos() + _px*llGetRootRotation());
    front(--step);
}
 
private void back(LSL_Types.LSLInteger step) {
    if(step == 0) return;
    llSetPos(llGetPos() + _nx*llGetRootRotation());
    back(--step);
}
 
private void left(LSL_Types.LSLInteger step) {
    if(step == 0) return;
    llSetPos(llGetPos() + _py*llGetRootRotation());
    front(--step);
}
 
private void right(LSL_Types.LSLInteger step) {
    if(step == 0) return;
    llSetPos(llGetPos() + _ny*llGetRootRotation());
    back(--step);
}
 
private void up(LSL_Types.LSLInteger step) {
    if(step == 0) return;
    llSetPos(llGetPos() + _pz*llGetRootRotation());
    up(--step);
}
 
private void down(LSL_Types.LSLInteger step) {
    if(step == 0) return;
    llSetPos(llGetPos() + _nz*llGetRootRotation());
    down(--step);
}
 
private void rollLeft(LSL_Types.LSLInteger degrees) {
    if(degrees == 0) return;
    llSetRot(llGetRot() / llEuler2Rot(_px * DEG_TO_RAD));
    rollLeft(--degrees);
}
 
private void rollRight(LSL_Types.LSLInteger degrees) {
    if(degrees == 0) return;
    llSetRot(llGetRot() * llEuler2Rot(_px * DEG_TO_RAD));
    rollRight(--degrees);
}
 
private void pitchUp(LSL_Types.LSLInteger degrees) {
    if(degrees == 0) return;
    llSetRot(llGetRot() / llEuler2Rot(_py * DEG_TO_RAD));
    pitchUp(--degrees);
}
 
private void pitchDown(LSL_Types.LSLInteger degrees) {
    if(degrees == 0) return;
    llSetRot(llGetRot() * llEuler2Rot(_py * DEG_TO_RAD));
    pitchDown(--degrees);
}
 
private void yawLeft(LSL_Types.LSLInteger degrees) {
    if(degrees == 0) return;
    llSetRot(llGetRot() / llEuler2Rot(_pz * DEG_TO_RAD));
    yawLeft(--degrees);
}
 
private void yawRight(LSL_Types.LSLInteger degrees) {
    if(degrees == 0) return;
    llSetRot(llGetRot() * llEuler2Rot(_pz * DEG_TO_RAD));
    yawRight(--degrees);
}