Wandering Soulstar Posted May 8, 2019 Share Posted May 8, 2019 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"); } } }  1 Link to comment Share on other sites More sharing options...
Rolig Loon Posted May 8, 2019 Share Posted May 8, 2019 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 More sharing options...
Wandering Soulstar Posted May 8, 2019 Author Share Posted May 8, 2019 Thanks @Rolig Loon .. I had gone into that particular thread but did not see an answer to my particular situation. Once I have tested and as well heard any feedback will definitely post to that/the library .. making it more generic so as to be not so particular to my solution. 1 Link to comment Share on other sites More sharing options...
animats Posted May 8, 2019 Share Posted May 8, 2019 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 More sharing options...
Wandering Soulstar Posted May 8, 2019 Author Share Posted May 8, 2019 (edited) @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 May 8, 2019 by Wandering Soulstar Link to comment Share on other sites More sharing options...
animats Posted May 8, 2019 Share Posted May 8, 2019 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 More sharing options...
Wandering Soulstar Posted May 9, 2019 Author Share Posted May 9, 2019 @animats Just had a light-bulb moment, remembering the definition of llGetTime(): Number of seconds since the script started ... so I get the issue now .. Thanks!!! (And very interesting the link!) Link to comment Share on other sites More sharing options...
Recommended Posts
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