Table of Contents

Reference

Scripts

There are two sets of scripts involved in this component:

Central

The central part of the build consists in the main axle of the build going all the way down through the exact centre of the collector.

Helix

The helix particles that connect the avatar to the cloud are part of the much larger script that is to be found within the main axle of the build that is also responsible for emitting the particle stream towards selected avatars.

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasMenuIndex = 0;
list wasDialogMenu(list input, list actions, string direction) {
    integer cut = 11-wasListCountExclude(actions, [""]);
    if(direction == ">" &&  (wasMenuIndex+1)*cut+wasMenuIndex+1 < llGetListLength(input)) {
        ++wasMenuIndex;
        jump slice;
    }
    if(direction == "<" && wasMenuIndex-1 >= 0) {
        --wasMenuIndex;
        jump slice;
    }
@slice;
    integer multiple = wasMenuIndex*cut;
    input = llList2List(input, multiple+wasMenuIndex, multiple+cut+wasMenuIndex);
    input = wasListMerge(input, actions, "");
    return input;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer wasListCountExclude(list input, list exclude) {
    if(llGetListLength(input) == 0) return 0;
    if(llListFindList(exclude, (list)llList2String(input, 0)) == -1) 
        return 1 + wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
    return wasListCountExclude(llDeleteSubList(input, 0, 0), exclude);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasListMerge(list l, list m, string merge) {
    if(llGetListLength(l) == 0 && llGetListLength(m) == 0) return [];
    string a = llList2String(m, 0);
    if(a != merge) return [ a ] + wasListMerge(l, llDeleteSubList(m, 0, 0), merge);
    return [ llList2String(l, 0) ] + wasListMerge(llDeleteSubList(l, 0, 0), llDeleteSubList(m, 0, 0), merge);
}
 
list menu = [];
list keys = [];
string texture = "";
integer seconds = 0;
integer channel;
 
default
{
    state_entry() {
        channel = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        llParticleSystem([  // start of particle settings
            // Texture Parameters:
            PSYS_SRC_TEXTURE, llGetInventoryName(INVENTORY_TEXTURE, 0),
            PSYS_PART_START_SCALE, <0.1, 0.1, FALSE>, PSYS_PART_END_SCALE, <0.1, 0.1, FALSE>, 
            PSYS_PART_START_COLOR, <1.00,1.00,1.00>,    PSYS_PART_END_COLOR, <1.00,1.00,1.00>, 
            PSYS_PART_START_ALPHA, (float) 1.0,         PSYS_PART_END_ALPHA, (float) 1.0,     
 
            // Production Parameters:
            PSYS_SRC_BURST_PART_COUNT, (integer)  2, 
            PSYS_SRC_BURST_RATE,         (float) 0.02,  
            PSYS_PART_MAX_AGE,           (float)  3.5, 
            // PSYS_SRC_MAX_AGE,            (float)  0.00, 
 
            // Placement Parameters:
            PSYS_SRC_PATTERN, (integer) 4, // 1=DROP, 2=EXPLODE, 4=ANGLE, 8=CONE,
 
            // Placement Parameters (for any non-DROP pattern):
            PSYS_SRC_BURST_SPEED_MIN, (float) 00.0,   PSYS_SRC_BURST_SPEED_MAX, (float) 00.0, 
            PSYS_SRC_BURST_RADIUS, (float) 00.33,
 
            // Placement Parameters (only for ANGLE & CONE patterns):
            PSYS_SRC_ANGLE_BEGIN, (float) 0.50 * PI,   PSYS_SRC_ANGLE_END, (float) 0.50 * PI,  
            PSYS_SRC_OMEGA, <00.00, 00.00, 00.5>,  
 
            // After-Effect & Influence Parameters:
            PSYS_SRC_ACCEL, < 00.00, 00.00, 00.2>,
            // PSYS_SRC_TARGET_KEY, (key) llGetLinkKey(llGetLinkNumber() + 1), 
 
            PSYS_PART_FLAGS, (integer) ( 0                  // Texture Options:     
                                | PSYS_PART_INTERP_COLOR_MASK   
                                | PSYS_PART_INTERP_SCALE_MASK   
                                | PSYS_PART_EMISSIVE_MASK   
                             // | PSYS_PART_FOLLOW_VELOCITY_MASK
                                                  // After-effect & Influence Options:
                             // | PSYS_PART_WIND_MASK            
                             // | PSYS_PART_BOUNCE_MASK          
                             // | PSYS_PART_FOLLOW_SRC_MASK     
                             // | PSYS_PART_TARGET_POS_MASK     
                             // | PSYS_PART_TARGET_LINEAR_MASK    
                            ) 
            //end of particle settings                     
        ]);
    }
    touch_start(integer total_number) {
        if(llDetectedKey(0) != llGetOwner()) {
            return;
        }
        llSensor("", "", AGENT, 96, TWO_PI);
    }
    sensor(integer num) {
        menu = [];
        keys = [];
        --num;
        do {
            menu += llGetSubString(llDetectedName(num), 0, 12);
            keys += llDetectedKey(num);
        } while(--num>-1);
 
        if(llGetListLength(menu) == 0) {
            return;
        }
        state agent;
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state agent {
    state_entry() {
        llListen(channel, "", llGetOwner(), "");
        llDialog(
            llGetOwner(), 
            "Select an agent.", 
            wasDialogMenu(
                menu, 
                [
                    "<-", "", "->"
                ], 
                ""
            ), 
            channel
        );
        llSetTimerEvent(60);
    }
    listen(integer channel, string name, key id, string message) {
        if(message == "<- Back") {
            llSetTimerEvent(60);
            llDialog(
                id, 
                "Select an agent.",  
                wasDialogMenu(
                    menu, 
                    [
                        "<-", "", "->"
                    ], 
                    "<"
                ), 
                channel
            );
            return;
        }
        if(message == "Next ->") {
            llSetTimerEvent(60);
            llDialog(
                id, 
                "Select an agent.", 
                wasDialogMenu(
                    menu, 
                    [
                        "<-", "", "->"
                    ], 
                    ">"
                ), 
                channel
            );
            return;
        }
        integer i = llGetListLength(menu) - 1;
        do {
            if(llSubStringIndex(llList2String(menu, i), message) == -1) {
                jump continue;
            }
            keys = (list)llList2Key(keys, i);
@continue;
        } while(--i>-1);
 
        menu = [ "0", "1", "1", "2", "3", "5", "6", "13", "21", "34", "55", "89" ];
        state time;
    }
    timer() {
        llSetTimerEvent(0);
        llOwnerSay("Menu timeout...");
        state default;
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state time {
    state_entry() {
        llListen(channel, "", llGetOwner(), "");
        llDialog(
            llGetOwner(),
            "Select the number of seconds.", 
            menu, 
            channel
        );
        llSetTimerEvent(60);
    }
    listen(integer channel, string name, key id, string message) {
        seconds = (integer)message;
        state emit;
    }
    timer() {
        llSetTimerEvent(0);
        state default;
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state emit {
    state_entry() {
        llParticleSystem([  // start of particle settings
            // Texture Parameters:
            //PSYS_SRC_TEXTURE, llGetInventoryName(INVENTORY_TEXTURE, 0),
            PSYS_PART_START_SCALE, <0.1, 0.1, FALSE>, PSYS_PART_END_SCALE, <0.1, 0.1, FALSE>, 
            PSYS_PART_START_COLOR, <1.00,1.00,1.00>,    PSYS_PART_END_COLOR, <1.00,1.00,1.00>, 
            PSYS_PART_START_ALPHA, (float) 1.0,         PSYS_PART_END_ALPHA, (float) 1.0,     
 
            // Production Parameters:
            PSYS_SRC_BURST_PART_COUNT, (integer)  2, 
            PSYS_SRC_BURST_RATE,         (float) 0.01,  
            PSYS_PART_MAX_AGE,           (float)  2.0, 
            // PSYS_SRC_MAX_AGE,            (float)  0.00, 
            PSYS_SRC_TARGET_KEY,(key) llList2Key(keys,0),
 
            // Placement Parameters:
            PSYS_SRC_PATTERN, (integer) 4, // 1=DROP, 2=EXPLODE, 4=ANGLE, 8=CONE,
 
            // Placement Parameters (for any non-DROP pattern):
            PSYS_SRC_BURST_SPEED_MIN, (float) 1.2,   PSYS_SRC_BURST_SPEED_MAX, (float) 1.2, 
            PSYS_SRC_BURST_RADIUS, (float) 00.1,
 
            // Placement Parameters (only for ANGLE & CONE patterns):
            //  PSYS_SRC_ANGLE_BEGIN, (float) 0.50 * PI,   PSYS_SRC_ANGLE_END, (float) 0.50 * PI,  
            PSYS_SRC_OMEGA, <1.00, 00.00, 0.00>,  
 
            // After-Effect & Influence Parameters:
            PSYS_SRC_ACCEL, < -1.00, -00.1, -00.1>,
            //  PSYS_SRC_TARGET_KEY, (key) llGetLinkKey(llGetLinkNumber() + 1), 
 
            PSYS_PART_FLAGS, (integer) ( 0                  // Texture Options:     
                                | PSYS_PART_INTERP_COLOR_MASK   
                                | PSYS_PART_INTERP_SCALE_MASK   
                                | PSYS_PART_EMISSIVE_MASK   
                                | PSYS_PART_FOLLOW_VELOCITY_MASK
                                                  // After-effect & Influence Options:
                              // | PSYS_PART_WIND_MASK            
                             // | PSYS_PART_BOUNCE_MASK          
                             // | PSYS_PART_FOLLOW_SRC_MASK     
                                | PSYS_PART_TARGET_POS_MASK     
                             // | PSYS_PART_TARGET_LINEAR_MASK    
                            ) 
            //end of particle settings                     
        ]);
        llSetTimerEvent(seconds);
    }
    touch_start(integer num) {
        if(llDetectedKey(0) != llGetOwner()) {
            return;
        }
        llSetTimerEvent(0);
        llParticleSystem([]);
        state default;
    }
    timer() {
        llSetTimerEvent(0);
        llParticleSystem([]);
        state default;
    }
    on_rez(integer num) {
        llResetScript();
    }
}

Animate

The animate script is a standard script that triggers an animation named levitate within the main axle of the build. When the avatar wears the collector, permissions are requested from the avatar and then the collector spins into position whilst activating the animation. Conversely, when the build is detached, permissions are requested to deanimate the avatar.

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
default {
    state_entry() {
        llRequestPermissions(
            llGetOwner(),
            PERMISSION_TRIGGER_ANIMATION
        );
    }
 
    run_time_permissions(integer perm) {
        if(perm & PERMISSION_TRIGGER_ANIMATION) {
            llStartAnimation("levitate");
            return;
        }
 
        if(llGetPermissions() & PERMISSION_TRIGGER_ANIMATION) {
            llStopAnimation("levitate");
        }
    }
    attach(key id) {
        llResetScript();
    }
}

Animation

The animation displays the avatar floating in mid air with hands and legs extended yet slightly curved. The levitate animation was not created by Wizardry and Steamworks yet found in-world and found to be suitable for this build.

levitate.bvh

Cuffs

The cuffing to the sphere is derived from the "ball, chain and cuff" system created by Wizardry and Steamworks. For the collector the system had to be adapted in many ways since the "ball, chain and cuff" system works only in pairs whereas for the collector the the avatar cuffs to the lower sphere with all the cuffs worn on the avatar.

Technically, this is somewhat tricky since it requires a one-to-many synchronization with the added difficulty that the avatar can choose at any point to unwear the collector that makes re-synchronization difficult if the avatar wears the collector again. The converse applies, such that if the avatar removes the cuffs, re-synchronization is necessary when the avatar wears the cuffs again. To tackle the issue a sort-of heartbeat system was implemented, emitting short messages on private channel shared between the cuffs and the notch attached to the sphere, with one master script in all four cuffs worn on the avatar and one slave system within a notch attached to the sphere. The cuffs are responsible for emitting the chain particles and the notch attached to the sphere is responsible for tracking the cuffs that attach or detach. Periodically, the notch scans for tracked cuffs and restarts if one of the cuffs have been lost in order to be able to re-synchronize.

Even though the explanation seems long to spell out, the scripts are still very simple. For the most part, the system even reduces the necessity to have an RLV-enabled viewer such that the LIDs system has been removed from the collector build.

In order to track what connects to what, the notch attached to the sphere is called an "eye" and the cuffs are called a "thread".

Eye

The eye script is placed within the notch that attaches to the sphere.

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
list masters = [];
integer channel;
 
default {
    state_entry() {
        channel = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        llWhisper(channel, "syncroot");
 
        state tell;
    }
}
 
state tell {
    state_entry() {
        llListen(
            channel, 
            "", 
            "", 
            ""
        );
        llSetTimerEvent(1);
    }
 
    listen(integer channel, string name, key id, string message) {
        llSetTimerEvent(1);
        if(message == "sync") {
            if(llListFindList(masters, [ id ]) == -1) {
                masters += id;
            }
            llWhisper(
                channel, 
                "sync="+(string)llGetKey()
            );
            return;
        }
    }
 
    timer() {
        integer count = llGetListLength(masters);
        if(count == 0) {
            llResetScript();
        }
 
        integer i;
        do {
            key master = llList2Key(masters, i);
            if(master) {
                jump continue;
            }
            llResetScript();
@continue;
        } while(++i < count);
    }
 
    on_rez(integer num) {
        llResetScript();
    }
 
    attach(key id) {
        llResetScript();
    }
}

Thread

The thread script is placed within each of the cuffs and it is the same script used for all cuffs worn by the avatar.

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
key prim = NULL_KEY;
integer channel;
 
default {
    state_entry() {
        channel = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        state tell;
    }
 
    on_rez(integer num) {
        llResetScript();
    }
 
    attach(key id) {
        llResetScript();
    }
}
 
state tell {
    state_entry() {
        llListen(
            channel, 
            "", 
            "", 
            ""
        );
        llSetTimerEvent(1);
    }
 
    timer() {
        llWhisper(channel, "sync");
    }
 
    listen(integer channel, string name, key id, string message) {
        prim = (key)wasKeyValueGet("sync", message);
        if(prim) {
            state stop;
        }
    }
 
    on_rez(integer num) {
        llResetScript();
    }
 
    attach(key id) {
        llResetScript();
    }
}
 
state stop {
    state_entry() {
        llParticleSystem(
            [
                PSYS_PART_START_SCALE, <.15,.15,0>,
                PSYS_PART_END_SCALE, <.15,.15,0>,
                PSYS_SRC_TEXTURE, "300defc4-739c-6d31-3f51-3ade4185c544",
                PSYS_SRC_BURST_PART_COUNT, 8192,
                PSYS_SRC_BURST_RATE, 0.001,
                PSYS_PART_MAX_AGE, 1,
                PSYS_SRC_MAX_AGE, 0,
                PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_DROP,
                PSYS_SRC_ACCEL,(vector) <0,0,-.01>,
                PSYS_SRC_TARGET_KEY,(key) prim,
                PSYS_PART_FLAGS,
                    PSYS_PART_EMISSIVE_MASK |
                    PSYS_PART_FOLLOW_VELOCITY_MASK |
                    PSYS_PART_FOLLOW_SRC_MASK |
                    PSYS_PART_TARGET_POS_MASK
            ]
        );
 
        llListen(
            channel, 
            "", 
            "", 
            "syncroot"
        );
    }
 
    listen(integer channel, string name, key id, string message) {
        llResetScript();
    }
 
    on_rez(integer num) {
        llResetScript();
    }
 
    attach(key id) {
        llResetScript();
    }
}