/* SFsail - a sailing engine for opensim Read more @ https://opensimworld.com/sfsail/ (c) Satyr Aeon This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* SFsail Settings */ string ACCESS = "A"; // A=All, G=Group, O=Owner float MAX_ANGLE = 70; // Max boom angle integer IS_UBODE =0; // 1 for ubODE integer HUD_ON=1; // 0 = don't show hover text integer ADV_HUD=1; // 0 = don't show advanced info on hover text integer THROTTLE_MAX=5; // Max motor throttle step float SPEEDUP=.9; // Overall speedup of the boat integer ENABLE_DYCAM = 1; // Enable dynamic camera float KEELING = 0.4; // Change for more/less keel action float LEEWAY = 0.2; // ditto for leeway // Your boat's max speed curve (values must be 0-1) per 15 degrees from 0 to 180 degrees. Please add an extra duplicate of the last point at the end of the list. list speedCurve = [-0.1, 0.0, 0.3, 0.8, 0.9, .9, 1, 1, .96, 0.9, 0.7, 0.8, 0.7, 0.7]; // Optimal boom angle for each true wind angle (per 15 degrees as above) list optAngle = [10, 10, 10, 15, 20, 30, 45, 50, 55, 60, 65, 70, 70]; /* End of SFsail Settings */ vector windVector = <-7.71667, 0, 0>; // default East wind float windDirection; // -180 , 180 float TWspeed; // m/s float MS_TO_KNOTS = 1.944; integer CHANNEL =0; string status; key avatar = NULL_KEY ; // Avatar controlling this boat integer throttle; integer turning; integer JIB; integer SAIL; integer BOOM; integer GENOA; integer SPINNAKER; integer GENNAKER; integer WINDVANE; integer STEER; integer INSTRUMENTS; integer jibIsUp; integer spinIsUp; integer genoaIsUp; integer spinAngle; float spinEff; float windvaneOffset = 0; // rad float AWangle; //-180 - 180 float boomAngle; // degrees float sheetAngle; // deg float totalMiles; float bestAngle; // deg float sog; // m/s vector wayPoint; // Current destination or 0 list wayPoints; float timeTick; float holdValue = 999.0; integer autoTrim=0; key followId = NULL_KEY; float curveAt(list curve, integer deg) // degrees 0-180 { integer idx = (integer)(deg / 15.0); float rem = (deg%15)/15.0; return llList2Float(curve, idx)*(1-rem) + llList2Float(curve, idx+1)*rem ; } float angleBetween(vector a, vector b) // signed between -PI and PI { vector c = a % b; return 2.*(0.5-(c.z>0))*llAtan2( llVecMag(c), a * b ); } float AWspeed; float TWangle; float maxSpeed; float efficiency; float northAngle; vector linear; vector angular; float power; updateCourse() { vector fwd = <1,0,0>*llGetRot(); vector vel = llGetVel(); vel.z=0; fwd.z =0; sog = llVecMag(vel); totalMiles += sog*0.000539957; vector AWvector = windVector-vel; AWspeed = llVecMag(AWvector); AWangle = angleBetween(-1*fwd, AWvector); TWangle = angleBetween(-1*fwd, windVector); updateSails(); maxSpeed = curveAt(speedCurve, (integer) llFabs(TWangle*RAD_TO_DEG) ); // 0-1 bestAngle = curveAt(optAngle, (integer) llFabs(TWangle*RAD_TO_DEG) -1); // degrees efficiency = llFabs(bestAngle-sheetAngle)*DEG_TO_RAD; efficiency = 1./((efficiency*efficiency*16.0) + 1.); northAngle = angleBetween(<0,1,0>, fwd)*RAD_TO_DEG; updateHud(); updateInstruments(); if (status == "sailing" || status == "motor") { if (!IS_UBODE && turning !=0) if (turning < 6) turning++; } if (status == "sailing") { linear.z = 0; power = 0.6; // Mainsail power if (jibIsUp) power += 0.3; else if (genoaIsUp) power += 0.4; if (spinIsUp ) { spinEff = llFabs(llCos(TWangle)) * llCos( ( (PI-TWangle) + spinAngle*DEG_TO_RAD )); // spinnaker efficiency power += spinEff*0.4; } if (power >1.0) power = 1.0; linear.x = SPEEDUP*efficiency*maxSpeed*TWspeed*power; float f = (llFabs(llSin(AWangle))+0.2)*llSin(boomAngle*DEG_TO_RAD + AWangle)*AWspeed; // Angle of attack * wind speed linear.y = f*LEEWAY; // leeway angular.x = - f * KEELING*power; // heel if (timeTick++>2) { timeTick=0; angular.y = -0.5; // Wavy motion } else angular.y=0; angular.z =0; if (autoTrim>0 && llFabs(bestAngle - sheetAngle)>3) { if (bestAngle>sheetAngle) sheetAngle+= 5; else sheetAngle-= 5; } if (sheetAngle > MAX_ANGLE) sheetAngle = MAX_ANGLE; if (sheetAngle < 5) sheetAngle = 5; if (followId != NULL_KEY) { list lst =llGetObjectDetails(followId, [OBJECT_POS]); if (llGetListLength(lst) ==0) { followId=NULL_KEY; wayPoint = ZERO_VECTOR; } else { wayPoint = llList2Vector(lst, 0); wayPoint.z=0; } } if (wayPoint != ZERO_VECTOR) { vector pos = llGetPos(); pos.z=0; if (llVecMag(wayPoint-pos) < 10) { if (llGetListLength(wayPoints)>0) { say((string)llGetListLength(wayPoints)+" waypoints left"); wayPoint = llList2Vector(wayPoints, 0); wayPoints = [] + llDeleteSubList(wayPoints, 0 , 0); } else if (followId == NULL_KEY) { say("Waypoint reached"); lowerSails(); wayPoint = ZERO_VECTOR; return; } } else { angular.z = angleBetween(wayPoint - pos, fwd)*1.2; } } else if (holdValue != 999.0) { if (llFabs(northAngle - holdValue)>1) angular.z = (northAngle - holdValue)*DEG_TO_RAD*5.5; } llSetVehicleVectorParam(VEHICLE_LINEAR_MOTOR_DIRECTION, linear); llSetVehicleVectorParam(VEHICLE_ANGULAR_MOTOR_DIRECTION, angular); } else if (status == "motor") { llSetVehicleVectorParam(VEHICLE_LINEAR_MOTOR_DIRECTION, linear); } } updateSails() { boomAngle = -1*TWangle*RAD_TO_DEG; if ( llFabs(boomAngle) > sheetAngle ) { if (TWangle > 0) boomAngle = -sheetAngle; else boomAngle = sheetAngle; } if (status == "sailing") { string s = "rot"; if (efficiency < 0.7 || maxSpeed < 0.1) s = "rotflap"; llMessageLinked(SAIL, (integer)boomAngle, s, ""); llMessageLinked(BOOM, (integer)boomAngle, s, ""); float ja = TWangle*RAD_TO_DEG; if (ja > 175 ) ja = - boomAngle; //butterfly wings when downwind else ja = boomAngle; if (jibIsUp) llMessageLinked(JIB, (integer)ja, s, ""); if (genoaIsUp) llMessageLinked(GENOA, (integer)ja, s, ""); if (spinIsUp ) { if ( spinEff > 0.7) s = "rot"; else s = "rotflap"; llMessageLinked(SPINNAKER, (integer)spinAngle, s, ""); } } } setVehicleParams() { llSetLinkPrimitiveParamsFast(LINK_ALL_CHILDREN, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_NONE]); // Makes everything but the root prim non-physical llSetVehicleFlags(0); llSetVehicleType (VEHICLE_TYPE_BOAT); llSetVehicleRotationParam(VEHICLE_REFERENCE_FRAME,ZERO_ROTATION); llSetVehicleFlags( VEHICLE_FLAG_HOVER_UP_ONLY | VEHICLE_FLAG_HOVER_WATER_ONLY ); if (IS_UBODE>0) { llSetVehicleFloatParam (VEHICLE_LINEAR_MOTOR_TIMESCALE,9.0); llSetVehicleFloatParam (VEHICLE_HOVER_HEIGHT,-.1); llSetVehicleFloatParam (VEHICLE_VERTICAL_ATTRACTION_EFFICIENCY,.3); llSetVehicleFloatParam (VEHICLE_ANGULAR_MOTOR_TIMESCALE, 1.5); } else { llSetVehicleFloatParam (VEHICLE_LINEAR_MOTOR_TIMESCALE,5.0); llSetVehicleFloatParam (VEHICLE_HOVER_HEIGHT,0.1); llSetVehicleFloatParam (VEHICLE_VERTICAL_ATTRACTION_EFFICIENCY,1.0); llSetVehicleFloatParam (VEHICLE_ANGULAR_MOTOR_TIMESCALE, 1.); } llSetVehicleFloatParam (VEHICLE_VERTICAL_ATTRACTION_TIMESCALE, 2.); llSetVehicleVectorParam (VEHICLE_ANGULAR_FRICTION_TIMESCALE, < 5., 2., 1.>); llSetVehicleFloatParam (VEHICLE_LINEAR_MOTOR_DECAY_TIMESCALE,1); llSetVehicleVectorParam (VEHICLE_LINEAR_FRICTION_TIMESCALE, <50.0, 1.0, 0.1>);; llSetVehicleVectorParam (VEHICLE_LINEAR_MOTOR_DIRECTION,ZERO_VECTOR); llSetVehicleFloatParam (VEHICLE_LINEAR_DEFLECTION_EFFICIENCY, 0.85); llSetVehicleFloatParam (VEHICLE_LINEAR_DEFLECTION_TIMESCALE, 10.0); llSetVehicleFloatParam (VEHICLE_ANGULAR_MOTOR_DECAY_TIMESCALE, 3.0); llSetVehicleVectorParam (VEHICLE_ANGULAR_MOTOR_DIRECTION,ZERO_VECTOR); llSetVehicleFloatParam (VEHICLE_ANGULAR_DEFLECTION_EFFICIENCY,1.0); llSetVehicleFloatParam (VEHICLE_ANGULAR_DEFLECTION_TIMESCALE, 10.0); llSetVehicleFloatParam (VEHICLE_BANKING_EFFICIENCY,0.0); //llSetVehicleFloatParam (VEHICLE_BANKING_MIX,1.0); //llSetVehicleFloatParam (VEHICLE_BANKING_TIMESCALE,1.2); llSetVehicleFloatParam (VEHICLE_HOVER_EFFICIENCY,1.0); llSetVehicleFloatParam (VEHICLE_HOVER_TIMESCALE,1.); llSetVehicleFloatParam (VEHICLE_BUOYANCY,1.0); } string num(float num) { return llGetSubString((string)num, 0,3); } updateInstruments() { if (WINDVANE) llSetLinkPrimitiveParamsFast(WINDVANE, [PRIM_ROT_LOCAL, llEuler2Rot(<0, 0, windvaneOffset - AWangle >) ] ); if (INSTRUMENTS) llMessageLinked(INSTRUMENTS, 887, (string)(sheetAngle)+"|"+(string)(sog*MS_TO_KNOTS)+"|"+(string)(TWangle*RAD_TO_DEG)+"|"+(string)(TWspeed*MS_TO_KNOTS)+"|"+(string)efficiency+"|"+(string)(northAngle) +"|"+(string)(AWangle*RAD_TO_DEG)+"|"+(string)(AWspeed*MS_TO_KNOTS), ""); } string strFull = "|||||--------"; updateHud() { if (!HUD_ON) { llSetText("", <1,1,1>, 1); return; } string s; s += (string)llRound((360+northAngle)%360) +"° " + getRose(northAngle) + " " + num(sog*MS_TO_KNOTS)+" Kn"; s += "\nWind: "+getRose(windDirection*RAD_TO_DEG)+ " " + num(TWspeed*MS_TO_KNOTS) +" Kn TWA: " + (string)llRound(llFabs(TWangle*RAD_TO_DEG))+"° "; if (autoTrim) s += "\nAutotrim"; else s += "\nTrim"; s += ": "+(string)((integer)sheetAngle)+"° "; integer t = llRound(efficiency*5.); s += ""+ llGetSubString(strFull, 5-t, 9-t ); if (ADV_HUD) { s += "\nAWA: "+(string)llRound(llFabs(AWangle*RAD_TO_DEG))+" AWS: "+num(AWspeed*MS_TO_KNOTS) + "\nM/S:" +num(maxSpeed)+ " Eff:"+num(efficiency)+" Opt: "+(string)((integer)bestAngle) + "\nMiles: "+num(totalMiles); if (wayPoint != ZERO_VECTOR) s +="\nWaypoint: "+(string)llRound(wayPoint.x)+", "+(string)llRound(wayPoint.y); // s += "\nLin: "+(string)(linear*MS_TO_KNOTS)+ " pow:"+(string)power ; } vector color = <.3, 1,1>; if (efficiency > 0.7) color = <0,1,0>; if (maxSpeed < 0.1) color = <1,0,0>; llSetText(s, color ,1.0); } scanLinks() { integer i; for (i=1; i <= llGetNumberOfPrims();i++) { string n = llGetLinkName(i); if (n == "boom") BOOM = i; else if (n == "jib") JIB = i; else if (n == "sail") SAIL= i; else if (n == "steer") STEER = i; else if (n == "spinnaker") SPINNAKER = i; else if (n == "genoa") GENOA = i; else if (n == "windvane") { WINDVANE = i; windvaneOffset = DEG_TO_RAD* ((float)llList2String( llGetLinkPrimitiveParams(i, [PRIM_DESC]), 0)); } else if (n == "instruments") INSTRUMENTS= i; } } string getArrow(float dir) { if ( dir <= 23 && dir >= -23) return "↑"; else if (dir <=-157 || dir >=157) return "↓"; else if (dir > 112) return "↘"; else if (dir > 67) return "→"; else if (dir > 23) return "↗"; else if (dir > -67) return "↖"; else if (dir > -112) return "←"; else return "↙"; } string getRose(float dir) { if ( dir <= 23 && dir >= -23) return "N"; else if (dir <=-157 || dir >=157) return "S"; else if (dir > 112) return "SE"; else if (dir > 67) return "E"; else if (dir > 23) return "NE"; else if (dir > -67) return "NW"; else if (dir > -112) return "W"; else return "SW"; } float getAngle(string dir) { if (dir=="nw") return 45; else if (dir=="ne") return -45; else if (dir=="e") return -90; else if (dir=="s") return 180; else if (dir=="sw") return 135; else if (dir=="se") return -135; else if (dir=="w") return 90; else return 0; } upright() { vector v = llRot2Euler(llGetRot()); v.x =0; v.y =0; llSetRot(llEuler2Rot(v)); } integer hasAccess(key u, string a) { if ( a=="A" || u==llGetOwner() || osIsNpc(u) || (a=="G" && llSameGroup(u)) ) return TRUE; return FALSE; } say(string str) { if (avatar != NULL_KEY && !osIsNpc(avatar)) llRegionSayTo(avatar, 0, str); else llSay(0, str); } setVisible(string what , float visible) { integer i=0; for (i =1; i <= llGetNumberOfPrims(); i++) if (llGetLinkName(i) == what) llSetLinkAlpha(i, visible, ALL_SIDES); } raiseSails() { setVehicleParams(); // sail params llSetStatus(STATUS_PHYSICS,TRUE); linear = ZERO_VECTOR; angular = ZERO_VECTOR; status = "sailing"; llSetText("[[ Sailing! ]]", <0,1,1>, 1); llMessageLinked(LINK_ALL_CHILDREN, 888, "start", NULL_KEY); llMessageLinked(SAIL, 0, "raise", NULL_KEY); llMessageLinked(JIB, 0, "raise", NULL_KEY); jibIsUp =1; spinIsUp =0; genoaIsUp=0; llSleep(.5); llSetTimerEvent(1.0); setVisible("fenders", 0.0); setVisible("anchor", 1.0); say("Sailing!"); } startMotor() { if (status == "sailing") lowerSails(); setVehicleParams(); // doesn't hurt to do it again sheetAngle=0; boomAngle =0; linear = ZERO_VECTOR; angular = ZERO_VECTOR; llSetStatus(STATUS_PHYSICS,TRUE); llSetText("[[ Motor ON ]]", <0,1,1>, 1); llSetTimerEvent(1.0); llMessageLinked(LINK_ALL_CHILDREN , 888, "motorstart", NULL_KEY); status = "motor"; setVisible("anchor", 0); } lowerSails() { say("Lowering sails."); llMessageLinked(LINK_ALL_CHILDREN , 888, "lower", NULL_KEY); status = "lower"; } setWindVector(vector v) { windVector = v; windDirection = angleBetween(<0,1,0>, - windVector); llMessageLinked(LINK_ALL_CHILDREN, (integer)(windDirection*RAD_TO_DEG), "windDirection", ""); } moor() { if (status == "sailing") lowerSails(); say("Mooring."); status = "moor"; llSetTimerEvent(0); llMessageLinked(LINK_ALL_CHILDREN , 888, "reset", NULL_KEY); reset(); llSleep(.2); upright(); setVisible("fenders", 1); setVisible("anchor", 0); llSetObjectDesc((string)totalMiles); llSetLinkPrimitiveParamsFast(LINK_ALL_CHILDREN, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_PRIM]); if (SPINNAKER) llSetLinkPrimitiveParamsFast(SPINNAKER, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_NONE]); if (GENOA) llSetLinkPrimitiveParamsFast(GENOA, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_NONE]); } stopMotor() { llMessageLinked(LINK_ALL_CHILDREN , 888, "motorstop", NULL_KEY); } integer setterListen; reset() { llSetStatus(STATUS_PHYSICS,FALSE); llSetStatus(STATUS_PHANTOM,FALSE); } getPerms( key u ) { // if (llGetPermissionsKey() != u || !(llGetPermissions()&PERMISSION_TAKE_CONTROLS)) llRequestPermissions(u, PERMISSION_TAKE_CONTROLS | PERMISSION_CONTROL_CAMERA| PERMISSION_TRIGGER_ANIMATION); } process(string msg) { if (msg=="raise") { setWindVector( windVector ); llTriggerSound("raise", 1.0); stopMotor(); if (status != "sailing") raiseSails(); } else if (msg=="motor") { lowerSails(); llTriggerSound("motor", 1.0); llSleep(.3); startMotor(); } else if (msg=="flip") llSetRot(llGetRot()*llEuler2Rot(<0,0,PI>)); else if (msg == "dycam") { ENABLE_DYCAM=!ENABLE_DYCAM; setDycam(ENABLE_DYCAM); } else if (msg == "jib") { if (JIB && status == "sailing") { if (jibIsUp) { llMessageLinked(JIB, 888, "lower", ""); jibIsUp=0; } else { if (!genoaIsUp) { jibIsUp=1; llMessageLinked(JIB, 888, "raise", ""); } else say("You must lower genoa"); } llTriggerSound("raise", 1); } else say("There is no jib!"); } else if (msg == "genoa") { if (GENOA && status == "sailing") { if (genoaIsUp) { llMessageLinked(GENOA, 888, "lower", ""); genoaIsUp=0; } else { if (!jibIsUp) { genoaIsUp=1; llMessageLinked(GENOA, 888, "raise", ""); } else say("You must lower jib"); } llTriggerSound("raise", 1); } else say("There is no genoa!"); } else if (msg == "spin") { if (SPINNAKER && status == "sailing") { if (spinIsUp) { llMessageLinked(SPINNAKER, 888, "lower", ""); spinIsUp=0; } else { if (llFabs(TWangle)*RAD_TO_DEG > 140) { spinIsUp=1; llMessageLinked(SPINNAKER, 888, "raise", ""); } else say("You must face away from the wind"); } llTriggerSound("raise", 1); } else say("There is no spinnaker!"); } else if (msg=="lower") { llTriggerSound("raise", 1.0); lowerSails(); } else if (msg=="moor") { llTriggerSound("raise", 1); moor(); } else if (msg=="hold") { holdValue = northAngle; say("Holding heading at "+llRound(holdValue)); } else if (msg=="trim") { autoTrim = !autoTrim; if (autoTrim) say("Auto trim enabled"); else say("Auto trim disabled"); } else if (msg=="setter") { if (setterListen<=0) { setterListen = llListen(-54001,"","",""); say("Wind setter enabled"); } else { llListenRemove(setterListen); setterListen = -1; say("Wind setter disabled"); } } else if (msg=="w" || msg == "e" || msg == "n" || msg == "s" || msg == "nw" || msg == "ne" || msg == "sw" || msg == "se") { if (setterListen <=0) { float ang = (float)getAngle(msg); ang *= DEG_TO_RAD; setWindVector ( TWspeed*<0, -1, 0>*llEuler2Rot(<0,0, ang>) ); say("Wind now blowing from "+msg+" "); } } else if (msg=="8" || msg == "11" || msg == "15" || msg == "18" || msg == "21" || msg == "25") { setWindVector( llVecNorm(windVector)*(float)msg/MS_TO_KNOTS ); TWspeed = llVecMag(windVector); say("Wind speed set to "+msg+" Knots"); } else if (msg=="help") { string s = "SFsail commands:\n---\n" +"raise - raise mainsail & jib (trim with up/down arrows)\n" +"lower - lower sails\n" +"moor - stop sailing / motor\n" +"motor - lower sails, start motor" +"jib - hoist/drop Jib\n" +"spin - hoist/drop Spinnaker (use PgUp & PgDn to trim)\n" +"genoa - hoist/drop Genoa\n" +"trim - enable/disable autotrim\n" +"sheet nn - add degrees to sheet\n" +"hold - hold current heading\n" +"dest x y [x2 y2 x3 y3 ...] - Enable autopilot to x y region coordinates [then to x2 y2 , x3 y3 ..., optionally]\n" +"follow - Use autopilot to follow a user or another boat\n" +"stop - Stop autopilot / following\n" +"flip - flip the boat when moored\n" +"---\n" +"n,s,e,w,nw,ne,sw,se- set wind direction\n" +"8,11,15,18,21,25 - set wind speed\n" +"setter - enable/disable OE/WeatherOS setter\n" +"hud - enable/disable hud (text)\n" +"adv - enable/disable advanced hud\n" +"milesreset - reset miles counter to zero\n" +"---\n" +"HUD indicates trim efficiency. Red means irons angle\n" +"\n"; say(s); } else if (msg=="adv" ) ADV_HUD = !ADV_HUD; else if (msg=="hud" ) HUD_ON = !HUD_ON; else if (msg == "milesreset") totalMiles =0; else if (llSubStringIndex(msg, "route ") ==0) { string r = llStringTrim(llGetSubString(msg, 6, -1), STRING_TRIM); llRegionSay(88731112, "ROUTE|"+r); } else if (llSubStringIndex(msg, "speedup ") ==0) { SPEEDUP = (float)llGetSubString(msg,7, -1); } else if (llSubStringIndex(msg, "dest ") ==0) { wayPoints = []; list tk = llParseString2List(msg, [" ", ",", "<"], []); wayPoint.x = llList2Float(tk, 1); wayPoint.y = llList2Float(tk, 2); if (llGetListLength(tk)>3) { integer i; for (i=3; i < llGetListLength(tk) -1 ; i+=2) { vector v =;; if (v != ZERO_VECTOR) wayPoints+= v; } } if (wayPoint!= ZERO_VECTOR) say((string)(llGetListLength(wayPoints)+1)+" Waypoint(s) set"); else { wayPoint = ZERO_VECTOR; say("Waypoint(s) cleared"); } } else if (llSubStringIndex(msg, "follow ") ==0) { string w = llStringTrim(llGetSubString(msg, 7, -1), STRING_TRIM); if (w != "") { wayPoints = []; wayPoint = ZERO_VECTOR; llSensor(w, NULL_KEY, AGENT | PASSIVE | ACTIVE, 96, PI); } } else if (msg =="stop") { wayPoints = []; wayPoint = ZERO_VECTOR; followId =NULL_KEY; say("Waypoint(s) cleared"); } else if (llSubStringIndex(msg, "umsg ") ==0) { say(llGetSubString(msg, 5, -1)); } } setDycam(integer isOn) { llClearCameraParams(); if (isOn) llSetCameraParams([ CAMERA_ACTIVE, 1, // 0=INACTIVE 1=ACTIVE CAMERA_BEHINDNESS_ANGLE, 15.0, // (0 to 180) DEGREES CAMERA_BEHINDNESS_LAG, 1., // (0 to 3) SECONDS CAMERA_DISTANCE, 4.0, // ( 0.5 to 10) METERS CAMERA_PITCH, 20.0, // (-45 to 80) DEGREES CAMERA_POSITION_LOCKED, FALSE, // (TRUE or FALSE) CAMERA_POSITION_LAG, 1., // (0 to 3) SECONDS CAMERA_POSITION_THRESHOLD, 4.0, // (0 to 4) METERS CAMERA_FOCUS_LOCKED, FALSE, // (TRUE or FALSE) CAMERA_FOCUS_LAG, 1. , // (0 to 3) SECONDS CAMERA_FOCUS_THRESHOLD, 0.1, // (0 to 4) METERS CAMERA_FOCUS_OFFSET, <0.0,0.0,1.0> // <-10,-10,-10> to <10,10,10> METERS ]); } default { state_entry() { TWspeed = llVecMag(windVector); setWindVector( windVector ); llSetText("",ZERO_VECTOR,1.0); reset(); totalMiles = (float)llGetObjectDesc(); upright(); scanLinks(); } on_rez(integer n) { llResetScript(); } changed(integer change) { if (change & CHANGED_LINK) { key u =llAvatarOnSitTarget(); // First avatar to sit controls the boat, if allowed if ( u == NULL_KEY) { if (avatar != NULL_KEY) { llTriggerSound("raise", 1); moor(); llReleaseControls(); llResetScript(); } } else if (avatar == NULL_KEY) { if (!hasAccess(u, ACCESS)) llWhisper(0,"You are not allowed to operate this boat."); else { avatar = u; getPerms(avatar); llListen(CHANNEL,"",avatar,""); //listen to first seated avatar only... say("Say 'raise' to raise sails, 'help' for commands..."); say("SFsail defaults to East Wind, 15 Knots..."); } } } } run_time_permissions(integer perms) { if (perms & (PERMISSION_TAKE_CONTROLS)) { llTakeControls(CONTROL_RIGHT | CONTROL_LEFT | CONTROL_ROT_RIGHT | CONTROL_ROT_LEFT | CONTROL_FWD | CONTROL_BACK | CONTROL_DOWN | CONTROL_UP,TRUE,FALSE); } if ((perms & PERMISSION_CONTROL_CAMERA) && ENABLE_DYCAM) { setDycam(1); } } listen(integer ch, string n, key id, string m) { if (ch==0) { process(m); } else if (ch==-54001) { list lines=llParseString2List(m,["\n"],[]); if (llList2String(lines,0)=="wind") { list tk=llParseString2List(llList2String(lines,1),["=",";","/"],[]); if (llList2String(tk,0)=="wvel") { vector newWind =llList2Vector(tk,1); if (newWind != windVector) { llSetTimerEvent(2); windVector = newWind; windVector.z =0; windDirection = angleBetween(<0,1,0>, - windVector); TWspeed = llVecMag(windVector); string s = "--------\nWIND CHANGE!\n "+getRose(windDirection*RAD_TO_DEG)+ " " + num(TWspeed*MS_TO_KNOTS) +" Kn\n--------"; llSetText(s, <1.000, 0.329, 0.990>, 1.); } } } } } link_message(integer from,integer to,string msg,key id) { if (to ==889) process(msg); // Comes from SFposer buttons or other } dataserver(key id, string s) { if (llGetOwnerKey(id) == llGetOwner()) { process(s); } } timer() { updateCourse(); } control(key id, integer held, integer change) { if ( held & (CONTROL_LEFT|CONTROL_ROT_LEFT|CONTROL_RIGHT|CONTROL_ROT_RIGHT) ) { if (holdValue!=999.0) { holdValue = 999.0; say("Course hold disabled."); } float spd; if (turning ==0) turning=1; if (held & (CONTROL_RIGHT|CONTROL_ROT_RIGHT)) spd = -turning*(.5 + sog/10.)/1.5; else spd = turning*(.5 + sog/10.)/1.5; llSetVehicleVectorParam(VEHICLE_ANGULAR_MOTOR_DIRECTION,); if (STEER && (change & (CONTROL_LEFT|CONTROL_ROT_LEFT|CONTROL_RIGHT|CONTROL_ROT_RIGHT)) ) llMessageLinked(STEER, 887, (string)spd, ""); } else if ( change & ~held & (CONTROL_LEFT|CONTROL_ROT_LEFT|CONTROL_RIGHT|CONTROL_ROT_RIGHT) ) { turning = 0; if (STEER ) llMessageLinked(STEER, 887, (string)0, ""); } if ( change & held & (CONTROL_FWD | CONTROL_BACK) ) { if (status == "sailing") { if (held & CONTROL_FWD) sheetAngle+=5; else sheetAngle -= 5; if (sheetAngle>MAX_ANGLE) sheetAngle=MAX_ANGLE; if (sheetAngle <5) sheetAngle =5; updateSails(); if (autoTrim) { autoTrim = 0; say("Auto trim disabled."); } } else if (status == "motor") { if (held & CONTROL_FWD) { if (throttle < THROTTLE_MAX) throttle++; } else { if (throttle > -THROTTLE_MAX) throttle --; } say("Throttle "+(string)throttle); linear = ; } } else if (change & held & (CONTROL_UP| CONTROL_DOWN)) { if (status == "sailing" && spinIsUp) { if ( (held&CONTROL_UP) && spinAngle< 50) spinAngle+=5; else if ( (held&CONTROL_DOWN) && spinAngle>- 50) spinAngle-=5; updateSails(); } } } no_sensor() { say("Could not find user or object to follow!"); followId =NULL_KEY; } sensor(integer n) { say("Following "+(string)llDetectedName(0)+". Say 'stop' to stop."); followId = llDetectedKey(0); } }