// ======================================== // Minimal keymotion touring script - CROSS-REGION VARIANT (v2) // 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 / THREAD LINK -- fill in here before sharing ]], // used as the base for years of tour-vehicle builds. This version adds // cross-region travel, a Stop/Resume feature, and per-waypoint region // name labels on top of that original core; everything else (menu, // notecard format, camera setup) is unchanged from it. // // WHAT'S NEW IN v2: each notecard line now starts with the region name // the waypoint was recorded in -- e.g. // Redwood Bay | | | speed | pause // The name is captured automatically by ADD POINT, purely as a // human-readable label so you can eyeball a notecard and tell which // region a block of waypoints belongs to. It doesn't drive the // movement math at all -- that's still the global-coordinate vector. // Optionally (see CHECK_START_REGION below) it can also be used as a // safety check before a route starts. // // *** 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 it 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. A fully precise // fix would track which keyframe segment was actually executing when // Stop was pressed, which is more bookkeeping than this simple version // aims for. // ======================================== 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 list regionNames; // parallel to points: region name label recorded for each waypoint (audit only, unless CHECK_START_REGION is TRUE) list newRegionNames; // parallel to newPoints, built during ADD POINT // If TRUE, refuse to start a route unless the vehicle is currently // sitting in the same region waypoint 0 was recorded in. Set to FALSE // if you don't want this check (e.g. you're fine starting a route from // wherever the vehicle happens to be). integer CHECK_START_REGION = TRUE; 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) { 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 0) { string expectedRegion = llList2String(regionNames, 0); string currentRegion = llGetRegionName(); if (expectedRegion != "" && expectedRegion != currentRegion) { llOwnerSay("Route NOT started: '" + cmd + "' expects to begin in region '" + expectedRegion + "' but the vehicle is currently in '" + currentRegion + "'."); return; } } beginRoute(); } else if (llGetSubString(cmd, 0, 1) == "GO") { string nc = llGetSubString(cmd, 2, -1); //llOwnerSay(nc); //DEBUG list lines = llParseString2List(osGetNotecard(nc), ["\n"], []); integer j=0; { list tk = llParseString2List(llList2String(lines,j), ["|"], []); vector p= llList2Vector(tk,0); // CROSS-REGION CHANGE: p is now a GLOBAL coordinate, so // convert back to local before positioning. llSetRegionPos(p - llGetRegionCorner()); llSetRot(llList2Rot(tk, 1)); } } } touch_start(integer n) { list opt = []; integer i; for (i=0; i < llGetInventoryNumber(INVENTORY_NOTECARD); i++) opt += llGetInventoryName(INVENTORY_NOTECARD,i); opt += "Stop"; if (hasStoppedMidRoute) { opt += "Resume"; } if (llGetOwner() == llDetectedKey(0)) { opt += "ADD POINT"; //activate when editing opt += "CLEAR PTS"; //activate when editing opt += "SAVE CARD"; //activate when editing } llDialog(llDetectedKey(0), "\nMenu", opt , channel); } run_time_permissions(integer perm){ if (perm) { llSetCameraParams([ CAMERA_ACTIVE, 1, // 0=INACTIVE 1=ACTIVE CAMERA_BEHINDNESS_ANGLE, 15.0, // (0 to 180) DEGREES CAMERA_BEHINDNESS_LAG, 1.0, // (0 to 3) SECONDS CAMERA_DISTANCE, 30.0, // ( 0.5 to 10) METERS CAMERA_PITCH, 6.0, // (-45 to 80) DEGREES CAMERA_POSITION_LOCKED, FALSE, // (TRUE or FALSE) CAMERA_POSITION_LAG, 0.01, // (0 to 3) SECONDS CAMERA_POSITION_THRESHOLD, 30.0, // (0 to 4) METERS CAMERA_FOCUS_LOCKED, FALSE, // (TRUE or FALSE) CAMERA_FOCUS_LAG, 0.01 , // (0 to 3) SECONDS CAMERA_FOCUS_THRESHOLD, 0.01, // (0 to 4) METERS CAMERA_FOCUS_OFFSET, <0.0,0.0,0.0> // <-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"); } }