// ======================================== // Minimal keymotion touring script - CROSS-REGION, GROUP-ONLY VARIANT // Same script as the open (ungated) version, with one addition: every // button (Stop, Resume, route selection, GO, and the owner-only edit // commands) requires the toucher to be in the object's set group. // // SETUP REQUIRED: this object must have its GROUP actually set // in-world (right-click -> Edit -> General tab -> Group) to whichever // group should have access. llSameGroup() checks the toucher against // THAT group -- if the object's group is left unset, nobody will pass // the check and the touch menu will just say Permission Denied for // everyone, owner included. // // Free to reuse for boats, carriages, cars, balloons, or anything else // on a keyframed route -- swap the notecard contents and go. // // CREDIT: Based on a keymotion touring script originally shared on OSW // ORIGINAL AUTHOR- Opensimworld, // used as the base for years of tour-vehicle builds. This version adds // cross-region travel, a Stop/Resume feature, and group-gated access // on top of that original core; everything else (notecard format, // camera setup) is unchanged from it. // // *** MIGRATION NOTE *** // Waypoints are now stored as GLOBAL (grid-wide) coordinates -- // llGetPos() + llGetRegionCorner() -- instead of bare llGetPos(). // Old notecards recorded with the original script are LOCAL // coordinates and will place the vehicle wrong if loaded here. // Re-record routes (ADD POINT / SAVE CARD) with this version first. // // Why cross-region works: llSetKeyframedMotion moves the object by // DELTAS between consecutive points. As long as both points in a // delta are expressed in one continuous coordinate space, the delta // comes out correctly sized even when it crosses a region border. // llGetRegionCorner() (the SW corner of the current region, in world // meters) + llGetPos() gives exactly that continuous space, for free, // regardless of region size (works the same for a plain 256m region // or a varregion). Nothing else about the route/delta logic below // needed to change. // // STOP / RESUME: // Stop halts the vehicle exactly where it is (llSetKeyframedMotion // KFM_CMD_STOP) and remembers the nearest recorded waypoint to that // position. Resume continues from there -- no teleport back to the // route's start, no reloading the notecard. Resume picks the CLOSEST // waypoint by straight-line distance, not necessarily the next one // ahead: on a leg with a sharp turn, this can occasionally land on the // waypoint just passed rather than the one coming up, which shows up // as one small backward hop before the vehicle continues forward. // Harmless, just worth knowing if you see it happen. // ======================================== list points; string route; list newPoints; float SPEED = 0.5; ///meters /second ///3.1 float DELAY = 0.0; // how many seconds to stop at every station integer channel = 13; list stops; list messageTimes; list speeds; list rotations; integer curMessage; integer hasStoppedMidRoute = FALSE; // TRUE after a manual Stop -- offer Resume integer resumeIndex = -1; // nearest waypoint index to where the vehicle stopped vector initPos; rotation initRot; beginRoute() { llSetKeyframedMotion( [], []); //llSetRot(ZERO_ROTATION); llSleep(0.3); messageTimes = []; curMessage = 0; //OFFSET=(float)llGetObjectDesc(); //WATERHEIGHT = llWater(ZERO_VECTOR); vector pos = llList2Vector(points,0); //pos.z = WATERHEIGHT; //llSay(0, (string)pos); rotation rot = llGetRot(); rot = llList2Rot(rotations, 0); // CROSS-REGION CHANGE: pos is now a GLOBAL coordinate. // llSetRegionPos() only accepts a position local to the // region we're currently in, so convert global -> local // by subtracting this region's corner. Assumes the route // always starts while the ship is sitting in its home // region (point 0's region). llSetRegionPos(pos - llGetRegionCorner()); llSetRot(rot); integer i; list kf; float totTime =0.0; messageTimes = []; for (i=0; i < llGetListLength(points); i++) { vector v = llList2Vector(points, i); rotation r2 = ZERO_ROTATION; r2 = llList2Rot(rotations, i); float t; if (i>0) { // v and pos are both GLOBAL here, so this delta is // correctly sized even when v is in a neighboring // region -- unchanged from the original math. t = llVecMag(v-pos) / llList2Float(speeds, i); // kf += (v-pos); kf += (r2/rot); kf += t; } else // the zero-th frame is just a rotation { kf += ZERO_VECTOR; kf += (r2/rot); kf += 2; //adjust for speed t = 2; } kf += ZERO_VECTOR; kf += ZERO_ROTATION; kf += DELAY; //adjust for speed t += DELAY; float f = llList2Float(stops, i); if (f >0) { kf += ZERO_VECTOR; kf += ZERO_ROTATION; kf += f; //adjust for speed t += f; } totTime+= t; pos = v; rot = r2; messageTimes += (t+1 ); } //llLoopSound("paddle", 1.0); llSetKeyframedMotion( kf, [KFM_DATA, KFM_TRANSLATION|KFM_ROTATION, KFM_MODE, KFM_FORWARD]); llSetTimerEvent(totTime + 2); llMessageLinked(LINK_SET, 0, "START", ""); } doStop() { llStopSound(); llSetTimerEvent(0); llMessageLinked(LINK_SET, 0, "STOP", ""); } // ======================================== // Continue from wherever the vehicle actually is, through the // remaining waypoints of whatever route is already loaded in `points`. // No teleport, no notecard reload -- this is what Resume calls instead // of re-running beginRoute() from waypoint 0. // ======================================== resumeRoute() { if (llGetListLength(points) == 0 || resumeIndex < 0 || resumeIndex >= llGetListLength(points)) { llOwnerSay("Nothing to resume."); return; } llSetKeyframedMotion( [], []); llSleep(0.3); messageTimes = []; curMessage = 0; // No teleport: start from where the vehicle actually is right now. vector pos = llGetPos() + llGetRegionCorner(); rotation rot = llGetRot(); integer i; list kf; float totTime = 0.0; for (i = resumeIndex; i < llGetListLength(points); i++) { vector v = llList2Vector(points, i); rotation r2 = llList2Rot(rotations, i); float t = llVecMag(v-pos) / llList2Float(speeds, i); if (t <= 0.0) { t = 0.1; // guard against a zero-distance/zero-speed leg if // the vehicle happened to stop exactly on a waypoint } kf += (v-pos); kf += (r2/rot); kf += t; kf += ZERO_VECTOR; kf += ZERO_ROTATION; kf += DELAY; t += DELAY; float f = llList2Float(stops, i); if (f > 0.0) { kf += ZERO_VECTOR; kf += ZERO_ROTATION; kf += f; t += f; } totTime += t; pos = v; rot = r2; messageTimes += (t+1); } llSetKeyframedMotion( kf, [KFM_DATA, KFM_TRANSLATION|KFM_ROTATION, KFM_MODE, KFM_FORWARD]); llSetTimerEvent(totTime + 2); hasStoppedMidRoute = FALSE; resumeIndex = -1; llMessageLinked(LINK_SET, 0, "START", ""); } default { state_entry() { llSetKeyframedMotion([], []); llSitTarget(ZERO_VECTOR, ZERO_ROTATION); llListen(channel,"", "",""); llMessageLinked(LINK_SET, 0, "STOP", ""); llSetCameraEyeOffset(ZERO_VECTOR); //<-10, 0, 3.5> ); llSetCameraAtOffset(ZERO_VECTOR); initRot = llGetRot(); initPos = llGetPos(); } on_rez(integer n) { llResetScript(); } listen(integer channel, string n, key sender, string cmd) { // GROUP-ONLY GATE: ignore anything from someone who isn't the // owner and isn't in the object's set group. This covers every // command below -- Stop, Resume, route selection, GO, and the // edit commands. if (sender != llGetOwner() && !llSameGroup(sender)) { return; } if (cmd == "Stop") { llSetKeyframedMotion( [], [KFM_COMMAND, KFM_CMD_STOP]); doStop(); // Capture an exact resume point at the moment Stop is // pressed: nearest recorded waypoint to the vehicle's // current global position. if (llGetListLength(points) > 0) { vector stopPosGlobal = llGetPos() + llGetRegionCorner(); integer nearestIdx = 0; float bestDist = -1.0; integer k; for (k=0; k // <-10,-10,-10> to <10,10,10> METERS ]); } } timer() { doStop(); //comment out for continuous llSetTimerEvent(0); //comment out for continuous hasStoppedMidRoute = FALSE; // route finished naturally -- nothing to resume resumeIndex = -1; llSay(0, "Arrived"); } }