XML Animation Overrider

From Second Life Wiki
Jump to: navigation, search

Created by Kira Komarov of the Wizardry and Steamworks group.

Legalities

Please see the license for this system for usage conditions and warranty. (Links to an empty page as of 2013-03-21)

Introduction

Recently we have been collaborating with several Universities from the BIO-SE group by helping set-up hypergrid to which the Wizardry and Steamworks hideout will be linked to as well. One of the challenges has been to polish the OpenSim code, as well as implement several needed features which are uncommon to OpenSim. We have achieved a lot, we believe, our performance and uptime peaking at an all time high from what one would expect from a partially broken implementation that does not yet fully conform to Second Life specifications.

Another challenge has been to recreate all the necessary teaching material for students. We have rebuilt most of that and we have supported the new BIO-SE with all our script armory. A sub-challenge of that was to create an animation overrider from ground-up that would have a drastically reduced complexity and resource consumption than the infamous ZHAO animator. The results are that, while the ZHAO animator spans some good 750 lines of code, the XML Animation Overrider (kXAO) consumes drastically less memory than the ZHAO animator.

We started off initially by trying to optimize the code written by ZHAO, however we soon noticed that, due to the state that the OpenSim LSL semantics are in, we were unable to successfully get the ZHAO animator to work properly as well as guarantee its correct operation in all instances. That lead us further and we thought that the only solution would be to recreate an animation overrider which is based more on theory rather than coding horsepower.

For the kXAO, we used StAX in order to be able to use XML-based notecards, with properly structured data, as well as the formalities described in our informative article Wizardry and Steamworks/State Machines since the similarity of changes between animations contrast well to non-deterministic, finite, automaton (NFA) state-changes.

State Machines

This lead us to the simple and readable code we offer here to the rest of the world. Due to the lack of free and full-permission animations, we were unable to implement the whole lot of animations that must be overridden for fluid avatar dynamics. However, by using the state-machine theory, you may notice that all our states are semantically equivalent to the ZHAO-style notecard definitions:

[ Standing ] -> state standing
[ Walking ] -> state walking

Furthermore, the states we use for the automaton are short and clean. Extending the kXAO is just a matter of extending the automaton by replicating states and changing them accordingly. We may contrast the code used in the Standing state with the code used for the Walking state.

Standing: <lsl> state standing {

   state_entry() {
       changeAnim("standing");
       state monitor;
   }

} </lsl>

Walking: <lsl> state walking {

   state_entry() {
       changeAnim("walking");
       state monitor;
   }

} </lsl>

As you you may observe, the semantics for the two states are equivalent with just a minor difference that we pass the name of the current state to the function changeAnim. This is regrettably because we cannot read the name of the current state which is needed in order not to trigger the same animation while in the same state.

We use one central stationary state called monitor which determines whether the automaton should commute to one of the states. The states themselves, change the animation and then commute back to the stationary state. The run-time of an individual state is thus minimal and no variables are allocated, except by the call to changeAnim which stops the previous animation and replaces it with the next one. Abstractly, each state replaces the current animation and then switches back to the monitor stationary state.

XML

The XML StaX reader implemented by Wizardry and Steamworks provides us with a clear, descriptive and hierarchical way of organizing animations which is definitely more readable than the ZHAO notecard-style. The kXAO XML notecards have a very simple and extendable syntax. For example, we used the following notecard in Second Life, along with a few copyable animations from some other animators, which resulted in the following notecard content:

<ao>
    <walk>
        <walking>sexy_brain_walk</walking>
    </walk>
    <fly>
        <flying>D0846-Fly.N</flying>
    </fly>
    <sit>
        <sitting>breathless</sitting>
        <sitting>caramel</sitting>
        <sitting>cuba</sitting>
    </sit>
    <stand>
        <standing>D0931-Stand.N(Teen)</standing>
    </stand>
</ao>

Further on, we use wasStaX_GetNodeValue in order to extract the tag-contents of a particular action such as walking or flying. The beauty of the XML-style notecards relies in the flexibility of the structural contents of the notecard. That is, a user may very well modify the tags or move them around. As for the developer, using the StAX-like implementation, allows them to easily extend the XML structure while still having the guarantee of a correct extraction of tag contents. For example, the tag ao is superfluous and may be replaced by root, or any other name for that matter.

All other tags not referenced in the code may be renamed or changed at will, as long as the structure of the XML notecard is well-formed. In case of tag abuse, the StAX reader implementation will throw an error, warning that the XML structure is not well-formed.

Initially we re-read the XML from the notecard on every state change. That worked fine for Second Life since the implementation is stable compared to the OpenSim environment. It also worked under OpenSim with the exception that we observed a noticeable delay between animation changes. We traced it down to wasStaX_GetNodeValue which under OpenSim, when called with high frequency (move,stop,move,stop...) would delay the script by a significative amount for an animation overrider.

To overcome that difficulty, we then switched to using lists and store the animations once. This would increase the memory usage, however due to the OpenSim VM we were not able to read the XML fast enough (perhaps some scheduling/priority issue?).

Another, perhaps possible solution in order to avoid the lists, would perhaps be to use the DOM XML reader instead since it makes use of very fast recursion and usually out-performs StaX (which, is in fact meant for stream-reading XML files).

Either way, the fine line to draw is, as always, between memory consumption and computational speed. Avoid global variables and you require more computational speed in order to be able to do things in time. Use global variables and munch up memory, yet gain computational speed.

Exploiting the StaX-Stream Reader

Given that the StaX-Stream Reader is able to process XML streams, it is feasible enough to use a listen event on a certain channel in order to allow others to feed your AO animation data. This may have to be preceded by an llGiveInventory in which that certain person transfers the animation into your HUD. Probably some negotiation protocol would have to be established beforehand in order to:

  • authorize the person to transfer an animation
  • choose a channel after the authorization
  • transfer the animation(s)
  • process the stream using StaX.
  • replace the default lists.

This could be exploited in order to animate people, starting from dancing and up to more interesting things. Noted in TODO, this may be an upcoming feature of the XML Animation Overrider.

"Try turning your AO off..."

This phrase is common to people using animation overriders which do not detect when an avatar is sitting on an object - that, however, might be circumvented by using an animation for the sitobject state which has the lowest possible priority. In other words, one could upload an animation that triggers when the agent is sitting on an object, with the lowest priority available in the selector and then referencing it in the notecard.

Conclusions

We would like to highlight the importance of using a consistent and verifiable configuration format. The XML format is verifiable, either by using StAX or DOM-style readers. We would also like to show that state machines are a beautiful feature of LSL which, when used properly, provide a very smooth transition from iterative code to more flexible and adaptive code-semantics. Furthermore, we urge programmers to keep readability as one of the highest priorities and prefer, in most cases, and to leave lower-level optimizations up to the compiler. Consider that any little change to the VM, either by expanding it or by modifying some behavioral parameters of data specifications may lead to defective code that has to be replaced. That, in turn, generates a lot of maintenance costs. Furthermore, lower-level optimizations tightly-knit the code to the characteristics of the VM. We have noticed on several occasions that even our own code, when ported over to OpenSim, generate errors which are not due to a faulty implementation but rather due to a different implementation parameters.

OpenSim

The code below, contains some particularities which may be interesting to observe (even for the AO-uninterested). We have a crazy way (don't we always) of de-allocating a list while stopping the animations as well at the same time:

<lsl> // in dataserver sList = llParseString2List(stream, [" "], ["<", ">", "/"]); // ... // in run_time_permissions sList = llParseString2List(llDumpList2String(sList, ""), ["<", ">", "/"], []); do {

   llStopAnimation(llList2String(sList, 0));

} while(sList = llDeleteSubList(sList, 0, 0)); </lsl> where the dataserver part stores the xmlstream in sList in order to be processed by StAX.

Later on, in run_time_permissions, before switching to the monitoring state, we re-arrange the list and get rid of the symbols. We then proceed by destroying the list in a do-while loop by traversing the list as a stack and shaving off the element at the top of the stack. We pop the element, we stop the animation (will fail silently if it is not an animation, in case of tags) and then we destroy the element.

This relies on the fact that: <lsl> list l = [] if(l) {} </lsl> the value of a list is false when the list is empty. We thus are able to terminate and use the while conditional to terminate once the list has been completely destroyed.

We are aware that: <lsl> sList = llDeleteSubList(sList, 0, 0) </lsl> is not considered a fast operation in LSL terms. However, for a language that is closest to LISP than any other programming language, we still prefer to do things the list way.

This trick will not work on OpenSim, since in OpenSim's implementation of LSL:

Error: CS0029: Cannot implicitly convert type `list' to `bool'

which just shows us that OpenSim's implementation of LSL is more tightly typed. Probably a consequence of the LSL VM being closer to C-Sharp.

That is no problem though, we can always drop down to the good-old boring order of business by using an interator. Thus, if used on OpenSim, the code-block above should be replaced by: <lsl> sList = llParseString2List(llDumpList2String(sList, ""), ["<", ">", "/"], []); integer itra = llGetListLength(sList)-1; do {

   llStopAnimation(llList2String(sList, itra));

} while(--itra>=0); sList = []; </lsl> which, in any case, whether OpenSim or Second Life, is more efficient in the first place than the interesting version. We just thought to point this out since the interesting version is, well, as we said... interesting.

We consider it interesting and even the proper way to remove a list, for the following reasons:

  • An assignment such as:

<lsl> some_list = []; </lsl>

where a list is really a collection of elements of the real primitive types, then that assignment would not trash the list completely in a pass-by-reference language. It would just be marked for the next sweep of the garbage collector (supposing it has such a thing) and be de-allocated when it is convenient, depending on the garbage collector implementation.

The interesting way of doing it, actually trashes it element by element by stripping the list until it is completely destroyed. However, since LSL is said to be a call-by-value language, then we suppose that the list is trashed completely even with just an assignment to the empty list.

We are not sure, in fact, there may be a conflict of statements. A list is not really a primitive type but rather a wrapper that may contain other primitive types. Additionally, a list may not contain other lists. This smells like call-by-reference to us when it comes to lists.

Extending

Extending should be easy since we have eliminated the llRequestPermissions from the states, which were largely superfluous.

In order to extend the XML Animation Overrider, you will have to edit:

  • The XML notecard itself and add a tag for it.
  • Add the list that will contain the animations for that tag to the variable ANIMATION_NODES.
  • Add the animation in changeAnimation to correspond to the list in the previous point.
  • Fill the animation list in the dataserver event based on the chosen tag name in the first step.
  • Add another state to the NFA, named after the tag in the first step.

Notecard

A simple XML notecard that can be used with the XML Animation Overrider, provided that all the animations are renamed correspondingly.

For example,

walk_1

is the name of a walking animation.

Example Notecard

<ao>
    <walk>
        <walking>walk_1</walking>
        <walking>walk_2</walking>
        <walking>walk_3</walking>
        <walking>walk_4</walking>
        <walking>walk_5</walking>
    </walk>
    <fly>
        <flying>fly_1</flying>
    </fly>
    <sit>
        <sitting>sit_1</sitting>
        <sitting>sit_2</sitting>
        <sitting>sit_3</sitting>
    </sit>
    <run>
        <running>run_1</running>
    </run>
    <type>
        <typing>type_1</typing>
    </type>
    <stand>
        <standing>stand_1</standing>
        <standing>stand_2</standing>
        <standing>stand_3</standing>
        <standing>stand_4</standing>
        <standing>stand_5</standing>
    </stand>
</ao>

Code

<lsl> ////////////////////////////////////////////////////////// // [K] Komarov - 2011, License: GPLv3 // // Please see: http://www.gnu.org/licenses/gpl.html // // for legal details, rights of fair usage and // // the disclaimer and warranty conditions. // //////////////////////////////////////////////////////////

key _owner = NULL_KEY; key nQuery = NULL_KEY; integer xLine = 0; integer agentInfo = 0; string _lastAnim = ""; string _lastAnimName = "";

string stream = ""; list sList = [];

//pragma inline string xName = "XAO";

list walk_list = []; list fly_list = []; list sit_list = []; list stand_list = []; list run_list = []; list type_list = []; list sitobj_list = [];

//pragma inline list ANIMATION_NODES = [

   "walking",
   "flying",
   "sitting",
   "standing",
   "running",
   "typing",
   "sitobject"

];

//pragma inline string wasStaX_GetNodeValue(string node) {

   list StaX = [];
   string value = "";
   xLine = 0;
   do {
       string current = llList2String(sList, xLine);
       string lookback = llList2String(sList, xLine-1);

       if(current != "/" && lookback == "<") {
           StaX += current;
           jump next_tag;
       }
       integer len = llGetListLength(StaX)-1;
       if(lookback == "/") {
           StaX = llDeleteSubList(StaX, len, len);
           jump next_tag;
       }
       if(current != ">" && current != "/" && current != "<") 
           if(llList2String(StaX,len) == node)
               value += current + " ";  

@next_tag;

   } while(++xLine<agentInfo);

   if(llGetListLength(StaX) != 0) {
       llSay(DEBUG_CHANNEL, "The following tags may be unmatched: " + llDumpList2String(StaX, ",") + ". Please check your file.");
   }
   return value;

}

//pragma inline changeAnimation(string animation) {

   string nAnim;
   if(animation == "walking") {
       nAnim = llList2String(walk_list, (integer)llFrand(llGetListLength(walk_list)));
       jump set_anim;
   }
   if(animation == "sitting") {
       nAnim = llList2String(sit_list, (integer)llFrand(llGetListLength(sit_list)));
       jump set_anim;
   }
   if(animation == "flying") {
       nAnim = llList2String(fly_list, (integer)llFrand(llGetListLength(fly_list)));
       jump set_anim;
   }
   if(animation == "standing") {
       nAnim = llList2String(stand_list, (integer)llFrand(llGetListLength(stand_list)));
   }
   if(animation == "running") {
       nAnim = llList2String(run_list, (integer)llFrand(llGetListLength(run_list)));
   }
   if(animation == "typing") {
       nAnim = llList2String(type_list, (integer)llFrand(llGetListLength(type_list)));
   }
   if(animation == "sitobject") {
       nAnim = llList2String(sitobj_list, (integer)llFrand(llGetListLength(sitobj_list)));
   }

@set_anim;

   llStartAnimation(nAnim);
   llStopAnimation(_lastAnimName);
   _lastAnimName = nAnim;
   _lastAnim = animation;

}

//pragma inline integer commute() {

   agentInfo = llGetAgentInfo(_owner);
   if(agentInfo & AGENT_WALKING) {
       return 0;
   }
   if(agentInfo & AGENT_FLYING) {
       return 1;
   }
   if(agentInfo & AGENT_ON_OBJECT) {
       return 2;
   }
   if(agentInfo & AGENT_SITTING) {
       return 3;
   }
   if(agentInfo & AGENT_ALWAYS_RUN) {
       return 4;
   }
   if(agentInfo & AGENT_TYPING) {
       return 5;
   }
   return 6;

}

default {

   state_entry() {
       _owner = llGetOwner();
       integer itra;
       for(itra=0, xLine=0, stream = ""; itra<llGetInventoryNumber(INVENTORY_NOTECARD); ++itra) {
           if(llGetInventoryName(INVENTORY_NOTECARD, itra) == xName)
               jump read;
       }
       llInstantMessage(llGetOwner(), "Failed to find notecard.");
       return;

@read;

       nQuery = llGetNotecardLine(xName, xLine);
   }
   dataserver(key id, string data) {
       if(id != nQuery) return;
       if(data == EOF) {
           llOwnerSay("Read XML...");
           sList = llParseString2List(stream, [" "], ["<", ">", "/"]);
           stream = "";
           agentInfo = llGetListLength(sList);
           integer itra = llGetListLength(ANIMATION_NODES)-1;
           do {
               if(llList2String(ANIMATION_NODES, itra) == "walking") {
                   walk_list = llParseString2List(wasStaX_GetNodeValue("walking"), [" "], []);
                   jump anim_type; 
               }
               if(llList2String(ANIMATION_NODES, itra) == "flying") {
                   fly_list = llParseString2List(wasStaX_GetNodeValue("flying"), [" "], []);
                   jump anim_type; 
               }
               if(llList2String(ANIMATION_NODES, itra) == "sitting") {
                   sit_list = llParseString2List(wasStaX_GetNodeValue("sitting"), [" "], []);
                   jump anim_type; 
               }
               if(llList2String(ANIMATION_NODES, itra) == "standing") {
                   stand_list = llParseString2List(wasStaX_GetNodeValue("standing"), [" "], []);
               }    
               if(llList2String(ANIMATION_NODES, itra) == "running") {
                   run_list = llParseString2List(wasStaX_GetNodeValue("running"), [" "], []);
               }     
               if(llList2String(ANIMATION_NODES, itra) == "typing") {
                   type_list = llParseString2List(wasStaX_GetNodeValue("typing"), [" "], []);
               }
               if(llList2String(ANIMATION_NODES, itra) == "sitobject") {
                   sitobj_list = llParseString2List(wasStaX_GetNodeValue("sitobject"), [" "], []);
               }   

@anim_type;

           } while(--itra>=0);
           llRequestPermissions(_owner, PERMISSION_TRIGGER_ANIMATION);
           return;
       }
       if(data == "") jump next_line;
       stream += data;

@next_line;

       nQuery = llGetNotecardLine(xName, ++xLine);
   }
   run_time_permissions(integer perm) {
       if(perm & PERMISSION_TRIGGER_ANIMATION) {
           llOwnerSay("Acquired permissions...");
           sList = llParseString2List(llDumpList2String(sList, ""), ["<", ">", "/"], []);
           integer itra=llGetListLength(sList)-1;
           do {
               llStopAnimation(llList2String(sList, itra));
           } while(--itra>=0);
           llOwnerSay("Starting...");
           state walking;
       }
   }
   changed(integer change) {
       if(change & CHANGED_OWNER) {
           _owner = llGetOwner();
           return;
       }
       if(change & CHANGED_INVENTORY) {
           llResetScript();
           return;
       }
   }
   on_rez(integer param) {
       _owner = llGetOwner();
   }

}

state walking {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

}

state standing {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

}

state flying {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

}

state sitting {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

}

state running {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

}

state typing {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

}

state sitobject {

   state_entry() {
       llSetTimerEvent((1.02-llGetRegionTimeDilation()));
   }

   timer() {
       // Get next state.
       agentInfo = commute();
       if(agentInfo == 0 && _lastAnim != "walking") { changeAnimation("walking"); state walking; }
       if(agentInfo == 1 && _lastAnim != "flying") { changeAnimation("flying"); state flying; }
       if(agentInfo == 2 && _lastAnim != "sitobject") { changeAnimation("sitobject"); state sitobject; }
       if(agentInfo == 3 && _lastAnim != "sitting") { changeAnimation("sitting"); state sitting; }
       if(agentInfo == 4 && _lastAnim != "running") { changeAnimation("running"); state running; }
       if(agentInfo == 5 && _lastAnim != "typing") { changeAnimation("typing"); state typing; }
       if(agentInfo == 6 && _lastAnim != "standing") { changeAnimation("standing"); state standing; }
   }

} </lsl>

TODO

  • Add streaming capabilities for animation exchange.