Jump to content

Managing MultipleTimers


You are about to reply to a thread that has been inactive for 1785 days.

Please take a moment to consider if this thread is worth bumping.

Recommended Posts

Hi all,

In my current project I found myself with a problem. I had created a script that would manage all the doors in the house I was building, so that would only need one script as opposed to in door scripts. After slogging through how I would keep track of what way they needed to move, how far, etc. I thought that I was done .. until I hit the 'extras'. First off my doors include a security check, which calls a central access script to determine if the user can open of not the door. Next, since these were sliding doors, with multiple panels I had added in a 'feature' where if you did a hold touch on the door, a menu would pop up for you to select how many panels you wanted open, and if you wanted this applied to the paired door (if any). Now each of these things:, the hold touch, the access check, the dialogs, all had timers associated with them. And worse I realised that multiple things could be happening at once from different doors in the house.

So the challenge was how could I manage an unknown number of multiple timers, of different duration, which had some data associated with them (user at the least). I've spent most of the afternoon thinking it through, with a few starts and restarts, but I think I have come up with an answer, and I'd appreciate any feedback, suggestions, pointers as to where it might not work .. fail. I've pasted the code below, the first section is the basic timer stack code .. there are a few specifics to my house solution, but these can be readily changed .. and then following is an example of how this is implemented .. again here with a number of specific functions to my solution .. most of which (functions etc) are not included.

So fire away 🙂

string EMPTY_STR = "";
string FUNC_SEP = "|";
string DATA_SEP = "^";

//stack ids
string ST_ACCESS = "acc";
string ST_DIALOG = "dia";
string ST_TOUCH = "tch";

//list of the timer lengths associated with the stack_id
list TIMER_LENGTHS = [ST_ACCESS, 5.0, ST_DIALOG, 30.0, ST_TOUCH, 0.75];

//strided list that holds the data of timer items in the stack
//end_time, stack_id, user_key, data
//end_time: when the timer event should fire
//stack_id: what called for a timer, in this case ST_DIALOG, ST_ACCESS, ST_TOUCH
//user_key: key of the AV involved in the event
//data: list of relevant data that needs to be stored with the timer call, seperated by ^ (DATA_SEP)
list gStack;

add_stack(string stack_id, key user, list data)
{
    //stack_id = ST_DIALOG, ST_ACCESS, ST_TOUCH
    //data = ST_DIALOG:set, type, action; ST_ACCESS:set, group, type; ST_TOUCH:set, group
    //type = ST_DIALOG:DT_MAIN, DT_SECOND; ST_ACCESS:AT_MENU, AT_OPEN
    
    //do the action as required by the type
    //these return data with a listen handle added and relevant data removed (dialog removes type, access removes group)
    if (stack_id == ST_DIALOG){data = show_dialog(user, data);}
    else if (stack_id == ST_ACCESS){data = check_access(user, data);}

    //get my end and current time
    float time = llGetTime();
    float end = llGetTime() + (float)llList2String(TIMER_LENGTHS, llListFindList(TIMER_LENGTHS, [stack_id]) + 1);
    float set = 0;

    //if the stack is empty .. its my timer to start
    if (llGetListLength(gStack) == 0){set = (end - time);}
    //or if next end is > my end reset timer to my end
    else {if (end < (float)llList2String(llListSort(gStack, 4, TRUE), 0)){set = (end - time);}}
    if (set > 0){llSetTimerEvent(set);}

    //finally add me to the stack
    gStack += [end, stack_id, user, llDumpList2String(data, DATA_SEP)];
}


list clear_stack(string stack_id, key user)
{
    //stack_id = ST_DIALOG, ST_ACCESS, ST_TOUCH
    //return stack_data
    //stack_data = ST_DIALOG:set, action; ST_ACCESS:set, type; ST_TOUCH:set, group

    //clear any active timer
    llSetTimerEvent(0.0);
    //find me 
    integer posit = llListFindList(gStack, [stack_id, user]);
    //get data to return
    list data = llParseString2List(llList2String(gStack, posit + 2), [DATA_SEP], [EMPTY_STR]);
    //if a dialog or access we need to remove the listen
    if (stack_id == ST_DIALOG || stack_id == ST_ACCESS){llListenRemove(llList2Integer(data, 2));}
    //remove me from the stack
    gStack = llDeleteSubList(gStack, posit - 1, posit + 2);
    //finally if there is still something in the stack reset the timer
    if (llGetListLength(gStack) != 0){llSetTimerEvent((float)llList2String(llListSort(gStack, 4, TRUE), 0) - llGetTime());}

    //return data, dropping handle if there
    return llList2List(data, 0, 1);
       
}

//checks to see if the user has a touch event in the stack, used to short-circuit the touch_end event
//#define touch_active(user) ~llListFindList(gStack, [ST_TOUCH, user])
integer touch_active(key user)
{
    //return t/f if user has an active touch
    return ~llListFindList(gStack, [ST_TOUCH, user]);
}

//gets the current event which has caused the timer event to fire
//#define get_active() llList2List(llListSort(gStack, 4, TRUE), 1, 2)
list get_active()
{
    //return flag, user for most recent
    return llList2List(llListSort(gStack, 4, TRUE), 1, 2);
}

//############ END Stack Functions ########################

//example of how to use
default
{
    listen(integer channel, string name, key id, string message)
    {
        if (channel == ACCESS_RETURN_CH)
        {
            list results = llParseString2List(message, [FUNC_SEP], [EMPTY_STR]);
            key user = (key)llList2String(results, 0);
            
            list data = clear_stack(ST_ACCESS, user);            
            if(llList2Integer(results, 1))
            {              
                if (llList2String(data, 1) == MENU){add_stack(ST_DIALOG, user,[llList2String(data, 0), MAIN, EMPTY_STR]);}
                else{move_door(llList2String(data, 0), EMPTY_STR, FALSE);}
            }
            else
            {
                llRegionSayTo(user, PUBLIC_CHANNEL,  "Sorry, you do not have keys");
            }
        }
        else if (channel == DIALOG_CH)
        {
            //set, action
            list data = clear_stack(ST_DIALOG, id);
            if (message == OPT_THIS || message == OPT_BOTH)
            {
                move_door(llList2String(data, 0), llList2String(data, 1), (message == OPT_THIS));
            }
            else if (message != OPT_CANCEL)
            {
                add_stack(ST_DIALOG, id, [llList2String(data, 0), SECOND, message]);
            }
        }        
    }
    
    touch_start(integer total_number)
    {
        while (total_number-- > 0){add_stack(ST_TOUCH, llDetectedKey(total_number), [llDetectedLinkNumber(total_number), llDetectedGroup(total_number)]);}        
    }

    touch_end(integer total_number)
    {
        //if the specific user key(s) have not had their touch ended .. continue
        integer x;
        while (total_number-- > 0)
        {
            key user = llDetectedKey(total_number);
            if (touch_active(user))
            {
                list data = clear_stack(ST_TOUCH, user);
                //check to see if this AV can open door
                if (user == OWNER_ID || gLockState == OPT_UNLOCK){move_door(llList2String(data, 0), EMPTY_STR, FALSE);}
                else {add_stack(ST_ACCESS, user, data + [OPEN]);}
            }
        }
    }

    timer()
    {

        list active = get_active();
        key user = (key)llList2String(active, 0);
        string flag = llList2String(active, 1);
        list data = clear_stack(flag, user);
        
        if (flag == ST_TOUCH)
        {
            if (user == OWNER_ID){show_dialog(user, [llList2String(data, 0), MAIN, EMPTY_STR]);}
            else {add_stack(ST_ACCESS, user, data + [MENU]);}  
        }
        else if (flag == ST_ACCESS)
        {
            llRegionSayTo(user, PUBLIC_CHANNEL, "Sorry, the access server is not working, cannot allow you to open the door");
            llInstantMessage(OWNER_ID, "Access server failed when " + llKey2Name(user) + " tried to open a door");
        }
        else if (flag == ST_DIALOG)
        {
            llRegionSayTo(user, PUBLIC_CHANNEL, "Dialog timed out");
        }        
    }    
}

 

  • Like 1
Link to comment
Share on other sites

I like it.  It's a detailed example of a multiplexed timer.  This is a very common challenge in scripting for SL.  We often have several things going at once and we have precious few tools to handle time with. If you look at the sticky thread at the top of this forum

you'll find a post dealing with multiple timers near the top of it, followed by a few posts with commentary.  I suggest posting a link to this thread there, so that it doesn't get buried, or maybe adding it to the Scripting Library forum once you are sure that you have tested and annotated it as much as you want to.

Link to comment
Share on other sites

Hm.

  • Shouldn't the timer event reset the timer to the time to the next event on the timer queue? It looks like queuing a big delay followed by a small delay will not work right.
  • llGetTime() returns a 32-bit float, with a 24-bit mantissa and an 8-bit exponent. It will take a while, but after 195 days, the resolution of the time will only be 1 second. After about a year, 2 seconds. Two years, 4 seconds. Two years downstream, your users will be puzzled at the ways this breaks. If you use llGetTime(), you have to use llResetTime() once in a while. Maybe clear the queue and timer each time the sim restarts. I've had something that needed 20ms time resolution break that way. For 20ms, four days is enough to cause trouble.
  • More efficient to keep the list in-order than to sort it on every timer event to find the one to remove. Only matters if you have a lot of items queued.
Link to comment
Share on other sites

@animats Thanks for the feedback .. I'll tackle your points one by one:

Quote

Shouldn't the timer event reset the timer to the time to the next event on the timer queue? It looks like queuing a big delay followed by a small delay will not work right.

Not quite sure what you mean. The idea is that it should always be setting as the time the next shortest one in the queue. This is so that the timer event fires (as close as possible) when it should based on when the user interacted. What I mean by this is: User A gets a dialog (30 second timer), then 10 seconds later user B does a touch on a door and holds (0.75 sec timer). User B's timer should fire, then the remaining time on User A's timer is set (30 - 10 - 7.5) .. does that make sense?

Quote

llGetTime() returns a 32-bit float, with a 24-bit mantissa and an 8-bit exponent. It will take a while, but after 195 days, the resolution of the time will only be 1 second. After about a year, 2 seconds. Two years, 4 seconds. Two years downstream, your users will be puzzled at the ways this breaks. If you use llGetTime(), you have to use llResetTime() once in a while. Maybe clear the queue and timer each time the sim restarts. I've had something that needed 20ms time resolution break that way. For 20ms, four days is enough to cause trouble

I had read about this .. but did not think it would really matter, and do not understand how this affects things .. the queue is going to be clear 95% of the time . and most cases may only have one current item in it, which will clear when done. Understand that  for a more generic version this might be needed .. but in the end nothing is in the queue for more than 30 secs .. and all the time comparisons are off llGetTime(). Or are you saying that a year in if I get the time and then 10 secs later get the time again .. it will not be 10 secs difference but rather +-2 secs from 10?

Quote

More efficient to keep the list in-order than to sort it on every timer event to find the one to remove. Only matters if you have a lot of items queued

I thought about this .. but I thought that walking the list to find the position and then inserting would be more onerous ... I'd guess again that for a more generic version of the code that would probably be the better solution to cover more scenarios.. but in a house with only 15 doors involved llSort should be ok.

Edited by Wandering Soulstar
Link to comment
Share on other sites

10 minutes ago, Wandering Soulstar said:

@animats Thanks for the feedback .. I'll tackle your points one by one:

Not quite sure what you mean. The idea is that it should always be setting as the time the next shortest one in the queue.

Oh, you're resetting the event timer in clear_stack. Missed that. Sorry.

Quote

I had read about this .. but did not think it would really matter, and do not understand how this affects things .. the queue is going to be clear 95% of the time . and most cases may only have one current item in it, which will clear when done. Understand that  for a more generic version this might be needed .. but in the end nothing is in the queue for more than 30 secs .. and all the time comparisons are off llGetTime(). Or are you saying that a year in if I get the time and then 10 secs later get the time again .. it will not be 10 secs difference but rather +-2 secs from 10?

Yes. It's a problem with the way floating point numbers are represented. They have a limited number of digits. When you start, llGetTime() returns small numbers, so they have lots of digits after the decimal point. As time goes on and the number of seconds increases, there are more digits before the decimal point and fewer after. In a year, llGetTime values will be advancing about 2 seconds at a time.

Anything using llGetTime needs to do a llResetTime at least once every few weeks.

See "Patriot Missile Failure" for a real-world example of this causing big trouble.

Quote

I thought about this .. but I thought that walking the list to find the position and then inserting would be more onerous ... I'd guess again that for a more generic version of the code that would probably be the better solution to cover more scenarios.. but in a house with only 15 doors involved llSort should be ok.

It's fine for door control. If someone is storing a big event list, it might matter.

Link to comment
Share on other sites

You are about to reply to a thread that has been inactive for 1785 days.

Please take a moment to consider if this thread is worth bumping.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...