// :SHOW: // :CATEGORY:Train // :NAME:OpensimTrain // :AUTHOR:Moundsa Mayo // :KEYWORDS: // :CREATED:2021-04-26 23:42:51 // :EDITED:2021-04-26 22:42:51 // :ID:1137 // :NUM:2028 // :REV:2.6.0// :WORLD:OpensimSim // :WORLD:Second Life // :DESCRIPTION: // this is setup for non-physical, phantom movement but can be modified for physical, etc. // :CODE: // [VRCLocomotiveOpensourceScript] string gsScriptVersion = "2.6.0"; // Created for VRC distribution by Moundsa Mayo // // Based on the original Opensource Hobo Train Script // written by Twisted Laws sometime in 2007 or 2008 // // Contact the VRC for the original script unaltered along with some // revision history removed from this version. // // train driver script example for SLRR. // // this is setup for non-physical, phantom movement but can be // modified for physical, etc. //===================================================================// //==== Global Constants ============================ ====// integer INDEX_INVALID = -1; integer LINK_INVALID = -32768; string NAME_WILDCARD = "*"; vector COLOR_GREEN = <0.00, 1.00, 0.00>; vector COLOR_BLACK = <0.00, 0.00, 0.00>; //==== Global Constants ============================== ====// //===================================================================// //===================================================================// //==== Utility Routines ============================ ====// string gsHexPrefix = "0x"; string gsHexChars = "0123456789ABCDEF"; string Int2Hex(integer iInt, integer iDigits) { integer iWork = iInt & 0xF; string sResult = llGetSubString(gsHexChars, iWork, iWork); iInt = (iInt >> 4) & 0x0FFFFFFF; while (iInt != 0) { iWork = iInt & 0xF; sResult = llGetSubString(gsHexChars, iWork, iWork) + sResult; iInt = iInt >> 4; } if (llStringLength(sResult) < iDigits) { sResult = "00000000" + sResult; sResult = llGetSubString(sResult, -iDigits, - 1); } return(gsHexPrefix + sResult); } // string Int2Hex // Force unsit all sitting agents integer UnsitAllAgents() { integer iIndex; list lLinkDetails = []; integer iAgentCount = 0; integer iLinkCount = llGetNumberOfPrims(); for (iIndex = 1; iIndex <= iLinkCount; ++iIndex) { lLinkDetails = llGetObjectDetails(llGetLinkKey(iIndex), [OBJECT_CREATOR]); // Probably an Agent, so unsit and add to count if (llList2Key(lLinkDetails, 0) == NULL_KEY) { ++iAgentCount; llUnSit(llGetLinkKey(iIndex)); } } // for all Links in Object return(iAgentCount); } // UnsitAllAgents // Sum up prims in linkset, omitting any sitting agents // (llGetNumberOfPrims includes sitting Agents in the returned count) integer PrimCount() { integer iIndex; list lLinkDetails = []; integer iPrimCount = 0; integer iLinkCount = llGetNumberOfPrims(); for (iIndex = 1; iIndex <= iLinkCount; ++iIndex) { lLinkDetails = llGetObjectDetails(llGetLinkKey(iIndex), [OBJECT_CREATOR]); // Probably not an Agent, so add to count if (llList2Key(lLinkDetails, 0) != NULL_KEY) { ++iPrimCount; } } // for all Links in Object // llOwnerSay("Links=" + (string)iPrimCount); return(iPrimCount); } // PrimCount string clip(float value) { string str = (string)value; integer where = llSubStringIndex(str, "."); return(llGetSubString(str, 0, where + 2)); } // clip integer NamedLinkFind(string sName) { // locate a linked prim integer i; integer m = llGetNumberOfPrims(); integer ret= LINK_INVALID; for (i = 1; i <= m; i++) { if (llGetLinkName(i) == sName) { ret = i; } } return ret; } // NamedLinkFind key ScanForAgentGone() { key kLinkID; integer iIndex; integer iRiderRollCount = llGetListLength(glRidersAboard); // Scan Rider roll for ID missing from linked objects beyond // original object linkset. Assume any missing are AGENT list lRiderLinks = []; for (iIndex = giLinkCountRiderless + 1; iIndex <= giLinkCountCurrent; ++iIndex) { kLinkID = llGetLinkKey(iIndex); lRiderLinks = lRiderLinks + (list)kLinkID; } for (iIndex = 0; iIndex < iRiderRollCount; ++iIndex) { kLinkID = llList2Key(glRidersAboard, iIndex); if (llListFindList(lRiderLinks, [kLinkID]) == INDEX_INVALID) { return(kLinkID); } } // for all high links (sitting atatars) return(NULL_KEY); } // ScanForAgentGone //==== Utility Routines ============================== ====// //===================================================================// //===================================================================// //==== Locomotive-Specific Values & Code =========== ====// // This code subsection defines a list of locomotive features encoded // as single-bit values into an integer. The feature set controls // which buttons are included in the Engineer's Menu and some aspects // of the locomotive's operation. integer FEATURE_SOUND_ENGINE = 0x00000001; integer FEATURE_SOUND_WHEELS = 0x00000002; integer FEATURE_SOUND_STEAM = 0x00000004; integer FEATURE_SOUND_RESERVED = 0x00000008; integer FEATURE_ALERT_BELL = 0x00000010; integer FEATURE_ALERT_WHISTLE = 0x00000020; integer FEATURE_ALERT_HORN = 0x00000040; integer FEATURE_ALERT_RESERVED = 0x00000080; integer FEATURE_LIGHTS_HEADLAMPS = 0x00000100; integer FEATURE_LIGHTS_TAILLAMPS = 0x00000200; integer FEATURE_LIGHTS_RUNNING = 0x00000400; integer FEATURE_LIGHTS_RESERVED = 0x00000800; integer FEATURE_CONTROL_SMOKE = 0x00010000; integer FEATURE_MOTION_NONPHYSICAL = 0x00100000; integer FEATURE_MOTION_PHYSICAL = 0x00200000; integer FEATURE_MOTION_FLIP = 0x00400000; integer FEATURE_MOTION_REVERSE = 0x00800000; integer FEATURE_DISPLACE_RERAIL = 0x01000000; integer FEATURE_DISPLACE_LEFT = 0x02000000; integer FEATURE_DISPLACE_RIGHT = 0x04000000; // Compute later due to LSL limitation rotation ROTATION_FLIP = ZERO_ROTATION; // This value defines the 'standard' featureset implemented in the VRC // Opensource Locomotive Script. TO remove a feature from a particular // build, Boolean (bitwise) AND the NOT of the feature code with the // FEATURESET_DEFAULT value. (See "VRC 2010 Train" entry in // configuration table below) // Compute at initialization time due to LSL limitation. Cannot include // in table due to LSL design. Instead, table can include a SINGLE // feature to be REMOVED from FEATURESET_DEFAULT integer FEATURESET_DEFAULT = 0; // This code subsection defines a list-based table of locomotive // builds identified by the first words in their ObjectName. Several // configuration values are preset in the table for selection by build // name // // Any build found in the table at script initialization time will be // assigned the values following its BUILD_TAG. // // Any build not identified in the table will be assigned the default // values. integer UNIT_BUILD_TAG_IDX = 0; integer UNIT_BUILD_GUIDE_OFFSET_IDX = 1; integer UNIT_BUILD_SIT_OFFSET_IDX = 2; integer UNIT_BUILD_SIT_ROTATION_IDX = 3; integer UNIT_BUILD_SPEED_MINIMUM_IDX = 4; integer UNIT_BUILD_SPEED_MAXIMUM_IDX = 5; integer UNIT_BUILD_SPEED_INCREMENT_IDX = 6; integer UNIT_BUILD_FEATURE_ADJUST_IDX = 7; integer UNIT_BUILD_TABLE_STRIDE = 8; // string sBuildTag, vector vGuideOffset, vector vSitOffset, // rotation rSitRotation list glUnitConfiguration = [ // DEFAULT BUILD - probably won't work well for ANY build "DEFAULT BUILD", <0.00, 0.00, 0.500>, <-4.000, -0.500, 1.000>, ZERO_ROTATION, 0.05, 8.00, 0.05, 0, // Desmond Shang "VRC Hobo Train", <0.00, 0.00, 0.585>, <-3.50816, -0.500, 1.000>, <-0.00876, -0.00003, 0.00000, 0.99996>, 0.0050, 0.1000, 0.0050, 0, // Lotek Ixtar // "NS2200", <0.00, 0.00, 0.720>, <-3.88150, -0.73620, 1.51784>, "[ix] NS 2201", <0.00, 0.00, 0.625>, <-3.594925, -1.217773, 1.880998>, <0.00000, 0.00000, 0.00000, -1.00000>, 0.005, 0.05, 0.005, 0x10034, // Veryfluffy Wingtips "Fluffies Diesel", <0.00, 0.00, 0.910>, <-1.95, 0.850, 0.950>, ZERO_ROTATION, 0.25, 10.00, 0.25, 0, // Garden Mole "Great Western 4500", <0.00, 0.00, 0.917>, <-3.50816, -0.500, 1.475>, ZERO_ROTATION, 0.10, 8.00, 0.10, 0 ]; //==== Operator Preference Subsection ============== ====// // This feature may be unacceptable at higher speeds or if subject to // motion sickness, so it is disable by default //integer CAMERA_DYNAMICS_ENABLED = TRUE; integer CAMERA_DYNAMICS_ENABLED = FALSE; //==== Operator Preference Subsection ================ ====// // CAUTION: Be VERY careful when adjusting these values - they are // set to meet SLRR published track layout standards and // have been thoroughly tested on the SLRR // Scan radius in meters float SENSOR_SEEK_RANGE_DEFAULT = 20.00; // Scan half-angle in degrees. Arc swept is this amount on either // side of X-axis of sensor prim float SENSOR_SEEK_ARC_DEFAULT = 25.00; string OBJECT_DESCRIPTION_TAG = "Script Version "; string VERSION_PREFIX_TAG = " Opensource V"; string ENGINEER_MENU_INSTRUCTION = "Touch train for Engineer's Menu"; string SIT_TEXT_OPERATOR = "Engineer"; string SIT_TEXT_PASSENGER = "Passenger"; // Buttons for Engineer's menu for all builds and script revisions string BTN_START = "Start"; string BTN_IDLE = "Idle"; string BTN_STOP = "Stop"; string BTN_FASTER = "Faster"; string BTN_SLOWER = "Slower"; string BTN_FLIP = "Flip"; string BTN_HELP = "Help"; // Buttons for Engineer's menu constructed according to featureset // Not all buttons or related functionality are implemented in a given // revision of this script string BTN_BLANK = " "; string BTN_BELL = "Bell"; string BTN_WHISTLE = "Whistle"; string BTN_HORN = "Horn"; string BTN_HEADLAMP = "Headlamp"; string BTN_RERAIL = "Rerail"; string BTN_NONPHYSICAL = "NONPhysical"; string BTN_PHYSICAL = "Physical"; string BTN_SMOKE = "Smoke"; // Name the smoke-emitting prim SMOKE_PRIM_NAME string SMOKE_PRIM_NAME = "Smoke"; integer SMOKE_OFF = 0; integer SMOKE_IDLE = 1; integer SMOKE_RUN = 2; integer SIT_ALLOW = TRUE; integer SIT_DENY = FALSE; integer ENGINE_STATUS_OFF = 0; integer ENGINE_STATUS_IDLING = 1; integer ENGINE_STATUS_ROLLING = 2; // Name the Engineer's display prim ENGINEERDISPLAY_PRIM_NAME string ENGINEERDISPLAY_PRIM_NAME = "EngineerDisplay"; string gsLocomotiveNameTag = ""; integer giLocomotiveFeatureSet = 0; integer giLocomotiveFeatureAdjust = 0; vector gvUnitToGuideOffset = ZERO_VECTOR; vector gvSitOffset = ZERO_VECTOR; rotation grSitOrientation = ZERO_ROTATION; integer giEngineStatus = 0; integer giNoHitsCount = 0; // Default for SLRR ROW string gsSensorTargetName = "Guide"; float gfSensorSeekRange = 20.00; float gfSensorScanArc = 0; integer giTravelDirection = 0; integer giFlipRequest = 0; vector gvWaypointPrior = ZERO_VECTOR; float gfSpeedPrior = 0.00; float gfSpeedCurrent = 0.00; float gfSpeedMinimum = 0.00; float gfSpeedMaximum = 0.00; float gfSpeedIncrement = 0.00; integer giStatusPhysical = TRUE; integer giFlagPhantom = TRUE; string gsStatusDisplayPrior = ""; integer giRerailRequest = TRUE; string gsDisplayPrimName = "EngineerDisplay"; integer giDisplayPrimLink = -32768; integer giLinkCountRiderless = 0; integer giLinkCountPrior = 0; integer giLinkCountCurrent = 0; integer giLinkGained = FALSE; string gsRegionCurrentName; vector gvRegionCurrentCorner; integer giSmokeEnabled = TRUE; integer giSmokeOn = TRUE; integer giDialogChannel = PUBLIC_CHANNEL; key gkOwnerID = NULL_KEY; key gkOperatorID = NULL_KEY; key gkPassengerID = NULL_KEY; integer giOperatorSeated = FALSE; integer giCameraDynamics = FALSE; // Table of all agents seated list glRidersAboard =[]; // Default Buttonset in all menu versions. At initialization time // build-specific feature buttons may be added as indiczted by // giFeatureSet list glMenuOperatorDefault = [ "HELP", "FASTER", "SLOWER", "FLIP", "START", "IDLE", "STOP" ]; list glMenuOperator = []; // These are activated if set in Operator Preference section list glCameraParameters = [ CAMERA_ACTIVE, TRUE, CAMERA_BEHINDNESS_ANGLE, 0.00, CAMERA_BEHINDNESS_LAG, 0.50, CAMERA_DISTANCE, 7.00, CAMERA_PITCH, 15.00, // CAMERA_FOCUS, CAMERA_FOCUS_LAG, 0.50, CAMERA_FOCUS_LOCKED, FALSE, CAMERA_FOCUS_THRESHOLD, 0.00, // CAMERA_POSITION, CAMERA_POSITION_LAG, 0.50, CAMERA_POSITION_LOCKED, FALSE, CAMERA_POSITION_THRESHOLD, 0.05, CAMERA_FOCUS_OFFSET, <0.00, 0.00, 1.00> ]; //===================================================================// //==== Locomotive-Specific Routines == ==================// list LocomotiveMenuCreateFromFeatureset(integer iFeatureSet) { list lMenu = []; // llOwnerSay("$00F0: FeatureSet=" + Int2Hex(iFeatureSet, 8)); if (iFeatureSet & FEATURE_CONTROL_SMOKE) { lMenu = BTN_SMOKE + lMenu; } if (iFeatureSet & FEATURE_ALERT_BELL) { lMenu = BTN_BELL + lMenu; } if (iFeatureSet & FEATURE_ALERT_HORN) { lMenu = BTN_HORN + lMenu; } // Temporary fix to accomodate an exception. Later // version will provide more flexible menu construction else { lMenu = BTN_BLANK + lMenu; } if (iFeatureSet & FEATURE_ALERT_WHISTLE) { lMenu = BTN_WHISTLE + lMenu; } if (iFeatureSet & FEATURE_LIGHTS_HEADLAMPS) { lMenu = BTN_HEADLAMP + lMenu; } if (~llGetListLength(glMenuOperator)) lMenu = lMenu + glMenuOperator; // llOwnerSay("Menu=" + llList2CSV(lMenu)); return(lMenu); } // LocomotiveMenuCreateFromFeatureset LocomotiveBuildConfigure() { integer iBuild; integer iBuildItems = llGetListLength(glUnitConfiguration); integer iBuildIndex = INDEX_INVALID; string sBuildTag; string sBuildName = llGetObjectName(); // Set to a default feature set, from which a single feature may later be removed integer giLocomotiveFeatureSet = FEATURE_SOUND_ENGINE | FEATURE_SOUND_WHEELS | FEATURE_SOUND_STEAM | FEATURE_ALERT_BELL | FEATURE_ALERT_WHISTLE | FEATURE_ALERT_HORN | FEATURE_LIGHTS_HEADLAMPS | FEATURE_LIGHTS_TAILLAMPS | FEATURE_CONTROL_SMOKE | FEATURE_MOTION_PHYSICAL; // FEATURE_CONTROL_SMOKE | FEATURE_MOTION_NONPHYSICAL; for (iBuild = 0; iBuild < iBuildItems; iBuild = iBuild + UNIT_BUILD_TABLE_STRIDE) { sBuildTag = llList2String(glUnitConfiguration, iBuild + UNIT_BUILD_TAG_IDX); if (llSubStringIndex(sBuildName, sBuildTag) == 0) { iBuildIndex = iBuild; gsLocomotiveNameTag = sBuildTag; } } // for all builds in table if (iBuildIndex == INDEX_INVALID) { iBuildIndex = 0; } gvUnitToGuideOffset = llList2Vector(glUnitConfiguration, iBuildIndex + UNIT_BUILD_GUIDE_OFFSET_IDX); gvSitOffset = llList2Vector(glUnitConfiguration, iBuildIndex + UNIT_BUILD_SIT_OFFSET_IDX); grSitOrientation = llList2Rot(glUnitConfiguration, iBuildIndex + UNIT_BUILD_SIT_ROTATION_IDX); gfSpeedMinimum = llList2Float(glUnitConfiguration, iBuildIndex + UNIT_BUILD_SPEED_MINIMUM_IDX); gfSpeedIncrement = llList2Float(glUnitConfiguration, iBuildIndex + UNIT_BUILD_SPEED_INCREMENT_IDX); gfSpeedMaximum = llList2Float(glUnitConfiguration, iBuildIndex + UNIT_BUILD_SPEED_MAXIMUM_IDX); giLocomotiveFeatureAdjust = llList2Integer(glUnitConfiguration, iBuildIndex + UNIT_BUILD_FEATURE_ADJUST_IDX); /* llOwnerSay("$0100: Buildname=" + sBuildName + " Build=" + (string)iBuildIndex + ":" + gsLocomotiveNameTag + " FeatureSet=" + Int2Hex(giLocomotiveFeatureSet, 8) + " FeatureAdjust=" + Int2Hex(giLocomotiveFeatureAdjust, 8) + " AdjustedFeatureSet=" + Int2Hex(giLocomotiveFeatureSet & (~giLocomotiveFeatureAdjust), 8)); */ // Adjust the default featureset by removing any single // unwanted feature. Need greater flexibilty here. giLocomotiveFeatureSet = giLocomotiveFeatureSet & (~giLocomotiveFeatureAdjust); glMenuOperator = LocomotiveMenuCreateFromFeatureset( giLocomotiveFeatureSet); // llOwnerSay("$0110: CONFIG=" + (string)gvUnitToGuideOffset + ", " + // (string)gvSitOffset + ", " + (string)grSitOrientation + // ", " + (string)gfSpeedMinimum + ", " + // (string)gfSpeedMaximum + ", " + (string)gfSpeedIncrement); } // LocomotiveBuildConfigure string LocomotiveHelpBuild() { string sHelpString = ""; sHelpString += "\n Page Up = Stopped: Idle->Running->Faster"; sHelpString += "\n Page Down = Running: Slower->Idle->Stop"; sHelpString += "\n Shift + Right Arrow = Instant Idle/Resume"; // sHelpString += "\nDown Arrow = Reverse"; sHelpString += "\n Shift + Left Arrow = Flip train around"; return(sHelpString); } // LocomotiveHelpBuild LocomotiveDisplaySet(string str, vector color) { if (gsStatusDisplayPrior != str) { gsStatusDisplayPrior = str; llSetLinkPrimitiveParamsFast(giDisplayPrimLink, [ PRIM_TEXT, gsStatusDisplayPrior, color, 1.0]); } } // LocomotiveDisplaySet LocomotiveDisplayUpdate() { vector color = <0.5, 1.0, 0.5>; string str = "Speed: " + clip(gfSpeedCurrent*1000) + "\n"; if (giEngineStatus == ENGINE_STATUS_ROLLING) { str += "\nRunning"; } else if (giEngineStatus == ENGINE_STATUS_IDLING) { str += "\nIdling"; } else { str = llGetObjectName() + "\n\n"; str += "\nStopped"; color = <0.5, 0.5, 1.0>; } if (llGetStatus(STATUS_PHANTOM)) { str += "\nPhantom"; } else { str += "\nNOT Phantom!!"; } str += "\nFollowing '" + gsSensorTargetName + "'"; if (giLinkCountCurrent != giLinkCountRiderless) { str += "\n \n \n" + ENGINEER_MENU_INSTRUCTION; } LocomotiveDisplaySet(str, color); } // LocomotiveDisplayUpdate integer LocomotiveRiderAboard(key gkRiderID) { return(NamedLinkFind(llKey2Name(gkRiderID)) != LINK_INVALID); } // LocomotiveRiderAboard LocomotivePassengerRejection(key kAgentID, integer iSitAllow) { if (iSitAllow) { llInstantMessage(kAgentID, "\nSorry, " + llKey2Name(kAgentID) + ", only the Owner may operate this locomotive."); } else { llInstantMessage(kAgentID, "\nSorry, " + llKey2Name(kAgentID) + ", only the Owner may operate this locomotive." + "\nAnd the engineer needs to climb aboard first"); llUnSit(kAgentID); } } // LocomotivePassengerRejection LocomotiveMoveTo(vector vWaypointNext, rotation rAttitudeNext) { if (llGetStatus(STATUS_PHYSICS)) { llRotLookAt(rAttitudeNext, 1 / llVecMag(llGetVel()), 1); llMoveToTarget( vWaypointNext, 1.0); } else { llSetLinkPrimitiveParamsFast(LINK_ROOT, [PRIM_POSITION, vWaypointNext, PRIM_ROTATION, rAttitudeNext]); } } // LocomotiveMoveTo // Opensim sim-crossing compatibility LocomotiveMoveToSlow(vector vWaypointNext, rotation rAttitudeNext) { if (llGetStatus(STATUS_PHYSICS)) { llRotLookAt(rAttitudeNext, 1 / llVecMag(llGetVel()), 1); llMoveToTarget( vWaypointNext, 1.0); } else { llSetPos(vWaypointNext); llSetRot(rAttitudeNext); } } LocomotiveFlip() { giStatusPhysical = llGetStatus(STATUS_PHYSICS); if (giStatusPhysical) { llSetStatus(STATUS_PHYSICS,FALSE); } LocomotiveDisplaySet("reverse", <1.0, 0.5, 0.5>); giFlipRequest = FALSE; // llSetRot(ROTATION_FLIP * llGetRot() ); llSetLinkPrimitiveParamsFast(LINK_THIS,[PRIM_ROTATION, (ROTATION_FLIP*llGetRot()) ]); giTravelDirection = (giTravelDirection + 1) % 2; if (giEngineStatus == ENGINE_STATUS_ROLLING) { llSleep(0.5); llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, gfSensorSeekRange, gfSensorScanArc); } if (giStatusPhysical) { llSetStatus(STATUS_PHYSICS,TRUE); } } // LocomotiveFlip float LocomotiveSpeedUpdate(float fSpeedDelta) { // llOwnerSay("SD=" + (string)fSpeedDelta); if (fSpeedDelta > 0.00) { if ( (gfSpeedCurrent + fSpeedDelta) <= gfSpeedMaximum) { gfSpeedCurrent += fSpeedDelta; } } else { if ( (gfSpeedCurrent + fSpeedDelta) > 0) { gfSpeedCurrent += fSpeedDelta; } } return(gfSpeedCurrent); } // LocomotiveSpeedUpdate LocomotiveSpeedUp() { if (giEngineStatus == ENGINE_STATUS_OFF) { LocomotiveIdle(); } else if (giEngineStatus == ENGINE_STATUS_IDLING) { LocomotiveRun(gfSpeedMinimum); } else if (giEngineStatus == ENGINE_STATUS_ROLLING) { LocomotiveSpeedUpdate(gfSpeedIncrement); } } // LocomotiveSpeedUp LocomotiveSlowDown() { if (giEngineStatus == ENGINE_STATUS_ROLLING) { if (gfSpeedCurrent > gfSpeedMinimum) { LocomotiveSpeedUpdate(-gfSpeedIncrement); } else { LocomotiveIdle(); } } else if (giEngineStatus == ENGINE_STATUS_IDLING) { LocomotiveStop(); } } // LocomotiveSlowDown LocomotiveIdle() { llMessageLinked(LINK_SET, 99, "idle", (key)NAME_WILDCARD); giSmokeOn = LocomotiveSmoke(SMOKE_IDLE); llSetLinkPrimitiveParamsFast(LINK_THIS,[PRIM_POSITION,llGetPos()+<-0.01,0.0,0.0>]); giEngineStatus = ENGINE_STATUS_IDLING; gfSpeedPrior = gfSpeedCurrent; gfSpeedCurrent = 0.00; // llVolumeDetect(TRUE); giFlagPhantom = TRUE; } // LocomotiveIdle LocomotiveRun(float fSpeedStartup) { llMessageLinked(LINK_SET, 99, "run", (key)NAME_WILDCARD); giSmokeOn = LocomotiveSmoke(SMOKE_RUN); llSetLinkPrimitiveParamsFast(LINK_THIS,[PRIM_POSITION,llGetPos()+<-0.01,0.0,0.0>]); llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, gfSensorSeekRange, gfSensorScanArc); giEngineStatus = ENGINE_STATUS_ROLLING; gfSpeedCurrent = fSpeedStartup; // llVolumeDetect(TRUE); giFlagPhantom = TRUE; } // LocomotiveRun LocomotiveStop() { gfSpeedCurrent = 0.00; llMessageLinked(LINK_SET, -1, "reset", (key)NAME_WILDCARD); giSmokeOn = LocomotiveSmoke(SMOKE_OFF); giStatusPhysical = llGetStatus(STATUS_PHYSICS); llSetStatus(STATUS_PHYSICS, FALSE); llSensorRemove(); giEngineStatus = ENGINE_STATUS_OFF; giFlagPhantom = FALSE; // llVolumeDetect(FALSE); llSetStatus(STATUS_PHANTOM, TRUE); } // LocomotiveStop integer LocomotiveSmoke(integer iSmokeLevel) { integer iSmokeNew = TRUE; if (giSmokeEnabled) { llMessageLinked(LINK_ALL_CHILDREN, iSmokeLevel, SMOKE_PRIM_NAME, NULL_KEY); } else { llMessageLinked(LINK_ALL_CHILDREN, SMOKE_OFF, SMOKE_PRIM_NAME, NULL_KEY); } if (iSmokeLevel == SMOKE_OFF) { iSmokeNew = FALSE; } return(iSmokeNew); } // LocomotiveSmoke //==== Locomotive-Specific Routines == ====================// ScriptInitialize() { ROTATION_FLIP = llEuler2Rot(<0.00, 0.00, 180.00> * DEG_TO_RAD); integer iLinkCount = llGetNumberOfPrims(); // TO clear any existing sit targets, uncomment the code below: // // integer iIndex; // for (iIndex = 0; iIndex > iLinkCount; iIndex++) // { // llSetText(".", COLOR_BLACK, 0.00); // llSitTarget(ZERO_VECTOR, ZERO_ROTATION); // } UnsitAllAgents(); llSetClickAction(CLICK_ACTION_SIT); giLinkCountRiderless = PrimCount(); giLinkCountCurrent = llGetNumberOfPrims(); giLinkCountPrior = giLinkCountCurrent; gsRegionCurrentName = llGetRegionName(); gvRegionCurrentCorner = llGetRegionCorner(); gfSensorScanArc = SENSOR_SEEK_ARC_DEFAULT * DEG_TO_RAD; gkOwnerID = llGetOwner(); llMessageLinked(LINK_SET, -1, "reset", (key)NAME_WILDCARD); llSetBuoyancy(1.0); llStopMoveToTarget(); llStopLookAt(); llSetText(" ",<1.00, 1.00, 1.00>, 1.00); llSetStatus(STATUS_PHANTOM,TRUE); giDisplayPrimLink = NamedLinkFind(gsDisplayPrimName); // llOwnerSay("Display=" + (string)giDisplayPrimLink + " " + // llGetLinkName(giDisplayPrimLink)); LocomotiveDisplaySet("-----", <0.50, 0.50, 1.00>); llSetObjectDesc(""); // For development and testing // llSetObjectName(SET TO BUILD NAME TO TEST AGAINST); glMenuOperator = glMenuOperatorDefault; LocomotiveBuildConfigure(); llSetObjectName(gsLocomotiveNameTag + VERSION_PREFIX_TAG + gsScriptVersion); llSetSitText(SIT_TEXT_OPERATOR); llSitTarget(gvSitOffset, grSitOrientation); llSetCameraAtOffset(<3.00, 0.00, 1.50>); llSetCameraEyeOffset(<-8.00, 0.00, 5.00>); LocomotiveDisplayUpdate(); key agent = llAvatarOnSitTarget(); if (agent != NULL_KEY) { llRequestPermissions(agent, PERMISSION_TAKE_CONTROLS); // llVolumeDetect(TRUE); } } // ScriptInitialize default { state_entry() { if (gkOperatorID == NULL_KEY) { ScriptInitialize(); } if (giRerailRequest) { state Rerail; } giDialogChannel = -llGetUnixTime(); llListen(giDialogChannel, "", "", ""); giSmokeOn = LocomotiveSmoke(SMOKE_OFF); LocomotiveDisplayUpdate(); } // state_entry // Only recognizes first Toucher touch_start(integer iTouching) { if (giLinkCountCurrent > giLinkCountRiderless) { if (llDetectedKey(0) == gkOperatorID) { string sPrompt = "\nSmoke "; if (giSmokeEnabled) { sPrompt += "ENABLED"; } else { sPrompt += "DISABLED"; } sPrompt += "\n\nOperate locomotive"; llDialog(gkOperatorID, sPrompt, glMenuOperator, giDialogChannel); } // else // { // LocomotivePassengerRejection(llDetectedKey(0), // SIT_ALLOW); // } } } // touch_start changed(integer iChanged) { if (iChanged & CHANGED_REGION) { gsRegionCurrentName = llGetRegionName(); gvRegionCurrentCorner = llGetRegionCorner(); LocomotiveRun(gfSpeedCurrent); LocomotiveDisplayUpdate(); } if (iChanged & CHANGED_LINK) { key kRiderID = NULL_KEY; giLinkCountCurrent = llGetNumberOfPrims(); // Gained a link if (giLinkCountCurrent > giLinkCountPrior) { giLinkGained = TRUE; giLinkCountPrior = giLinkCountCurrent; kRiderID = llGetLinkKey(giLinkCountCurrent); glRidersAboard = glRidersAboard + (list)kRiderID; // If driver needed. if (gkOperatorID == NULL_KEY) { // owner only can ride if (kRiderID == gkOwnerID) { // seat as driver gkOperatorID = kRiderID; llSetSitText(SIT_TEXT_PASSENGER); llSetClickAction(CLICK_ACTION_TOUCH); llVolumeDetect(TRUE); llRequestPermissions(gkOperatorID, PERMISSION_TRIGGER_ANIMATION | PERMISSION_TAKE_CONTROLS | PERMISSION_CONTROL_CAMERA); } else { LocomotivePassengerRejection(kRiderID, SIT_DENY); } } // else seat as passenger else { gkPassengerID = llGetLinkKey(giLinkCountCurrent); // seat passenger at the right spot llSetLinkPrimitiveParamsFast(giLinkCountCurrent, [ PRIM_POS_LOCAL, gvSitOffset+<.5,1,0.4>, PRIM_ROT_LOCAL, grSitOrientation ]); // llSetCameraAtOffset(<3.00, 0.00, 1.50>); // llSetCameraEyeOffset(<-8.00, 0.00, 5.00>); llSetSitText(SIT_TEXT_OPERATOR); llInstantMessage(gkPassengerID, "\nWelcome aboard, " + llKey2Name(gkPassengerID) + "!" + "\nYou can get your own train at:" + "\n (todo: put hg address here)"); } } // gained a link // Lost a link else { giLinkGained = FALSE; giLinkCountPrior = giLinkCountCurrent; // Scan the rider roll to see who is no longer linked kRiderID = ScanForAgentGone(); integer iRider = llListFindList(glRidersAboard, (list)kRiderID); if (iRider != INDEX_INVALID) { glRidersAboard = llDeleteSubList(glRidersAboard, iRider, iRider); } // Engineer unsat if (kRiderID == gkOperatorID) { giOperatorSeated = FALSE; gkOperatorID = NULL_KEY; llStopAnimation("sit"); if (llGetPermissions() & PERMISSION_TAKE_CONTROLS) { llReleaseControls(); } if ( (giCameraDynamics) && (llGetPermissions() & PERMISSION_CONTROL_CAMERA) ) { llClearCameraParams(); } // Just leaving this commented here, as a warning. // The following triggers a crash of the avatar after unseating: llStopAnimation(llGetInventoryName(INVENTORY_ANIMATION, 0)); llSetClickAction(CLICK_ACTION_SIT); llSetSitText(SIT_TEXT_OPERATOR); llSitTarget(gvSitOffset, grSitOrientation); // set camera for when engineer reseats llSetCameraAtOffset(<3.00, 0.00, 1.50>); llSetCameraEyeOffset(<-8.00, 0.00, 5.00>); } // Driver dismounted // Passenger dismounted else { if (gkOperatorID == NULL_KEY) { llSetSitText(SIT_TEXT_OPERATOR); } else { llSetSitText(SIT_TEXT_PASSENGER); } gkPassengerID = NULL_KEY; } // Passenger dismounted } // lost a link // llOwnerSay("$1890: Riderless=" + // (string)giLinkCountRiderless + // " Prior=" + (string)giLinkCountPrior + // " Current=" + (string)giLinkCountCurrent); // Force any count errors our way if (giLinkCountCurrent < giLinkCountRiderless) { giLinkCountCurrent = giLinkCountRiderless; } // If no-one aboard, stop locomotive if ( ((giEngineStatus == ENGINE_STATUS_ROLLING) || (giEngineStatus == ENGINE_STATUS_IDLING)) && (giLinkCountCurrent == giLinkCountRiderless)) { LocomotiveStop(); llSetSitText(SIT_TEXT_OPERATOR); llSetClickAction(CLICK_ACTION_SIT); } LocomotiveDisplayUpdate(); } // CHANGED_LINK } // changed run_time_permissions(integer iPermissions) { if (iPermissions & PERMISSION_TAKE_CONTROLS) { // llTakeControls(CONTROL_UP | CONTROL_DOWN | CONTROL_FWD | // CONTROL_BACK | CONTROL_LEFT | CONTROL_RIGHT, // TRUE, FALSE); llTakeControls(CONTROL_UP | CONTROL_DOWN | CONTROL_LEFT | CONTROL_RIGHT, TRUE, FALSE); llInstantMessage(llAvatarOnSitTarget(), "\n" + llKey2Name(llAvatarOnSitTarget()) + ", " + ENGINEER_MENU_INSTRUCTION + LocomotiveHelpBuild()); } if ( (iPermissions & PERMISSION_TRIGGER_ANIMATION) && (gkOperatorID != NULL_KEY) ) { llStopAnimation("sit"); llStartAnimation(llGetInventoryName( INVENTORY_ANIMATION, 0)); llSetClickAction(CLICK_ACTION_TOUCH); } if ( (giCameraDynamics) && (iPermissions & PERMISSION_CONTROL_CAMERA) ) { llSetCameraParams(glCameraParameters); } } // run_time_permissions control(key kID, integer level, integer edge) { if (edge & level & CONTROL_UP) { LocomotiveSpeedUp(); } // CONTROL_UP // if (edge & level & CONTROL_BACK) // { // Reserved for reverse/forward (as opposed to Flip // } if (edge & level & CONTROL_DOWN) { LocomotiveSlowDown(); } // CONTROL_DOWN if (edge & level & CONTROL_LEFT) { if (giEngineStatus == ENGINE_STATUS_ROLLING) { giFlipRequest=TRUE; } else { LocomotiveFlip(); } } // CONTROL_LEFT if (edge & level & CONTROL_RIGHT) { if (giEngineStatus == ENGINE_STATUS_ROLLING) { LocomotiveIdle(); } else if (giEngineStatus == ENGINE_STATUS_IDLING) { LocomotiveRun(gfSpeedPrior); } } // CONTROL_ROT_RIGHT LocomotiveDisplayUpdate(); } // control listen(integer iChannel, string sName, key kID, string sMessage) { if ((sMessage == "Stop") && ((giEngineStatus == ENGINE_STATUS_IDLING) || giEngineStatus == ENGINE_STATUS_ROLLING) ) { LocomotiveStop(); } // Stop else if ( (sMessage == "Idle") && ((giEngineStatus == ENGINE_STATUS_OFF) || (giEngineStatus == ENGINE_STATUS_ROLLING)) ) { LocomotiveIdle(); } // Idle else if ((sMessage == "Start") && (giEngineStatus != ENGINE_STATUS_ROLLING)) { LocomotiveRun(gfSpeedMinimum); } // Start else if (sMessage == "Flip") { if (giEngineStatus == ENGINE_STATUS_ROLLING) { giFlipRequest = TRUE; } else { LocomotiveFlip(); } } // Flip else if (sMessage == "Rerail") { state Rerail; } // Rerail else if (sMessage == "Faster") { LocomotiveSpeedUp(); } // Faster else if (sMessage == "Slower") { LocomotiveSlowDown(); } // Slower else if (sMessage == "Smoke") { giSmokeEnabled = !giSmokeEnabled; giSmokeOn = LocomotiveSmoke(giEngineStatus); } // Smoke else if (sMessage == "Notecard") { llWhisper(PUBLIC_CHANNEL, "No Notecard available yet"); } // Notecard else if (sMessage == "Help") { llOwnerSay(LocomotiveHelpBuild()); } // Help else if ( (sMessage == "Bell") || (sMessage == "Horn") || (sMessage == "Whistle") || (sMessage == "Headlamp") ) { llMessageLinked(LINK_SET, 0, sMessage, sMessage); } // Bell | Horn | Whistle giStatusPhysical = llGetStatus(STATUS_PHYSICS); LocomotiveDisplayUpdate(); } // listen sensor(integer iSensed) { if (giEngineStatus == ENGINE_STATUS_ROLLING) { integer index = 0; giNoHitsCount = 0; if (giFlipRequest) { LocomotiveFlip(); return; } if (llGetPos() == llDetectedPos(0) + gvUnitToGuideOffset) { if (iSensed > 1) { index++; } else { vector next = llGetPos() + <15.00, 0.00, 0.00> * llGetRot(); if (next.x < 1.00 || next.x > 255.00 || next.y < 1.00 || next.y > 255.00) { LocomotiveDisplaySet("sim crossing 1", <0.50, 0.50, 1.00>); LocomotiveMoveTo(llGetPos() + <2.00, 0.00, 0.00> * llGetRot(), llGetRot()); } else { LocomotiveFlip(); LocomotiveDisplayUpdate(); return; } LocomotiveDisplayUpdate(); llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, gfSensorSeekRange, gfSensorScanArc); return; } } rotation rHeadingNext = llDetectedRot(0); // Reverse Target Heading if travelling opposite to Guide // X-axis polarity if (giTravelDirection) { rHeadingNext = ROTATION_FLIP * rHeadingNext; } // If angle between unit and next Guide greater than acceptable // flip Target Heading 180 degrees if (llFabs(llAngleBetween(rHeadingNext, llGetRot())) > PI_BY_TWO) { rHeadingNext = ROTATION_FLIP * rHeadingNext; } vector vPositionCurrent = llGetPos(); vector vWaypointNext = llDetectedPos(index) + gvUnitToGuideOffset; if (llVecDist(vPositionCurrent, gvWaypointPrior) < llVecDist(vPositionCurrent, vWaypointNext)) { rHeadingNext = llGetRot(); } else { gvWaypointPrior = vWaypointNext; } vector vPositionNext = vPositionCurrent + (llVecNorm(vWaypointNext - vPositionCurrent) * gfSpeedCurrent); LocomotiveMoveTo(vPositionNext, rHeadingNext); LocomotiveDisplayUpdate(); llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, gfSensorSeekRange, gfSensorScanArc); } // if running } // sensor no_sensor() { if (++giNoHitsCount > 4) { llResetScript(); } else { gsRegionCurrentName = llGetRegionName(); LocomotiveDisplayUpdate(); vector vPositionNext = llGetPos() + <15.00, 0.00, 0.00> * llGetRot(); if (vPositionNext.x < 1.00 || vPositionNext.x > 255.00 || vPositionNext.y < 1.00 || vPositionNext.y > 255.00) { LocomotiveDisplaySet("sim crossing 2", <0.50, 0.50, 1.00>); LocomotiveMoveToSlow(llGetPos() + <5.00, 0.00, 0.00> * llGetRot(), llGetRot()); } else { LocomotiveFlip(); LocomotiveDisplayUpdate(); return; } LocomotiveDisplayUpdate(); // Second Life: // Give llSensor a rest //llSleep(1.00); //llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, // gfSensorSeekRange, gfSensorScanArc); // XEngine in OpenSim doesn't like llSleep so we use // a timer and do the llSensor there instead: llSetTimerEvent(1.0); } // if still seeking Guide } // no_sensor timer() { llSetTimerEvent(0); llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, gfSensorSeekRange, gfSensorScanArc); } on_rez(integer iRezParameter) { llResetScript(); } // on_rez } // state default state Rerail { state_entry() { LocomotiveDisplayUpdate(); giRerailRequest = FALSE; // Note full-sphere sensor scan angle when seeking ROW llSensor(gsSensorTargetName, "", ACTIVE | PASSIVE, gfSensorSeekRange, PI); gfSensorSeekRange = SENSOR_SEEK_RANGE_DEFAULT; } // state_entry sensor(integer iSensed) { // In theory, the first hit reported will always be the // closest scan target detected LocomotiveMoveTo(llDetectedPos(0) + gvUnitToGuideOffset, llDetectedRot(0)); state default; } // sensor no_sensor() { llOwnerSay("Scan target '" + gsSensorTargetName + "' not within " + (string)llRound(gfSensorSeekRange) + "m"); state default; } // no_sensor } // state Rerail //==== Locomotive-Specific Values & Code ============= ====// //===================================================================// // [VRCLocomotiveOpensourceScript]