Thursday, 23 July 2015

PARAMOUR RELEASE: Mirror Script

A little while ago, Jessie Campbell came to me with an interesting scripting question. She had created a series of poses and animations in pairs, where each pair consists of an animation and a mirrored version of it. What she was looking for was a matching script that would allow someone to "sit" on a mirror-like object and have a clone NPC of the person rezzed on the opposite side of the mirror; then have the sitter and clone play the mirrored animation pair.

After a little thought I came up with a script for her that I thought I'd release to the general public in case anyone else has a use for it. It's not as elegant as using a PMAC method (which would also be a possibility with a custom add-on) but it does the trick. It will work for a single animation pair,  or if you want to add more, touching the mirror object will advance to the next one (and when you reach the end it cycles back to the start again). When the sitter stands the NPC is removed.

Like the PMAC system, this script handles positions in such a way that no poseballs are used and will work perfectly in a single prim (or mesh) object.

This doesn't use or store appearance notecards. It only mirrors the sitter exactly as they are at the time they sit down. If they subsequently change appearance while still seated, the NPC will remain unchanged, looking the same way it did when initially rezzed.

Be aware the any scripted object you are wearing when you sit will also be part of the NPC and that some sims have extreme difficulty with this since the bug to (at least partially) fix the issue wasn't applied to Opensim until after the release of 0.8.1 stable. People running current git master (or something relatively recent in the 0.8.2 dev branch) should be okay. Even so, it's best to avoid wearing scripted objects it at all possible when using it.

There is a global sit target that can be set in the script is you're using it for a single pose, or if all of your poses have the identical position. If you need to adjust this on a per-pose basis you can add a sit target override to the description of the animations for the main sitter in the format <vector sitPos>::<rotation sitRot> (double colon is used as the separator). If no override is found, the script's base sit target is used instead.

There are a couple extra settings at the top of the script in the "User Settings" section that might be handy.

EDIT: Big thanks to +Fred Beckhusen  for supplying a link to a site that can mirror-convert a BVH for you automatically so all you need is a source BVH and it will produce the companion file for you.

What You Need:

For this script to be any use to you you'll need the following:
  • An object that will act as the mirror -- it can be anything you want (and can be invisible) but the critical issue is that the animations will be mirrored on the XZ plane of the root prim. The script method works perfectly for a single prim....no poseballs! If you want to mirror on a different plane you can adapt the positioning function of the script with only minimal scripting knowledge.
  • The mirror script (see below) which must be placed in the root prim
  • Pairs of animations where one is the mirror of the other. The animation to be played by the sitter can be any name you like as long as it doesn't end with the word mirror. The mirror animation must use the identical name with the word "mirror" added to the end of it. Example: "sit 1" and "sit 1 mirror" would be fine. If you add or remove animations you will need to reset the script to have it pick up the changes.
  • This will only work in a region where OSSL functions are enabled, where NPCs are enabled, and where the script owner has permission to use the functions: osGetInventoryDesc(), osNpcCreate(), osNpcRemove(), osAvatarPlayAniamtion(), and osAvatarStopAnimation().
  • A base pose uploaded with priority 1 to use for synch and to get around the issue where NPC tend not to cancel the default SL sit animation correctly until the next region persist cycle. I don't have the ability to include one as part of this post however you will find one in every PMAC system (it's called "~~~~~base_DO_NOT_DELETE_ME!!!!!") and in my Paramour Dancemaster dance ball system (with the name "*****base__stand priority 1") which will both work perfectly. Copies of these are available in my Hedonism club lobby at regugegrid.com:8002:Hedonism and at many other places throughout the metaverse. You can also easily make one yourself.
I do not currently have a copy of this in my Hedonism club because I don't have suitable animation pose pairs for it. You might wish to speak to Jessie Campbell (@OSGrid) since she has been creating some and will likely have some finished products available in the near future.

Setting It Up

Setting up an object is simple:
  • Rez the object to ground
  • Add the base pose (priority 1 aniamtion)
  • Add your own animations in mirrored pairs with the correct naming convention
  • Add the main script (see below).
  • Edit the user settings to correctly identify your base pose as well as your global sit target and any other settings you want.
  • Reset the script.
  • Hop on and have fun!

The Script

Here is the script itself. Copy everything below and paste it into a new script:

********************** DO NOT COPY THIS LINE ************************

// Mirror script
// by Aine Caoimhe (c. LACM) July 2015
// Provided under Creative Commons Attribution-Non-Commercial-ShareAlike 4.0 International license.
// Please be sure you read and adhere to the terms of this license: https://creativecommons.org/licenses/by-nc-sa/4.0/
//
// When an avatar sits on an object containing this script the avi will be cloned to an NPC who will also sit
// Paired animations supplied in the object will be played, making it appear as though the NPC is the mirrored user
// If the sitter touches the mirror it will advance to the next pose (if there are more to play)
// when the sitter stands, the NPC is removed
// (Note: if sitter changes appearance during use the NPC will not change to copy that change...it will continue to appear as initially rezzed)
//
// Place this script in the ROOT prim which can either be the mirror object itself or an invisible prim placed at that location.
// It is assumed that the root prim's XZ-plane is the axis to be mirrored
//
// Add animations to the contents of the root prim in pairs where each pair consists of:
//      - an animation for the sitter to play
//      - an animation that mirrors this, that has the IDENTICAL NAME with " mirror" (including the leading space) added after it
//      example: "sit 1" and "sit 1 mirror"
// DO NOT have a main sitter pose end with the word mirror because it would be interpretted as being a mirror pose and ignored
//
// ***** IF YOU ADD OR REMOVE ANIMATIONS YOU WILL NEED TO RESET THE SCRIPT TO PICK UP THE CHANGES!!! *****
//
// The sitter's pose position is determined by the base sitPos and sitRot settings (see user settings section below) UNLESS you
// override these for an animation by placing new values in the DESCRIPTION field of the main pose in the format:
//      <sitpose>::<sit rot>        <--- note the double colon separator between position and rotation
//                                  example:  <0.0, 1.0, 0.0>::<0.0, 0.0, 0.0, 1.0>
// at which point the sitter will be moved to that sit target position instead
//
// In all cases, the clone (mirror NPC) will be positioned exactly mirrored on the root prim's LOCAL XZ plane
// If you have already rotated the mirror pose animation on axis when creating the mirror and do not need it subsequently rotated
// when the clone NPC plays it, set the user variable mirrorRot to FALSE, otherwise leave it TRUE (this applies to all animations played
// so you can't use a mixture of both, sorry)
//
// USER SETTINGS
// use something like the Magic Sit Kit to set the value of these two
integer sayPoseName=TRUE;                   // TURE = owner will be told the name of the pose each time one is activated, FALSE = mirror will be silent
integer userName=FALSE;                     // TRUE = NPC will have the sitter's name, FALSE = NPC will have empty name
vector sitPos=<0.0, 1.0, 0.0>;              // base sit target position to set for the main sitter relative to the mirror - NPC clone will mirror this position on the opposite side of the mirror
rotation sitRot=<0.0, 0.0, 0.0, 1.0>;       // base sit target rotation to set for the main sitter relative to the mirror - NPC clone will mirror this on the opposite side of the mirror
integer mirrorRot=TRUE;     // FALSE = the mirror animations you are supplying are already rotated on root; TRUE = they need to be rotated by the script
string baseAn="*****base__stand priority 1";    // name of a base priority 1 animation to use for underlying synch (no mirror pose of it is needed)
//
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// DO NOT CHANGE ANYTHING BELOW HERE UNLESS YOU KNOW WHAT YOU'RE DOING
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
list poses;
list mirrors;
integer anIndex;
key user;
key npc;
integer linkNpc;
integer linkUser;

updatePositions()
{
    vector pos=sitPos;
    rotation rot=sitRot;
    // see if this pose requires position override
    string posOverride=osGetInventoryDesc(llList2String(poses,anIndex));
    if (llSubStringIndex(posOverride,"::")>=0)
    {
        list orData=llParseString2List(posOverride,["::"],[]);
        pos=llList2Vector(orData,0);
        rot=llList2Rot(orData,1);
    }
    vector size = llGetAgentSize(user);
    float fAdjust = ((((0.008906 * size.z) + -0.049831) * size.z) + 0.088967) * size.z;
    vector newPos=(pos + <0.0, 0.0, 0.4>) - (llRot2Up(rot) * fAdjust);
    llSetLinkPrimitiveParamsFast(linkUser, [PRIM_POS_LOCAL,newPos,PRIM_ROT_LOCAL, rot]);
    newPos.y=-newPos.y;
    if (mirrorRot)
    {
        vector aviRot=llRot2Euler(rot);
        aviRot.z=-aviRot.z;
        aviRot.x=-aviRot.x;
        rot =llEuler2Rot(aviRot);
    }
    llSetLinkPrimitiveParamsFast(linkNpc, [PRIM_POS_LOCAL,newPos,PRIM_ROT_LOCAL, rot]);
    if (sayPoseName) llOwnerSay("New pose: "+llList2String(poses,anIndex));
}
buildPoseList()
{
    list an;
    poses=[];
    mirrors=[];
    integer i=llGetInventoryNumber(INVENTORY_ANIMATION);
    while (--i>=0) { an=[]+[llGetInventoryName(INVENTORY_ANIMATION,i)]+an; }
    i=llListFindList(an,[baseAn]);
    if (i==-1)
    {
        llOwnerSay("ERROR! Unable to locate the base priototy 1 aniamtion: "+baseAn);
        user="ERROR";
    }
    else an=[]+llDeleteSubList(an,i,i);
    i=llGetListLength(an);
    while (--i>=0)
    {
        string name=llList2String(an,i);
        if (llGetSubString(name,-7,-1)!=" mirror")
        {
            integer m=llListFindList(an,[name+" mirror"]);
            if (m==-1) llOwnerSay("WARNING: found an animation with the name \""+name+"\" but no matching animation was located");
            else
            {
                poses=[]+[name]+poses;
                mirrors=[]+[name+" mirror"]+mirrors;
            }
        }
    }
    // double-check even though mismatch ought to be impossible;
    if (llGetListLength(poses)!=llGetListLength(mirrors))
    {
        llOwnerSay("ERROR! Somehow build pose and mirror lists with mismatched lengths. Dump of data:\n\nPOSES: "+llDumpList2String(poses,", ")+"\nMIRRORS: "+llDumpList2String(mirrors,", "));
        user="ERROR";
    }
    else
    {
        anIndex=0;
        if (llGetListLength(poses)==1) llOwnerSay("Pose list built. Found only 1 mirrored pose in inventory so disabling touch advance");
        else llOwnerSay("Pose list built. Found "+(string)llGetListLength(poses)+" mirrored pose in inventory");
    }
}
cleanSitters()
{
    user=NULL_KEY;
    npc=NULL_KEY;
    integer l=llGetNumberOfPrims();
    while (l>0)
    {
        key who=llGetLinkKey(l);
        if (llGetAgentSize(who)==ZERO_VECTOR) l=0; // agents are always the last link numbers
        else
        {
            if (osIsNpc(who)) osNpcRemove(who);
            else
            {
                llRegionSayTo(who,0,"Sorry, the system has been reset so you will need to sit down again");
                llUnSit(who);
            }
        }
        l--;
    }
}
default
{
    state_entry()
    {
        buildPoseList();
        llSitTarget(<0.0, 0.0, 0.000001>,ZERO_ROTATION);
        llSetClickAction(CLICK_ACTION_SIT);
        if (user=="ERROR") llOwnerSay("Cannot active device until animation list building errors are correct");
        else cleanSitters();
    }
    on_rez(integer foo)
    {
        llResetScript();
    }
    touch_start(integer num)
    {
        key who=llDetectedKey(0);
        if (who!=user)
        {
            if (user==NULL_KEY) llRegionSayTo(who,0,"Please sit on me to use me");
            else llRegionSayTo(who,0,"Sorry, only the current user can touch me");
        }
        else
        {
            if (llGetListLength(poses)==1) return;   // with only 1 pose no changing
            osAvatarStopAnimation(user,llList2String(poses,anIndex));
            osAvatarStopAnimation(npc,llList2String(mirrors,anIndex));
            anIndex++;
            if (anIndex>=llGetListLength(poses)) anIndex=0;
            osAvatarPlayAnimation(user,llList2String(poses,anIndex));
            osAvatarPlayAnimation(npc,llList2String(mirrors,anIndex));
            updatePositions();
        }
    }
    changed (integer change)
    {
        if (change & CHANGED_OWNER) llResetScript();
        if (change & CHANGED_REGION_START) llResetScript();
        if (change & CHANGED_INVENTORY) llOwnerSay("Detected a change in inventory. Remember to reset the script if you have changed the animations");
        if (change & CHANGED_LINK)
        {
            // find out who is currently sitting
            list aviSitters=[];
            list npcSitters=[];
            integer l=llGetNumberOfPrims();
            while (l>0)
            {
                key who=llGetLinkKey(l);
                if (llGetAgentSize(who)==ZERO_VECTOR) l=0;  // sitters are always last links
                else
                {
                    if (osIsNpc(who))
                    {
                        npcSitters=[]+[who]+npcSitters;
                        if (who==npc) linkNpc=l;
                    }
                    else
                    {
                        aviSitters=[]+[who]+aviSitters;
                        if (who==user) linkUser=l;
                    }
                }
                l--;
            }
            if (user==NULL_KEY)
            {
                l=llGetListLength(npcSitters);
                while (--l>=0) { osNpcRemove(llList2Key(npcSitters,l)); }
                npc=NULL_KEY;
                if (llGetListLength(aviSitters)==0)
                {
                    llSetClickAction(CLICK_ACTION_SIT); // back to sit as default action
                    return;     // link change triggered by user standing or a change in some other linkset action
                }
                // getting here means a new avi sat
                user=llList2Key(aviSitters,0);
                osAvatarPlayAnimation(user,baseAn); // initiate base an
                llSetClickAction(CLICK_ACTION_TOUCH);   // now in use so default action should be touch
                string first=" ";
                string last=" ";
                if (userName)
                {
                    string nameToParse=llList2String(llGetObjectDetails(user,[OBJECT_NAME]),0);
                    list parsedName=llParseString2List(nameToParse,[" ",".","@"],[]);
                    first=llList2String(parsedName,0);
                    last=llList2String(parsedName,1);
                }
                npc=osNpcCreate(first,last,llGetPos()+<0.0,0.0,1.0>,user);
                osAvatarPlayAnimation(npc,baseAn);
                osNpcSit(npc,llGetKey(),OS_NPC_SIT_NOW);
                // everything else handled when NPC is detected as sitting
            }
            else if (user=="ERROR")
            {
                l=llGetListLength(npcSitters);
                while (--l>=0) { osNpcRemove(llList2Key(npcSitters,l)); }
                l=llGetListLength(aviSitters);
                while (--l>=0)
                {
                    llRegionSayTo(llList2Key(aviSitters,l),0,"Sorry, the mirror has encountered an error and needs to be reset");
                    llUnSit(llList2Key(aviSitters,l));
                }
            }
            else
            {
                // we already have a user on record
                if (llListFindList(aviSitters,[user])==-1)
                {
                    // but no longer sitting so need to release animations, clear user, and remove NPC
                    if (llGetAgentSize(user)!=ZERO_VECTOR)
                    {
                        osAvatarPlayAnimation(user,"Stand");
                        osAvatarStopAnimation(user,llList2Key(poses,anIndex));
                        osAvatarStopAnimation(user,baseAn);
                    }
                    user=NULL_KEY;
                    if (osIsNpc(npc))
                    {
                        osNpcRemove(npc);
                        npc=NULL_KEY;
                    }
                    llSetClickAction(CLICK_ACTION_SIT); // back to sit as default action
                }
                // make sure someone else didn't sit down when user was set
                l=llGetListLength(aviSitters);
                while (--l>=0)
                {
                    if (llList2Key(aviSitters,l)!=user)
                    {
                        llRegionSayTo(llList2Key(aviSitters,l),0,"Sorry, the mirror is already being used by someone else");
                        llUnSit(llList2Key(aviSitters,l));
                    }
                }
                // prevent any NPC from sitting other than the current one on record
                l=llGetListLength(npcSitters);
                while (--l>=0) { if (llList2Key(npcSitters,l)!=npc) osNpcRemove(llList2Key(npcSitters,l)); }
                if ((npc!=NULL_KEY) && osIsNpc(npc))
                {
                    // most of the time we only get here if a new NPC sat after initial creation so this should trigger start of animation
                    // need to wait briefly for base an to kick in
                    llSleep(0.25);
                    // then clear any existing AO/sit animations
                    key dontStop=llGetInventoryKey(baseAn);
                    list anToStop=llGetAnimationList(user);
                    l=llGetListLength(anToStop);
                    while (--l>=0) { if (llList2Key(anToStop,l)!=dontStop) osAvatarStopAnimation(user,llList2Key(anToStop,l)); }
                    anToStop=[]+llGetAnimationList(npc);
                    l=llGetListLength(anToStop);
                    while (--l>=0) { if (llList2Key(anToStop,l)!=dontStop) osAvatarStopAnimation(npc,llList2Key(anToStop,l)); }
                    // now start the mirrored animations
                    osAvatarPlayAnimation(user,llList2String(poses,anIndex));
                    osAvatarPlayAnimation(npc,llList2String(mirrors,anIndex));
                    updatePositions();
                }
            }
        }
    }
}

Monday, 13 July 2015

RELEASE ANNOUNCEMENT: PMAC ADD-ON SET 1

I am pleased to announce the release of the PMAC Add-On Set 1 which contains a trio of add-on scripts I have written for the PMAC multi-animation controller and am now releasing to the general public.


PAO Child Add-On

This add-on provides limited controls over prims in the PMAC linkset, allowing you to reposition it or change the alpha (usually either to show or hide it). Only child prims can be repositioned but alpha changes can be applied to any linked prim (including root). Multiple child prims can be changed within a single command.

I wrote it initially for hot-tub so I could control the water level and transparency for each different animation (because I'm short and was tired of continually looking like was drowning) but it can be used for a huge range of handy applications...for example have it move or hide a blanket or pillow that's part of the PMAC link-set.

This is relatively simple to set-up and includes a convenience feature to help get the values you need for the notecard; however this does require careful manual edit of the .menu notecard.


PAO Message Object Add-On

This add-on allows you to send a message to any scene object located in the same region as the PMAC object.

Typical usage:
  • to message a nearby light source to turn itself on or off
  • to message a region controller script to change windlight settings, parcel media, parcel sound, etc
  • to tell a permanent scene object (that isn't part of the PMAC linkset) to do something like move, hide/show itself, change colour, change size, etc...
  • or anything else you're capable of scripting using a dataserver trigger...there's tons of interesting things you can do
This add-on assumes that the person using it has the necessary scripting skills to accomplish the desired effect via a script in the target object. The add-on's purpose is simply to make this easier to achieve without needing to write your own custom add-on script as well.

The basic mechanism used by the add-on is to be supplied (via the data stored in the .menu notecard) the UUID of the target object and a message to send to it when the animation is played (using osMessageObject). This then raises a dataserver event in the object which would then be scripted to respond to the messages as desired.

You can send almost any message you like which gives you tremendous flexibility in terms of being able to send a huge array of commands and even sets of sub-data arguments for them. It's simply a matter of writing your target's script and then adhering to its requirements in the messages you tell this add-on to send.

Messages are only sent when each new animation is called...no "end message" is sent when a user stands or when a different animation is selected that does not contain this command so you will need to bear this in mind when building your objects and writing your scripts.

PAO Message Sitters Add-On

This add-on allows you to send a message to all attachments of your sitters on a per-animation basis. The attachment would then be scripted to respond to this message in some way.

Typical usage:
  • to hide or show an attachment (or part of one) on a per-animation basis
  • to trigger an attachment colour change
  • to trigger an attachment particle effect (for instance have worn cuffs "connect" themselves to a specific prim whose key is supplied as part of the string
  • assorted "scripted manhoods" can be adapted to use the dataserver event to automatically set visibility/position/etc based on the animation being played
  • the dataserver event could, in turn, be scripted to automatically trigger/send a RLV command
  • or anything else you're capable of scripting using a dataserver trigger...there's tons of interesting things you can do

Like the Message Object add-on, this assumes that the person using it has the necessary scripting skills to accomplish the desired effect via a script in the attachment object. The add-on's purpose is simply to make this easier to achieve without needing to write your own custom add-on script as well.

The basic mechanism used by the add-on is to be supplied (via the data stored in the .menu notecard) a message to be sent to each of the sitters (using osMessageAttachments(). Each sitter is given a separate message string rather than a simple global broadcast to all sitters. This then raises a dataserver event in all attachments worn by the sitting in that position which would then be scripted to respond to the messages as desired.

You can send almost any message you like which gives you tremendous flexibility in terms of being able to send a huge array of commands and even sets of sub-data arguments for them. It's simply a matter of writing your attachment's script and then adhering to its requirements in the messages you tell this add-on to send.

Messages are only sent when each new animation is called...no "end message" is sent when a user stands or when a different animation is selected that does not contain this command so you will need to bear this in mind when building your objects and writing your scripts.

How to Use Them

As with all of my products, a "Read Me" notecard is included for each script detailing how to use it. The two messaging add-ons requite a medium level of scripting knowledge and would not be suitable for a novice. The child add-on is simple to use and set-up even with no scripting knowledge.


Availability

You can pick up a boxed copy of all three scripts in the lobby of my Hedonism dance club on Refugegrid.

HGTP to refugegrid.com:8002
Then take the local portal to Hedonism

The box is sitting right beside the PMAC Builder's kit that already contains the PAO Expressions add-on and the props add-on by  +Neothar Cortex . While you're there you might also want to pick up the box containing the Content-Giver add-on by +Aaack Aardvark

All three scripts are released under Creative Commons Attribution-Non-Commercial-ShareAlike 4.0 International license.

Tuesday, 7 July 2015

RELEASE ANNOUNCEMENT: PMAC Content Giver Add-On

It gives me great pleasure to announce the release of a new add-on for the PMAC system: the Content Giver, written and donated to the community by +Aaack Aardvark of Littlefield Grid.

This add-on script allows you to place objects inside your PMAC object and adds a button to the specials menu that will give the specified items to the user in a new folder in their inventory.

You can pick up a full-perm copy from the lobby of my Hedonism Dance Club at refugegrid.com:8002:Hedonism -- it's the box sitting right beside the PMAC Builder's kit.

Huge thanks to Aaack, both for creating it and for making it available for the entire community to anjoy.