There is a well-known script fragment called warpPos, which moves an object a long distance quickly, exploiting an interesting property of llSetPrimitiveParams. My purpose here is to analyze warppos, test it, possibly improve it.
The usual non-physical way of positioning an object might use llSetPos. But llSetPos is limited to ten meters of motion. If you try to go further, your object will just move ten meters in the right direction.
The llSetPrimitiveParams function, on the other hand, takes a list of actions. One of those actions is PRIM_POSITION, which sets the prim position. PRIM_POSITION is also limited to ten meters … but you can put more than one of them in the list. So with a long enough list, you can move anywhere in the region, effectively instantly.
The function warpPos looks like this:
warpPos( vector destpos) {
integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
if (jumps > 200 ) jumps = 200;
list rules = [ PRIM_POSITION, destpos ];
integer count = 1;
while ( ( count = count << 1 ) < jumps)
rules = (rules=[]) + rules + rules;
llSetPrimitiveParams( rules + llList2List( rules, (count - jumps) << 1, count) );
while ( --jumps )
llSetPos( destpos );
}
If this is not clear to you, let me assure you that you aren’t alone. We’ll try to make progress together.
My purpose is this: I’m building a simple teleport object to sell on XStreetSL. Yes, finally, I’m going to put some of my good stuff on the market. No, really, this time I’m going to do it. Really. Anyway, I have two issues with warppos. First, what is its range? Second, I’d like to understand it better. I plan to resolve those issues using tests and refactoring. Let’s look at some of the code and reason about it first.
integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
if (jumps > 200 ) jumps = 200;
What’s going on here? Two things. The variable “jumps” gets the number of ten-meter jumps it will take to get where we’re going. That value is 1 (we always jump once) plus the integer value of one-tenth the distance we’re going. One-tenth because we go in ten-meter jumps.
Now I’m not entirely sure I like this. For example, if the distance we have to go is 9, then this snippet will get an answer of 1 jump, which is correct. But if the distance is 10, it will get 2, which is not correct. But … it’s only wrong for exact multiples of ten, so the author probably figured it was close enough. Something to keep in mind.
The second line, sets jumps to 200 if it is larger than 200. The reason for this, I believe, is to limit the size of the list we are about to create. If we make it too large we may get a memory overflow. So my primary purpose in this article is to determine how big that number can be.
Following that code, we find the list-building code:
list rules = [ PRIM_POSITION, destpos ];
integer count = 1;
while ( ( count = count << 1 ) < jumps)
rules = (rules=[]) + rules + rules;
Frankly, I find that pretty obscure. Here’s what I think is happening. We set the rules list to have one call to PRIM_POSITION, pointing to destpos, the destination position. Now the phrase count = count << 1 doubles count by shifting it one bit to the left. (If you’re not into binary, trust me on this. He could have said count = count*2 but that would have been less efficient, and the author of this script cared about that.
If the doubled count is less than the number of jumps, the script then doubles the rules list, using this odd bit of code:
rules = (rules=[]) + rules + rules;
You can probably see that rules + rules would double the list. The bit at the front, (rules=[]) empties the list first, again in the name of efficiency. It is, apparently, faster to do it this way. It’s not even clear that it should work, is it, since you’d think that if rules was set empty, doubling it would leave it empty. Well, it doesn’t work that way for reasons known only to the LSL compiler.
So this bit of code is doubling the list up until right before doubling it again would make it larger than jumps. It’s faster than putting them in one at a time, and that matters to our original author. Then there’s this final bit:
llSetPrimitiveParams( rules + llList2List( rules, (count - jumps) << 1, count) );
Notice that the list he uses is our current rules plus a new list, taken from rules, making up for the difference between the part we doubled, and the total jumps number. The value …
(count - jumps) << 1
… is negative. We double it by shifting left with the <<, and of course it is now the negative of twice the number of jumps we still need. In llList2List, that means to start from the end of the list and count back. So he is just picking up the needed additional items. He could have stored those into rules and then called the llSetPrimitiveParams with rules, but again our author wanted to save some time.
You know my views on all this. I prefer code to be clear, even at a reasonable cost in efficiency. Part of what I’ll do to get this code under test will be to refactor it, and that will, I hope, make it more clear as well. First, though, we need to look at the last bits of the warppos:
if ( llVecDist( llGetPos(), destpos ) > .001 )
while ( --jumps )
llSetPos( destpos );
What’s this??? Well, what is happening is that we are checking to see “Are we there yet?” If not, we do repeated calls to llSetPos() to get there. Note that we do them all … as if we hadn’t moved a bit. This code is there for safety, in case the code above does not work, and it is expected never to be executed.
OK. We have about all the understanding we can get. Let’s get to work on our problems, which are to get a bit more understanding, and to find out the limits to the size of that list.
I’m going to begin, unfortunately, by refactoring without the support of tests, because I can’t see how to test this as it stands. But I have an idea for something that might work. I’ll change the program this way:
warpPos( vector destpos) {
integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
if (jumps > 200 ) jumps = 200;
list rules = [ PRIM_POSITION, destpos ];
integer count = 1;
while ( ( count = count << 1 ) < jumps)
rules = (rules=[]) + rules + rules;
fakeSetPrimitiveParams( rules + llList2List( rules, (count - jumps) < .001 )
while ( --jumps )
fakeSetPos( destpos );
}
fakeSetPrimitiveParams(list params) {
}
fakeSetPos(vector destpos) {
}
Now I have taken out those troubling motion parts and I have a place to stand for testing. The warppos function won’t move the object unless I fill in llSetPrimitiveParams and llSetPos in those fake methods. (I’m already regretting those names but they are the ones I had in mind: “Hmm, if I had fake methods that didn’t move …” We can always rename them later.
Now I’m ready to test. Time to think of a simple test. Let’s try to move about 25 meters, and see if our list is three items long. Here is my first test with a little testing framework thrown in:
list testList;
integer passed = 0;
integer failed = 0;
warpPos( vector destpos) {
integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
if (jumps > 200 ) jumps = 200;
list rules = [ PRIM_POSITION, destpos ];
integer count = 1;
while ( ( count = count << 1 ) < jumps)
rules = (rules=[]) + rules + rules;
fakeSetPrimitiveParams( rules + llList2List( rules, (count - jumps) < .001 )
while ( --jumps )
fakeSetPos( destpos );
}
fakeSetPrimitiveParams(list params) {
testList = params;
}
fakeSetPos(vector destpos) {
}
test25metersIsSize3() {
warpPos(llGetPos()+);
checkListSize(testList, 3);
}
checkListSize(list l, integer desired) {
integer size = llGetListLength(l) / 2;
if ( size == desired ) {
passed++;
} else {
llOwnerSay("Expected size " + (string) desired + " but was " + (string) size);
failed++;
}
}
reportTestResults() {
integer total = passed + failed;
llOwnerSay( (string) total + " tests: " + (string) passed + " passed, " + (string) failed + " failed.");
}
default {
state_entry() {
llSetText("Scripting Goddess at Work", , 1);
test25metersIsSize3();
reportTestResults();
}
}
When I save this script, the test runs, and I get the message: “warpPos test: 1 tests: 1 passed, 0 failed.”. It’s all good.
I should probably move the testing to the touch_start event but for now my purpose is to learn and to find out how long that list can be. That will be a different kind of test, since we have to test until the object explodes.
To get that tested most easily, in situ, I would like for the existing function to have broken out the part that produces a list of size N. Let’s work on that. I’m going to rely on this single test to detect any errors. Do you think that’s too risky? We’ll find out.
warpPos( vector destpos) {
integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
if (jumps > 200 ) jumps = 200;
list rules = [ PRIM_POSITION, destpos ];
integer count = 1;
while ( ( count = count << 1 ) < jumps)
rules = (rules=[]) + rules + rules;
rules = rules + llList2List( rules, (count - jumps) << 1, count);
fakeSetPrimitiveParams( rules );
if ( llVecDist( llGetPos(), destpos ) > .001 )
while ( --jumps )
fakeSetPos( destpos );
}
First, I move the final calculation on the rules list outside the call to fakeSetPrimitiveParams. The test still runs. That leaves me free to create a function to calculate the list as a unit. Then I can call that function and make it explode when I wish.
list makeRules( integer jumps, vector destpos) {
if (jumps > 200 ) jumps = 200;
list rules = [ PRIM_POSITION, destpos ];
integer count = 1;
while ( ( count = count << 1 ) < jumps)
rules = (rules=[]) + rules + rules;
rules = rules + llList2List( rules, (count - jumps) << 1, count);
return rules;
}
warpPos( vector destpos) {
integer jumps = (integer)(llVecDist(destpos, llGetPos()) / 10.0) + 1;
list rules = makeRules(jumps, destpos);
fakeSetPrimitiveParams( rules );
if ( llVecDist( llGetPos(), destpos ) > .001 )
while ( --jumps )
fakeSetPos( destpos );
}
OK, now there is a separate function for making the list, and the test still runs. Now I’ll try making the script explode, by calling that function with large numbers of jumps. I think I’ll start with 500 and work down. Here’s my test:
testListSize(integer n) {
list rules = makeRules(n, ZERO_VECTOR);
checkListSize( rules, n );
}
default {
state_entry() {
llSetText("Scripting Goddess at Work", , 1);
test25metersIsSize3();
testListSize(500);
reportTestResults();
}
}
The test fails. It says: “warpPos test: Expected size 500 but was 200”. I’ve forgotten to remove that check for 200. Let me remove that for now and see what happens.
OK, the test runs at 500. I’m not very surprised, actually, as mono allows way more memory than normal LSL. Just for fun I’ll run in non-mono mode and see if it fails.
Yes! in normal LSL we get a stack-heap collision. OK. I’ll go back to mono and bump the number up until it breaks. Wow! It breaks somewhere between 3500, which works, and 3750, which does not. That means that warpPos could warp up to 35,000 meters! More than enough for working inside one region, which is all that I need.
state_entry() {
llSetText("Scripting Goddess at Work", , 1);
test25metersIsSize3();
testListSize(3500);
reportTestResults();
}
Enough for now, I’m going to take a break and publish this. Tune in next time, when I’ll see about refactoring warpPos a bit more for clarity. Thanks for reading!
Leave a Reply