https://wiki.secondlife.com/w/api.php?action=feedcontributions&user=Felis+Darwin&feedformat=atomSecond Life Wiki - User contributions [en]2024-03-19T03:47:11ZUser contributionsMediaWiki 1.36.1https://wiki.secondlife.com/w/index.php?title=Template:LSL_All_Functions/Name&diff=1199810Template:LSL All Functions/Name2016-04-03T22:30:14Z<p>Felis Darwin: </p>
<hr />
<div><!-- If you are going to edit this, please edit Template:LSL_All_Functions/Generate instead. This was last updated on 2015-02-14--><br />
<ul style="{{NewStyle|column-width|20.5em|moz=*|webkit=*}}"><br />
<li> [[llAbs{{#var:lang}}|llAbs]]<br />
<li> [[llAcos{{#var:lang}}|llAcos]]<br />
<li> [[llAddToLandBanList{{#var:lang}}|llAddToLandBanList]]<br />
<li> [[llAddToLandPassList{{#var:lang}}|llAddToLandPassList]]<br />
<li> [[llAdjustSoundVolume{{#var:lang}}|llAdjustSoundVolume]]<br />
<li> [[llAgentInExperience{{#var:lang}}|llAgentInExperience]] {{LSL New}} {{LSL I}}<br />
<li> [[llAllowInventoryDrop{{#var:lang}}|llAllowInventoryDrop]]<br />
<li> [[llAngleBetween{{#var:lang}}|llAngleBetween]]<br />
<li> [[llApplyImpulse{{#var:lang}}|llApplyImpulse]]<br />
<li> [[llApplyRotationalImpulse{{#var:lang}}|llApplyRotationalImpulse]]<br />
<li> [[llAsin{{#var:lang}}|llAsin]]<br />
<li> [[llAtan2{{#var:lang}}|llAtan2]]<br />
<li> [[llAttachToAvatar{{#var:lang}}|llAttachToAvatar]]<br />
<li> [[llAttachToAvatarTemp{{#var:lang}}|llAttachToAvatarTemp]] {{LSL I}}<br />
<li> [[llAvatarOnLinkSitTarget{{#var:lang}}|llAvatarOnLinkSitTarget]] {{LSL I}}<br />
<li> [[llAvatarOnSitTarget{{#var:lang}}|llAvatarOnSitTarget]]<br />
<li> [[llAxes2Rot{{#var:lang}}|llAxes2Rot]]<br />
<li> [[llAxisAngle2Rot{{#var:lang}}|llAxisAngle2Rot]]<br />
<li> [[llBase64ToInteger{{#var:lang}}|llBase64ToInteger]]<br />
<li> [[llBase64ToString{{#var:lang}}|llBase64ToString]]<br />
<li> [[llBreakAllLinks{{#var:lang}}|llBreakAllLinks]]<br />
<li> [[llBreakLink{{#var:lang}}|llBreakLink]]<br />
<li> [[llCastRay{{#var:lang}}|llCastRay]] {{LSL I}}<br />
<li> [[llCeil{{#var:lang}}|llCeil]]<br />
<li> [[llClearCameraParams{{#var:lang}}|llClearCameraParams]]<br />
<li> [[llClearLinkMedia{{#var:lang}}|llClearLinkMedia]] {{LSL I}}<br />
<li> [[llClearPrimMedia{{#var:lang}}|llClearPrimMedia]]<br />
<li> [[llCloseRemoteDataChannel{{#var:lang}}|llCloseRemoteDataChannel]]<br />
<li> <s>[[llCloud{{#var:lang}}|llCloud]]</s> {{LSL_D}}<br />
<li> [[llCollisionFilter{{#var:lang}}|llCollisionFilter]]<br />
<li> [[llCollisionSound{{#var:lang}}|llCollisionSound]]<br />
<li> [[llCollisionSprite{{#var:lang}}|llCollisionSprite]] {{LSL BR}}<br />
<li> [[llCos{{#var:lang}}|llCos]]<br />
<li> [[llCreateCharacter{{#var:lang}}|llCreateCharacter]] {{LSL I}}<br />
<li> [[llCreateKeyValue{{#var:lang}}|llCreateKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llCreateLink{{#var:lang}}|llCreateLink]]<br />
<li> [[llCSV2List{{#var:lang}}|llCSV2List]]<br />
<li> [[llDataSizeKeyValue{{#var:lang}}|llDataSizeKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llDeleteCharacter{{#var:lang}}|llDeleteCharacter]] {{LSL I}}<br />
<li> [[llDeleteKeyValue{{#var:lang}}|llDeleteKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llDeleteSubList{{#var:lang}}|llDeleteSubList]]<br />
<li> [[llDeleteSubString{{#var:lang}}|llDeleteSubString]]<br />
<li> [[llDetachFromAvatar{{#var:lang}}|llDetachFromAvatar]]<br />
<li> [[llDetectedGrab{{#var:lang}}|llDetectedGrab]]<br />
<li> [[llDetectedGroup{{#var:lang}}|llDetectedGroup]]<br />
<li> [[llDetectedKey{{#var:lang}}|llDetectedKey]]<br />
<li> [[llDetectedLinkNumber{{#var:lang}}|llDetectedLinkNumber]]<br />
<li> [[llDetectedName{{#var:lang}}|llDetectedName]]<br />
<li> [[llDetectedOwner{{#var:lang}}|llDetectedOwner]]<br />
<li> [[llDetectedPos{{#var:lang}}|llDetectedPos]]<br />
<li> [[llDetectedRot{{#var:lang}}|llDetectedRot]]<br />
<li> [[llDetectedTouchBinormal{{#var:lang}}|llDetectedTouchBinormal]]<br />
<li> [[llDetectedTouchFace{{#var:lang}}|llDetectedTouchFace]]<br />
<li> [[llDetectedTouchNormal{{#var:lang}}|llDetectedTouchNormal]]<br />
<li> [[llDetectedTouchPos{{#var:lang}}|llDetectedTouchPos]]<br />
<li> [[llDetectedTouchST{{#var:lang}}|llDetectedTouchST]]<br />
<li> [[llDetectedTouchUV{{#var:lang}}|llDetectedTouchUV]]<br />
<li> [[llDetectedType{{#var:lang}}|llDetectedType]]<br />
<li> [[llDetectedVel{{#var:lang}}|llDetectedVel]]<br />
<li> [[llDialog{{#var:lang}}|llDialog]]<br />
<li> [[llDie{{#var:lang}}|llDie]]<br />
<li> [[llDumpList2String{{#var:lang}}|llDumpList2String]]<br />
<li> [[llEdgeOfWorld{{#var:lang}}|llEdgeOfWorld]]<br />
<li> [[llEjectFromLand{{#var:lang}}|llEjectFromLand]]<br />
<li> [[llEmail{{#var:lang}}|llEmail]]<br />
<li> [[llEscapeURL{{#var:lang}}|llEscapeURL]]<br />
<li> [[llEuler2Rot{{#var:lang}}|llEuler2Rot]]<br />
<li> [[llEvade{{#var:lang}}|llEvade]] {{LSL I}}<br />
<li> [[llExecCharacterCmd{{#var:lang}}|llExecCharacterCmd]] {{LSL I}}<br />
<li> [[llFabs{{#var:lang}}|llFabs]]<br />
<li> [[llFleeFrom{{#var:lang}}|llFleeFrom]] {{LSL I}}<br />
<li> [[llFloor{{#var:lang}}|llFloor]]<br />
<li> [[llForceMouselook{{#var:lang}}|llForceMouselook]]<br />
<li> [[llFrand{{#var:lang}}|llFrand]]<br />
<li> [[llGenerateKey{{#var:lang}}|llGenerateKey]] {{LSL I}}<br />
<li> [[llGetAccel{{#var:lang}}|llGetAccel]]<br />
<li> [[llGetAgentInfo{{#var:lang}}|llGetAgentInfo]]<br />
<li> [[llGetAgentLanguage{{#var:lang}}|llGetAgentLanguage]]<br />
<li> [[llGetAgentList{{#var:lang}}|llGetAgentList]] {{LSL I}}<br />
<li> [[llGetAgentSize{{#var:lang}}|llGetAgentSize]]<br />
<li> [[llGetAlpha{{#var:lang}}|llGetAlpha]]<br />
<li> [[llGetAndResetTime{{#var:lang}}|llGetAndResetTime]]<br />
<li> [[llGetAnimation{{#var:lang}}|llGetAnimation]]<br />
<li> [[llGetAnimationList{{#var:lang}}|llGetAnimationList]]<br />
<li> [[llGetAnimationOverride{{#var:lang}}|llGetAnimationOverride]] {{LSL New}} {{LSL I}}<br />
<li> [[llGetAttached{{#var:lang}}|llGetAttached]]<br />
<li> [[llGetAttachedList{{#var:lang}}|llGetAttachedList]] {{LSL New}} {{LSL I}}<br />
<li> [[llGetBoundingBox{{#var:lang}}|llGetBoundingBox]]<br />
<li> [[llGetCameraPos{{#var:lang}}|llGetCameraPos]]<br />
<li> [[llGetCameraRot{{#var:lang}}|llGetCameraRot]]<br />
<li> [[llGetCenterOfMass{{#var:lang}}|llGetCenterOfMass]]<br />
<li> [[llGetClosestNavPoint{{#var:lang}}|llGetClosestNavPoint]] {{LSL I}}<br />
<li> [[llGetColor{{#var:lang}}|llGetColor]]<br />
<li> [[llGetCreator{{#var:lang}}|llGetCreator]]<br />
<li> [[llGetDate{{#var:lang}}|llGetDate]]<br />
<li> [[llGetDisplayName{{#var:lang}}|llGetDisplayName]]<br />
<li> [[llGetEnergy{{#var:lang}}|llGetEnergy]]<br />
<li> [[llGetEnv{{#var:lang}}|llGetEnv]]<br />
<li> [[llGetExperienceDetails{{#var:lang}}|llGetExperienceDetails]] {{LSL New}} {{LSL I}}<br />
<li> [[llGetExperienceErrorMessage{{#var:lang}}|llGetExperienceErrorMessage]] {{LSL New}} {{LSL I}}<br />
<li> [[llGetForce{{#var:lang}}|llGetForce]]<br />
<li> [[llGetFreeMemory{{#var:lang}}|llGetFreeMemory]]<br />
<li> [[llGetFreeURLs{{#var:lang}}|llGetFreeURLs]]<br />
<li> [[llGetGeometricCenter{{#var:lang}}|llGetGeometricCenter]]<br />
<li> [[llGetGMTclock{{#var:lang}}|llGetGMTclock]]<br />
<li> [[llGetHTTPHeader{{#var:lang}}|llGetHTTPHeader]]<br />
<li> [[llGetInventoryCreator{{#var:lang}}|llGetInventoryCreator]]<br />
<li> [[llGetInventoryKey{{#var:lang}}|llGetInventoryKey]]<br />
<li> [[llGetInventoryName{{#var:lang}}|llGetInventoryName]]<br />
<li> [[llGetInventoryNumber{{#var:lang}}|llGetInventoryNumber]]<br />
<li> [[llGetInventoryPermMask{{#var:lang}}|llGetInventoryPermMask]]<br />
<li> [[llGetInventoryType{{#var:lang}}|llGetInventoryType]]<br />
<li> [[llGetKey{{#var:lang}}|llGetKey]]<br />
<li> [[llGetLandOwnerAt{{#var:lang}}|llGetLandOwnerAt]]<br />
<li> [[llGetLinkKey{{#var:lang}}|llGetLinkKey]]<br />
<li> [[llGetLinkMedia{{#var:lang}}|llGetLinkMedia]] {{LSL I}}<br />
<li> [[llGetLinkName{{#var:lang}}|llGetLinkName]]<br />
<li> [[llGetLinkNumber{{#var:lang}}|llGetLinkNumber]]<br />
<li> [[llGetLinkNumberOfSides{{#var:lang}}|llGetLinkNumberOfSides]]<br />
<li> [[llGetLinkPrimitiveParams{{#var:lang}}|llGetLinkPrimitiveParams]]<br />
<li> [[llGetListEntryType{{#var:lang}}|llGetListEntryType]]<br />
<li> [[llGetListLength{{#var:lang}}|llGetListLength]]<br />
<li> [[llGetLocalPos{{#var:lang}}|llGetLocalPos]]<br />
<li> [[llGetLocalRot{{#var:lang}}|llGetLocalRot]]<br />
<li> [[llGetMass{{#var:lang}}|llGetMass]]<br />
<li> [[llGetMassMKS{{#var:lang}}|llGetMassMKS]] {{LSL I}}<br />
<li> [[llGetMaxScaleFactor{{#var:lang}}|llGetMaxScaleFactor]] {{LSL New}} {{LSL I}}<br />
<li> [[llGetMemoryLimit{{#var:lang}}|llGetMemoryLimit]] {{LSL I}}<br />
<li> [[llGetMinScaleFactor{{#var:lang}}|llGetMinScaleFactor]] {{LSL New}} {{LSL I}}<br />
<li> [[llGetNextEmail{{#var:lang}}|llGetNextEmail]]<br />
<li> [[llGetNotecardLine{{#var:lang}}|llGetNotecardLine]]<br />
<li> [[llGetNumberOfNotecardLines{{#var:lang}}|llGetNumberOfNotecardLines]]<br />
<li> [[llGetNumberOfPrims{{#var:lang}}|llGetNumberOfPrims]]<br />
<li> [[llGetNumberOfSides{{#var:lang}}|llGetNumberOfSides]]<br />
<li> [[llGetObjectDesc{{#var:lang}}|llGetObjectDesc]]<br />
<li> [[llGetObjectDetails{{#var:lang}}|llGetObjectDetails]]<br />
<li> [[llGetObjectMass{{#var:lang}}|llGetObjectMass]]<br />
<li> [[llGetObjectName{{#var:lang}}|llGetObjectName]]<br />
<li> [[llGetObjectPermMask{{#var:lang}}|llGetObjectPermMask]]<br />
<li> [[llGetObjectPrimCount{{#var:lang}}|llGetObjectPrimCount]]<br />
<li> [[llGetOmega{{#var:lang}}|llGetOmega]]<br />
<li> [[llGetOwner{{#var:lang}}|llGetOwner]]<br />
<li> [[llGetOwnerKey{{#var:lang}}|llGetOwnerKey]]<br />
<li> [[llGetParcelDetails{{#var:lang}}|llGetParcelDetails]]<br />
<li> [[llGetParcelFlags{{#var:lang}}|llGetParcelFlags]]<br />
<li> [[llGetParcelMaxPrims{{#var:lang}}|llGetParcelMaxPrims]]<br />
<li> [[llGetParcelMusicURL{{#var:lang}}|llGetParcelMusicURL]] {{LSL I}}<br />
<li> [[llGetParcelPrimCount{{#var:lang}}|llGetParcelPrimCount]]<br />
<li> [[llGetParcelPrimOwners{{#var:lang}}|llGetParcelPrimOwners]]<br />
<li> [[llGetPermissions{{#var:lang}}|llGetPermissions]]<br />
<li> [[llGetPermissionsKey{{#var:lang}}|llGetPermissionsKey]]<br />
<li> [[llGetPhysicsMaterial{{#var:lang}}|llGetPhysicsMaterial]] {{LSL I}}<br />
<li> [[llGetPos{{#var:lang}}|llGetPos]]<br />
<li> [[llGetPrimitiveParams{{#var:lang}}|llGetPrimitiveParams]]<br />
<li> [[llGetPrimMediaParams{{#var:lang}}|llGetPrimMediaParams]]<br />
<li> [[llGetRegionAgentCount{{#var:lang}}|llGetRegionAgentCount]]<br />
<li> [[llGetRegionCorner{{#var:lang}}|llGetRegionCorner]]<br />
<li> [[llGetRegionFlags{{#var:lang}}|llGetRegionFlags]]<br />
<li> [[llGetRegionFPS{{#var:lang}}|llGetRegionFPS]]<br />
<li> [[llGetRegionName{{#var:lang}}|llGetRegionName]]<br />
<li> [[llGetRegionTimeDilation{{#var:lang}}|llGetRegionTimeDilation]]<br />
<li> [[llGetRootPosition{{#var:lang}}|llGetRootPosition]]<br />
<li> [[llGetRootRotation{{#var:lang}}|llGetRootRotation]]<br />
<li> [[llGetRot{{#var:lang}}|llGetRot]]<br />
<li> [[llGetScale{{#var:lang}}|llGetScale]]<br />
<li> [[llGetScriptName{{#var:lang}}|llGetScriptName]]<br />
<li> [[llGetScriptState{{#var:lang}}|llGetScriptState]]<br />
<li> [[llGetSimStats{{#var:lang}}|llGetSimStats]] {{LSL I}}<br />
<li> [[llGetSimulatorHostname{{#var:lang}}|llGetSimulatorHostname]]<br />
<li> [[llGetSPMaxMemory{{#var:lang}}|llGetSPMaxMemory]] {{LSL I}}<br />
<li> [[llGetStartParameter{{#var:lang}}|llGetStartParameter]]<br />
<li> [[llGetStaticPath{{#var:lang}}|llGetStaticPath]] {{LSL I}}<br />
<li> [[llGetStatus{{#var:lang}}|llGetStatus]]<br />
<li> [[llGetSubString{{#var:lang}}|llGetSubString]]<br />
<li> [[llGetSunDirection{{#var:lang}}|llGetSunDirection]]<br />
<li> [[llGetTexture{{#var:lang}}|llGetTexture]]<br />
<li> [[llGetTextureOffset{{#var:lang}}|llGetTextureOffset]]<br />
<li> [[llGetTextureRot{{#var:lang}}|llGetTextureRot]]<br />
<li> [[llGetTextureScale{{#var:lang}}|llGetTextureScale]]<br />
<li> [[llGetTime{{#var:lang}}|llGetTime]]<br />
<li> [[llGetTimeOfDay{{#var:lang}}|llGetTimeOfDay]]<br />
<li> [[llGetTimestamp{{#var:lang}}|llGetTimestamp]]<br />
<li> [[llGetTorque{{#var:lang}}|llGetTorque]]<br />
<li> [[llGetUnixTime{{#var:lang}}|llGetUnixTime]]<br />
<li> [[llGetUsedMemory{{#var:lang}}|llGetUsedMemory]] {{LSL I}}<br />
<li> [[llGetUsername{{#var:lang}}|llGetUsername]]<br />
<li> [[llGetVel{{#var:lang}}|llGetVel]]<br />
<li> [[llGetWallclock{{#var:lang}}|llGetWallclock]]<br />
<li> [[llGiveInventory{{#var:lang}}|llGiveInventory]]<br />
<li> [[llGiveInventoryList{{#var:lang}}|llGiveInventoryList]]<br />
<li> [[llGiveMoney{{#var:lang}}|llGiveMoney]]<br />
<li> <s>[[llGodLikeRezObject{{#var:lang}}|llGodLikeRezObject]]</s> {{LSL_GM}}<br />
<li> [[llGround{{#var:lang}}|llGround]]<br />
<li> [[llGroundContour{{#var:lang}}|llGroundContour]]<br />
<li> [[llGroundNormal{{#var:lang}}|llGroundNormal]]<br />
<li> [[llGroundRepel{{#var:lang}}|llGroundRepel]]<br />
<li> [[llGroundSlope{{#var:lang}}|llGroundSlope]]<br />
<li> [[llHTTPRequest{{#var:lang}}|llHTTPRequest]]<br />
<li> [[llHTTPResponse{{#var:lang}}|llHTTPResponse]]<br />
<li> [[llInsertString{{#var:lang}}|llInsertString]]<br />
<li> [[llInstantMessage{{#var:lang}}|llInstantMessage]]<br />
<li> [[llIntegerToBase64{{#var:lang}}|llIntegerToBase64]]<br />
<li> [[llJson2List{{#var:lang}}|llJson2List]] {{LSL New}} {{LSL I}}<br />
<li> [[llJsonGetValue{{#var:lang}}|llJsonGetValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llJsonSetValue{{#var:lang}}|llJsonSetValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llJsonValueType{{#var:lang}}|llJsonValueType]] {{LSL New}} {{LSL I}}<br />
<li> [[llKey2Name{{#var:lang}}|llKey2Name]]<br />
<li> [[llKeyCountKeyValue{{#var:lang}}|llKeyCountKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llKeysKeyValue{{#var:lang}}|llKeysKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llLinkParticleSystem{{#var:lang}}|llLinkParticleSystem]]<br />
<li> [[llLinkSitTarget{{#var:lang}}|llLinkSitTarget]] {{LSL I}}<br />
<li> [[llList2CSV{{#var:lang}}|llList2CSV]]<br />
<li> [[llList2Float{{#var:lang}}|llList2Float]]<br />
<li> [[llList2Integer{{#var:lang}}|llList2Integer]]<br />
<li> [[llList2Json{{#var:lang}}|llList2Json]] {{LSL New}} {{LSL I}}<br />
<li> [[llList2Key{{#var:lang}}|llList2Key]]<br />
<li> [[llList2List{{#var:lang}}|llList2List]]<br />
<li> [[llList2ListStrided{{#var:lang}}|llList2ListStrided]]<br />
<li> [[llList2Rot{{#var:lang}}|llList2Rot]]<br />
<li> [[llList2String{{#var:lang}}|llList2String]]<br />
<li> [[llList2Vector{{#var:lang}}|llList2Vector]]<br />
<li> [[llListen{{#var:lang}}|llListen]]<br />
<li> [[llListenControl{{#var:lang}}|llListenControl]]<br />
<li> [[llListenRemove{{#var:lang}}|llListenRemove]]<br />
<li> [[llListFindList{{#var:lang}}|llListFindList]]<br />
<li> [[llListInsertList{{#var:lang}}|llListInsertList]]<br />
<li> [[llListRandomize{{#var:lang}}|llListRandomize]]<br />
<li> [[llListReplaceList{{#var:lang}}|llListReplaceList]]<br />
<li> [[llListSort{{#var:lang}}|llListSort]]<br />
<li> [[llListStatistics{{#var:lang}}|llListStatistics]]<br />
<li> [[llLoadURL{{#var:lang}}|llLoadURL]]<br />
<li> [[llLog{{#var:lang}}|llLog]]<br />
<li> [[llLog10{{#var:lang}}|llLog10]]<br />
<li> [[llLookAt{{#var:lang}}|llLookAt]]<br />
<li> [[llLoopSound{{#var:lang}}|llLoopSound]]<br />
<li> [[llLoopSoundMaster{{#var:lang}}|llLoopSoundMaster]]<br />
<li> [[llLoopSoundSlave{{#var:lang}}|llLoopSoundSlave]]<br />
<li> <s>[[llMakeExplosion{{#var:lang}}|llMakeExplosion]]</s> {{LSL_D}}<br />
<li> <s>[[llMakeFire{{#var:lang}}|llMakeFire]]</s> {{LSL_D}}<br />
<li> <s>[[llMakeFountain{{#var:lang}}|llMakeFountain]]</s> {{LSL_D}}<br />
<li> <s>[[llMakeSmoke{{#var:lang}}|llMakeSmoke]]</s> {{LSL_D}}<br />
<li> [[llManageEstateAccess{{#var:lang}}|llManageEstateAccess]] {{LSL I}}<br />
<li> [[llMapDestination{{#var:lang}}|llMapDestination]]<br />
<li> [[llMD5String{{#var:lang}}|llMD5String]]<br />
<li> [[llMessageLinked{{#var:lang}}|llMessageLinked]]<br />
<li> [[llMinEventDelay{{#var:lang}}|llMinEventDelay]]<br />
<li> [[llModifyLand{{#var:lang}}|llModifyLand]]<br />
<li> [[llModPow{{#var:lang}}|llModPow]]<br />
<li> [[llMoveToTarget{{#var:lang}}|llMoveToTarget]]<br />
<li> [[llNavigateTo{{#var:lang}}|llNavigateTo]] {{LSL I}}<br />
<li> [[llOffsetTexture{{#var:lang}}|llOffsetTexture]]<br />
<li> [[llOpenRemoteDataChannel{{#var:lang}}|llOpenRemoteDataChannel]]<br />
<li> [[llOverMyLand{{#var:lang}}|llOverMyLand]]<br />
<li> [[llOwnerSay{{#var:lang}}|llOwnerSay]]<br />
<li> [[llParcelMediaCommandList{{#var:lang}}|llParcelMediaCommandList]]<br />
<li> [[llParcelMediaQuery{{#var:lang}}|llParcelMediaQuery]]<br />
<li> [[llParseString2List{{#var:lang}}|llParseString2List]]<br />
<li> [[llParseStringKeepNulls{{#var:lang}}|llParseStringKeepNulls]]<br />
<li> [[llParticleSystem{{#var:lang}}|llParticleSystem]]<br />
<li> [[llPassCollisions{{#var:lang}}|llPassCollisions]]<br />
<li> [[llPassTouches{{#var:lang}}|llPassTouches]]<br />
<li> [[llPatrolPoints{{#var:lang}}|llPatrolPoints]] {{LSL I}}<br />
<li> [[llPlaySound{{#var:lang}}|llPlaySound]]<br />
<li> [[llPlaySoundSlave{{#var:lang}}|llPlaySoundSlave]]<br />
<li> <s>[[llPointAt{{#var:lang}}|llPointAt]]</s> {{LSL_D}}<br />
<li> [[llPow{{#var:lang}}|llPow]]<br />
<li> [[llPreloadSound{{#var:lang}}|llPreloadSound]]<br />
<li> [[llPursue{{#var:lang}}|llPursue]] {{LSL I}}<br />
<li> [[llPushObject{{#var:lang}}|llPushObject]]<br />
<li> [[llReadKeyValue{{#var:lang}}|llReadKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> <s>[[llRefreshPrimURL{{#var:lang}}|llRefreshPrimURL]]</s> {{LSL_D}}<br />
<li> [[llRegionSay{{#var:lang}}|llRegionSay]]<br />
<li> [[llRegionSayTo{{#var:lang}}|llRegionSayTo]]<br />
<li> <s>[[llReleaseCamera{{#var:lang}}|llReleaseCamera]]</s> {{LSL_D}}<br />
<li> [[llReleaseControls{{#var:lang}}|llReleaseControls]]<br />
<li> [[llReleaseURL{{#var:lang}}|llReleaseURL]]<br />
<li> [[llRemoteDataReply{{#var:lang}}|llRemoteDataReply]]<br />
<li> <s>[[llRemoteDataSetRegion{{#var:lang}}|llRemoteDataSetRegion]]</s> {{LSL_D}}<br />
<li> <s>[[llRemoteLoadScript{{#var:lang}}|llRemoteLoadScript]]</s> {{LSL_D}}<br />
<li> [[llRemoteLoadScriptPin{{#var:lang}}|llRemoteLoadScriptPin]]<br />
<li> [[llRemoveFromLandBanList{{#var:lang}}|llRemoveFromLandBanList]]<br />
<li> [[llRemoveFromLandPassList{{#var:lang}}|llRemoveFromLandPassList]]<br />
<li> [[llRemoveInventory{{#var:lang}}|llRemoveInventory]]<br />
<li> [[llRemoveVehicleFlags{{#var:lang}}|llRemoveVehicleFlags]]<br />
<li> [[llRequestAgentData{{#var:lang}}|llRequestAgentData]]<br />
<li> [[llRequestDisplayName{{#var:lang}}|llRequestDisplayName]]<br />
<li> [[llRequestExperiencePermissions{{#var:lang}}|llRequestExperiencePermissions]] {{LSL New}} {{LSL I}}<br />
<li> [[llRequestInventoryData{{#var:lang}}|llRequestInventoryData]]<br />
<li> [[llRequestPermissions{{#var:lang}}|llRequestPermissions]]<br />
<li> [[llRequestSecureURL{{#var:lang}}|llRequestSecureURL]]<br />
<li> [[llRequestSimulatorData{{#var:lang}}|llRequestSimulatorData]]<br />
<li> [[llRequestURL{{#var:lang}}|llRequestURL]]<br />
<li> [[llRequestUsername{{#var:lang}}|llRequestUsername]]<br />
<li> [[llResetAnimationOverride{{#var:lang}}|llResetAnimationOverride]] {{LSL New}} {{LSL I}}<br />
<li> [[llResetLandBanList{{#var:lang}}|llResetLandBanList]]<br />
<li> [[llResetLandPassList{{#var:lang}}|llResetLandPassList]]<br />
<li> [[llResetOtherScript{{#var:lang}}|llResetOtherScript]]<br />
<li> [[llResetScript{{#var:lang}}|llResetScript]]<br />
<li> [[llResetTime{{#var:lang}}|llResetTime]]<br />
<li> [[llReturnObjectsByID{{#var:lang}}|llReturnObjectsByID]] {{LSL New}} {{LSL I}}<br />
<li> [[llReturnObjectsByOwner{{#var:lang}}|llReturnObjectsByOwner]] {{LSL New}} {{LSL I}}<br />
<li> [[llRezAtRoot{{#var:lang}}|llRezAtRoot]]<br />
<li> [[llRezObject{{#var:lang}}|llRezObject]]<br />
<li> [[llRot2Angle{{#var:lang}}|llRot2Angle]]<br />
<li> [[llRot2Axis{{#var:lang}}|llRot2Axis]]<br />
<li> [[llRot2Euler{{#var:lang}}|llRot2Euler]]<br />
<li> [[llRot2Fwd{{#var:lang}}|llRot2Fwd]]<br />
<li> [[llRot2Left{{#var:lang}}|llRot2Left]]<br />
<li> [[llRot2Up{{#var:lang}}|llRot2Up]]<br />
<li> [[llRotateTexture{{#var:lang}}|llRotateTexture]]<br />
<li> [[llRotBetween{{#var:lang}}|llRotBetween]]<br />
<li> [[llRotLookAt{{#var:lang}}|llRotLookAt]]<br />
<li> [[llRotTarget{{#var:lang}}|llRotTarget]]<br />
<li> [[llRotTargetRemove{{#var:lang}}|llRotTargetRemove]]<br />
<li> [[llRound{{#var:lang}}|llRound]]<br />
<li> [[llSameGroup{{#var:lang}}|llSameGroup]]<br />
<li> [[llSay{{#var:lang}}|llSay]]<br />
<li> [[llScaleByFactor{{#var:lang}}|llScaleByFactor]] {{LSL New}} {{LSL I}}<br />
<li> [[llScaleTexture{{#var:lang}}|llScaleTexture]]<br />
<li> [[llScriptDanger{{#var:lang}}|llScriptDanger]]<br />
<li> [[llScriptProfiler{{#var:lang}}|llScriptProfiler]] {{LSL I}}<br />
<li> [[llSendRemoteData{{#var:lang}}|llSendRemoteData]]<br />
<li> [[llSensor{{#var:lang}}|llSensor]]<br />
<li> [[llSensorRemove{{#var:lang}}|llSensorRemove]]<br />
<li> [[llSensorRepeat{{#var:lang}}|llSensorRepeat]]<br />
<li> [[llSetAlpha{{#var:lang}}|llSetAlpha]]<br />
<li> [[llSetAngularVelocity{{#var:lang}}|llSetAngularVelocity]] {{LSL I}}<br />
<li> [[llSetAnimationOverride{{#var:lang}}|llSetAnimationOverride]] {{LSL New}} {{LSL I}}<br />
<li> [[llSetBuoyancy{{#var:lang}}|llSetBuoyancy]]<br />
<li> [[llSetCameraAtOffset{{#var:lang}}|llSetCameraAtOffset]]<br />
<li> [[llSetCameraEyeOffset{{#var:lang}}|llSetCameraEyeOffset]]<br />
<li> [[llSetCameraParams{{#var:lang}}|llSetCameraParams]]<br />
<li> [[llSetClickAction{{#var:lang}}|llSetClickAction]]<br />
<li> [[llSetColor{{#var:lang}}|llSetColor]]<br />
<li> [[llSetContentType{{#var:lang}}|llSetContentType]] {{LSL I}}<br />
<li> [[llSetDamage{{#var:lang}}|llSetDamage]]<br />
<li> [[llSetForce{{#var:lang}}|llSetForce]]<br />
<li> [[llSetForceAndTorque{{#var:lang}}|llSetForceAndTorque]]<br />
<li> [[llSetHoverHeight{{#var:lang}}|llSetHoverHeight]]<br />
<li> <s>[[llSetInventoryPermMask{{#var:lang}}|llSetInventoryPermMask]]</s> {{LSL_GM}}<br />
<li> [[llSetKeyframedMotion{{#var:lang}}|llSetKeyframedMotion]] {{LSL I}}<br />
<li> [[llSetLinkAlpha{{#var:lang}}|llSetLinkAlpha]]<br />
<li> [[llSetLinkCamera{{#var:lang}}|llSetLinkCamera]] {{LSL I}}<br />
<li> [[llSetLinkColor{{#var:lang}}|llSetLinkColor]]<br />
<li> [[llSetLinkMedia{{#var:lang}}|llSetLinkMedia]] {{LSL I}}<br />
<li> [[llSetLinkPrimitiveParams{{#var:lang}}|llSetLinkPrimitiveParams]]<br />
<li> [[llSetLinkPrimitiveParamsFast{{#var:lang}}|llSetLinkPrimitiveParamsFast]]<br />
<li> [[llSetLinkTexture{{#var:lang}}|llSetLinkTexture]]<br />
<li> [[llSetLinkTextureAnim{{#var:lang}}|llSetLinkTextureAnim]]<br />
<li> [[llSetLocalRot{{#var:lang}}|llSetLocalRot]]<br />
<li> [[llSetMemoryLimit{{#var:lang}}|llSetMemoryLimit]] {{LSL I}}<br />
<li> [[llSetObjectDesc{{#var:lang}}|llSetObjectDesc]]<br />
<li> [[llSetObjectName{{#var:lang}}|llSetObjectName]]<br />
<li> <s>[[llSetObjectPermMask{{#var:lang}}|llSetObjectPermMask]]</s> {{LSL_GM}}<br />
<li> [[llSetParcelMusicURL{{#var:lang}}|llSetParcelMusicURL]]<br />
<li> [[llSetPayPrice{{#var:lang}}|llSetPayPrice]]<br />
<li> [[llSetPhysicsMaterial{{#var:lang}}|llSetPhysicsMaterial]] {{LSL I}}<br />
<li> [[llSetPos{{#var:lang}}|llSetPos]]<br />
<li> [[llSetPrimitiveParams{{#var:lang}}|llSetPrimitiveParams]]<br />
<li> [[llSetPrimMediaParams{{#var:lang}}|llSetPrimMediaParams]]<br />
<li> <s>[[llSetPrimURL{{#var:lang}}|llSetPrimURL]]</s> {{LSL_D}}<br />
<li> [[llSetRegionPos{{#var:lang}}|llSetRegionPos]] {{LSL I}}<br />
<li> [[llSetRemoteScriptAccessPin{{#var:lang}}|llSetRemoteScriptAccessPin]]<br />
<li> [[llSetRot{{#var:lang}}|llSetRot]]<br />
<li> [[llSetScale{{#var:lang}}|llSetScale]]<br />
<li> [[llSetScriptState{{#var:lang}}|llSetScriptState]]<br />
<li> [[llSetSitText{{#var:lang}}|llSetSitText]]<br />
<li> [[llSetSoundQueueing{{#var:lang}}|llSetSoundQueueing]]<br />
<li> [[llSetSoundRadius{{#var:lang}}|llSetSoundRadius]]<br />
<li> [[llSetStatus{{#var:lang}}|llSetStatus]]<br />
<li> [[llSetText{{#var:lang}}|llSetText]]<br />
<li> [[llSetTexture{{#var:lang}}|llSetTexture]]<br />
<li> [[llSetTextureAnim{{#var:lang}}|llSetTextureAnim]]<br />
<li> [[llSetTimerEvent{{#var:lang}}|llSetTimerEvent]]<br />
<li> [[llSetTorque{{#var:lang}}|llSetTorque]]<br />
<li> [[llSetTouchText{{#var:lang}}|llSetTouchText]]<br />
<li> [[llSetVehicleFlags{{#var:lang}}|llSetVehicleFlags]]<br />
<li> [[llSetVehicleFloatParam{{#var:lang}}|llSetVehicleFloatParam]]<br />
<li> [[llSetVehicleRotationParam{{#var:lang}}|llSetVehicleRotationParam]]<br />
<li> [[llSetVehicleType{{#var:lang}}|llSetVehicleType]]<br />
<li> [[llSetVehicleVectorParam{{#var:lang}}|llSetVehicleVectorParam]]<br />
<li> [[llSetVelocity{{#var:lang}}|llSetVelocity]] {{LSL I}}<br />
<li> [[llSHA1String{{#var:lang}}|llSHA1String]]<br />
<li> [[llShout{{#var:lang}}|llShout]]<br />
<li> [[llSin{{#var:lang}}|llSin]]<br />
<li> [[llSitTarget{{#var:lang}}|llSitTarget]]<br />
<li> [[llSleep{{#var:lang}}|llSleep]]<br />
<li> <s>[[llSound{{#var:lang}}|llSound]]</s> {{LSL_D}}<br />
<li> <s>[[llSoundPreload{{#var:lang}}|llSoundPreload]]</s> {{LSL_D}}<br />
<li> [[llSqrt{{#var:lang}}|llSqrt]]<br />
<li> [[llStartAnimation{{#var:lang}}|llStartAnimation]]<br />
<li> [[llStopAnimation{{#var:lang}}|llStopAnimation]]<br />
<li> [[llStopHover{{#var:lang}}|llStopHover]]<br />
<li> [[llStopLookAt{{#var:lang}}|llStopLookAt]]<br />
<li> [[llStopMoveToTarget{{#var:lang}}|llStopMoveToTarget]]<br />
<li> <s>[[llStopPointAt{{#var:lang}}|llStopPointAt]]</s> {{LSL_D}}<br />
<li> [[llStopSound{{#var:lang}}|llStopSound]]<br />
<li> [[llStringLength{{#var:lang}}|llStringLength]]<br />
<li> [[llStringToBase64{{#var:lang}}|llStringToBase64]]<br />
<li> [[llStringTrim{{#var:lang}}|llStringTrim]]<br />
<li> [[llSubStringIndex{{#var:lang}}|llSubStringIndex]]<br />
<li> <s>[[llTakeCamera{{#var:lang}}|llTakeCamera]]</s> {{LSL_D}}<br />
<li> [[llTakeControls{{#var:lang}}|llTakeControls]]<br />
<li> [[llTan{{#var:lang}}|llTan]]<br />
<li> [[llTarget{{#var:lang}}|llTarget]]<br />
<li> [[llTargetOmega{{#var:lang}}|llTargetOmega]]<br />
<li> [[llTargetRemove{{#var:lang}}|llTargetRemove]]<br />
<li> [[llTeleportAgent{{#var:lang}}|llTeleportAgent]] {{LSL I}}<br />
<li> [[llTeleportAgentGlobalCoords{{#var:lang}}|llTeleportAgentGlobalCoords]] {{LSL I}}<br />
<li> [[llTeleportAgentHome{{#var:lang}}|llTeleportAgentHome]]<br />
<li> [[llTextBox{{#var:lang}}|llTextBox]]<br />
<li> [[llToLower{{#var:lang}}|llToLower]]<br />
<li> [[llToUpper{{#var:lang}}|llToUpper]]<br />
<li> [[llTransferLindenDollars{{#var:lang}}|llTransferLindenDollars]] {{LSL I}}<br />
<li> [[llTriggerSound{{#var:lang}}|llTriggerSound]]<br />
<li> [[llTriggerSoundLimited{{#var:lang}}|llTriggerSoundLimited]]<br />
<li> [[llUnescapeURL{{#var:lang}}|llUnescapeURL]]<br />
<li> [[llUnSit{{#var:lang}}|llUnSit]]<br />
<li> [[llUpdateCharacter{{#var:lang}}|llUpdateCharacter]] {{LSL I}}<br />
<li> [[llUpdateKeyValue{{#var:lang}}|llUpdateKeyValue]] {{LSL New}} {{LSL I}}<br />
<li> [[llVecDist{{#var:lang}}|llVecDist]]<br />
<li> [[llVecMag{{#var:lang}}|llVecMag]]<br />
<li> [[llVecNorm{{#var:lang}}|llVecNorm]]<br />
<li> [[llVolumeDetect{{#var:lang}}|llVolumeDetect]]<br />
<li> [[llWanderWithin{{#var:lang}}|llWanderWithin]] {{LSL I}}<br />
<li> [[llWater{{#var:lang}}|llWater]]<br />
<li> [[llWhisper{{#var:lang}}|llWhisper]]<br />
<li> [[llWind{{#var:lang}}|llWind]]<br />
<li> [[llXorBase64{{#var:lang}}|llXorBase64]] {{LSL New}} {{LSL I}}<br />
<li> <s>[[llXorBase64Strings{{#var:lang}}|llXorBase64Strings]]</s> {{LSL_D}}<br />
<li> <s>[[llXorBase64StringsCorrect{{#var:lang}}|llXorBase64StringsCorrect]]</s> {{LSL_D}}<br />
</ul> {{LSL_All_Functions/Generate}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Talk:LlCSV2List&diff=1188141Talk:LlCSV2List2014-03-07T02:16:54Z<p>Felis Darwin: /* Keeping Nulls */ new section</p>
<hr />
<div>{{Talk}}<br />
<br />
Strife, there is a cut-off sentence here.<br />
<br />
"llList2Vector, llList2Float, llList2Integer, llList2Rot should not be....."<br />
<br />
I can't correct as I don't know how you meant to finish the sentence.<br />
<br />
--[[User:Chaz Longstaff|Chaz Longstaff]]<br />
<br />
:Yeah I forgot to delete it after I wrote the table, it would have been a huge amount of text to explain everything that is in the table without the table and I only realized that as I was trying to do so. -- [[User:Strife Onizuka|Strife Onizuka]] 16:42, 14 July 2008 (PDT)<br />
<br />
----<br />
<br />
Small sugestion.<br />
<br />
Whouldnt it be nice if every code exemple has 2 rows that say what the code is doing, and maybe how the output looks and can be used?<br />
<br />
I think this is a exemple of code that creats more confusion then help.<br />
<br />
Specialy windows users who create code by drag and drop, or click and drag have this habbit.<br />
--[[User:Anylyn Hax|Anylyn Hax]]<br />
<br />
:It's common practice to put the script output after the script or in the script in comments. -- [[User:Strife Onizuka|Strife Onizuka]] 16:03, 12 August 2007 (PDT)<br />
<br />
:There is a sentence on here that says: "Do not confuse this function with the CSV format, it is not the CSV format." What does this mean? Are newlines not consumed and parsed? I will verify this in the future and make the appropriate changes but the above comment isn't useful... how is it different? [[User:Epilort Byrne|Epilort Byrne]] 16:03, 8 March 2010 (UTC)<br />
<br />
== According to the page all leading and trailing spaces are removed.... ==<br />
<br />
This is not true according to this:-<br />
<lsl>integer line_count;<br />
integer all;<br />
<br />
key get_line_count;<br />
key line;<br />
<br />
list text_list;<br />
<br />
default<br />
{<br />
on_rez(integer param)<br />
{<br />
llResetScript();<br />
}<br />
state_entry()<br />
{<br />
get_line_count = llGetNumberOfNotecardLines("Text Notecard");<br />
all = 0;<br />
line = llGetNotecardLine("Text Notecard", all);<br />
}<br />
changed(integer change)<br />
{<br />
if(change & CHANGED_INVENTORY)<br />
{<br />
llResetScript();<br />
}<br />
}<br />
dataserver(key qid, string data)<br />
{<br />
if(qid == get_line_count)<br />
{<br />
line_count = (integer)data;<br />
}<br />
if(qid == line)<br />
{<br />
if(all < line_count)<br />
{<br />
//text_list = text_list + llStringTrim(llGetSubString(llStringTrim(llList2String(llCSV2List(data), 3), STRING_TRIM), 1, -2), STRING_TRIM);<br />
text_list = text_list + llList2String(llCSV2List(data), 3);<br />
line = llGetNotecardLine("Text Notecard", ++all);<br />
}<br />
if(all == line_count)<br />
{<br />
llInstantMessage(llGetOwner(), llDumpList2String(text_list, "\n"));<br />
++all;<br />
}<br />
}<br />
}<br />
}</lsl>Reading from this style notecard:-<br />
----<br />
<lsl>value , value , value , < This is a test text ><br />
value , value , value , < This line is second ><br />
value , value , value , < This line is here decribing itself ><br />
value , value , value , < This one isn't, ><br />
value , value , value , < niether is this one. ><br />
value , value , value , < This line is another line ><br />
value , value , value , < This is another line as well ><br />
value , value , value , < so is this, ><br />
value , value , value , < and this. ><br />
value , value , value , < This line marks the end of the test text ></lsl><br />
----<br />
<br />
The IMmed result of the oranged-out line (my version before reading that [[llCSV2List]] '''supposedly''' trims the strings) is clean with '''no''' spare whitespace. The result of the untrimmed version (for the sake of ease we will ignore the keeping of the "<"s and ">"s for now. ok?) has a leading space before each line. The following is a mock up of the results since I have excluded some valuable info (thus the "value", "value" etc.).<br />
----<br />
<nowiki>[11:12]</nowiki>Trimming Object:This is a test text....etc.<br />
<br />
<nowiki>[11:12]</nowiki>Object: This is a test text....etc. (note the space before "This")<br />
----<br />
Even though I have mocked it up, (ignoring the fact that my trimming code chops the "<>"s off as well) unless a leading space is supposed to be applied to vectors the claim that leading whitespace is trimmed off is false. -- '''[[User:EddyFragment Robonaught|Eddy]]''' <sup><small>([[User talk:EddyFragment_Robonaught|talk]]|[[Special:Contributions/EddyFragment_Robonaught|contribs]])</small></sup> 18:53, 13 June 2009 (UTC)<br />
<br />
:I don't have time to get in world at the moment (I need to get to bed) but I'll look into this in the coming days. -- '''[[User:Strife_Onizuka|Strife]]''' <sup><small>([[User talk:Strife_Onizuka|talk]]|[[Special:Contributions/Strife_Onizuka|contribs]])</small></sup> 12:43, 14 June 2009 (UTC)<br />
<br />
== Keeping Nulls ==<br />
<br />
I need to do some tests, but the page here doesn't mention if this does or does not keep null entries, e.g. if I pass the function "1,2,,3" do I get ["1","2","3"] or ["1","2","","3"]? -- [[User:Felis Darwin|Felis Darwin]] 18:16, 6 March 2014 (PST)</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/RestrainedLoveAPI&diff=1188060LSL Protocol/RestrainedLoveAPI2014-03-05T12:21:13Z<p>Felis Darwin: /* Unofficial Commands */</p>
<hr />
<div>{{LSL Header|ml=*}}<br />
=Restrained Love viewer v2.8.1 Specification=<br />
<br />
By [[User:Marine Kelley|Marine Kelley]]<br />
<br />
==Audience==<br />
<br />
This document is for people who wish to modify or create their own [[LSL]] scripts to use the features of the [http://realrestraint.blogspot.com RestrainedLove viewer]. It does not explain [[LSL]] concepts such as messages and events, nor universal concepts such as [[UUID]]s.<br />
<br />
This document contains the specification for RestrainedLove viewer itself. If you need information about the RestrainedLove viewer relay(RLV relay), please see the [[LSL_Protocol/Restrained_Love_Relay/Specification|RLV relay specification]]<br />
<br />
==Introduction==<br />
<br />
The [http://realrestraint.blogspot.com RestrainedLove viewer] executes certain behaviours when receiving special messages from scripts in-world. These messages are mostly calls to the [[llOwnerSay]]() [[LSL]] function.<br />
<br />
==Architecture==<br />
<br />
The [http://realrestraint.blogspot.com RestrainedLove viewer] intercepts every [[llOwnerSay]] message sent to the viewer. Lines that begin with an at-sign (''''@'''') are parsed as RLV commands. Other lines are forwarded to the user in the Local Chat window, as usual. For instance, a call to [[llOwnerSay]] ("@detach=n") sends the ''detach'' command with parameter ''n'' to the viewer on behalf of the object running the script.<br />
<br />
The syntax of a message is:<br />
<br />
: <code>@<command1>[:option1]=<param1>,<command2>[:option2]=<param2>,...,<commandN>[:optionN]=<paramN></code><br />
<br />
Note that there is only one '@' sign, placed at the beginning of the message. The viewer interprets this as "this entire [[llOwnerSay]]() message contains one or more commands to execute". For documentation purposes, commands are always presented with the leading ''''@''''. However, it is an error to put the ''''@'''' in front of each command within a multi-command message, and the subsequent commands will fail.<br />
<br />
: Historical Note: Prior to Version '''1.10''', RLV only allowed one command per message. Version '''1.10''' added the ability to include multiple commands in one message, to avoid spamming users who are not using this viewer.<br />
<br />
If at least one command fails (e.g. a typo), the viewer says "... fails command : ... " and prints the entire message. However, correct commands are still parsed and executed, only the incorrect ones are ignored.<br />
<br />
Many of these commands determine the subsequent ''behaviour'' of the object or avatar. For example, the '''@detach=n''' command locks the given object, making it undetachable. Some commands set ''global behaviours'', which aren't limited to the object sending the command. For example, the '''@sendchat=n''' command will prevent the user from talking in local chat.<br />
<br />
'''NOTE''' about commands with exceptions, such as @sendim or @sendchannel... @(rule):(exception)=n actually (and counter-intuitively) '''adds an exception''' for the given rule. @sendchannel:1=n, for example, '''allows''' chat on channel 1. This has been the source of at least two scripters' confusion. =add (which means the same as =n) and =rem (which means =y) exist for the purpose of adding and removing exceptions, respectively. Use them.<br />
<br />
{{hint|mode=warning|desc=These behaviours are '''not''' persistent between sessions. Since an object changes its [[UUID]] every time it [[rez]]zes, the object ''must'' resend its status (undetachable, preventing IMs...) in the [[on_rez]]() event as well as whenever it changes its status.}}<br />
<br />
==List of commands==<br />
<br />
'''Note:''' These commands are not case-sensitive but are spacing-sensitive. In other words, "@detach = n" will '''not''' work.<br />
<br />
'''Notation convention:''' Parameters in [square brackets] are optional parameters that can be omitted. The pipe | and slash / signs separate options from which one must be used. <angle brackets> enclose parameters that are mandatory.<br />
<br />
'''Footnotes:''' "(*)" are footnotes and will be explained at the end of the list<br />
<br />
===Version Checking===<br />
<br />
* '''''Automated version checking''''' : "@version=<channel_number>"<br />
''Implemented in v1.0b''<br />
<br />
Makes the viewer automatically say the version of the RLV API it implements, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
'''Warning''' : when logging in, the [[on_rez]] event of all the attachments occurs way before the avatar can actually send chat messages (about half the way through the login progress bar). This means the timeout should be long enough, like 30 seconds to one minute in order to receive the automatic reply from the viewer.<br />
<br />
'''Warning 2''' : On 02/22/2010, Linden Lab has released their Third Party Viewer policy which forbids using the term "Life" in the name of Third Party Viewers. Therefore "Restrained Life" had to be renamed to "Restrained Love". However, for compatibility purposes, this @version command still works and will keep working, however you are encouraged to '''not''' use it in new scripts, and to '''not''' show the terms "Restrained Life" to the user anywhere. For new scripts, please use @versionnew below instead.<br />
<br />
<br />
* '''''Automated version checking''''' : "@versionnew=<channel_number>"<br />
''Implemented in v1.23''<br />
<br />
Makes the viewer automatically say the version of the RLV API it implements, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
This command is the successor of @version and replaces it, although @version is kept for ascending compatibility purposes. It returns "RestrainedLove viewer v... (SL ...)" ("RestrainedLove" is in one word).<br />
<br />
'''Warning''' : when logging in, the [[on_rez]] event of all the attachments occurs way before the avatar can actually send chat messages (about half the way through the login progress bar). This means the timeout should be long enough, like 30 seconds to one minute in order to receive the automatic reply from the viewer.<br />
<br />
<br />
* '''''Automated version number checking''''' : "@versionnum=<channel_number>"<br />
''Implemented in v1.21''<br />
<br />
Makes the viewer automatically say the version number of the RLV API it implements (please note that this is different from the version of the viewer, which the scripts should not have to care about), immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. This command is less cumbersome than @version, since the script does not have to parse the response, it gets the version number immediately.<br />
<br />
The version number is a mere integer that represents the version of the API. If the version is X.Y.Z.P, then the number will be X.10^6 + Y.10^4 + Z.10^2 + P. For example, 1.21.1 would be 1210100.<br />
<br />
<br />
* '''''Automated version checking, second way''''' : llGetAgentLanguage (key id) '''''DEPRECATED: DO NOT USE !'''''<br />
''Implemented in v1.16''<br />
<br />
When calling this LSL function, the result is obtained immediately (no need to use a listener and a timer), is exactly equal to the one given by "@version" and cannot be hidden by the user. This string takes the place of the language returned by the regular SL viewer, which could answer values like "en-us", "fr", "ko" etc. Or nothing at all, if the user chose to hide their language setting. Being optional in the regular viewer, it cannot be trusted by a script, so "hijacking" this feature for the much more useful synchronous version checking in the RLV makes sense. IMPORTANT NOTE: this feature cannot be implemented in viewers prior to v1.21, even when they do implement RestrainedLove v1.16, so make sure you do fall back to the @version method whenever llGetAgentLanguage() returns an empty string. ALSO NOTE: In RestrainedLove 1.16, llGetAgentLanguage() will return an empty string when called by on_rez during login unless the call is delayed by several seconds (how many seconds may vary). FINAL NOTE: This feature was removed from v1.16.1 (and v1.16b, for the Cool SL Viewer).<br />
<br />
<br />
* '''''Manual version checking''''' : "@version"<br />
''Implemented in v1.0a''<br />
<br />
This command must be sent in IM from an avatar to the user (will not work from objects). The viewer automatically answers its version to the sender in IM, but neither the message nor the answer appears in the user's IM window, so it's usually totally stealthy. However, some viewers, such as Firestorm, have the ability to send an autoreply message when someone begins typing an IM to the user. If the user has that option enabled, an IM window will open and display the auto response as soon as the @ is typed by the sender, but nothing else.<br />
<br />
===Blacklist handling===<br />
<br />
The blacklist (implemented in v2.8) is a list of RLV commands that the viewer is meant to ignore. It is modifiable at any time, but a restart is needed to take the changes into account. When a command is issued and it is part of the blacklist, the RLV will simply ignore it. Modifying the blacklist won't clear existing restrictions though, once they are issued, a restart is needed. When a command is received, a positive acknowledgement is sent to the script, whether the command was actually accepted or not. This way scripts that wait for notifications won't break if they can't handle a denial.<br />
<br />
<br />
* '''''Automated version number checking, followed by the blacklist''''' : "@versionnumbl=<channel_number>"<br />
''Implemented in v2.8''<br />
<br />
Makes the viewer automatically say the version number of the RLV API it implements (please note that this is different from the version of the viewer, which the scripts should not have to care about), followed by a comma (",") and the contents of the blacklist, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. This command is less cumbersome than @version followed by @getblacklist, since the script does not have to parse the response, it gets the version number immediately, and it does not have to send a second asynchronous request after the response to the first.<br />
<br />
For example, "@versionnumbl=2222" will answer "2080000,sendim,recvim" if the blacklist is currently "sendim,recvim". LSL allows for casting such a string into an integer without any trouble, it would return 2080000, discarding the first comma and all that is after it.<br />
<br />
<br />
* '''''Get the contents of the blacklist, with a filter''''' : "@getblacklist[:filter]=<channel_number>"<br />
''Implemented in v2.8''<br />
<br />
Makes the viewer automatically reply with the contents of the blacklist (if a filter is present, then only the commands containing that text will be part of the reply), immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
<br />
* '''''Manual blacklist checking''''' : "@getblacklist"<br />
''Implemented in v2.8''<br />
<br />
This command must be sent in IM from an avatar to the user (will not work from objects). The viewer automatically answers the contents of its blacklist to the sender in IM, but neither the message nor the answer appears in the user's IM window, so it's usually totally stealthy, with the same caveat mentioned above under @version.<br />
<br />
===Miscellaneous===<br />
<br />
* '''''Start/stop notifications on a private channel''''' : "@notify:<channel_number>[;word]=<rem/add>"<br />
''Implemented in v1.20, improved in v2.2 (and v1.24)''<br />
<br />
Makes the viewer automatically repeat any restriction it adds or removes on the specified channel, or only the restrictions which name contains the word specified after the semicolon (";") character. The response on the private channel <channel_number> is preceded with a slash ("/") to avoid making the avatar send commands to other scripts without knowing it, and followed by an equal sign ("=") and "n" or "y" according to whether the restriction is applied or lifted respectively. The "@clear" command will not add an equal sign. There is no way to know what object issued the restriction or lifted it, to avoid disclosing too much information about foreign scripts. It does not repeat one-shot commands either (force commands). For example, "@notify:2222;detach=add" will send "/detach=n" whenever an object is locked, and "/detach=y" whenever an object is unlocked, on channel 2222 to which the script will listen to.<br />
<br />
Note : Since v2.2 (and v1.24) you can also set a notification for inventory offers. When your object gives an item or a folder, the avatar using a RLV v2.2 (and v1.24) or higher will respond automatically on the given channel one of the following :<br />
<br />
:* /accepted_in_rlv inv_offer <folder> : The folder has been accepted and is now available under #RLV (don't forget that the viewer renames it, removing the "#RLV/" prefix).<br />
<br />
:* /accepted_in_inv inv_offer <folder> : The folder has been accepted but is not shared.<br />
<br />
:* /declined inv_offer <folder> : The folder has been declined and/or the user has pressed "Block" (formerly "Mute").<br />
<br />
Where <folder> is the full path of the folder or item given. For example, #RLV/~MyCuffs. There is a space before "inv_offer", which is a token chosen in a way that it is easy to set a notification for it. If you just want to know whether your folder named #RLV/~MyCuffs has been accepted in the #RLV folder, issue a "@notify:2222;accepted_in_rlv inv_offer #RLV/~MyCuffs=add" command. If you just want to know whether the avatar has received something, issue a simple "@notify:2222;inv_offer=add" command.<br />
<br />
Note 2 : Since v2.5 the viewer also sends notifications when wearing outfits :<br />
<br />
:* /worn legally <layer> : The avatar has just worn a piece of clothing on the indicated layer.<br />
<br />
:* /unworn legally <layer> : The avatar has just removed a piece of clothing from the indicated layer.<br />
<br />
:* /attached legally <attach_point_name> : The avatar has just attached an object to the indicated attachment point.<br />
<br />
:* /attached illegally <attach_point_name> : The avatar has just attached an object to the indicated attachment point, but was not allowed to (probably a script attached it automatically), and it will be detached in a few seconds.<br />
<br />
:* /detached legally <attach_point_name> : The avatar has just detached an object from the indicated attachment point.<br />
<br />
:* /detached illegally <attach_point_name> : The avatar has just detached an object from the indicated attachment point, but was not allowed to (probably a script kicked it off), and it will be re-attached in a few seconds.<br />
<br />
<br />
* '''''Allow/deny permissive exceptions''''' : "@permissive=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When denied, all restrictions turn into their "secure" counterparts (if any). This means an exception to a restriction will be ignored if it is not issued by the same object that issued the restriction. Using non-secure restrictions (the original ones, like @sendim, @recvim etc) and not using @permissive allow the avatar to benefit from exceptions issued by different objects.<br />
<br />
'''Warning''' : Using this command (or any secure version of the original commands) may silently discard exceptions issued by different objects (it is even its primary purpose), hence some products may appear to cease working while this restriction is in effect. For example, a product that allows the avatar to always be able to send IMs a particular friend will not be able to overcome a @sendim_sec or a @permissive command sent by another object, and will look like it is broken. Therefore, use with caution and make the user aware of how secure your own product is !<br />
<br />
<br />
* '''''Clear all the rules tied to an object''''' : "@clear"<br />
''Implemented in v1.0a, but working only since v1.04a''<br />
<br />
This command clears all the restrictions and exceptions tied to a particular [[UUID]].<br />
<br />
'''Warning''' : when triggered on detach by default, this might prevent the automatic reattach when @defaultwear is active, as @clear will also lift @detach=n, thus the viewer thinks the item that gets detached by accident by a default-wear-action is unlocked and will not reattach it.<br />
<br />
Possible workarounds:<br />
:* only lift the exact restrictions you added with @clear=<pattern> <br />
:* only trigger @clear on detach when you are sure the attachment is not locked<br />
:* don't trigger @clear on detach at all and wait for the viewer to lift the set restrictions<br />
<br />
<br />
* '''''Clear a subset of the rules tied to an object''''' : "@clear=<string>"<br />
''Implemented in v1.0a, but working only since v1.04a''<br />
<br />
This command clears all the restrictions and exceptions tied to a particular [[UUID]] which name contains <string>. A good example would be "@clear=tp" which clears all the [[teleport]] restrictions and exceptions tied to that object, whereas "@clear=tplure:" would only clear the exceptions to the "teleport-by-friend" restriction<br />
<br />
<br />
* '''''Get the list of restrictions the avatar is currently submitted to''''' : @getstatus[:<part_of_rule>[;<custom_separator>]]=<channel><br />
''Implemented in v1.10, slightly tweaked in v1.16 and v2.8''<br />
<br />
Makes the viewer automatically answer the list of rules the avatar is currently under, which would only contains the restrictions issued by the object that sends this command, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is a list of rules, separated by slashes ('/') or by any other separator if specified. Attention : since v1.16 a slash is prepended at the beginning of the string. This does not confuse llParseString2List() calls, but does confuse llParseStringKeepNulls() calls !<br />
<br />
Since v2.8, if <custom_separator> is specified, it will replace the slash ('/') with the provided separator. Attention, the option part must be present, therefore there must be a colon (':') before the semicolon (';'), even if <part_of_rule> is absent.<br />
<br />
This command is useful for people who write scripts that may conflict with other scripts in the same object (for instance : third-party plugins). Conflicts do not occur in different objects, that's why this command only replies the restrictions issued by the object calling it.<br />
<br />
<part_of_rule> is the name of a rule, or a part of it, useful if the script only needs to know about a certain restriction.<br />
<br />
Example : If the avatar is under tploc, tplure, tplm and sittp, here is what the script would get :<br />
@getstatus=2222 => /tploc/tplure/tplm/sittp<br />
@getstatus:sittp=2222 => /sittp<br />
@getstatus:tpl=2222 => /tploc/tplure/tplm (because "tpl" is part of "tploc", "tplure" and "tplm" but not "sittp")<br />
@getstatus:tpl;#=2222 => #tploc#tplure#tplm (because "tpl" is part of "tploc", "tplure" and "tplm" but not "sittp", and the<br />
specified separator is "#")<br />
@getstatus:;#=2222 => #tploc#tplure#tplm#sittp (because the specified separator is "#")<br />
@getstatus:;=2222 => /tploc/tplure/tplm/sittp (because the specified separator is empty, so it defaults to "/")<br />
<br />
<br />
* '''''Get the list of all the restrictions the avatar is currently submitted to''''' : @getstatusall[:<part_of_rule>[;<custom_separator>]]=<channel><br />
''Implemented in v1.15, slightly tweaked in v1.16 and v2.8''<br />
<br />
Makes the viewer automatically answer the list of rules the avatar is currently under, for all the objects regardless of their UUID, contrary to @getstatus, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is a list of rules, separated by slashes ('/') or by any other separator if specified. Attention : since v1.16 a slash is prepended at the beginning of the string. This does not confuse llParseString2List() calls, but does confuse llParseStringKeepNulls() calls !<br />
<br />
Since v2.8, if <custom_separator> is specified, it will replace the slash ('/') with the provided separator. Attention, the option part must be present, therefore there must be a colon (':') before the semicolon (';'), even if <part_of_rule> is absent.<br />
<br />
===Movement===<br />
<br />
* '''''Allow/prevent flying''''' : @fly=<y/n><br />
''Implemented in v1.12.2''<br />
<br />
When prevented, the user is unable to fly.<br />
<br />
<br />
* '''''Allow/prevent running by double-tapping an arrow key''''' : @temprun=<y/n><br />
''Implemented in v2.7''<br />
<br />
When prevented, the user is unable to run by double-tapping an arrow key. If you want to prevent the user from running at all, you must also use @alwaysrun.<br />
<br />
<br />
* '''''Allow/prevent always running''''' : @alwaysrun=<y/n><br />
''Implemented in v2.7''<br />
<br />
When prevented, the user is unable to switch running mode on by pressing Ctrl-R. If you want to prevent the user from running at all, you must also use @temprun. This command is useful when you want to force the user to accelerate before running, rather than running all the time, for example during combats or sports games.<br />
<br />
<br />
* '''''Force rotate the avatar to a set direction''''' : @setrot:<angle_in_radians>=force<br />
''Implemented in v1.17''<br />
<br />
Forces the avatar to rotate towards a direction set by an angle in radians from the north. Note that this command is not very precise, nor will do anything if the action attempts to rotate the avatar by less than 10° (experimental value, it has been mentioned somewhere that 6° was the minimum). In other words, it is best to either check with a llGetRot() first, or to make the avatar turn twice, first 180° plus the desired angle, then by the angle we need. It isn't very elegant but it works.<br />
<br />
See [[User:brattle Resident|this snippet]] for an example of how to convert a rotation as returned from llGetRootRotation to an angle in radians suitable for @setrot.<br />
<br />
* '''''Change the height of the avatar''''' : @adjustheight:<distance_pelvis_to_foot_in_meters>;<factor>[;delta_in_meters]=force<br />
''Implemented in v2.5''<br />
<br />
Forces the avatar to modify its "Z-offset", in other words its altitude. This value can already be changed through a debug setting in most third party viewers, this command allows to automate the change according to the animation.<br />
<br />
Rather than explaining it in full here, please go to this link for further info : [http://sldev.free.fr/forum/viewtopic.php?f=7&t=447]<br />
<br />
This function has been broken by Linden Lab as part of the Project Sunshine (server-side appearance/server-side baking) changes to Second Life, and will not function as expected in a SSA-capable viewer.<br />
<br />
===Chat, Emotes and Instant Messages===<br />
====Chat====<br />
* '''''Allow/prevent sending chat messages''''' : "@sendchat=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, everything typed on [[Chat channel|channel]] 0 will be discarded. However, emotes and messages beginning with a slash ('/') will go through, truncated to strings of 30 and 15 characters long respectively (likely to change later). Messages with special signs like ()"-*=_^ are prohibited, and will be discarded. When a period ('.') is present, the rest of the message is discarded.<br />
<br />
<br />
* '''''Allow/prevent shouting''''' : "@chatshout=<y/n>"<br />
''Implemented in v1.15''<br />
<br />
When prevented, the avatar will chat normally even when the user tries to shout. This does not change the message in any way, only its range.<br />
<br />
<br />
* '''''Allow/prevent chatting at normal volume''''' : "@chatnormal=<y/n>"<br />
''Implemented in v1.15''<br />
<br />
When prevented, the avatar will whisper even when the user tries to shout or chat normally. This does not change the message in any way, only its range.<br />
<br />
<br />
* '''''Allow/prevent whispering''''' : "@chatwhisper=<y/n>"<br />
''Implemented in v1.15''<br />
<br />
When prevented, the avatar will chat normally even when the user tries to whisper. This does not change the message in any way, only its range.<br />
<br />
<br />
* '''''Redirect public chat to private channels''''' : "@redirchat:<channel_number>=<rem/add>"<br />
''Implemented in v1.16''<br />
<br />
When active, this restriction redirects whatever the user says on the public channel ("/0") to the private channel provided in the option field. If several redirections are issued, the chat message will be redirected to each channel. It does not apply to emotes, and will not trigger any animation (typing start, typing stop, nodding) when talking. This restriction does not supercede @sendchannel.<br />
'''NOTE:''' As of RLV v1.22.1 / RLVa 1.1.0, it had a bug that @redirchat also truncates emotes on channel 0. An additional @emote=add works around this side-effect. This bug was fixed in the Cool VL Viewer starting with v1.22g (but Marine's RLV v1.23 still had this bug) and RLV v2.0 (it is safe to assume it was fixed in all viewers starting with v1.24 and v2.0).<br />
<br />
<br />
* '''''Allow/prevent receiving chat messages''''' : "@recvchat=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, everything heard in public chat will be discarded except emotes.<br />
<br />
<br />
* '''''Allow/prevent receiving chat messages, secure way''''' : "@recvchat_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, everything heard in public chat will be discarded except emotes. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the chat message receiving prevention''''' : "@recvchat:<UUID>=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding an exception, the user can hear chat messages from the sender whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent receiving chat messages from someone in particular''''' : "@recvchatfrom:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, everything heard in public chat from the specified avatar will be discarded except emotes.<br />
<br />
<br />
====Emotes====<br />
* '''''Remove/add an exception to the emote truncation above''''' : "@emote=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding this exception, the emotes are not truncated anymore (however, special signs will still discard the message).<br />
<br />
<br />
* '''''Redirect public emotes to private channels''''' : "@rediremote:<channel_number>=<rem/add>"<br />
''Implemented in v1.19''<br />
<br />
When active, this restriction redirects whatever emote the user says on the public channel ("/0") to the private channel provided in the option field. If several redirections are issued, the emote will be redirected to each channel.<br />
<br />
<br />
* '''''Allow/prevent seeing emotes''''' : "@recvemote=<y/n>"<br />
''Implemented in v1.19''<br />
<br />
When prevented, every emote seen in public chat will be discarded.<br />
<br />
<br />
* '''''Allow/prevent receiving emotes seen in public chat from someone in particular''''' : "@recvemotefrom:<UUID>=<y/n>"<br />
''Implemented in v2.4''<br />
<br />
When prevented, everything emote seen in public chat from the specified avatar will be discarded.<br />
<br />
<br />
* '''''Allow/prevent seeing emotes, secure way''''' : "@recvemote_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, every emote seen in public chat will be discarded. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the emote seeing prevention''''' : "@recvemote:<UUID>=<rem/add>"<br />
''Implemented in v1.19''<br />
<br />
When adding an exception, the user can see emotes from the sender whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
====Private Channels====<br />
* '''''Allow/prevent using any chat channel but certain channels''''' : @sendchannel[:<channel>]=<y/n><br />
''Implemented in v1.10''<br />
<br />
Complimentary of @sendchat, this command prevents the user from sending messages on non-public Chat channels. If channel is specified, it becomes an exception to the aforementioned restriction (then it is better to use "rem" or "add" instead of "y" or "n" respectively). It does not prevent the viewer automatic replies like @version=nnnn, @getstatus=nnnn etc. <br />
<br />
<br />
* '''''Allow/prevent using any chat channel but certain channels, secure way''''' : @sendchannel_sec[:<channel>]=<y/n><br />
''Implemented in v1.10''<br />
<br />
Complimentary of @sendchat, this command prevents the user from sending messages on non-public channels. If channel is specified, it becomes an exception to the aforementioned restriction (then it is better to use "rem" or "add" instead of "y" or "n" respectively). It does not prevent the viewer automatic replies like @version=nnnn, @getstatus=nnnn etc. This particular command only accepts exceptions issued from the same object, opposed to its non-secure version which accepts exceptions from any other object.<br />
<br />
<br />
====Instant Messages====<br />
* '''''Allow/prevent sending instant messages''''' : "@sendim=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, everything typed in IM will be discarded and a bogus message will be sent to the receiver instead.<br />
<br />
<br />
* '''''Allow/prevent sending instant messages, secure way''''' : "@sendim_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, everything typed in IM will be discarded and a bogus message will be sent to the receiver instead. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the instant message sending prevention''''' : "@sendim:<UUID>=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding an exception, the user can send IMs to the receiver whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent sending instant messages to someone in particular''''' : "@sendimto:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, everything typed in IM to the specified avatar will be discarded and a bogus message will be sent instead.<br />
<br />
<br />
* '''''Allow/prevent starting an IM session with anyone''''' : "@startim=<y/n>"<br />
''Implemented in v2.6''<br />
<br />
When prevented, the user is unable to start an IM session with anyone. Sessions that are already open are not impacted though.<br />
<br />
<br />
* '''''Remove/add exceptions to the IM session start prevention''''' : "@startim:<UUID>=<rem/add>"<br />
''Implemented in v2.6''<br />
<br />
When adding an exception, the user can start an IM session with the receiver whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent starting an IM session with someone in particular''''' : "@startimto:<UUID>=<y/n>"<br />
''Implemented in v2.6''<br />
<br />
When prevented, the user is unable to start an IM session with that person. Sessions that are already open are not impacted though.<br />
<br />
<br />
* '''''Allow/prevent receiving instant messages''''' : "@recvim=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, every incoming IM will be discarded and the sender will be notified that the user cannot read them.<br />
<br />
<br />
* '''''Allow/prevent receiving instant messages, secure way''''' : "@recvim_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, every incoming IM will be discarded and the sender will be notified that the user cannot read them. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the instant message receiving prevention''''' : "@recvim:<UUID>=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding an exception, the user can read instant messages from the sender whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent receiving instant messages from someone in particular''''' : "@recvimfrom:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, every IM received from the the specified avatar will be discarded and the sender will be notified that the user cannot read them.<br />
<br />
===Teleportation===<br />
<br />
* '''''Allow/prevent teleporting to a landmark''''' : "@tplm=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When prevented, the user cannot use a [[landmark]], pick or any other preset location to [[teleport]] there.<br />
<br />
<br />
* '''''Allow/prevent teleporting to a location''''' : "@tploc=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When prevented, the user cannot use [[teleport]] to a coordinate by using the [[map]] and such.<br />
<br />
<br />
* '''''Allow/prevent teleporting by a friend''''' : "@tplure=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When prevented, the user automatically discards any [[teleport]] offer, and the avatar who initiated the offer is notified.<br />
<br />
<br />
* '''''Allow/prevent teleporting by a friend, secure way''''' : "@tplure_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, the user automatically discards any [[teleport]] offer, and the avatar who initiated the offer is notified. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the friend teleport prevention''''' : "@tplure:<UUID>=<rem/add>"<br />
''Implemented in v1.0''<br />
<br />
When adding an exception, the user can be teleported by the avatar whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Unlimit/limit sit-tp''''' : "@sittp=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When limited, the avatar cannot sit on a [[prim]] unless it is closer than 1.5 m. This allows cages to be secure, preventing the avatar from warping its position through the walls (unless the prim is too close).<br />
<br />
<br />
* '''''Allow/prevent standing up at a different location than where we sat down''''' : @standtp=<y/n><br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
When this restriction is active and the avatar stands up, it is automatically teleported back to the location where it initially sat down. Please note that the "last standing location" is also stored when the restriction is issued, so this won't be a problem for grabbers and the like, that sit the victim, then move them inside a cell, which issues its restrictions, and then unsits them. In this case the avatar will stay in the cell.<br />
<br />
<br />
* '''''Force-Teleport the user''''' : @tpto:<X>/<Y>/<Z>=force (*)<br />
''Implemented in v1.12''<br />
<br />
This command forces the avatar to teleport to the indicated coordinates. Note that these coordinates are always '''global''', hence the script that calls this command will not be trivial. Moreso, if the destination contains a telehub or a landing point, the user will land there instead of the desired point. This is a SL limitation. Also keep in mind that @tpto is inhibited by @tploc=n, and from v1.15 and above, by @unsit too.<br />
<br />
Here is a sample code to call that command properly :<br />
<br />
<lsl><br />
<br />
// FORCE TELEPORT EXAMPLE<br />
// Listens on channel 4 for local coordinates and a sim name<br />
// and tells your viewer to teleport you there.<br />
//<br />
// By Marine Kelley 2008-08-26<br />
// RLV version required : 1.12 and above<br />
//<br />
// HOW TO USE :<br />
// * Create a script inside a box<br />
// * Overwrite the contents of the script with this one<br />
// * Wear the box<br />
// * Say the destination coords Region/X/Y/Z on channel 4 :<br />
// Example : /4 Help Island Public/128/128/50<br />
<br />
key kRequestHandle; // UUID of the dataserver request<br />
vector vLocalPos; // local position extracted from the<br />
<br />
Init () {<br />
kRequestHandle = NULL_KEY;<br />
llListen (4, "", llGetOwner (), "");<br />
}<br />
<br />
<br />
default<br />
{<br />
state_entry () {<br />
Init ();<br />
}<br />
<br />
on_rez(integer start_param) {<br />
Init ();<br />
}<br />
<br />
listen(integer channel, string name, key id, string message) {<br />
list tokens = llParseString2List (message, ["/"], []);<br />
integer L = llGetListLength (tokens);<br />
<br />
if (L==4) {<br />
// Extract local X, Y and Z<br />
vLocalPos.x = llList2Float (tokens, 1);<br />
vLocalPos.y = llList2Float (tokens, 2);<br />
vLocalPos.z = llList2Float (tokens, 3);<br />
<br />
// Request info about the sim<br />
kRequestHandle=llRequestSimulatorData (llList2String (tokens, 0), DATA_SIM_POS);<br />
}<br />
}<br />
<br />
dataserver(key queryid, string data) {<br />
if (queryid == kRequestHandle) {<br />
// Parse the dataserver response (it is a vector cast to a string)<br />
list tokens = llParseString2List (data, ["<", ",", ">"], []);<br />
string pos_str = "";<br />
vector global_pos;<br />
<br />
// The coordinates given by the dataserver are the ones of the<br />
// South-West corner of this sim<br />
// => offset with the specified local coordinates<br />
global_pos.x = llList2Float (tokens, 0);<br />
global_pos.y = llList2Float (tokens, 1);<br />
global_pos.z = llList2Float (tokens, 2);<br />
global_pos += vLocalPos;<br />
<br />
// Build the command<br />
pos_str = (string)((integer)global_pos.x)<br />
+"/"+(string)((integer)global_pos.y)<br />
+"/"+(string)((integer)global_pos.z);<br />
llOwnerSay ("Global position : "+(string)pos_str); // Debug purposes<br />
<br />
// Fire !<br />
llOwnerSay ("@tpto:"+pos_str+"=force");<br />
}<br />
}<br />
<br />
}<br />
<br />
</lsl><br />
<br />
<br />
<br />
* '''''Remove/add auto-accept teleport offers from a particular avatar''''' : "@accepttp[:<UUID>]=<rem/add>"<br />
''Implemented in v1.15, slightly improved in v1.16''<br />
<br />
Adding this rule will make the user automatically accept any teleport offer from the avatar which key is <UUID>, exactly like if that avatar was a Linden (no confirmation box, no message, no Cancel button). This rule does not supercede nor deprecate @tpto because the former teleports to someone, while the latter teleports to an arbitrary location. Attention : in v1.16 the UUID becomes optional, which means that @accepttp=add will force the user to accept teleport offers from anyone ! Use with caution !<br />
<br />
===Inventory, Editing and Rezzing===<br />
<br />
* '''''Allow/prevent using inventory''''' : @showinv=<y/n><br />
''Implemented in v1.10''<br />
<br />
Forces the [[inventory]] windows to close and stay closed.<br />
<br />
<br />
* '''''Allow/prevent reading notecards''''' : @viewnote=<y/n><br />
''Implemented in v1.10''<br />
<br />
Prevents from opening [[notecards]] but does not close the ones already open.<br />
<br />
<br />
* '''''Allow/prevent opening scripts''''' : @viewscript=<y/n><br />
''Implemented in v1.22''<br />
<br />
Prevents from opening [[scripts]] but does not close the ones already open.<br />
<br />
<br />
* '''''Allow/prevent opening textures''''' : @viewtexture=<y/n><br />
''Implemented in v1.22''<br />
<br />
Prevents from opening [[textures]] (and snapshots) but does not close the ones already open.<br />
<br />
<br />
* '''''Allow/prevent editing objects''''' : "@edit=<y/n>"<br />
''Implemented in v1.03''<br />
<br />
When prevented from editing and opening objects, the Build & Edit window will refuse to open.<br />
<br />
<br />
* '''''Remove/add exceptions to the edit prevention''''' : "@edit:<UUID>=<rem/add>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When adding an exception, the user can edit or open this object in particular.<br />
<br />
<br />
* '''''Allow/prevent rezzing inventory''''' : "@rez=<y/n>"<br />
''Implemented in v1.03''<br />
<br />
When prevented from [[rez]]zing stuff, creating and deleting objects, drag-dropping from inventory and dropping attachments will fail.<br />
<br />
<br />
* '''''Allow/prevent editing particular objects''''' : "@editobj:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, the Build & Edit window will refuse to open when trying to edit or open the specified object.<br />
<br />
===Sitting===<br />
<br />
* '''''Allow/prevent standing up''''' : @unsit=<y/n><br />
''Implemented in v1.10, modified in v1.15 to prevent teleporting as well''<br />
<br />
Hides the Stand up button. From v1.15 it also prevents teleporting, which was a way to stand up.<br />
<br />
<br />
* '''''Force sit on an object''''' : @sit:<UUID>=force (*)<br />
''Implemented in v1.10''<br />
<br />
Does not work if the user is prevented from sit-tping and further than 1.5 meters away, or when prevented from unsitting.<br />
<br />
<br />
* '''''Get the UUID of the object the avatar is sitting on''''' : @getsitid=<channel_number><br />
''Implemented in v1.12.4 but broken in all versions older than v1.24 and v2.2 (was reporting the UUID of the last object any avatar within draw distance sat upon)''<br />
<br />
Makes the viewer automatically answer the UUID of the object the avatar is currently sitting on, or NULL_KEY if they are not sitting.<br />
<br />
<br />
* '''''Force unsit''''' : @unsit=force (*)<br />
''Implemented in v1.10''<br />
<br />
Self-explanatory but for some reason it randomly fails, so don't rely on it for now. Further testing is needed.<br />
<br />
<br />
* '''''Allow/prevent sitting down''''' : @sit=<y/n><br />
''Implemented in v1.16.2''<br />
<br />
Prevents the user from sitting on anything, including with @sit:<UUID>=force.<br />
<br />
===Clothing and Attachments===<br />
<br />
* '''''Render an object detachable/nondetachable''''' : "@detach=<y/n>"<br />
''Implemented in v1.0a''<br />
<br />
When called with the "n" option, the object sending this message (which must be an attachment) will be made nondetachable. It can be detached again when the "y" option is called.<br />
<br />
<br />
* '''''Unlock/Lock an attachment point''''' : "@detach:<attach_point_name>=<y/n>"<br />
''Implemented in v1.20''<br />
<br />
When called with the "n" option, the attachment point of name <attach_point_name> will be locked either full (if it is occupied by an object at that time) or empty (if not). Any object that is occupying this point when the restriction is issued will be considered as undetachable, exactly like if it had issued a "@detach=n" command itself. If the point is empty it will stay that way, no item will be able to be attached there, and llAttachToAvatar() calls will fail (the object will be attached, then detached right away).<br />
<br />
<br />
* '''''Unlock/Lock an attachment point empty''''' : "@addattach[:<attach_point_name>]=<y/n>"<br />
''Implemented in v1.22''<br />
<br />
When called with the "n" option, the attachment point of name <attach_point_name> will be locked empty. Any object that is occupying this point when the restriction is issued can be detached, but nothing can be attached there. If the point is empty it will stay that way, no item will be able to be attached there, and llAttachToAvatar() calls will fail (the object will be attached, then detached right away). If <attach_point_name> is not specified, then all the attachment points will be concerned. This command is the counterpart to @addoutfit, for attachments.<br />
<br />
<br />
* '''''Unlock/Lock an attachment point full''''' : "@remattach[:<attach_point_name>]=<y/n>"<br />
''Implemented in v1.22''<br />
<br />
When called with the "n" option, the attachment point of name <attach_point_name> will be locked full. Any object that is occupying this point when the restriction is issued will be rendered undetachable. If the point is empty it will allow the user to wear something, but then that object will become undetachable too, no item will be able to replace it, and llAttachToAvatar() calls will fail (the object will be attached, then detached right away). If <attach_point_name> is not specified, then all the attachment points will be concerned. This command is the counterpart to @remoutfit, for attachments.<br />
<br />
<br />
* '''''Allow/deny the "Wear" contextual menu''''' : "@defaultwear=<y/n><br />
''Implemented in v1.21''<br />
<br />
When allowed, the user is always able to choose the "Wear"command on the contextual menu of the inventory, even when an object is locked on their avatar. This holds the risk of kicking that locked object, but it will be reattached automatically within 5 seconds (and successive locked objects every second until there is nothing left to reattach). However some objects may be scripted in a way that they drop their restrictions when detached, or simply not take into account the fact that even a locked object can be detached when using the RLV.<br />
<br />
Therefore, using this command with the "n" option will suppress this comman, but it will still be available for objects that contain the target attachment point in their name or in the name of their parent folder, exactly like pre-1.21 RLV. This is a little less user-friendly but more secure when it comes to make sure no locked object may be detached accidentally.<br />
<br />
<br />
* '''''Force removing attachments''''' : @detach[:attachpt]=force (*) <br />
''Implemented in v1.10''<br />
<br />
Where part is :<br />
chest|skull|left shoulder|right shoulder|left hand|right hand|left foot|right foot|spine|<br />
pelvis|mouth|chin|left ear|right ear|left eyeball|right eyeball|nose|r upper arm|r forearm|<br />
l upper arm|l forearm|right hip|r upper leg|r lower leg|left hip|l upper leg|l lower leg|stomach|left pec|<br />
right pec|center 2|top right|top|top left|center|bottom left|bottom|bottom right|neck|root<br />
If part is not specified, removes everything.<br />
<br />
<br />
* '''''Force removing attachments (alias)''''' : @remattach[:attachpt]=force (*) <br />
''Implemented in v1.22''<br />
<br />
This command is an alias to @detach[:attachpt]=force (to keep things consistent).<br />
<br />
<br />
* '''''Allow/prevent wearing clothes''''' : @addoutfit[:<part>]=<y/n><br />
''Implemented in v1.10, added skin hair and eyes in v1.10.1, added physics in 2.6.1''<br />
<br />
Where part is :<br />
gloves|jacket|pants|shirt|shoes|skirt|socks|underpants|undershirt|skin|eyes|hair|shape|alpha|tattoo|physics<br />
If part is not specified, prevents from wearing anything beyond what the avatar is already wearing.<br />
<br />
'''Note:''' Since the release of Viewer 2.0 there are two new avatar skin layers: Tattoo and Avatar Transparency Mask. The alpha and tattoo layers will only be supported by RLV compliant viewers that implement the new Viewer 2.0 features.<br />
<br />
<br />
* '''''Allow/prevent removing clothes''''' : @remoutfit[:<part>]=<y/n> (underpants and undershirt are kept for teens)<br />
''Implemented in v1.10, added skin hair and eyes in v1.10.1, added physics in 2.6.1''<br />
<br />
Where part is :<br />
gloves|jacket|pants|shirt|shoes|skirt|socks|underpants|undershirt|skin|eyes|hair|shape|alpha|tattoo|physics<br />
If part is not specified, prevents from removing anything in what the avatar is wearing.<br />
<br />
'''Note:''' Since the release of Viewer 2.0 there are two new avatar skin layers: Tattoo and Avatar Transparency Mask. The alpha and tattoo layers will only be supported by RLV compliant viewers that implement the new Viewer 2.0 features.<br />
<br />
<br />
* '''''Force removing clothes''''' : @remoutfit[:<part>]=force (*) (teens can't be forced to remove underpants and undershirt)<br />
''Implemented in v1.10''<br />
<br />
Where part is :<br />
gloves|jacket|pants|shirt|shoes|skirt|socks|underpants|undershirt|alpha|tattoo|physics<br />
If part is not specified, removes everything.<br />
<br />
'''Note:''' Since the release of Viewer 2.0 there are two new avatar skin layers: Tattoo and Avatar Transparency Mask. The alpha and tattoo layers will only be supported by RLV compliant viewers that implement the new Viewer 2.0 features.<br />
<br />
'''Note:''' skin, shape, eyes and hair cannot be removed since they are body parts (and removing any would result in an unrezzed avatar).<br />
<br />
<br />
* '''''Get the list of worn clothes''''' : @getoutfit[:part]=<channel_number><br />
''Implemented in v1.10, added skin hair and eyes in v1.10.1, added physics in 2.6.1''<br />
<br />
Makes the viewer automatically answer the current occupation of clothes layers as a list of 0s (empty) and 1s (occupied) immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The list of 0s and 1s corresponds to :<br />
gloves,jacket,pants,shirt,shoes,skirt,socks,underpants,undershirt,skin,eyes,hair,shape<br />
in that order.<br />
<br />
If a part is specified, answers a single 0 (empty) or 1 (occupied) corresponding to the part.<br />
Ex 1 : @getoutfit=2222 => "0011000111" => avatar is wearing pants, shirt, underpants and undershirt, and of course a skin.<br />
Ex 2 : @getoutfit:socks=2222 => "0" => the avatar is not wearing socks.<br />
<br />
'''Note:''' For viewers that implement the new Viewer 2.0 features, the list is:<br />
<br />
gloves,jacket,pants,shirt,shoes,skirt,socks,underpants,undershirt,skin,eyes,hair,shape,alpha,tattoo<br />
<br />
<br />
* '''''Get the list of worn attachments''''' : @getattach[:attachpt]=<channel_number><br />
''Implemented in v1.10''<br />
<br />
Makes the viewer automatically answer the current occupation of attachment points as a list of 0s (empty) and 1s (occupied) immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The list of 0s and 1s corresponds to :<br />
none,chest,skull,left shoulder,right shoulder,left hand,right hand,left foot,right foot,spine,<br />
pelvis,mouth,chin,left ear,right ear,left eyeball,right eyeball,nose,r upper arm,r forearm,<br />
l upper arm,l forearm,right hip,r upper leg,r lower leg,left hip,l upper leg,l lower leg,stomach,left pec,<br />
right pec,center 2,top right,top,top left,center,bottom left,bottom,bottom right,neck,root<br />
in that order.<br />
<br />
If an attachment point is specified, answers a single 0 (empty) or 1 (occupied) corresponding to the point.<br />
Ex 1 : @getattach=2222 => "011000011010000000000000100100000000101" => avatar is wearing attachments on <br />
chest, skull, left and right foot, pelvis, l and r lower leg, HUD bottom left and HUD bottom right.<br />
Ex 2 : @getattach:chest=2222 => "1" => avatar is wearing something on the chest.<br />
<br />
''Note'' : The first character ("none") is always '0', so the index of each attach point in the string is '''exactly equal''' to the corresponding ATTACH_* macro in LSL. For instance, the index 9 in the string is ATTACH_BACK (which means "spine"). Remember the indices start at zero.<br />
<br />
<br />
* '''''Force the viewer to automatically accept attach and take control permission requests''''' : @acceptpermission=<rem/add><br />
''Implemented in v1.16''<br />
<br />
Forces the avatar to automatically accept attach and take control permission requests. The dialog box doesn't even show up. This command does not supercede @denypermission, of course.<br />
<br />
<br />
* '''''Allow/prevent accepting attach and take control permissions''''' : @denypermission=<rem/add><br />
''Implemented in v1.16, DEPRECATED in v1.16.2''<br />
<br />
When prevented, all attach and take control permission requests are automatically declined, without even showing the dialog box. Due to the extreme annoyance it was making, and because locked objects automatically reattach themselves since v1.16.1, this command is NOW DEPRECATED, DON'T USE IT !<br />
<br />
<br />
* '''''Force detach an item''''' : @detachme=force (*)<br />
''Implemented in v1.16.2''<br />
<br />
This command forces the object that issues it to detach itself from the avatar. It is there as a convenience to avoid a race condition when calling @clear then llDetachFromAvatar(), sometimes the object could detach itself before clearing its restrictions, making it reattach automatically after a while. With this command one can issue a @clear,detachme=force to be sure @clear is executed first.<br />
<br />
===Clothing and Attachments (Shared Folders)===<br />
<br />
* '''''Allow/prevent wearing clothes and attachments that are not part of the #RLV folder''''' : @unsharedwear=<y/n><br />
''Implemented in v2.5''<br />
<br />
When prevented, no object, piece of clothing or bodypart can be worn unless it is part of the #RLV folder (i.e. "shared").<br />
<br />
<br />
* '''''Allow/prevent removing clothes and attachments that are not part of the #RLV folder''''' : @unsharedunwear=<y/n><br />
''Implemented in v2.5''<br />
<br />
When prevented, no object, piece of clothing or bodypart can be removed from the avatar unless it is part of the #RLV folder (i.e. "shared").<br />
<br />
<br />
* '''''Get the list of shared folders in the avatar's inventory''''' : @getinv[:folder1/.../folderN]=<channel_number><br />
''Implemented in v1.11, added sub-folders in v1.13''<br />
<br />
Makes the viewer automatically answer the list of folders contained into the folder named "#RLV" (if it exists), immediately on the chat channel number <channel_number> that the script can listen to. If folders are specified, it will give the list of sub-folders contained into the folder located at that path instead of the shared root (example : "@getinv:Restraints/Leather cuffs/Arms=2222"). Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The answer is a list of names, separated by commas (","). Folders which names begin with a dot (".") will be ignored.<br />
<br />
<br />
* '''''Get the list of shared folders in the avatar's inventory, with information about worn items''''' : @getinvworn[:folder1/.../folderN]=<channel_number><br />
''Implemented in v1.15''<br />
<br />
Makes the viewer automatically answer the list of folders contained into the folder named "#RLV" (if it exists), immediately on the chat channel number <channel_number> that the script can listen to. If folders are specified, it will give the list of sub-folders contained into the folder located at that path instead of the shared root (example : "@getinvworn:Restraints/Leather cuffs/Arms=2222"). Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The answer is a comma-separated list of names, each one followed with a pipe ("|") and two digits. The current folder is put in first position (as opposed to @getinv which does not show the current folder, obviously), but without a name, only the pipe and the two digits.<br />
<br />
Object : "@getinvworn:Restraints/Leather cuffs=2222"<br />
Viewer : "|02,Arms|30,Legs|10"<br />
<br />
Folders which names begin with a dot (".") will be ignored. The two digits are calculated as follows :<br />
<br />
First digit : Proportion of items worn in the corresponding folder (including no-mod items). In this example, the "3" of "30" means "all the items in the "Arms" folder are currently worn, while the "1" of "10" means "no item in the Legs folder is currently worn, but there are items to wear".<br />
<br />
Second digit : Proportion of items globally worn in all the folders contained inside the corresponding folder. In this example, the "2" of "02" means "some items are worn in some of the folders contained into "Leather cuffs".<br />
<br />
The digits, comprised between 0 and 3 included, have the following meaning :<br />
<br />
:* 0 : No item is present in that folder<br />
:* 1 : Some items are present in that folder, but none of them is worn<br />
:* 2 : Some items are present in that folder, and some of them are worn<br />
:* 3 : Some items are present in that folder, and all of them are worn<br />
<br />
<br />
* '''''Get the path to a shared folder by giving a search criterion''''' : @findfolder:part1[&&...&&partN]=<channel_number><br />
''Implemented in v1.13.1''<br />
<br />
Makes the viewer automatically answer the path to the first shared folder which name contains <part1> and <part2> and ... and <partN>, immediately on the chat channel number <channel_number> that the script can listen to. The search is in depth first, notice the separator which is "&&" like "and". Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. It does not take disabled folders into account (folders which name begins with a dot "."), nor folders which name begins with a tilde ("~"). The answer is a list of folders, separated by slashes ('/').<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attach:<folder1/.../folderN>=force (*)<br />
''Implemented in v1.11, added no-mod items in v1.12, added sub-folders in v1.13''<br />
<br />
Forces the viewer to attach every object and wear every piece of clothing contained inside the folder located at the specified path (which must be under "#RLV"). Objects names '''must''' contain the name of their target attachment point or they won't be attached. Each no-modify object '''must''' be contained inside a folder (one object per folder), which name contains the name of its target attachment point since it can't be renamed. Names cannot begin with a dot (".") since such folders are invisible to the scripts.<br />
<br />
Attachment point names are the same as the ones contained into the "Attach To" submenu : "skull", "chest", "l forearm"...<br />
<br />
Note 1 : Folder names '''can''' contain slashes, and will be chosen in priority when able (for instance, if "@attach:Restraints/cuffs=force" is issued, the "Restraints/cuffs" folder will be chosen before a "cuffs" folder contained inside a "Restraints" parent folder.<br />
<br />
Note 2 : If the name of a folder begins with a plus sign ("+"), then this command will act exactly like @attachover. This rule can be changed through the use of the "RestrainedLoveStackWhenFolderBeginsWith" debug setting.<br />
<br />
'''Attention''' : This command will likely change in the future, to revert back to how it used to behave in version 1.x, i.e. never add an object if the target attachment point is already taken, but rather replace the old object. The current behaviour is intended to be ensured by @attachoverorreplace and its derivatives. For now @attachoverorreplace is a synonym to @attach, but this won't always be the case. In other words, if you intend to make your script always replace existing attachments when attaching new ones, use @attach. If you want your script to always make attachments stack, use @attachover. If you want to give the user the choice through the name of the folder (as indicated above, by prepending the name by a "+" sign by default), use @attachoverorreplace.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, without replacing what is already being worn''''' : @attachover:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attach described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attachoverorreplace:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attach described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, and its children recursively''''' : @attachall:<folder1/.../folderN>=force (*)<br />
''Implemented in v1.15''<br />
<br />
This command works exactly like @attach described hereabove, but also attaches whatever is contained into children folders.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, and its children recursively, without replacing what is already being worn''''' : @attachallover:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attachall described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, and its children recursively''''' : @attachalloverorreplace:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attachall described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force detach items contained inside a shared folder''''' : @detach:<folder_name>=force (*)<br />
''Implemented in v1.11''<br />
<br />
Forces the viewer to detach every object and unwear every piece of clothing contained inside <folder_name>(which must be directly under "#RLV"). If "@detach" is used with an attachment point name (skull, pelvis... see above), it takes priority over this way of detaching since it is the same command.<br />
<br />
<br />
* '''''Force detach items contained inside a shared folder, and its children recursively''''' : @detachall:<folder1/.../folderN>=force (*)<br />
''Implemented in v1.15''<br />
<br />
This command works exactly like @detach described hereabove, but also detaches whatever is contained into children folders.<br />
<br />
<br />
* '''''Get the path to the shared folder containing a particular object/clothing worn on a point''''' : @getpath[:<attachpt> or <clothing_layer>]=<channel_number><br />
''Implemented in v1.16''<br />
<br />
Makes the viewer automatically answer the path to the shared folder containing the item that :<br />
:* issues this command if no option is set<br />
:* is attached on the attach point provided in the option field, ex : @getpath:spine=2222 => "Restraints/Collar"<br />
:* is worn on the clothing layer provided in the option field, ex : @getpath:pants=2222 => "Casual/Jeans/Tight"<br />
Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. It does not take disabled folders into account (folders which name begins with a dot "."). The answer is a list of folders, separated by slashes ('/').<br />
<br />
Please note : As version 1.40.4 is now live on the main grid, wearing several objects on the same attachment point is now possible. Therefore this command does not make much sense anymore since it can only respond with one folder, while the several objects could belong to several folders. Therefore it is better to use @getpathnew, since @getpath will slowly become deprecated as more and more users switch to 2.1 and beyond.<br />
<br />
<br />
* '''''Get the all paths to the shared folders containing the objects/clothing worn on a point''''' : @getpathnew[:<attachpt> or <clothing_layer>]=<channel_number><br />
''Implemented in v2.1 and v1.24''<br />
<br />
Makes the viewer automatically answer the paths to the shared folders containing the item(s) that :<br />
:* issues this command if no option is set<br />
:* are attached on the attach point provided in the option field, ex : @getpathnew:spine=2222 => "Restraints/Collar,Jewelry/Cute necklace"<br />
:* is worn on the clothing layer provided in the option field, ex : @getpathnew:pants=2222 => "Casual/Jeans/Tight"<br />
Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. It does not take disabled folders into account (folders which name begins with a dot "."). The answer is a list of folders, separated by slashes ('/'), if several paths must be returned because several outfits are concerned, they are organized in a list of strings separated by commas (',').<br />
<br />
This command has been added to replace @getpath, since in 2.1 several objects can be worn on the same attachment point.<br />
<br />
<br />
* '''''Force attach items contained into a shared folder that contains a particular object/clothing''''' : @attachthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with an @attach command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, without replacing what is already being worn''''' : @attachthisover[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attachthis described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attachthisoverorreplace[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attachthis described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force attach items contained into a shared folder that contains a particular object/clothing, and its children folders''''' : @attachallthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with an @attachall command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, without replacing what is already being worn''''' : @attachallthisover[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attachallthis described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attachallthisoverorreplace[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attachallthis described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force detach items contained into a shared folder that contains a particular object/clothing''''' : @detachthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with a @detach command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Force detach items contained into a shared folder that contains a particular object/clothing, and its children folders''''' : @detachallthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with a @detachall command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Allow/prevent removing some folders''''' : @detachthis[:<layer>|<attachpt>|<path_to_folder>]=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, the user is unable to remove a folder if either of these conditions is filled :<br />
:* no option is specified and the folder contains the object that issues this restriction<br />
:* the "layer" option is set (shirt, pants...) and the folder contains a piece of clothing that is worn on this layer<br />
:* the "attachpt" option is set (l forearm, spine...) and the folder contains an attachment that is worn on this point<br />
:* the "path_to_folder" option is set and the folder corresponds to this location<br />
<br />
Moreso, this folder or these folders cannot be renamed, moved, deleted or modified.<br />
<br />
<br />
* '''''Allow/prevent removing some folders and their children''''' : @detachallthis[:<layer>|<attachpt>|<path_to_folder>]=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
These commands do exactly like @detachthis, but also apply to their children folders recursively.<br />
<br />
<br />
* '''''Allow/prevent wearing some folders''''' : @attachthis:<layer>|<attachpt>|<path_to_folder>=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, the user is unable to attach a folder if either of these conditions is filled :<br />
:* the "layer" option is set (shirt, pants...) and the folder contains a piece of clothing that is meant to be worn on this layer<br />
:* the "attachpt" option is set (l forearm, spine...) and the folder contains an attachment that is meant to be worn on this point<br />
:* the "path_to_folder" option is set and the folder corresponds to this location<br />
<br />
Moreso, this folder or these folders cannot be renamed, moved, deleted or modified.<br />
<br />
<br />
* '''''Allow/prevent wearing some folders and their children''''' : @attachallthis[:<layer>|<attachpt>|<path_to_folder>]=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
These commands do exactly like @attachthis, but also apply to their children folders recursively.<br />
<br />
<br />
* '''''Remove/add exceptions to the detachallthis restriction, for one folder only''''' : "@detachthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can remove the items contained into the indicated folder.<br />
<br />
<br />
* '''''Remove/add exceptions to the detachallthis restriction, for one folder and its children''''' : "@detachallthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can remove the items contained into the indicated folder, or in any of its children.<br />
<br />
<br />
* '''''Remove/add exceptions to the attachallthis restriction, for one folder only''''' : "@attachthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can wear the items contained into the indicated folder.<br />
<br />
<br />
* '''''Remove/add exceptions to the attachallthis restriction, for one folder and its children''''' : "@attachallthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can wear the items contained into the indicated folder, or in any of its children.<br />
<br />
<br />
Note : These exceptions will be taken into account only for the restrictions that have been issued by the '''same object''', you cannot put such an exception to a restriction issued by another object.<br />
<br />
Note : The viewer checks which exception or restriction is the "closest parent" in the folders hierarchy to the folder that the user is trying to wear or remove. If the closest is an @attach[all]this_except or a @detach[all]this_except exception , then the folder can be worn or removed respectively. If the closest is an @attach[all]this or a @detach[all]this restriction, then the folder is locked, no matter how many exceptions are pointing on the folders that are parent to this one.<br />
<br />
Example :<br />
A script issues a @attachallthis:=n restriction, preventing the whole #RLV folder and its children from being attached. It also issues a<br />
@detachallthis:=n restriction, preventing the whole #RLV folder and its children from being removed as well.<br />
Therefore the #RLV folder is now completely frozen.<br />
<br />
However, the same object issues a @attachallthis:Jewelry/Gold=add exception, then a @detachallthis:Jewelry/Gold=add one, making the Jewelry/Gold<br />
folder available for wearing and removing.<br />
Finally, it issues a @attachallthis:Jewelry/Gold/Watch=n restriction followed by a @detachallthis:Jewelry/Gold/Watch=n restriction.<br />
As a result, the user can wear and remove only what is contained inside the Jewelry/Gold folder, except what is in Jewelry/Gold/Watch, and the<br />
rest is out of reach.<br />
<br />
===Touch===<br />
<br />
* '''''Allow/prevent touching objects located further than 1.5 meters away from the avatar''''' : @fartouch=<y/n><br />
''Implemented in v1.11''<br />
<br />
When prevented, the avatar is unable to touch/grab objects from more than 1.5 m away, this command makes restraints more realistic since the avatar litterally has to press against the object in order to click on it.<br />
<br />
<br />
* '''''Allow/prevent touching objects located further than 1.5 meters away from the avatar''''' : @touchfar=<y/n><br />
''Implemented in v2.4''<br />
<br />
This command is a synonym of @fartouch<br />
<br />
<br />
* '''''Allow/prevent touching any objects''''' : @touchall=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch/grab any object and attachment. This does not apply to HUDs.<br />
<br />
<br />
* '''''Allow/prevent touching objects in-world''''' : @touchworld=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch/grab objects rezzed in-world, i.e. not attachments and HUDs.<br />
<br />
<br />
* '''''Remove/add exceptions to the touchworld prevention''''' : "@touchworld:<UUID>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can touch this object in particular.<br />
<br />
<br />
* '''''Allow/prevent touching one object in particular''''' : @touchthis:<UUID>=<rem/add><br />
''Implemented in v2.5''<br />
<br />
When prevented, the avatar is unable to touch/grab the object which UUID corresponds to the one specified in the command.<br />
<br />
<br />
* '''''Remove/add an exception to the touch* preventions, for one object only''''' : "@touchme=<rem/add>"<br />
''Implemented in v2.6''<br />
<br />
When adding such an exception, the user can touch this object in particular.<br />
<br />
<br />
* '''''Allow/prevent touching attachments''''' : @touchattach=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch attachments (theirs and other avatars'), but this does not apply to HUDs.<br />
<br />
<br />
* '''''Allow/prevent touching one's attachments''''' : @touchattachself=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch their own attachments (theirs but can touch other people's), but this does not apply to HUDs.<br />
<br />
<br />
* '''''Allow/prevent touching other people's attachments''''' : @touchattachother=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch other people's attachments (but they can touch their owns). This does not apply to HUDs.<br />
<br />
===Location===<br />
<br />
* '''''Allow/prevent viewing the world map''''' : @showworldmap=<y/n><br />
''Implemented in v1.11''<br />
<br />
When prevented, the avatar is unable to view the world map, and it closes if it is open when the restriction becomes active.<br />
<br />
<br />
* '''''Allow/prevent viewing the mini map''''' : @showminimap=<y/n><br />
''Implemented in v1.11''<br />
<br />
When prevented, the avatar is unable to view the mini map, and it closes if it is open when the restriction becomes active.<br />
<br />
<br />
* '''''Allow/prevent knowing the current location''''' : @showloc=<y/n><br />
''Implemented in v1.12''<br />
<br />
When prevented, the user is unable to know where they are : the world map is hidden, the parcel and region name on the top menubar are hidden, they can't create landmarks, nor buy the land, nor see what land they have just left after a teleport, nor see the location in the About box, and even system and object messages are obfuscated if they contain the name of the region and/or the name of the parcel. However, [[llOwnerSay]] calls are ''not'' obfuscated so radars ''will'' still work (and RL commands as well).<br />
<br />
===Name Tags and Hovertext===<br />
<br />
* '''''Allow/prevent seeing the names of the people around''''' : @shownames=<y/n><br />
''Implemented in v1.12.2, added more dummy names in v1.16''<br />
<br />
When prevented, the user is unable to know who is around. The names don't show on the screen, the names on the chat are replaced by "dummy" names such as "Someone", "A resident", the tooltips are hidden, the pie menu is almost useless so the user can't get the profile directly etc.<br />
<br />
<br />
* '''''Allow/prevent seeing all the hovertexts''''' : @showhovertextall=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read any hovertext (2D text floating above some prims).<br />
<br />
<br />
* '''''Allow/prevent seeing one hovertext in particular''''' : @showhovertext:<UUID>=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read the hovertext floating above the prim which id is UUID. This is made that way so that the restriction can be issued on an object, by another one (unlike @detach which can only set this restriction on itself).<br />
<br />
<br />
* '''''Allow/prevent seeing the hovertexts on the HUD of the user''''' : @showhovertexthud=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read any hovertext showing over their HUD objects, but will be able to see the ones in-world.<br />
<br />
<br />
* '''''Allow/prevent seeing the hovertexts in-world''''' : @showhovertextworld=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read any hovertext showing over their in-world objects, but will be able to see the ones over their HUD.<br />
<br />
===Group===<br />
<br />
* '''''Force the agent to change the active group''''' : @setgroup:<group_name>=force<br />
''Implemented in v2.5''<br />
<br />
Forces the agent to change the active group, to the specified one. Of course, they must already be a member of this group. If <group_name> is "none", then the agent will deactivate the current group and not show any group tag at all.<br />
<br />
<br />
* '''''Allow/prevent activating a group''''' : @setgroup=<y/n><br />
''Implemented in v2.5''<br />
<br />
When prevented, the user is unable to change the active group.<br />
<br />
<br />
* '''''Get the name of the active group''''' : @getgroup=<channel_number><br />
''Implemented in v2.5''<br />
<br />
Makes the viewer automatically answer the name of the currently active group, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer will simply be "none" if no group is active at the time. Please note that there is no way to obtain the UUID of the group, only the name.<br />
<br />
<br />
===Viewer Control===<br />
<br />
* '''''Allow/prevent changing some debug settings''''' : @setdebug=<y/n><br />
''Implemented in v1.16''<br />
<br />
When prevented, the user is unable to change some viewer debug settings (Advanced > Debug Settings). As most debug settings are either useless or critical to the user's experience, a whitelist approach is taken : only a few debug settings are locked, the others are always available and untouched. At the time of this writing, the allowed debug settings are :<br />
:* AvatarSex (0 : Female, 1 : Male) : gender of the avatar at creation.<br />
:* RenderResolutionDivisor (1 -> ...) : "blurriness" of the screen. Combined to clever @setenv commands, can simulate nice effects. Note: renderresolutiondivisor is a Windlight only option (Basic Shaders must be enabled in graphics preferences) and as such, is not available in v1.19.0.5 or older viewers.<br />
<br />
* '''''Force change a debug setting''''' : @setdebug_<setting>:<value>=force (*)<br />
''Implemented in v1.16''<br />
<br />
Forces the viewer to change a particular debug setting and set it to <value>. This command is actually a package of many sub-commands, that are regrouped under "@setdebug_...", for instance "@setdebug_avatarsex:0=force", "@setdebug_renderresolutiondivisor:64=force" etc.<br />
<br />
See the list of allowed debug settings in the @setdebug command hereabove.<br />
<br />
<br />
* '''''Get the value of a debug setting''''' : @getdebug_<setting>=<channel_number><br />
''Implemented in v1.16''<br />
<br />
Makes the viewer automatically answer the value of a debug setting, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is the value that has been set with the <setting> part of the matching @setdebug command, or by hand.<br />
<br />
See the list of allowed debug settings in the @setdebug command hereabove.<br />
<br />
<br />
* '''''Allow/prevent changing the environment settings''''' : @setenv=<y/n><br />
''Implemented in v1.14''<br />
<br />
When prevented, the user is unable to change the viewer environment settings (World > Environment Settings > Sunrise/Midday/Sunset/Midnight/Revert to region default/Environment editor are all locked out).<br />
<br />
<br />
* '''''Force change an environment setting''''' : @setenv_<setting>:<value>=force (*)<br />
''Implemented in v1.14''<br />
<br />
Forces the viewer to change a particular environment setting (time of day or Windlight) and set it to <value>. This command is actually a package of many sub-commands, that are regrouped under "@setenv_...", for instance "@setenv_daytime:0.5=force", "@setenv_bluehorizonr:0.21=force" etc.<br />
<br />
This command (like any other "force" command) is silently discarded if the corresponding restriction has been set, here "@setenv", but in this case the restriction is ignored if the change is issued from the object that has created it. In other words, a collar can restrict environment changes, yet force a change by itself, while another object could not do it until the collar lifts the restriction.<br />
<br />
Although a range is specified for every value, no check is made in the viewer so a script can do what the UI can't do, for interesting effects. Use at your own risk, though. The ranges indicated here are merely the ones available on the sliders on the Environment Editor, for reference.<br />
<br />
Each particular sub-command works as follows (the names are chosen to be as close to the Windlight panels of the viewer as possible) :<br />
<br />
{| border="1" cellpadding="5"<br />
| '''@setenv_XXX:<value>=force where XXX is...''' || '''<value> range''' || '''Sets...'''<br />
|-<br />
| daytime || 0.0-1.0 and <0 || Time of day (sunrise:0.25, midday:0.567, sunset:0.75, midnight:0.0, set back to region default:<0). '''Attention, resets all other Windlight parameters'''<br />
|-<br />
| preset || String || A Preset environment, e.g. Gelatto, Foggy. '''Attention, loading a Preset is heavy on the viewer and can slow it down for a short while, don't do it every second'''<br />
|-<br />
| ambientr || 0.0-1.0 || Ambient light, Red channel<br />
|-<br />
| ambientg || 0.0-1.0 || Ambient light, Green channel<br />
|-<br />
| ambientb || 0.0-1.0 || Ambient light, Blue channel<br />
|-<br />
| ambienti || 0.0-1.0 || Ambient light, Intensity<br />
|-<br />
| bluedensityr || 0.0-1.0 || Blue Density, Red channel<br />
|-<br />
| bluedensityg || 0.0-1.0 || Blue Density, Green channel<br />
|-<br />
| bluedensityb || 0.0-1.0 || Blue Density, Blue channel<br />
|-<br />
| bluedensityi || 0.0-1.0 || Blue Density, Intensity<br />
|-<br />
| bluehorizonr || 0.0-1.0 || Blue Horizon, Red channel<br />
|-<br />
| bluehorizong || 0.0-1.0 || Blue Horizon, Green channel<br />
|-<br />
| bluehorizonb || 0.0-1.0 || Blue Horizon, Blue channel<br />
|-<br />
| bluehorizoni || 0.0-1.0 || Blue Horizon, Intensity<br />
|-<br />
| cloudcolorr || 0.0-1.0 || Cloud color, Red channel<br />
|-<br />
| cloudcolorg || 0.0-1.0 || Cloud color, Green channel<br />
|-<br />
| cloudcolorb || 0.0-1.0 || Cloud color, Blue channel<br />
|-<br />
| cloudcolori || 0.0-1.0 || Cloud color, Intensity<br />
|-<br />
| cloudcoverage || 0.0-1.0 || Cloud coverage<br />
|-<br />
| cloudx || 0.0-1.0 || Cloud offset X<br />
|-<br />
| cloudy || 0.0-1.0 || Cloud offset Y<br />
|-<br />
| cloudd || 0.0-1.0 || Cloud density<br />
|-<br />
| clouddetailx || 0.0-1.0 || Cloud detail X<br />
|-<br />
| clouddetaily || 0.0-1.0 || Cloud detail Y<br />
|-<br />
| clouddetaild || 0.0-1.0 || Cloud detail density<br />
|-<br />
| cloudscale || 0.0-1.0 || Cloud scale<br />
|-<br />
| cloudscrollx || 0.0-1.0 || Cloud scroll X<br />
|-<br />
| cloudscrolly || 0.0-1.0 || Cloud scroll Y<br />
|-<br />
| densitymultiplier || 0.0-0.9 || Density multiplier of the fog<br />
|-<br />
| distancemultiplier || 0.0-100.0 || Distance multiplier of the fog<br />
|-<br />
| eastangle || 0.0-1.0 || Position of the east, 0.0 is normal<br />
|-<br />
| hazedensity || 0.0-1.0 || Density of the haze<br />
|-<br />
| hazehorizon || 0.0-1.0 || Haze at the horizon<br />
|-<br />
| maxaltitude || 0.0-4000.0 || Maximum altitude of the fog<br />
|-<br />
| scenegamma || 0.0-10.0 || Overall gamma, 1.0 is normal<br />
|-<br />
| starbrightness|| 0.0-2.0 || Brightness of the stars<br />
|-<br />
| sunglowfocus || 0.0-0.5 || Focus of the glow of the sun<br />
|-<br />
| sunglowsize || 1.0-2.0 || Size of the glow of the sun<br />
|-<br />
| sunmooncolorr || 0.0-1.0 || Sun and moon, Red channel<br />
|-<br />
| sunmooncolorg || 0.0-1.0 || Sun and moon, Green channel<br />
|-<br />
| sunmooncolorb || 0.0-1.0 || Sun and moon, Blue channel<br />
|-<br />
| sunmooncolori || 0.0-1.0 || Sun and moon, Intensity<br />
|-<br />
| sunmoonposition || 0.0-1.0 || Position of the sun/moon, different from "daytime", '''use this to set the apparent sunlight after loading a Preset'''<br />
|}<br />
<br />
Note: from the above settings, only the "daytime" one is supported by v1.19.0 (or older) viewers implementing RestrainedLove v1.14 and later. The other settings are ignored. This is because these viewers do not implement the Windlight renderer.<br />
<br />
* '''''Get the value of an environment setting''''' : @getenv_<setting>=<channel_number><br />
''Implemented in v1.15''<br />
<br />
Makes the viewer automatically answer the value of an environment setting, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is the value that has been set with the <setting> part of the matching @setenv command, or by hand. See the table hereabove for a list of settings.<br /><br />
Note: only @getenv_daytime is supported by v1.19.0 (or older, i.e. non Windlight) viewers implementing RestrainedLove v1.15 and later.<br />
<br />
===Unofficial Commands===<br />
Certain viewers use a different restriction system, based on the RestrainedLove API, which includes a number of extra commands. These extra commands are '''not''' part of the RestrainedLove API specification, but are documented here for convenience. These commands should not be relied on, as only certain viewers will be able to process them.<br />
<br />
* '''''Allow/prevent touching HUDs''''' : @touchhud[:<UUID>]=<y/n><br />
<br />
When prevented, the avatar is unable to touch any HUDs. If sent with a UUID, the avatar is prevented from touching only the HUD indicated by the UUID.<br />
<br />
* '''''Allow/prevent touching objects or attachments, editing, or rezzing''''' : @interact=<y/n><br />
<br />
When prevented, the avatar is unable to touch any objects, attachments, or HUDs, cannot edit or rez, and cannot sit on objects.<br />
<br />
* '''''Allow/prevent the disabling of the automatic Away/AFK indicator''''' : @allowidle=<y/n><br />
<br />
When prevented, the automatic activation of the Away status indicator after a period of avatar inactivity cannot be disabled. If the idle timeout duration has been set to zero, the default timeout of 30 minutes will be used.<br />
<br />
===Footnotes===<br />
<br />
(*) Silently discarded if the user is prevented from doing so by the corresponding restriction. This is on purpose.<br />
Ex : Force detach won't work if the object is undetachable. Force undress won't work if the user is prevented from undressing.<br />
<br />
==Important note about the global behaviours such as sendchat==<br />
Such behaviours are global, which means they don't depend on a particular object. However, they are triggered by objects with a set [[UUID]] which can change, and several objects can add the same behaviour, which will be stored several times as the [[UUID]]s are different. <br />
<br />
This has a nice side effect : when wearing 2 locked devices that prevent chat, it is necessary to unlock them both to be able to chat again. But it has a nasty side effect, too : if the item changes [[UUID]] (for instance it was derezzed and rezzed again), and it doesn't allow chat beforehand, then the user will have to wait a short moment because the rule stays "orphaned" (its [[UUID]] is defunct) until the '''garbage collector''' kicks in.<br />
<br />
Please note : Since 1.16.1 any locked object that is kicked off by any mean (llAttachToAvatar for example) will be reattached automatically by the viewer after a few seconds. This means that calling @clear on detaching will actually unlock the object, which will have to be relocked after being reattached. It is therefore not recommended anymore to call @clear on detach, as opposed to pre-1.16.1 versions.<br />
<br />
==Shared Folders==<br />
<br />
Since v1.11, the viewer can "share" some of your items with scripts in world in order to let them force you to attach, detach and list what you have shared.<br />
<br />
"Share" does NOT mean they will be taken by other people if they want to (some of the items may be no-transfer anyway), but only that they can force YOU to wear/unwear them at will through the use of a script YOUR restraints contain. They will remain in your inventory. In fact, this feature would be best named "Exposed folder".<br />
<br />
To do this :<br />
* Create a folder named "#RLV" (without the quotes) directly under "My Inventory" (right-click on "My Inventory", select "New Folder"). We'll call this folder the "shared root".<br />
* Move a folder containing restraints or other attachments directly into this new folder.<br />
* Wear the contents of that folder, that's it !<br />
<br />
So it would look like this :<br />
<br />
My Inventory<br />
|- #RLV<br />
| |- cuffs<br />
| | |- left cuff (l forearm) (no copy)<br />
| | \- right cuff (r forearm) (no copy)<br />
| \- gag<br />
| \- gag (mouth) (no copy)<br />
|- Animations<br />
|- Body Parts<br />
.<br />
.<br />
.<br />
<br />
For example : If you're owning a set of RR Straps and want to share them, just move the folder "Straps BOXED" under the shared root.<br />
<br />
Either wear all the items of the folders you have just moved (one folder at a time !) or rename your items yourself, so that each item name contains the name of the target attachment point. For example : "left cuff (l forearm)", "right ankle cuff (r lower leg)". Please note that no-modify items are a bit more complex to share, because they cannot be renamed either by you or by the viewer. More on that below.<br />
<br />
The attachment point name is the same as the one you find in the "Attach To" menu of your inventory, and is case insensitive (for example : "chest", "skull", "stomach", "left ear", "r upper arm"...). If you wear the item without renaming it first it will be renamed automatically, but only if it is in a shared folder, and does not contain any attachment point name already, and is mod. If you want to wear it on another attachment point, you'll need to rename it by hand first.<br />
<br />
Pieces of clothing are treated exactly the same way (in fact they can even be put in the folder of a set of restraints and be worn with the same command). Shoes, for instance, are a good example of mixed outfits : some attachments and the Shoes layer. Clothes are NOT renamed automatically when worn, since their very type decides where they are to be worn (skirt, jacket, undershirt...).<br />
<br />
HOW TO SHARE NO-MODIFY ITEMS :<br />
As you already know, no-mod items cannot be renamed so the technique is a bit more complex. Create a sub-folder inside the outfit folder (such as "cuffs" in the example above), put ONE no-modify item in it. When wearing the object, you'll see the folder itself be renamed (that's why you must not put more than one object inside it). So if your outfit contains several no-mod objects, you'll need to create as many folders and put the no-mod objects in them, one in each folder.<br />
<br />
Example with no-modify shoes :<br />
<br />
My Inventory<br />
|- #RLV<br />
| \- shoes<br />
| |- left shoe (left foot)<br />
| | \- left shoe (no modify) (no transfer) <-- no-mod object<br />
| |- right shoe (right foot)<br />
| | \- right shoe (no modify) (no transfer) <-- no-mod object<br />
| \- shoe base (no modify) (no transfer) <-- this is not an object<br />
|- Animations<br />
|- Body Parts<br />
.<br />
.<br />
.<br />
<br />
GOTCHAS :<br />
* Do NOT put a comma (',') in the name of the folders under the shared root or it would screw the list up.<br />
* Don't forget to rename the items in the shared folders (or to wear these items at least once to have them be renamed automatically) or the force attach command will appear to do nothing at all.<br />
* Avoid cluttering the shared root with many folders, since some scripts may rely on the list they got with the @getinv command and chat messages are limited to 1023 characters. Choose wisely, and use short names. But with 9 characters per folder name average, you can expect to have about 100 folders available.<br />
* Remember to put no-modify items in sub-folders, one each, so their names can be used by the viewer do find out where to attach them. They can't be shared like modify items since they can't be renamed, and the outfit folder itself will not be renamed (since it contains several items).<br />
<br />
<br />
'''''Accept sub-folders given via llGiveInventoryList() into the shared folder''''' :<br />
<br />
Starting with RestrainedLove v1.16.2, you may give a list of items to a victim and have them stored as a sub-folder inside the #RLV folder (thus allowing you to @attach the given items later).<br />
<br />
Issuing a llGiveInventoryList(victim_id, "#RLV/~subfolder_name", list_of_stuff) command in a script makes a standard Keep/Discard/Mute dialog appear in the viewer of the victim (the avatar which key is victim_id).<br />
<br />
Should the victim accept the offer, the list_of_stuff items are put into a new sub-folder of the #RLV folder. The name of this sub-folder is "~subfolder_name" (it is the scripter's responsibility to use unique sub-folder names: if the name is the same as an existing sub-folder, two sub-folders with the same name will appear in the #RLV folder).<br />
<br />
Note that the tilde character *must* be used as the first character for the name of the sub-folder (this is so that the victim can easily spot any sub-folder given to them in this way, and so that such sub-folder names appear last in the #RLV folder).<br />
<br />
Note also that this feature may be disabled by the user, (by setting the RestrainedLoveForbidGiveToRLV debug setting to TRUE): in this case the given items are put into a folder named "#RLV/~subfolder_name" at the root of the inventory instead of inside the #RLV folder.<br />
<br />
Since the user may either refuse the offer or have the feature disabled in their viewer, and since SL may take quite some time to perform the actual transfer of the objects on laggy days, you must check that the given folder is present (with @getinv), before attempting to @attach the given objects.<br />
<br />
==For your information==<br />
Here is how it works internally, for a better understanding of the gotchas you may encounter :<br />
* Each command is parsed into a '''Behaviour''' (ex: "remoutfit"), an '''Option''' (ex: "shirt") and a '''Param''' (ex: "force") and comes from an [[UUID]] (the unique identifier of the emitting object).<br />
<br />
* There are two types of commands : '''one-shot''' commands (those which Param is "force" and those which Param is a number such as the channel number of a "version" call) and '''rules''' (those which Param is "y", "n", "add" or "rem"). "clear" is special but can be seen as a one-shot command.<br />
<br />
* When the command takes a channel number, this number may be either strictly positive or (for RestrainedLove v1.23a (@versionnum = 1230001) and above) strictly negative. Using channel 0 is not allowed. Note that RestrainedLove can send a maximum of 255 characters on negative channels, while it can send up to 1023 characters on positive channels. Negative channels are useful to prevent the user from cheating, for example when asking for the @versionnum (since the user could use a non-RestrainedLove viewer and make the RestrainedLove devices believe they run within a RestrainedLove viewer by spoofing the reply to the version command on a positive channel, which they can't do on negative channels). Positive channels are best used for commands that may return large reply strings (@getpath, for example).<br />
<br />
* Parameters "n" and "add" are '''exactly equal''' and are treated '''exactly the same way''', they are just '''synonyms'''. Same goes for "y" and "rem". The only purpose is to distinguish rules ("sendchannel=n") from exceptions ("sendchannel:8=add") in a script for clarity.<br />
<br />
* Rules are stored inside a table linking the [[UUID]] of the emitter to the rule itself. They are '''added''' when receiving a "n"/"add" Param, and '''removed''' when receiving a "y"/"rem" Param.<br />
If '''''UUID1''''' is a collar and '''''UUID2''''' is a gag :<br />
<br />
'''''UUID1''''' => detach, tploc, tplm, tplure, sittp<br />
<br />
'''''UUID2''''' => detach, sendim, sendim:(keyholder)<br />
<br />
Those two rules mean that the user cannot send IMs except to their keyholder, and cannot TP at all. Those two items are not detachable. Now if the collar sends "@sendim=n", the table becomes :<br />
<br />
'''''UUID1''''' => detach, tploc, tplm, tplure, sittp, sendim<br />
<br />
'''''UUID2''''' => detach, sendim, sendim:(keyholder)<br />
<br />
If it sends "@sendim=n" a second time nothing changes, as there is a check for its existence prior to adding it. If the gag is unlocked and detached, either it sends a "@clear" or the garbage collector kicks in so the rules linked to '''''UUID2''''' disappear. However, the avatar is still unable to send IMs even to their keyholder, as the exception has disappeared as well. This is because rules linked to one object don't conflict with rules linked to another one.<br />
<br />
* One-shot commands, on the other hand, are executed on-the-fly and are not stored.<br />
<br />
* When logging on, the avatar stays non-operational (cannot chat, cannot move) for some time, while the user sees the progress bar. However, worn scripted objects [[rez]] in the meantime and start sending rules and commands before the viewer can execute them. Therefore it stores them in a buffer and executes them only when the user is given controls (when the progress bar disappears).<br />
<br />
* The viewer periodically (every N seconds) checks all its rules and removes the ones linked to an [[UUID]] that does not exist around anymore ("garbage collector"). This means that rules issued by an '''unworn''' owned object will be discarded as soon as the avatar [[teleport|teleports]] elsewhere.<br />
<br />
[[Category:Third Party Client]]<br />
[[Category:RestrainedLove]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/RestrainedLoveAPI&diff=1188046LSL Protocol/RestrainedLoveAPI2014-03-04T13:56:07Z<p>Felis Darwin: /* Unofficial Commands */ Note these unofficial commands, so they are at least noted somewhere</p>
<hr />
<div>{{LSL Header|ml=*}}<br />
=Restrained Love viewer v2.8.1 Specification=<br />
<br />
By [[User:Marine Kelley|Marine Kelley]]<br />
<br />
==Audience==<br />
<br />
This document is for people who wish to modify or create their own [[LSL]] scripts to use the features of the [http://realrestraint.blogspot.com RestrainedLove viewer]. It does not explain [[LSL]] concepts such as messages and events, nor universal concepts such as [[UUID]]s.<br />
<br />
This document contains the specification for RestrainedLove viewer itself. If you need information about the RestrainedLove viewer relay(RLV relay), please see the [[LSL_Protocol/Restrained_Love_Relay/Specification|RLV relay specification]]<br />
<br />
==Introduction==<br />
<br />
The [http://realrestraint.blogspot.com RestrainedLove viewer] executes certain behaviours when receiving special messages from scripts in-world. These messages are mostly calls to the [[llOwnerSay]]() [[LSL]] function.<br />
<br />
==Architecture==<br />
<br />
The [http://realrestraint.blogspot.com RestrainedLove viewer] intercepts every [[llOwnerSay]] message sent to the viewer. Lines that begin with an at-sign (''''@'''') are parsed as RLV commands. Other lines are forwarded to the user in the Local Chat window, as usual. For instance, a call to [[llOwnerSay]] ("@detach=n") sends the ''detach'' command with parameter ''n'' to the viewer on behalf of the object running the script.<br />
<br />
The syntax of a message is:<br />
<br />
: <code>@<command1>[:option1]=<param1>,<command2>[:option2]=<param2>,...,<commandN>[:optionN]=<paramN></code><br />
<br />
Note that there is only one '@' sign, placed at the beginning of the message. The viewer interprets this as "this entire [[llOwnerSay]]() message contains one or more commands to execute". For documentation purposes, commands are always presented with the leading ''''@''''. However, it is an error to put the ''''@'''' in front of each command within a multi-command message, and the subsequent commands will fail.<br />
<br />
: Historical Note: Prior to Version '''1.10''', RLV only allowed one command per message. Version '''1.10''' added the ability to include multiple commands in one message, to avoid spamming users who are not using this viewer.<br />
<br />
If at least one command fails (e.g. a typo), the viewer says "... fails command : ... " and prints the entire message. However, correct commands are still parsed and executed, only the incorrect ones are ignored.<br />
<br />
Many of these commands determine the subsequent ''behaviour'' of the object or avatar. For example, the '''@detach=n''' command locks the given object, making it undetachable. Some commands set ''global behaviours'', which aren't limited to the object sending the command. For example, the '''@sendchat=n''' command will prevent the user from talking in local chat.<br />
<br />
'''NOTE''' about commands with exceptions, such as @sendim or @sendchannel... @(rule):(exception)=n actually (and counter-intuitively) '''adds an exception''' for the given rule. @sendchannel:1=n, for example, '''allows''' chat on channel 1. This has been the source of at least two scripters' confusion. =add (which means the same as =n) and =rem (which means =y) exist for the purpose of adding and removing exceptions, respectively. Use them.<br />
<br />
{{hint|mode=warning|desc=These behaviours are '''not''' persistent between sessions. Since an object changes its [[UUID]] every time it [[rez]]zes, the object ''must'' resend its status (undetachable, preventing IMs...) in the [[on_rez]]() event as well as whenever it changes its status.}}<br />
<br />
==List of commands==<br />
<br />
'''Note:''' These commands are not case-sensitive but are spacing-sensitive. In other words, "@detach = n" will '''not''' work.<br />
<br />
'''Notation convention:''' Parameters in [square brackets] are optional parameters that can be omitted. The pipe | and slash / signs separate options from which one must be used. <angle brackets> enclose parameters that are mandatory.<br />
<br />
'''Footnotes:''' "(*)" are footnotes and will be explained at the end of the list<br />
<br />
===Version Checking===<br />
<br />
* '''''Automated version checking''''' : "@version=<channel_number>"<br />
''Implemented in v1.0b''<br />
<br />
Makes the viewer automatically say the version of the RLV API it implements, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
'''Warning''' : when logging in, the [[on_rez]] event of all the attachments occurs way before the avatar can actually send chat messages (about half the way through the login progress bar). This means the timeout should be long enough, like 30 seconds to one minute in order to receive the automatic reply from the viewer.<br />
<br />
'''Warning 2''' : On 02/22/2010, Linden Lab has released their Third Party Viewer policy which forbids using the term "Life" in the name of Third Party Viewers. Therefore "Restrained Life" had to be renamed to "Restrained Love". However, for compatibility purposes, this @version command still works and will keep working, however you are encouraged to '''not''' use it in new scripts, and to '''not''' show the terms "Restrained Life" to the user anywhere. For new scripts, please use @versionnew below instead.<br />
<br />
<br />
* '''''Automated version checking''''' : "@versionnew=<channel_number>"<br />
''Implemented in v1.23''<br />
<br />
Makes the viewer automatically say the version of the RLV API it implements, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
This command is the successor of @version and replaces it, although @version is kept for ascending compatibility purposes. It returns "RestrainedLove viewer v... (SL ...)" ("RestrainedLove" is in one word).<br />
<br />
'''Warning''' : when logging in, the [[on_rez]] event of all the attachments occurs way before the avatar can actually send chat messages (about half the way through the login progress bar). This means the timeout should be long enough, like 30 seconds to one minute in order to receive the automatic reply from the viewer.<br />
<br />
<br />
* '''''Automated version number checking''''' : "@versionnum=<channel_number>"<br />
''Implemented in v1.21''<br />
<br />
Makes the viewer automatically say the version number of the RLV API it implements (please note that this is different from the version of the viewer, which the scripts should not have to care about), immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. This command is less cumbersome than @version, since the script does not have to parse the response, it gets the version number immediately.<br />
<br />
The version number is a mere integer that represents the version of the API. If the version is X.Y.Z.P, then the number will be X.10^6 + Y.10^4 + Z.10^2 + P. For example, 1.21.1 would be 1210100.<br />
<br />
<br />
* '''''Automated version checking, second way''''' : llGetAgentLanguage (key id) '''''DEPRECATED: DO NOT USE !'''''<br />
''Implemented in v1.16''<br />
<br />
When calling this LSL function, the result is obtained immediately (no need to use a listener and a timer), is exactly equal to the one given by "@version" and cannot be hidden by the user. This string takes the place of the language returned by the regular SL viewer, which could answer values like "en-us", "fr", "ko" etc. Or nothing at all, if the user chose to hide their language setting. Being optional in the regular viewer, it cannot be trusted by a script, so "hijacking" this feature for the much more useful synchronous version checking in the RLV makes sense. IMPORTANT NOTE: this feature cannot be implemented in viewers prior to v1.21, even when they do implement RestrainedLove v1.16, so make sure you do fall back to the @version method whenever llGetAgentLanguage() returns an empty string. ALSO NOTE: In RestrainedLove 1.16, llGetAgentLanguage() will return an empty string when called by on_rez during login unless the call is delayed by several seconds (how many seconds may vary). FINAL NOTE: This feature was removed from v1.16.1 (and v1.16b, for the Cool SL Viewer).<br />
<br />
<br />
* '''''Manual version checking''''' : "@version"<br />
''Implemented in v1.0a''<br />
<br />
This command must be sent in IM from an avatar to the user (will not work from objects). The viewer automatically answers its version to the sender in IM, but neither the message nor the answer appears in the user's IM window, so it's usually totally stealthy. However, some viewers, such as Firestorm, have the ability to send an autoreply message when someone begins typing an IM to the user. If the user has that option enabled, an IM window will open and display the auto response as soon as the @ is typed by the sender, but nothing else.<br />
<br />
===Blacklist handling===<br />
<br />
The blacklist (implemented in v2.8) is a list of RLV commands that the viewer is meant to ignore. It is modifiable at any time, but a restart is needed to take the changes into account. When a command is issued and it is part of the blacklist, the RLV will simply ignore it. Modifying the blacklist won't clear existing restrictions though, once they are issued, a restart is needed. When a command is received, a positive acknowledgement is sent to the script, whether the command was actually accepted or not. This way scripts that wait for notifications won't break if they can't handle a denial.<br />
<br />
<br />
* '''''Automated version number checking, followed by the blacklist''''' : "@versionnumbl=<channel_number>"<br />
''Implemented in v2.8''<br />
<br />
Makes the viewer automatically say the version number of the RLV API it implements (please note that this is different from the version of the viewer, which the scripts should not have to care about), followed by a comma (",") and the contents of the blacklist, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. This command is less cumbersome than @version followed by @getblacklist, since the script does not have to parse the response, it gets the version number immediately, and it does not have to send a second asynchronous request after the response to the first.<br />
<br />
For example, "@versionnumbl=2222" will answer "2080000,sendim,recvim" if the blacklist is currently "sendim,recvim". LSL allows for casting such a string into an integer without any trouble, it would return 2080000, discarding the first comma and all that is after it.<br />
<br />
<br />
* '''''Get the contents of the blacklist, with a filter''''' : "@getblacklist[:filter]=<channel_number>"<br />
''Implemented in v2.8''<br />
<br />
Makes the viewer automatically reply with the contents of the blacklist (if a filter is present, then only the commands containing that text will be part of the reply), immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
<br />
* '''''Manual blacklist checking''''' : "@getblacklist"<br />
''Implemented in v2.8''<br />
<br />
This command must be sent in IM from an avatar to the user (will not work from objects). The viewer automatically answers the contents of its blacklist to the sender in IM, but neither the message nor the answer appears in the user's IM window, so it's usually totally stealthy, with the same caveat mentioned above under @version.<br />
<br />
===Miscellaneous===<br />
<br />
* '''''Start/stop notifications on a private channel''''' : "@notify:<channel_number>[;word]=<rem/add>"<br />
''Implemented in v1.20, improved in v2.2 (and v1.24)''<br />
<br />
Makes the viewer automatically repeat any restriction it adds or removes on the specified channel, or only the restrictions which name contains the word specified after the semicolon (";") character. The response on the private channel <channel_number> is preceded with a slash ("/") to avoid making the avatar send commands to other scripts without knowing it, and followed by an equal sign ("=") and "n" or "y" according to whether the restriction is applied or lifted respectively. The "@clear" command will not add an equal sign. There is no way to know what object issued the restriction or lifted it, to avoid disclosing too much information about foreign scripts. It does not repeat one-shot commands either (force commands). For example, "@notify:2222;detach=add" will send "/detach=n" whenever an object is locked, and "/detach=y" whenever an object is unlocked, on channel 2222 to which the script will listen to.<br />
<br />
Note : Since v2.2 (and v1.24) you can also set a notification for inventory offers. When your object gives an item or a folder, the avatar using a RLV v2.2 (and v1.24) or higher will respond automatically on the given channel one of the following :<br />
<br />
:* /accepted_in_rlv inv_offer <folder> : The folder has been accepted and is now available under #RLV (don't forget that the viewer renames it, removing the "#RLV/" prefix).<br />
<br />
:* /accepted_in_inv inv_offer <folder> : The folder has been accepted but is not shared.<br />
<br />
:* /declined inv_offer <folder> : The folder has been declined and/or the user has pressed "Block" (formerly "Mute").<br />
<br />
Where <folder> is the full path of the folder or item given. For example, #RLV/~MyCuffs. There is a space before "inv_offer", which is a token chosen in a way that it is easy to set a notification for it. If you just want to know whether your folder named #RLV/~MyCuffs has been accepted in the #RLV folder, issue a "@notify:2222;accepted_in_rlv inv_offer #RLV/~MyCuffs=add" command. If you just want to know whether the avatar has received something, issue a simple "@notify:2222;inv_offer=add" command.<br />
<br />
Note 2 : Since v2.5 the viewer also sends notifications when wearing outfits :<br />
<br />
:* /worn legally <layer> : The avatar has just worn a piece of clothing on the indicated layer.<br />
<br />
:* /unworn legally <layer> : The avatar has just removed a piece of clothing from the indicated layer.<br />
<br />
:* /attached legally <attach_point_name> : The avatar has just attached an object to the indicated attachment point.<br />
<br />
:* /attached illegally <attach_point_name> : The avatar has just attached an object to the indicated attachment point, but was not allowed to (probably a script attached it automatically), and it will be detached in a few seconds.<br />
<br />
:* /detached legally <attach_point_name> : The avatar has just detached an object from the indicated attachment point.<br />
<br />
:* /detached illegally <attach_point_name> : The avatar has just detached an object from the indicated attachment point, but was not allowed to (probably a script kicked it off), and it will be re-attached in a few seconds.<br />
<br />
<br />
* '''''Allow/deny permissive exceptions''''' : "@permissive=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When denied, all restrictions turn into their "secure" counterparts (if any). This means an exception to a restriction will be ignored if it is not issued by the same object that issued the restriction. Using non-secure restrictions (the original ones, like @sendim, @recvim etc) and not using @permissive allow the avatar to benefit from exceptions issued by different objects.<br />
<br />
'''Warning''' : Using this command (or any secure version of the original commands) may silently discard exceptions issued by different objects (it is even its primary purpose), hence some products may appear to cease working while this restriction is in effect. For example, a product that allows the avatar to always be able to send IMs a particular friend will not be able to overcome a @sendim_sec or a @permissive command sent by another object, and will look like it is broken. Therefore, use with caution and make the user aware of how secure your own product is !<br />
<br />
<br />
* '''''Clear all the rules tied to an object''''' : "@clear"<br />
''Implemented in v1.0a, but working only since v1.04a''<br />
<br />
This command clears all the restrictions and exceptions tied to a particular [[UUID]].<br />
<br />
'''Warning''' : when triggered on detach by default, this might prevent the automatic reattach when @defaultwear is active, as @clear will also lift @detach=n, thus the viewer thinks the item that gets detached by accident by a default-wear-action is unlocked and will not reattach it.<br />
<br />
Possible workarounds:<br />
:* only lift the exact restrictions you added with @clear=<pattern> <br />
:* only trigger @clear on detach when you are sure the attachment is not locked<br />
:* don't trigger @clear on detach at all and wait for the viewer to lift the set restrictions<br />
<br />
<br />
* '''''Clear a subset of the rules tied to an object''''' : "@clear=<string>"<br />
''Implemented in v1.0a, but working only since v1.04a''<br />
<br />
This command clears all the restrictions and exceptions tied to a particular [[UUID]] which name contains <string>. A good example would be "@clear=tp" which clears all the [[teleport]] restrictions and exceptions tied to that object, whereas "@clear=tplure:" would only clear the exceptions to the "teleport-by-friend" restriction<br />
<br />
<br />
* '''''Get the list of restrictions the avatar is currently submitted to''''' : @getstatus[:<part_of_rule>[;<custom_separator>]]=<channel><br />
''Implemented in v1.10, slightly tweaked in v1.16 and v2.8''<br />
<br />
Makes the viewer automatically answer the list of rules the avatar is currently under, which would only contains the restrictions issued by the object that sends this command, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is a list of rules, separated by slashes ('/') or by any other separator if specified. Attention : since v1.16 a slash is prepended at the beginning of the string. This does not confuse llParseString2List() calls, but does confuse llParseStringKeepNulls() calls !<br />
<br />
Since v2.8, if <custom_separator> is specified, it will replace the slash ('/') with the provided separator. Attention, the option part must be present, therefore there must be a colon (':') before the semicolon (';'), even if <part_of_rule> is absent.<br />
<br />
This command is useful for people who write scripts that may conflict with other scripts in the same object (for instance : third-party plugins). Conflicts do not occur in different objects, that's why this command only replies the restrictions issued by the object calling it.<br />
<br />
<part_of_rule> is the name of a rule, or a part of it, useful if the script only needs to know about a certain restriction.<br />
<br />
Example : If the avatar is under tploc, tplure, tplm and sittp, here is what the script would get :<br />
@getstatus=2222 => /tploc/tplure/tplm/sittp<br />
@getstatus:sittp=2222 => /sittp<br />
@getstatus:tpl=2222 => /tploc/tplure/tplm (because "tpl" is part of "tploc", "tplure" and "tplm" but not "sittp")<br />
@getstatus:tpl;#=2222 => #tploc#tplure#tplm (because "tpl" is part of "tploc", "tplure" and "tplm" but not "sittp", and the<br />
specified separator is "#")<br />
@getstatus:;#=2222 => #tploc#tplure#tplm#sittp (because the specified separator is "#")<br />
@getstatus:;=2222 => /tploc/tplure/tplm/sittp (because the specified separator is empty, so it defaults to "/")<br />
<br />
<br />
* '''''Get the list of all the restrictions the avatar is currently submitted to''''' : @getstatusall[:<part_of_rule>[;<custom_separator>]]=<channel><br />
''Implemented in v1.15, slightly tweaked in v1.16 and v2.8''<br />
<br />
Makes the viewer automatically answer the list of rules the avatar is currently under, for all the objects regardless of their UUID, contrary to @getstatus, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is a list of rules, separated by slashes ('/') or by any other separator if specified. Attention : since v1.16 a slash is prepended at the beginning of the string. This does not confuse llParseString2List() calls, but does confuse llParseStringKeepNulls() calls !<br />
<br />
Since v2.8, if <custom_separator> is specified, it will replace the slash ('/') with the provided separator. Attention, the option part must be present, therefore there must be a colon (':') before the semicolon (';'), even if <part_of_rule> is absent.<br />
<br />
===Movement===<br />
<br />
* '''''Allow/prevent flying''''' : @fly=<y/n><br />
''Implemented in v1.12.2''<br />
<br />
When prevented, the user is unable to fly.<br />
<br />
<br />
* '''''Allow/prevent running by double-tapping an arrow key''''' : @temprun=<y/n><br />
''Implemented in v2.7''<br />
<br />
When prevented, the user is unable to run by double-tapping an arrow key. If you want to prevent the user from running at all, you must also use @alwaysrun.<br />
<br />
<br />
* '''''Allow/prevent always running''''' : @alwaysrun=<y/n><br />
''Implemented in v2.7''<br />
<br />
When prevented, the user is unable to switch running mode on by pressing Ctrl-R. If you want to prevent the user from running at all, you must also use @temprun. This command is useful when you want to force the user to accelerate before running, rather than running all the time, for example during combats or sports games.<br />
<br />
<br />
* '''''Force rotate the avatar to a set direction''''' : @setrot:<angle_in_radians>=force<br />
''Implemented in v1.17''<br />
<br />
Forces the avatar to rotate towards a direction set by an angle in radians from the north. Note that this command is not very precise, nor will do anything if the action attempts to rotate the avatar by less than 10° (experimental value, it has been mentioned somewhere that 6° was the minimum). In other words, it is best to either check with a llGetRot() first, or to make the avatar turn twice, first 180° plus the desired angle, then by the angle we need. It isn't very elegant but it works.<br />
<br />
See [[User:brattle Resident|this snippet]] for an example of how to convert a rotation as returned from llGetRootRotation to an angle in radians suitable for @setrot.<br />
<br />
* '''''Change the height of the avatar''''' : @adjustheight:<distance_pelvis_to_foot_in_meters>;<factor>[;delta_in_meters]=force<br />
''Implemented in v2.5''<br />
<br />
Forces the avatar to modify its "Z-offset", in other words its altitude. This value can already be changed through a debug setting in most third party viewers, this command allows to automate the change according to the animation.<br />
<br />
Rather than explaining it in full here, please go to this link for further info : [http://sldev.free.fr/forum/viewtopic.php?f=7&t=447]<br />
<br />
This function has been broken by Linden Lab as part of the Project Sunshine (server-side appearance/server-side baking) changes to Second Life, and will not function as expected in a SSA-capable viewer.<br />
<br />
===Chat, Emotes and Instant Messages===<br />
====Chat====<br />
* '''''Allow/prevent sending chat messages''''' : "@sendchat=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, everything typed on [[Chat channel|channel]] 0 will be discarded. However, emotes and messages beginning with a slash ('/') will go through, truncated to strings of 30 and 15 characters long respectively (likely to change later). Messages with special signs like ()"-*=_^ are prohibited, and will be discarded. When a period ('.') is present, the rest of the message is discarded.<br />
<br />
<br />
* '''''Allow/prevent shouting''''' : "@chatshout=<y/n>"<br />
''Implemented in v1.15''<br />
<br />
When prevented, the avatar will chat normally even when the user tries to shout. This does not change the message in any way, only its range.<br />
<br />
<br />
* '''''Allow/prevent chatting at normal volume''''' : "@chatnormal=<y/n>"<br />
''Implemented in v1.15''<br />
<br />
When prevented, the avatar will whisper even when the user tries to shout or chat normally. This does not change the message in any way, only its range.<br />
<br />
<br />
* '''''Allow/prevent whispering''''' : "@chatwhisper=<y/n>"<br />
''Implemented in v1.15''<br />
<br />
When prevented, the avatar will chat normally even when the user tries to whisper. This does not change the message in any way, only its range.<br />
<br />
<br />
* '''''Redirect public chat to private channels''''' : "@redirchat:<channel_number>=<rem/add>"<br />
''Implemented in v1.16''<br />
<br />
When active, this restriction redirects whatever the user says on the public channel ("/0") to the private channel provided in the option field. If several redirections are issued, the chat message will be redirected to each channel. It does not apply to emotes, and will not trigger any animation (typing start, typing stop, nodding) when talking. This restriction does not supercede @sendchannel.<br />
'''NOTE:''' As of RLV v1.22.1 / RLVa 1.1.0, it had a bug that @redirchat also truncates emotes on channel 0. An additional @emote=add works around this side-effect. This bug was fixed in the Cool VL Viewer starting with v1.22g (but Marine's RLV v1.23 still had this bug) and RLV v2.0 (it is safe to assume it was fixed in all viewers starting with v1.24 and v2.0).<br />
<br />
<br />
* '''''Allow/prevent receiving chat messages''''' : "@recvchat=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, everything heard in public chat will be discarded except emotes.<br />
<br />
<br />
* '''''Allow/prevent receiving chat messages, secure way''''' : "@recvchat_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, everything heard in public chat will be discarded except emotes. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the chat message receiving prevention''''' : "@recvchat:<UUID>=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding an exception, the user can hear chat messages from the sender whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent receiving chat messages from someone in particular''''' : "@recvchatfrom:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, everything heard in public chat from the specified avatar will be discarded except emotes.<br />
<br />
<br />
====Emotes====<br />
* '''''Remove/add an exception to the emote truncation above''''' : "@emote=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding this exception, the emotes are not truncated anymore (however, special signs will still discard the message).<br />
<br />
<br />
* '''''Redirect public emotes to private channels''''' : "@rediremote:<channel_number>=<rem/add>"<br />
''Implemented in v1.19''<br />
<br />
When active, this restriction redirects whatever emote the user says on the public channel ("/0") to the private channel provided in the option field. If several redirections are issued, the emote will be redirected to each channel.<br />
<br />
<br />
* '''''Allow/prevent seeing emotes''''' : "@recvemote=<y/n>"<br />
''Implemented in v1.19''<br />
<br />
When prevented, every emote seen in public chat will be discarded.<br />
<br />
<br />
* '''''Allow/prevent receiving emotes seen in public chat from someone in particular''''' : "@recvemotefrom:<UUID>=<y/n>"<br />
''Implemented in v2.4''<br />
<br />
When prevented, everything emote seen in public chat from the specified avatar will be discarded.<br />
<br />
<br />
* '''''Allow/prevent seeing emotes, secure way''''' : "@recvemote_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, every emote seen in public chat will be discarded. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the emote seeing prevention''''' : "@recvemote:<UUID>=<rem/add>"<br />
''Implemented in v1.19''<br />
<br />
When adding an exception, the user can see emotes from the sender whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
====Private Channels====<br />
* '''''Allow/prevent using any chat channel but certain channels''''' : @sendchannel[:<channel>]=<y/n><br />
''Implemented in v1.10''<br />
<br />
Complimentary of @sendchat, this command prevents the user from sending messages on non-public Chat channels. If channel is specified, it becomes an exception to the aforementioned restriction (then it is better to use "rem" or "add" instead of "y" or "n" respectively). It does not prevent the viewer automatic replies like @version=nnnn, @getstatus=nnnn etc. <br />
<br />
<br />
* '''''Allow/prevent using any chat channel but certain channels, secure way''''' : @sendchannel_sec[:<channel>]=<y/n><br />
''Implemented in v1.10''<br />
<br />
Complimentary of @sendchat, this command prevents the user from sending messages on non-public channels. If channel is specified, it becomes an exception to the aforementioned restriction (then it is better to use "rem" or "add" instead of "y" or "n" respectively). It does not prevent the viewer automatic replies like @version=nnnn, @getstatus=nnnn etc. This particular command only accepts exceptions issued from the same object, opposed to its non-secure version which accepts exceptions from any other object.<br />
<br />
<br />
====Instant Messages====<br />
* '''''Allow/prevent sending instant messages''''' : "@sendim=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, everything typed in IM will be discarded and a bogus message will be sent to the receiver instead.<br />
<br />
<br />
* '''''Allow/prevent sending instant messages, secure way''''' : "@sendim_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, everything typed in IM will be discarded and a bogus message will be sent to the receiver instead. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the instant message sending prevention''''' : "@sendim:<UUID>=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding an exception, the user can send IMs to the receiver whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent sending instant messages to someone in particular''''' : "@sendimto:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, everything typed in IM to the specified avatar will be discarded and a bogus message will be sent instead.<br />
<br />
<br />
* '''''Allow/prevent starting an IM session with anyone''''' : "@startim=<y/n>"<br />
''Implemented in v2.6''<br />
<br />
When prevented, the user is unable to start an IM session with anyone. Sessions that are already open are not impacted though.<br />
<br />
<br />
* '''''Remove/add exceptions to the IM session start prevention''''' : "@startim:<UUID>=<rem/add>"<br />
''Implemented in v2.6''<br />
<br />
When adding an exception, the user can start an IM session with the receiver whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent starting an IM session with someone in particular''''' : "@startimto:<UUID>=<y/n>"<br />
''Implemented in v2.6''<br />
<br />
When prevented, the user is unable to start an IM session with that person. Sessions that are already open are not impacted though.<br />
<br />
<br />
* '''''Allow/prevent receiving instant messages''''' : "@recvim=<y/n>"<br />
''Implemented in v1.0b''<br />
<br />
When prevented, every incoming IM will be discarded and the sender will be notified that the user cannot read them.<br />
<br />
<br />
* '''''Allow/prevent receiving instant messages, secure way''''' : "@recvim_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, every incoming IM will be discarded and the sender will be notified that the user cannot read them. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the instant message receiving prevention''''' : "@recvim:<UUID>=<rem/add>"<br />
''Implemented in v1.01''<br />
<br />
When adding an exception, the user can read instant messages from the sender whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Allow/prevent receiving instant messages from someone in particular''''' : "@recvimfrom:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, every IM received from the the specified avatar will be discarded and the sender will be notified that the user cannot read them.<br />
<br />
===Teleportation===<br />
<br />
* '''''Allow/prevent teleporting to a landmark''''' : "@tplm=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When prevented, the user cannot use a [[landmark]], pick or any other preset location to [[teleport]] there.<br />
<br />
<br />
* '''''Allow/prevent teleporting to a location''''' : "@tploc=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When prevented, the user cannot use [[teleport]] to a coordinate by using the [[map]] and such.<br />
<br />
<br />
* '''''Allow/prevent teleporting by a friend''''' : "@tplure=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When prevented, the user automatically discards any [[teleport]] offer, and the avatar who initiated the offer is notified.<br />
<br />
<br />
* '''''Allow/prevent teleporting by a friend, secure way''''' : "@tplure_sec=<y/n>"<br />
''Implemented in v1.21''<br />
<br />
When prevented, the user automatically discards any [[teleport]] offer, and the avatar who initiated the offer is notified. This particular command accepts exceptions issued from the same object only, opposed to the non-secure way that accepts exceptions from any object.<br />
<br />
<br />
* '''''Remove/add exceptions to the friend teleport prevention''''' : "@tplure:<UUID>=<rem/add>"<br />
''Implemented in v1.0''<br />
<br />
When adding an exception, the user can be teleported by the avatar whose [[UUID]] is specified in the command. This overrides the prevention for this avatar only (there is no limit to the number of exceptions), don't forget to remove it when it becomes obsolete.<br />
<br />
<br />
* '''''Unlimit/limit sit-tp''''' : "@sittp=<y/n>"<br />
''Implemented in v1.0''<br />
<br />
When limited, the avatar cannot sit on a [[prim]] unless it is closer than 1.5 m. This allows cages to be secure, preventing the avatar from warping its position through the walls (unless the prim is too close).<br />
<br />
<br />
* '''''Allow/prevent standing up at a different location than where we sat down''''' : @standtp=<y/n><br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
When this restriction is active and the avatar stands up, it is automatically teleported back to the location where it initially sat down. Please note that the "last standing location" is also stored when the restriction is issued, so this won't be a problem for grabbers and the like, that sit the victim, then move them inside a cell, which issues its restrictions, and then unsits them. In this case the avatar will stay in the cell.<br />
<br />
<br />
* '''''Force-Teleport the user''''' : @tpto:<X>/<Y>/<Z>=force (*)<br />
''Implemented in v1.12''<br />
<br />
This command forces the avatar to teleport to the indicated coordinates. Note that these coordinates are always '''global''', hence the script that calls this command will not be trivial. Moreso, if the destination contains a telehub or a landing point, the user will land there instead of the desired point. This is a SL limitation. Also keep in mind that @tpto is inhibited by @tploc=n, and from v1.15 and above, by @unsit too.<br />
<br />
Here is a sample code to call that command properly :<br />
<br />
<lsl><br />
<br />
// FORCE TELEPORT EXAMPLE<br />
// Listens on channel 4 for local coordinates and a sim name<br />
// and tells your viewer to teleport you there.<br />
//<br />
// By Marine Kelley 2008-08-26<br />
// RLV version required : 1.12 and above<br />
//<br />
// HOW TO USE :<br />
// * Create a script inside a box<br />
// * Overwrite the contents of the script with this one<br />
// * Wear the box<br />
// * Say the destination coords Region/X/Y/Z on channel 4 :<br />
// Example : /4 Help Island Public/128/128/50<br />
<br />
key kRequestHandle; // UUID of the dataserver request<br />
vector vLocalPos; // local position extracted from the<br />
<br />
Init () {<br />
kRequestHandle = NULL_KEY;<br />
llListen (4, "", llGetOwner (), "");<br />
}<br />
<br />
<br />
default<br />
{<br />
state_entry () {<br />
Init ();<br />
}<br />
<br />
on_rez(integer start_param) {<br />
Init ();<br />
}<br />
<br />
listen(integer channel, string name, key id, string message) {<br />
list tokens = llParseString2List (message, ["/"], []);<br />
integer L = llGetListLength (tokens);<br />
<br />
if (L==4) {<br />
// Extract local X, Y and Z<br />
vLocalPos.x = llList2Float (tokens, 1);<br />
vLocalPos.y = llList2Float (tokens, 2);<br />
vLocalPos.z = llList2Float (tokens, 3);<br />
<br />
// Request info about the sim<br />
kRequestHandle=llRequestSimulatorData (llList2String (tokens, 0), DATA_SIM_POS);<br />
}<br />
}<br />
<br />
dataserver(key queryid, string data) {<br />
if (queryid == kRequestHandle) {<br />
// Parse the dataserver response (it is a vector cast to a string)<br />
list tokens = llParseString2List (data, ["<", ",", ">"], []);<br />
string pos_str = "";<br />
vector global_pos;<br />
<br />
// The coordinates given by the dataserver are the ones of the<br />
// South-West corner of this sim<br />
// => offset with the specified local coordinates<br />
global_pos.x = llList2Float (tokens, 0);<br />
global_pos.y = llList2Float (tokens, 1);<br />
global_pos.z = llList2Float (tokens, 2);<br />
global_pos += vLocalPos;<br />
<br />
// Build the command<br />
pos_str = (string)((integer)global_pos.x)<br />
+"/"+(string)((integer)global_pos.y)<br />
+"/"+(string)((integer)global_pos.z);<br />
llOwnerSay ("Global position : "+(string)pos_str); // Debug purposes<br />
<br />
// Fire !<br />
llOwnerSay ("@tpto:"+pos_str+"=force");<br />
}<br />
}<br />
<br />
}<br />
<br />
</lsl><br />
<br />
<br />
<br />
* '''''Remove/add auto-accept teleport offers from a particular avatar''''' : "@accepttp[:<UUID>]=<rem/add>"<br />
''Implemented in v1.15, slightly improved in v1.16''<br />
<br />
Adding this rule will make the user automatically accept any teleport offer from the avatar which key is <UUID>, exactly like if that avatar was a Linden (no confirmation box, no message, no Cancel button). This rule does not supercede nor deprecate @tpto because the former teleports to someone, while the latter teleports to an arbitrary location. Attention : in v1.16 the UUID becomes optional, which means that @accepttp=add will force the user to accept teleport offers from anyone ! Use with caution !<br />
<br />
===Inventory, Editing and Rezzing===<br />
<br />
* '''''Allow/prevent using inventory''''' : @showinv=<y/n><br />
''Implemented in v1.10''<br />
<br />
Forces the [[inventory]] windows to close and stay closed.<br />
<br />
<br />
* '''''Allow/prevent reading notecards''''' : @viewnote=<y/n><br />
''Implemented in v1.10''<br />
<br />
Prevents from opening [[notecards]] but does not close the ones already open.<br />
<br />
<br />
* '''''Allow/prevent opening scripts''''' : @viewscript=<y/n><br />
''Implemented in v1.22''<br />
<br />
Prevents from opening [[scripts]] but does not close the ones already open.<br />
<br />
<br />
* '''''Allow/prevent opening textures''''' : @viewtexture=<y/n><br />
''Implemented in v1.22''<br />
<br />
Prevents from opening [[textures]] (and snapshots) but does not close the ones already open.<br />
<br />
<br />
* '''''Allow/prevent editing objects''''' : "@edit=<y/n>"<br />
''Implemented in v1.03''<br />
<br />
When prevented from editing and opening objects, the Build & Edit window will refuse to open.<br />
<br />
<br />
* '''''Remove/add exceptions to the edit prevention''''' : "@edit:<UUID>=<rem/add>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When adding an exception, the user can edit or open this object in particular.<br />
<br />
<br />
* '''''Allow/prevent rezzing inventory''''' : "@rez=<y/n>"<br />
''Implemented in v1.03''<br />
<br />
When prevented from [[rez]]zing stuff, creating and deleting objects, drag-dropping from inventory and dropping attachments will fail.<br />
<br />
<br />
* '''''Allow/prevent editing particular objects''''' : "@editobj:<UUID>=<y/n>"<br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, the Build & Edit window will refuse to open when trying to edit or open the specified object.<br />
<br />
===Sitting===<br />
<br />
* '''''Allow/prevent standing up''''' : @unsit=<y/n><br />
''Implemented in v1.10, modified in v1.15 to prevent teleporting as well''<br />
<br />
Hides the Stand up button. From v1.15 it also prevents teleporting, which was a way to stand up.<br />
<br />
<br />
* '''''Force sit on an object''''' : @sit:<UUID>=force (*)<br />
''Implemented in v1.10''<br />
<br />
Does not work if the user is prevented from sit-tping and further than 1.5 meters away, or when prevented from unsitting.<br />
<br />
<br />
* '''''Get the UUID of the object the avatar is sitting on''''' : @getsitid=<channel_number><br />
''Implemented in v1.12.4 but broken in all versions older than v1.24 and v2.2 (was reporting the UUID of the last object any avatar within draw distance sat upon)''<br />
<br />
Makes the viewer automatically answer the UUID of the object the avatar is currently sitting on, or NULL_KEY if they are not sitting.<br />
<br />
<br />
* '''''Force unsit''''' : @unsit=force (*)<br />
''Implemented in v1.10''<br />
<br />
Self-explanatory but for some reason it randomly fails, so don't rely on it for now. Further testing is needed.<br />
<br />
<br />
* '''''Allow/prevent sitting down''''' : @sit=<y/n><br />
''Implemented in v1.16.2''<br />
<br />
Prevents the user from sitting on anything, including with @sit:<UUID>=force.<br />
<br />
===Clothing and Attachments===<br />
<br />
* '''''Render an object detachable/nondetachable''''' : "@detach=<y/n>"<br />
''Implemented in v1.0a''<br />
<br />
When called with the "n" option, the object sending this message (which must be an attachment) will be made nondetachable. It can be detached again when the "y" option is called.<br />
<br />
<br />
* '''''Unlock/Lock an attachment point''''' : "@detach:<attach_point_name>=<y/n>"<br />
''Implemented in v1.20''<br />
<br />
When called with the "n" option, the attachment point of name <attach_point_name> will be locked either full (if it is occupied by an object at that time) or empty (if not). Any object that is occupying this point when the restriction is issued will be considered as undetachable, exactly like if it had issued a "@detach=n" command itself. If the point is empty it will stay that way, no item will be able to be attached there, and llAttachToAvatar() calls will fail (the object will be attached, then detached right away).<br />
<br />
<br />
* '''''Unlock/Lock an attachment point empty''''' : "@addattach[:<attach_point_name>]=<y/n>"<br />
''Implemented in v1.22''<br />
<br />
When called with the "n" option, the attachment point of name <attach_point_name> will be locked empty. Any object that is occupying this point when the restriction is issued can be detached, but nothing can be attached there. If the point is empty it will stay that way, no item will be able to be attached there, and llAttachToAvatar() calls will fail (the object will be attached, then detached right away). If <attach_point_name> is not specified, then all the attachment points will be concerned. This command is the counterpart to @addoutfit, for attachments.<br />
<br />
<br />
* '''''Unlock/Lock an attachment point full''''' : "@remattach[:<attach_point_name>]=<y/n>"<br />
''Implemented in v1.22''<br />
<br />
When called with the "n" option, the attachment point of name <attach_point_name> will be locked full. Any object that is occupying this point when the restriction is issued will be rendered undetachable. If the point is empty it will allow the user to wear something, but then that object will become undetachable too, no item will be able to replace it, and llAttachToAvatar() calls will fail (the object will be attached, then detached right away). If <attach_point_name> is not specified, then all the attachment points will be concerned. This command is the counterpart to @remoutfit, for attachments.<br />
<br />
<br />
* '''''Allow/deny the "Wear" contextual menu''''' : "@defaultwear=<y/n><br />
''Implemented in v1.21''<br />
<br />
When allowed, the user is always able to choose the "Wear"command on the contextual menu of the inventory, even when an object is locked on their avatar. This holds the risk of kicking that locked object, but it will be reattached automatically within 5 seconds (and successive locked objects every second until there is nothing left to reattach). However some objects may be scripted in a way that they drop their restrictions when detached, or simply not take into account the fact that even a locked object can be detached when using the RLV.<br />
<br />
Therefore, using this command with the "n" option will suppress this comman, but it will still be available for objects that contain the target attachment point in their name or in the name of their parent folder, exactly like pre-1.21 RLV. This is a little less user-friendly but more secure when it comes to make sure no locked object may be detached accidentally.<br />
<br />
<br />
* '''''Force removing attachments''''' : @detach[:attachpt]=force (*) <br />
''Implemented in v1.10''<br />
<br />
Where part is :<br />
chest|skull|left shoulder|right shoulder|left hand|right hand|left foot|right foot|spine|<br />
pelvis|mouth|chin|left ear|right ear|left eyeball|right eyeball|nose|r upper arm|r forearm|<br />
l upper arm|l forearm|right hip|r upper leg|r lower leg|left hip|l upper leg|l lower leg|stomach|left pec|<br />
right pec|center 2|top right|top|top left|center|bottom left|bottom|bottom right|neck|root<br />
If part is not specified, removes everything.<br />
<br />
<br />
* '''''Force removing attachments (alias)''''' : @remattach[:attachpt]=force (*) <br />
''Implemented in v1.22''<br />
<br />
This command is an alias to @detach[:attachpt]=force (to keep things consistent).<br />
<br />
<br />
* '''''Allow/prevent wearing clothes''''' : @addoutfit[:<part>]=<y/n><br />
''Implemented in v1.10, added skin hair and eyes in v1.10.1, added physics in 2.6.1''<br />
<br />
Where part is :<br />
gloves|jacket|pants|shirt|shoes|skirt|socks|underpants|undershirt|skin|eyes|hair|shape|alpha|tattoo|physics<br />
If part is not specified, prevents from wearing anything beyond what the avatar is already wearing.<br />
<br />
'''Note:''' Since the release of Viewer 2.0 there are two new avatar skin layers: Tattoo and Avatar Transparency Mask. The alpha and tattoo layers will only be supported by RLV compliant viewers that implement the new Viewer 2.0 features.<br />
<br />
<br />
* '''''Allow/prevent removing clothes''''' : @remoutfit[:<part>]=<y/n> (underpants and undershirt are kept for teens)<br />
''Implemented in v1.10, added skin hair and eyes in v1.10.1, added physics in 2.6.1''<br />
<br />
Where part is :<br />
gloves|jacket|pants|shirt|shoes|skirt|socks|underpants|undershirt|skin|eyes|hair|shape|alpha|tattoo|physics<br />
If part is not specified, prevents from removing anything in what the avatar is wearing.<br />
<br />
'''Note:''' Since the release of Viewer 2.0 there are two new avatar skin layers: Tattoo and Avatar Transparency Mask. The alpha and tattoo layers will only be supported by RLV compliant viewers that implement the new Viewer 2.0 features.<br />
<br />
<br />
* '''''Force removing clothes''''' : @remoutfit[:<part>]=force (*) (teens can't be forced to remove underpants and undershirt)<br />
''Implemented in v1.10''<br />
<br />
Where part is :<br />
gloves|jacket|pants|shirt|shoes|skirt|socks|underpants|undershirt|alpha|tattoo|physics<br />
If part is not specified, removes everything.<br />
<br />
'''Note:''' Since the release of Viewer 2.0 there are two new avatar skin layers: Tattoo and Avatar Transparency Mask. The alpha and tattoo layers will only be supported by RLV compliant viewers that implement the new Viewer 2.0 features.<br />
<br />
'''Note:''' skin, shape, eyes and hair cannot be removed since they are body parts (and removing any would result in an unrezzed avatar).<br />
<br />
<br />
* '''''Get the list of worn clothes''''' : @getoutfit[:part]=<channel_number><br />
''Implemented in v1.10, added skin hair and eyes in v1.10.1, added physics in 2.6.1''<br />
<br />
Makes the viewer automatically answer the current occupation of clothes layers as a list of 0s (empty) and 1s (occupied) immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The list of 0s and 1s corresponds to :<br />
gloves,jacket,pants,shirt,shoes,skirt,socks,underpants,undershirt,skin,eyes,hair,shape<br />
in that order.<br />
<br />
If a part is specified, answers a single 0 (empty) or 1 (occupied) corresponding to the part.<br />
Ex 1 : @getoutfit=2222 => "0011000111" => avatar is wearing pants, shirt, underpants and undershirt, and of course a skin.<br />
Ex 2 : @getoutfit:socks=2222 => "0" => the avatar is not wearing socks.<br />
<br />
'''Note:''' For viewers that implement the new Viewer 2.0 features, the list is:<br />
<br />
gloves,jacket,pants,shirt,shoes,skirt,socks,underpants,undershirt,skin,eyes,hair,shape,alpha,tattoo<br />
<br />
<br />
* '''''Get the list of worn attachments''''' : @getattach[:attachpt]=<channel_number><br />
''Implemented in v1.10''<br />
<br />
Makes the viewer automatically answer the current occupation of attachment points as a list of 0s (empty) and 1s (occupied) immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The list of 0s and 1s corresponds to :<br />
none,chest,skull,left shoulder,right shoulder,left hand,right hand,left foot,right foot,spine,<br />
pelvis,mouth,chin,left ear,right ear,left eyeball,right eyeball,nose,r upper arm,r forearm,<br />
l upper arm,l forearm,right hip,r upper leg,r lower leg,left hip,l upper leg,l lower leg,stomach,left pec,<br />
right pec,center 2,top right,top,top left,center,bottom left,bottom,bottom right,neck,root<br />
in that order.<br />
<br />
If an attachment point is specified, answers a single 0 (empty) or 1 (occupied) corresponding to the point.<br />
Ex 1 : @getattach=2222 => "011000011010000000000000100100000000101" => avatar is wearing attachments on <br />
chest, skull, left and right foot, pelvis, l and r lower leg, HUD bottom left and HUD bottom right.<br />
Ex 2 : @getattach:chest=2222 => "1" => avatar is wearing something on the chest.<br />
<br />
''Note'' : The first character ("none") is always '0', so the index of each attach point in the string is '''exactly equal''' to the corresponding ATTACH_* macro in LSL. For instance, the index 9 in the string is ATTACH_BACK (which means "spine"). Remember the indices start at zero.<br />
<br />
<br />
* '''''Force the viewer to automatically accept attach and take control permission requests''''' : @acceptpermission=<rem/add><br />
''Implemented in v1.16''<br />
<br />
Forces the avatar to automatically accept attach and take control permission requests. The dialog box doesn't even show up. This command does not supercede @denypermission, of course.<br />
<br />
<br />
* '''''Allow/prevent accepting attach and take control permissions''''' : @denypermission=<rem/add><br />
''Implemented in v1.16, DEPRECATED in v1.16.2''<br />
<br />
When prevented, all attach and take control permission requests are automatically declined, without even showing the dialog box. Due to the extreme annoyance it was making, and because locked objects automatically reattach themselves since v1.16.1, this command is NOW DEPRECATED, DON'T USE IT !<br />
<br />
<br />
* '''''Force detach an item''''' : @detachme=force (*)<br />
''Implemented in v1.16.2''<br />
<br />
This command forces the object that issues it to detach itself from the avatar. It is there as a convenience to avoid a race condition when calling @clear then llDetachFromAvatar(), sometimes the object could detach itself before clearing its restrictions, making it reattach automatically after a while. With this command one can issue a @clear,detachme=force to be sure @clear is executed first.<br />
<br />
===Clothing and Attachments (Shared Folders)===<br />
<br />
* '''''Allow/prevent wearing clothes and attachments that are not part of the #RLV folder''''' : @unsharedwear=<y/n><br />
''Implemented in v2.5''<br />
<br />
When prevented, no object, piece of clothing or bodypart can be worn unless it is part of the #RLV folder (i.e. "shared").<br />
<br />
<br />
* '''''Allow/prevent removing clothes and attachments that are not part of the #RLV folder''''' : @unsharedunwear=<y/n><br />
''Implemented in v2.5''<br />
<br />
When prevented, no object, piece of clothing or bodypart can be removed from the avatar unless it is part of the #RLV folder (i.e. "shared").<br />
<br />
<br />
* '''''Get the list of shared folders in the avatar's inventory''''' : @getinv[:folder1/.../folderN]=<channel_number><br />
''Implemented in v1.11, added sub-folders in v1.13''<br />
<br />
Makes the viewer automatically answer the list of folders contained into the folder named "#RLV" (if it exists), immediately on the chat channel number <channel_number> that the script can listen to. If folders are specified, it will give the list of sub-folders contained into the folder located at that path instead of the shared root (example : "@getinv:Restraints/Leather cuffs/Arms=2222"). Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The answer is a list of names, separated by commas (","). Folders which names begin with a dot (".") will be ignored.<br />
<br />
<br />
* '''''Get the list of shared folders in the avatar's inventory, with information about worn items''''' : @getinvworn[:folder1/.../folderN]=<channel_number><br />
''Implemented in v1.15''<br />
<br />
Makes the viewer automatically answer the list of folders contained into the folder named "#RLV" (if it exists), immediately on the chat channel number <channel_number> that the script can listen to. If folders are specified, it will give the list of sub-folders contained into the folder located at that path instead of the shared root (example : "@getinvworn:Restraints/Leather cuffs/Arms=2222"). Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout.<br />
<br />
The answer is a comma-separated list of names, each one followed with a pipe ("|") and two digits. The current folder is put in first position (as opposed to @getinv which does not show the current folder, obviously), but without a name, only the pipe and the two digits.<br />
<br />
Object : "@getinvworn:Restraints/Leather cuffs=2222"<br />
Viewer : "|02,Arms|30,Legs|10"<br />
<br />
Folders which names begin with a dot (".") will be ignored. The two digits are calculated as follows :<br />
<br />
First digit : Proportion of items worn in the corresponding folder (including no-mod items). In this example, the "3" of "30" means "all the items in the "Arms" folder are currently worn, while the "1" of "10" means "no item in the Legs folder is currently worn, but there are items to wear".<br />
<br />
Second digit : Proportion of items globally worn in all the folders contained inside the corresponding folder. In this example, the "2" of "02" means "some items are worn in some of the folders contained into "Leather cuffs".<br />
<br />
The digits, comprised between 0 and 3 included, have the following meaning :<br />
<br />
:* 0 : No item is present in that folder<br />
:* 1 : Some items are present in that folder, but none of them is worn<br />
:* 2 : Some items are present in that folder, and some of them are worn<br />
:* 3 : Some items are present in that folder, and all of them are worn<br />
<br />
<br />
* '''''Get the path to a shared folder by giving a search criterion''''' : @findfolder:part1[&&...&&partN]=<channel_number><br />
''Implemented in v1.13.1''<br />
<br />
Makes the viewer automatically answer the path to the first shared folder which name contains <part1> and <part2> and ... and <partN>, immediately on the chat channel number <channel_number> that the script can listen to. The search is in depth first, notice the separator which is "&&" like "and". Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. It does not take disabled folders into account (folders which name begins with a dot "."), nor folders which name begins with a tilde ("~"). The answer is a list of folders, separated by slashes ('/').<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attach:<folder1/.../folderN>=force (*)<br />
''Implemented in v1.11, added no-mod items in v1.12, added sub-folders in v1.13''<br />
<br />
Forces the viewer to attach every object and wear every piece of clothing contained inside the folder located at the specified path (which must be under "#RLV"). Objects names '''must''' contain the name of their target attachment point or they won't be attached. Each no-modify object '''must''' be contained inside a folder (one object per folder), which name contains the name of its target attachment point since it can't be renamed. Names cannot begin with a dot (".") since such folders are invisible to the scripts.<br />
<br />
Attachment point names are the same as the ones contained into the "Attach To" submenu : "skull", "chest", "l forearm"...<br />
<br />
Note 1 : Folder names '''can''' contain slashes, and will be chosen in priority when able (for instance, if "@attach:Restraints/cuffs=force" is issued, the "Restraints/cuffs" folder will be chosen before a "cuffs" folder contained inside a "Restraints" parent folder.<br />
<br />
Note 2 : If the name of a folder begins with a plus sign ("+"), then this command will act exactly like @attachover. This rule can be changed through the use of the "RestrainedLoveStackWhenFolderBeginsWith" debug setting.<br />
<br />
'''Attention''' : This command will likely change in the future, to revert back to how it used to behave in version 1.x, i.e. never add an object if the target attachment point is already taken, but rather replace the old object. The current behaviour is intended to be ensured by @attachoverorreplace and its derivatives. For now @attachoverorreplace is a synonym to @attach, but this won't always be the case. In other words, if you intend to make your script always replace existing attachments when attaching new ones, use @attach. If you want your script to always make attachments stack, use @attachover. If you want to give the user the choice through the name of the folder (as indicated above, by prepending the name by a "+" sign by default), use @attachoverorreplace.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, without replacing what is already being worn''''' : @attachover:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attach described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attachoverorreplace:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attach described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, and its children recursively''''' : @attachall:<folder1/.../folderN>=force (*)<br />
''Implemented in v1.15''<br />
<br />
This command works exactly like @attach described hereabove, but also attaches whatever is contained into children folders.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, and its children recursively, without replacing what is already being worn''''' : @attachallover:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attachall described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, and its children recursively''''' : @attachalloverorreplace:<folder1/.../folderN>=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attachall described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force detach items contained inside a shared folder''''' : @detach:<folder_name>=force (*)<br />
''Implemented in v1.11''<br />
<br />
Forces the viewer to detach every object and unwear every piece of clothing contained inside <folder_name>(which must be directly under "#RLV"). If "@detach" is used with an attachment point name (skull, pelvis... see above), it takes priority over this way of detaching since it is the same command.<br />
<br />
<br />
* '''''Force detach items contained inside a shared folder, and its children recursively''''' : @detachall:<folder1/.../folderN>=force (*)<br />
''Implemented in v1.15''<br />
<br />
This command works exactly like @detach described hereabove, but also detaches whatever is contained into children folders.<br />
<br />
<br />
* '''''Get the path to the shared folder containing a particular object/clothing worn on a point''''' : @getpath[:<attachpt> or <clothing_layer>]=<channel_number><br />
''Implemented in v1.16''<br />
<br />
Makes the viewer automatically answer the path to the shared folder containing the item that :<br />
:* issues this command if no option is set<br />
:* is attached on the attach point provided in the option field, ex : @getpath:spine=2222 => "Restraints/Collar"<br />
:* is worn on the clothing layer provided in the option field, ex : @getpath:pants=2222 => "Casual/Jeans/Tight"<br />
Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. It does not take disabled folders into account (folders which name begins with a dot "."). The answer is a list of folders, separated by slashes ('/').<br />
<br />
Please note : As version 1.40.4 is now live on the main grid, wearing several objects on the same attachment point is now possible. Therefore this command does not make much sense anymore since it can only respond with one folder, while the several objects could belong to several folders. Therefore it is better to use @getpathnew, since @getpath will slowly become deprecated as more and more users switch to 2.1 and beyond.<br />
<br />
<br />
* '''''Get the all paths to the shared folders containing the objects/clothing worn on a point''''' : @getpathnew[:<attachpt> or <clothing_layer>]=<channel_number><br />
''Implemented in v2.1 and v1.24''<br />
<br />
Makes the viewer automatically answer the paths to the shared folders containing the item(s) that :<br />
:* issues this command if no option is set<br />
:* are attached on the attach point provided in the option field, ex : @getpathnew:spine=2222 => "Restraints/Collar,Jewelry/Cute necklace"<br />
:* is worn on the clothing layer provided in the option field, ex : @getpathnew:pants=2222 => "Casual/Jeans/Tight"<br />
Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. It does not take disabled folders into account (folders which name begins with a dot "."). The answer is a list of folders, separated by slashes ('/'), if several paths must be returned because several outfits are concerned, they are organized in a list of strings separated by commas (',').<br />
<br />
This command has been added to replace @getpath, since in 2.1 several objects can be worn on the same attachment point.<br />
<br />
<br />
* '''''Force attach items contained into a shared folder that contains a particular object/clothing''''' : @attachthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with an @attach command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, without replacing what is already being worn''''' : @attachthisover[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attachthis described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attachthisoverorreplace[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attachthis described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force attach items contained into a shared folder that contains a particular object/clothing, and its children folders''''' : @attachallthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with an @attachall command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder, without replacing what is already being worn''''' : @attachallthisover[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.1.2 and v1.24''<br />
<br />
This command works exactly like @attachallthis described hereabove, except that it won't kick objects and pieces of clothing that are already being worn.<br />
<br />
<br />
* '''''Force attach items contained inside a shared folder''''' : @attachallthisoverorreplace[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v2.5''<br />
<br />
This command works exactly like @attachallthis described hereabove, it is a synonym.<br />
<br />
<br />
* '''''Force detach items contained into a shared folder that contains a particular object/clothing''''' : @detachthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with a @detach command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Force detach items contained into a shared folder that contains a particular object/clothing, and its children folders''''' : @detachallthis[:<attachpt> or <clothing_layer>]=force (*)<br />
''Implemented in v1.16''<br />
<br />
This command is a shortcut for a @getpath followed with a @detachall command (this saves a listener and a timeout).<br />
<br />
<br />
* '''''Allow/prevent removing some folders''''' : @detachthis[:<layer>|<attachpt>|<path_to_folder>]=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, the user is unable to remove a folder if either of these conditions is filled :<br />
:* no option is specified and the folder contains the object that issues this restriction<br />
:* the "layer" option is set (shirt, pants...) and the folder contains a piece of clothing that is worn on this layer<br />
:* the "attachpt" option is set (l forearm, spine...) and the folder contains an attachment that is worn on this point<br />
:* the "path_to_folder" option is set and the folder corresponds to this location<br />
<br />
Moreso, this folder or these folders cannot be renamed, moved, deleted or modified.<br />
<br />
<br />
* '''''Allow/prevent removing some folders and their children''''' : @detachallthis[:<layer>|<attachpt>|<path_to_folder>]=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
These commands do exactly like @detachthis, but also apply to their children folders recursively.<br />
<br />
<br />
* '''''Allow/prevent wearing some folders''''' : @attachthis:<layer>|<attachpt>|<path_to_folder>=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
When prevented, the user is unable to attach a folder if either of these conditions is filled :<br />
:* the "layer" option is set (shirt, pants...) and the folder contains a piece of clothing that is meant to be worn on this layer<br />
:* the "attachpt" option is set (l forearm, spine...) and the folder contains an attachment that is meant to be worn on this point<br />
:* the "path_to_folder" option is set and the folder corresponds to this location<br />
<br />
Moreso, this folder or these folders cannot be renamed, moved, deleted or modified.<br />
<br />
<br />
* '''''Allow/prevent wearing some folders and their children''''' : @attachallthis[:<layer>|<attachpt>|<path_to_folder>]=<y/n><br />
''Implemented in v2.3 and v1.25''<br />
<br />
These commands do exactly like @attachthis, but also apply to their children folders recursively.<br />
<br />
<br />
* '''''Remove/add exceptions to the detachallthis restriction, for one folder only''''' : "@detachthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can remove the items contained into the indicated folder.<br />
<br />
<br />
* '''''Remove/add exceptions to the detachallthis restriction, for one folder and its children''''' : "@detachallthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can remove the items contained into the indicated folder, or in any of its children.<br />
<br />
<br />
* '''''Remove/add exceptions to the attachallthis restriction, for one folder only''''' : "@attachthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can wear the items contained into the indicated folder.<br />
<br />
<br />
* '''''Remove/add exceptions to the attachallthis restriction, for one folder and its children''''' : "@attachallthis_except:<folder>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can wear the items contained into the indicated folder, or in any of its children.<br />
<br />
<br />
Note : These exceptions will be taken into account only for the restrictions that have been issued by the '''same object''', you cannot put such an exception to a restriction issued by another object.<br />
<br />
Note : The viewer checks which exception or restriction is the "closest parent" in the folders hierarchy to the folder that the user is trying to wear or remove. If the closest is an @attach[all]this_except or a @detach[all]this_except exception , then the folder can be worn or removed respectively. If the closest is an @attach[all]this or a @detach[all]this restriction, then the folder is locked, no matter how many exceptions are pointing on the folders that are parent to this one.<br />
<br />
Example :<br />
A script issues a @attachallthis:=n restriction, preventing the whole #RLV folder and its children from being attached. It also issues a<br />
@detachallthis:=n restriction, preventing the whole #RLV folder and its children from being removed as well.<br />
Therefore the #RLV folder is now completely frozen.<br />
<br />
However, the same object issues a @attachallthis:Jewelry/Gold=add exception, then a @detachallthis:Jewelry/Gold=add one, making the Jewelry/Gold<br />
folder available for wearing and removing.<br />
Finally, it issues a @attachallthis:Jewelry/Gold/Watch=n restriction followed by a @detachallthis:Jewelry/Gold/Watch=n restriction.<br />
As a result, the user can wear and remove only what is contained inside the Jewelry/Gold folder, except what is in Jewelry/Gold/Watch, and the<br />
rest is out of reach.<br />
<br />
===Touch===<br />
<br />
* '''''Allow/prevent touching objects located further than 1.5 meters away from the avatar''''' : @fartouch=<y/n><br />
''Implemented in v1.11''<br />
<br />
When prevented, the avatar is unable to touch/grab objects from more than 1.5 m away, this command makes restraints more realistic since the avatar litterally has to press against the object in order to click on it.<br />
<br />
<br />
* '''''Allow/prevent touching objects located further than 1.5 meters away from the avatar''''' : @touchfar=<y/n><br />
''Implemented in v2.4''<br />
<br />
This command is a synonym of @fartouch<br />
<br />
<br />
* '''''Allow/prevent touching any objects''''' : @touchall=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch/grab any object and attachment. This does not apply to HUDs.<br />
<br />
<br />
* '''''Allow/prevent touching objects in-world''''' : @touchworld=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch/grab objects rezzed in-world, i.e. not attachments and HUDs.<br />
<br />
<br />
* '''''Remove/add exceptions to the touchworld prevention''''' : "@touchworld:<UUID>=<rem/add>"<br />
''Implemented in v2.5''<br />
<br />
When adding an exception, the user can touch this object in particular.<br />
<br />
<br />
* '''''Allow/prevent touching one object in particular''''' : @touchthis:<UUID>=<rem/add><br />
''Implemented in v2.5''<br />
<br />
When prevented, the avatar is unable to touch/grab the object which UUID corresponds to the one specified in the command.<br />
<br />
<br />
* '''''Remove/add an exception to the touch* preventions, for one object only''''' : "@touchme=<rem/add>"<br />
''Implemented in v2.6''<br />
<br />
When adding such an exception, the user can touch this object in particular.<br />
<br />
<br />
* '''''Allow/prevent touching attachments''''' : @touchattach=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch attachments (theirs and other avatars'), but this does not apply to HUDs.<br />
<br />
<br />
* '''''Allow/prevent touching one's attachments''''' : @touchattachself=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch their own attachments (theirs but can touch other people's), but this does not apply to HUDs.<br />
<br />
<br />
* '''''Allow/prevent touching other people's attachments''''' : @touchattachother=<y/n><br />
''Implemented in v2.4''<br />
<br />
When prevented, the avatar is unable to touch other people's attachments (but they can touch their owns). This does not apply to HUDs.<br />
<br />
===Location===<br />
<br />
* '''''Allow/prevent viewing the world map''''' : @showworldmap=<y/n><br />
''Implemented in v1.11''<br />
<br />
When prevented, the avatar is unable to view the world map, and it closes if it is open when the restriction becomes active.<br />
<br />
<br />
* '''''Allow/prevent viewing the mini map''''' : @showminimap=<y/n><br />
''Implemented in v1.11''<br />
<br />
When prevented, the avatar is unable to view the mini map, and it closes if it is open when the restriction becomes active.<br />
<br />
<br />
* '''''Allow/prevent knowing the current location''''' : @showloc=<y/n><br />
''Implemented in v1.12''<br />
<br />
When prevented, the user is unable to know where they are : the world map is hidden, the parcel and region name on the top menubar are hidden, they can't create landmarks, nor buy the land, nor see what land they have just left after a teleport, nor see the location in the About box, and even system and object messages are obfuscated if they contain the name of the region and/or the name of the parcel. However, [[llOwnerSay]] calls are ''not'' obfuscated so radars ''will'' still work (and RL commands as well).<br />
<br />
===Name Tags and Hovertext===<br />
<br />
* '''''Allow/prevent seeing the names of the people around''''' : @shownames=<y/n><br />
''Implemented in v1.12.2, added more dummy names in v1.16''<br />
<br />
When prevented, the user is unable to know who is around. The names don't show on the screen, the names on the chat are replaced by "dummy" names such as "Someone", "A resident", the tooltips are hidden, the pie menu is almost useless so the user can't get the profile directly etc.<br />
<br />
<br />
* '''''Allow/prevent seeing all the hovertexts''''' : @showhovertextall=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read any hovertext (2D text floating above some prims).<br />
<br />
<br />
* '''''Allow/prevent seeing one hovertext in particular''''' : @showhovertext:<UUID>=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read the hovertext floating above the prim which id is UUID. This is made that way so that the restriction can be issued on an object, by another one (unlike @detach which can only set this restriction on itself).<br />
<br />
<br />
* '''''Allow/prevent seeing the hovertexts on the HUD of the user''''' : @showhovertexthud=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read any hovertext showing over their HUD objects, but will be able to see the ones in-world.<br />
<br />
<br />
* '''''Allow/prevent seeing the hovertexts in-world''''' : @showhovertextworld=<y/n><br />
''Implemented in v1.19''<br />
<br />
When prevented, the user is unable to read any hovertext showing over their in-world objects, but will be able to see the ones over their HUD.<br />
<br />
===Group===<br />
<br />
* '''''Force the agent to change the active group''''' : @setgroup:<group_name>=force<br />
''Implemented in v2.5''<br />
<br />
Forces the agent to change the active group, to the specified one. Of course, they must already be a member of this group. If <group_name> is "none", then the agent will deactivate the current group and not show any group tag at all.<br />
<br />
<br />
* '''''Allow/prevent activating a group''''' : @setgroup=<y/n><br />
''Implemented in v2.5''<br />
<br />
When prevented, the user is unable to change the active group.<br />
<br />
<br />
* '''''Get the name of the active group''''' : @getgroup=<channel_number><br />
''Implemented in v2.5''<br />
<br />
Makes the viewer automatically answer the name of the currently active group, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer will simply be "none" if no group is active at the time. Please note that there is no way to obtain the UUID of the group, only the name.<br />
<br />
<br />
===Viewer Control===<br />
<br />
* '''''Allow/prevent changing some debug settings''''' : @setdebug=<y/n><br />
''Implemented in v1.16''<br />
<br />
When prevented, the user is unable to change some viewer debug settings (Advanced > Debug Settings). As most debug settings are either useless or critical to the user's experience, a whitelist approach is taken : only a few debug settings are locked, the others are always available and untouched. At the time of this writing, the allowed debug settings are :<br />
:* AvatarSex (0 : Female, 1 : Male) : gender of the avatar at creation.<br />
:* RenderResolutionDivisor (1 -> ...) : "blurriness" of the screen. Combined to clever @setenv commands, can simulate nice effects. Note: renderresolutiondivisor is a Windlight only option (Basic Shaders must be enabled in graphics preferences) and as such, is not available in v1.19.0.5 or older viewers.<br />
<br />
* '''''Force change a debug setting''''' : @setdebug_<setting>:<value>=force (*)<br />
''Implemented in v1.16''<br />
<br />
Forces the viewer to change a particular debug setting and set it to <value>. This command is actually a package of many sub-commands, that are regrouped under "@setdebug_...", for instance "@setdebug_avatarsex:0=force", "@setdebug_renderresolutiondivisor:64=force" etc.<br />
<br />
See the list of allowed debug settings in the @setdebug command hereabove.<br />
<br />
<br />
* '''''Get the value of a debug setting''''' : @getdebug_<setting>=<channel_number><br />
''Implemented in v1.16''<br />
<br />
Makes the viewer automatically answer the value of a debug setting, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is the value that has been set with the <setting> part of the matching @setdebug command, or by hand.<br />
<br />
See the list of allowed debug settings in the @setdebug command hereabove.<br />
<br />
<br />
* '''''Allow/prevent changing the environment settings''''' : @setenv=<y/n><br />
''Implemented in v1.14''<br />
<br />
When prevented, the user is unable to change the viewer environment settings (World > Environment Settings > Sunrise/Midday/Sunset/Midnight/Revert to region default/Environment editor are all locked out).<br />
<br />
<br />
* '''''Force change an environment setting''''' : @setenv_<setting>:<value>=force (*)<br />
''Implemented in v1.14''<br />
<br />
Forces the viewer to change a particular environment setting (time of day or Windlight) and set it to <value>. This command is actually a package of many sub-commands, that are regrouped under "@setenv_...", for instance "@setenv_daytime:0.5=force", "@setenv_bluehorizonr:0.21=force" etc.<br />
<br />
This command (like any other "force" command) is silently discarded if the corresponding restriction has been set, here "@setenv", but in this case the restriction is ignored if the change is issued from the object that has created it. In other words, a collar can restrict environment changes, yet force a change by itself, while another object could not do it until the collar lifts the restriction.<br />
<br />
Although a range is specified for every value, no check is made in the viewer so a script can do what the UI can't do, for interesting effects. Use at your own risk, though. The ranges indicated here are merely the ones available on the sliders on the Environment Editor, for reference.<br />
<br />
Each particular sub-command works as follows (the names are chosen to be as close to the Windlight panels of the viewer as possible) :<br />
<br />
{| border="1" cellpadding="5"<br />
| '''@setenv_XXX:<value>=force where XXX is...''' || '''<value> range''' || '''Sets...'''<br />
|-<br />
| daytime || 0.0-1.0 and <0 || Time of day (sunrise:0.25, midday:0.567, sunset:0.75, midnight:0.0, set back to region default:<0). '''Attention, resets all other Windlight parameters'''<br />
|-<br />
| preset || String || A Preset environment, e.g. Gelatto, Foggy. '''Attention, loading a Preset is heavy on the viewer and can slow it down for a short while, don't do it every second'''<br />
|-<br />
| ambientr || 0.0-1.0 || Ambient light, Red channel<br />
|-<br />
| ambientg || 0.0-1.0 || Ambient light, Green channel<br />
|-<br />
| ambientb || 0.0-1.0 || Ambient light, Blue channel<br />
|-<br />
| ambienti || 0.0-1.0 || Ambient light, Intensity<br />
|-<br />
| bluedensityr || 0.0-1.0 || Blue Density, Red channel<br />
|-<br />
| bluedensityg || 0.0-1.0 || Blue Density, Green channel<br />
|-<br />
| bluedensityb || 0.0-1.0 || Blue Density, Blue channel<br />
|-<br />
| bluedensityi || 0.0-1.0 || Blue Density, Intensity<br />
|-<br />
| bluehorizonr || 0.0-1.0 || Blue Horizon, Red channel<br />
|-<br />
| bluehorizong || 0.0-1.0 || Blue Horizon, Green channel<br />
|-<br />
| bluehorizonb || 0.0-1.0 || Blue Horizon, Blue channel<br />
|-<br />
| bluehorizoni || 0.0-1.0 || Blue Horizon, Intensity<br />
|-<br />
| cloudcolorr || 0.0-1.0 || Cloud color, Red channel<br />
|-<br />
| cloudcolorg || 0.0-1.0 || Cloud color, Green channel<br />
|-<br />
| cloudcolorb || 0.0-1.0 || Cloud color, Blue channel<br />
|-<br />
| cloudcolori || 0.0-1.0 || Cloud color, Intensity<br />
|-<br />
| cloudcoverage || 0.0-1.0 || Cloud coverage<br />
|-<br />
| cloudx || 0.0-1.0 || Cloud offset X<br />
|-<br />
| cloudy || 0.0-1.0 || Cloud offset Y<br />
|-<br />
| cloudd || 0.0-1.0 || Cloud density<br />
|-<br />
| clouddetailx || 0.0-1.0 || Cloud detail X<br />
|-<br />
| clouddetaily || 0.0-1.0 || Cloud detail Y<br />
|-<br />
| clouddetaild || 0.0-1.0 || Cloud detail density<br />
|-<br />
| cloudscale || 0.0-1.0 || Cloud scale<br />
|-<br />
| cloudscrollx || 0.0-1.0 || Cloud scroll X<br />
|-<br />
| cloudscrolly || 0.0-1.0 || Cloud scroll Y<br />
|-<br />
| densitymultiplier || 0.0-0.9 || Density multiplier of the fog<br />
|-<br />
| distancemultiplier || 0.0-100.0 || Distance multiplier of the fog<br />
|-<br />
| eastangle || 0.0-1.0 || Position of the east, 0.0 is normal<br />
|-<br />
| hazedensity || 0.0-1.0 || Density of the haze<br />
|-<br />
| hazehorizon || 0.0-1.0 || Haze at the horizon<br />
|-<br />
| maxaltitude || 0.0-4000.0 || Maximum altitude of the fog<br />
|-<br />
| scenegamma || 0.0-10.0 || Overall gamma, 1.0 is normal<br />
|-<br />
| starbrightness|| 0.0-2.0 || Brightness of the stars<br />
|-<br />
| sunglowfocus || 0.0-0.5 || Focus of the glow of the sun<br />
|-<br />
| sunglowsize || 1.0-2.0 || Size of the glow of the sun<br />
|-<br />
| sunmooncolorr || 0.0-1.0 || Sun and moon, Red channel<br />
|-<br />
| sunmooncolorg || 0.0-1.0 || Sun and moon, Green channel<br />
|-<br />
| sunmooncolorb || 0.0-1.0 || Sun and moon, Blue channel<br />
|-<br />
| sunmooncolori || 0.0-1.0 || Sun and moon, Intensity<br />
|-<br />
| sunmoonposition || 0.0-1.0 || Position of the sun/moon, different from "daytime", '''use this to set the apparent sunlight after loading a Preset'''<br />
|}<br />
<br />
Note: from the above settings, only the "daytime" one is supported by v1.19.0 (or older) viewers implementing RestrainedLove v1.14 and later. The other settings are ignored. This is because these viewers do not implement the Windlight renderer.<br />
<br />
* '''''Get the value of an environment setting''''' : @getenv_<setting>=<channel_number><br />
''Implemented in v1.15''<br />
<br />
Makes the viewer automatically answer the value of an environment setting, immediately on the chat channel number <channel_number> that the script can listen to. Always use a positive integer. Remember that regular viewers do not answer anything at all so remove the listener after a timeout. The answer is the value that has been set with the <setting> part of the matching @setenv command, or by hand. See the table hereabove for a list of settings.<br /><br />
Note: only @getenv_daytime is supported by v1.19.0 (or older, i.e. non Windlight) viewers implementing RestrainedLove v1.15 and later.<br />
<br />
===Unofficial Commands===<br />
Certain viewers use a different restriction system, based on the RestrainedLove API, which includes a number of extra commands. These extra commands are '''not''' part of the RestrainedLove API specification, but are documented here for convenience. These commands should not be relied on, as only certain viewers will be able to process them.<br />
<br />
* '''''Allow/prevent touching HUDs''''' : @touchhud[:<UUID>]=<y/n><br />
<br />
When prevented, the avatar is unable to touch any HUDs. If sent with a UUID, the avatar is prevented from touching only the HUD indicated by the UUID.<br />
<br />
* '''''Allow/prevent touching objects or attachments, editing, or rezzing''''' : @interact=<y/n><br />
<br />
When prevented, the avatar is unable to touch any objects, attachments, or HUDs, and cannot edit or rez.<br />
<br />
* '''''Allow/prevent the disabling of the automatic Away/AFK indicator''''' : @allowidle=<y/n><br />
<br />
When prevented, the automatic activation of the Away status indicator after a period of avatar inactivity cannot be disabled. If the idle timeout duration has been set to zero, the default timeout of 30 minutes will be used.<br />
<br />
===Footnotes===<br />
<br />
(*) Silently discarded if the user is prevented from doing so by the corresponding restriction. This is on purpose.<br />
Ex : Force detach won't work if the object is undetachable. Force undress won't work if the user is prevented from undressing.<br />
<br />
==Important note about the global behaviours such as sendchat==<br />
Such behaviours are global, which means they don't depend on a particular object. However, they are triggered by objects with a set [[UUID]] which can change, and several objects can add the same behaviour, which will be stored several times as the [[UUID]]s are different. <br />
<br />
This has a nice side effect : when wearing 2 locked devices that prevent chat, it is necessary to unlock them both to be able to chat again. But it has a nasty side effect, too : if the item changes [[UUID]] (for instance it was derezzed and rezzed again), and it doesn't allow chat beforehand, then the user will have to wait a short moment because the rule stays "orphaned" (its [[UUID]] is defunct) until the '''garbage collector''' kicks in.<br />
<br />
Please note : Since 1.16.1 any locked object that is kicked off by any mean (llAttachToAvatar for example) will be reattached automatically by the viewer after a few seconds. This means that calling @clear on detaching will actually unlock the object, which will have to be relocked after being reattached. It is therefore not recommended anymore to call @clear on detach, as opposed to pre-1.16.1 versions.<br />
<br />
==Shared Folders==<br />
<br />
Since v1.11, the viewer can "share" some of your items with scripts in world in order to let them force you to attach, detach and list what you have shared.<br />
<br />
"Share" does NOT mean they will be taken by other people if they want to (some of the items may be no-transfer anyway), but only that they can force YOU to wear/unwear them at will through the use of a script YOUR restraints contain. They will remain in your inventory. In fact, this feature would be best named "Exposed folder".<br />
<br />
To do this :<br />
* Create a folder named "#RLV" (without the quotes) directly under "My Inventory" (right-click on "My Inventory", select "New Folder"). We'll call this folder the "shared root".<br />
* Move a folder containing restraints or other attachments directly into this new folder.<br />
* Wear the contents of that folder, that's it !<br />
<br />
So it would look like this :<br />
<br />
My Inventory<br />
|- #RLV<br />
| |- cuffs<br />
| | |- left cuff (l forearm) (no copy)<br />
| | \- right cuff (r forearm) (no copy)<br />
| \- gag<br />
| \- gag (mouth) (no copy)<br />
|- Animations<br />
|- Body Parts<br />
.<br />
.<br />
.<br />
<br />
For example : If you're owning a set of RR Straps and want to share them, just move the folder "Straps BOXED" under the shared root.<br />
<br />
Either wear all the items of the folders you have just moved (one folder at a time !) or rename your items yourself, so that each item name contains the name of the target attachment point. For example : "left cuff (l forearm)", "right ankle cuff (r lower leg)". Please note that no-modify items are a bit more complex to share, because they cannot be renamed either by you or by the viewer. More on that below.<br />
<br />
The attachment point name is the same as the one you find in the "Attach To" menu of your inventory, and is case insensitive (for example : "chest", "skull", "stomach", "left ear", "r upper arm"...). If you wear the item without renaming it first it will be renamed automatically, but only if it is in a shared folder, and does not contain any attachment point name already, and is mod. If you want to wear it on another attachment point, you'll need to rename it by hand first.<br />
<br />
Pieces of clothing are treated exactly the same way (in fact they can even be put in the folder of a set of restraints and be worn with the same command). Shoes, for instance, are a good example of mixed outfits : some attachments and the Shoes layer. Clothes are NOT renamed automatically when worn, since their very type decides where they are to be worn (skirt, jacket, undershirt...).<br />
<br />
HOW TO SHARE NO-MODIFY ITEMS :<br />
As you already know, no-mod items cannot be renamed so the technique is a bit more complex. Create a sub-folder inside the outfit folder (such as "cuffs" in the example above), put ONE no-modify item in it. When wearing the object, you'll see the folder itself be renamed (that's why you must not put more than one object inside it). So if your outfit contains several no-mod objects, you'll need to create as many folders and put the no-mod objects in them, one in each folder.<br />
<br />
Example with no-modify shoes :<br />
<br />
My Inventory<br />
|- #RLV<br />
| \- shoes<br />
| |- left shoe (left foot)<br />
| | \- left shoe (no modify) (no transfer) <-- no-mod object<br />
| |- right shoe (right foot)<br />
| | \- right shoe (no modify) (no transfer) <-- no-mod object<br />
| \- shoe base (no modify) (no transfer) <-- this is not an object<br />
|- Animations<br />
|- Body Parts<br />
.<br />
.<br />
.<br />
<br />
GOTCHAS :<br />
* Do NOT put a comma (',') in the name of the folders under the shared root or it would screw the list up.<br />
* Don't forget to rename the items in the shared folders (or to wear these items at least once to have them be renamed automatically) or the force attach command will appear to do nothing at all.<br />
* Avoid cluttering the shared root with many folders, since some scripts may rely on the list they got with the @getinv command and chat messages are limited to 1023 characters. Choose wisely, and use short names. But with 9 characters per folder name average, you can expect to have about 100 folders available.<br />
* Remember to put no-modify items in sub-folders, one each, so their names can be used by the viewer do find out where to attach them. They can't be shared like modify items since they can't be renamed, and the outfit folder itself will not be renamed (since it contains several items).<br />
<br />
<br />
'''''Accept sub-folders given via llGiveInventoryList() into the shared folder''''' :<br />
<br />
Starting with RestrainedLove v1.16.2, you may give a list of items to a victim and have them stored as a sub-folder inside the #RLV folder (thus allowing you to @attach the given items later).<br />
<br />
Issuing a llGiveInventoryList(victim_id, "#RLV/~subfolder_name", list_of_stuff) command in a script makes a standard Keep/Discard/Mute dialog appear in the viewer of the victim (the avatar which key is victim_id).<br />
<br />
Should the victim accept the offer, the list_of_stuff items are put into a new sub-folder of the #RLV folder. The name of this sub-folder is "~subfolder_name" (it is the scripter's responsibility to use unique sub-folder names: if the name is the same as an existing sub-folder, two sub-folders with the same name will appear in the #RLV folder).<br />
<br />
Note that the tilde character *must* be used as the first character for the name of the sub-folder (this is so that the victim can easily spot any sub-folder given to them in this way, and so that such sub-folder names appear last in the #RLV folder).<br />
<br />
Note also that this feature may be disabled by the user, (by setting the RestrainedLoveForbidGiveToRLV debug setting to TRUE): in this case the given items are put into a folder named "#RLV/~subfolder_name" at the root of the inventory instead of inside the #RLV folder.<br />
<br />
Since the user may either refuse the offer or have the feature disabled in their viewer, and since SL may take quite some time to perform the actual transfer of the objects on laggy days, you must check that the given folder is present (with @getinv), before attempting to @attach the given objects.<br />
<br />
==For your information==<br />
Here is how it works internally, for a better understanding of the gotchas you may encounter :<br />
* Each command is parsed into a '''Behaviour''' (ex: "remoutfit"), an '''Option''' (ex: "shirt") and a '''Param''' (ex: "force") and comes from an [[UUID]] (the unique identifier of the emitting object).<br />
<br />
* There are two types of commands : '''one-shot''' commands (those which Param is "force" and those which Param is a number such as the channel number of a "version" call) and '''rules''' (those which Param is "y", "n", "add" or "rem"). "clear" is special but can be seen as a one-shot command.<br />
<br />
* When the command takes a channel number, this number may be either strictly positive or (for RestrainedLove v1.23a (@versionnum = 1230001) and above) strictly negative. Using channel 0 is not allowed. Note that RestrainedLove can send a maximum of 255 characters on negative channels, while it can send up to 1023 characters on positive channels. Negative channels are useful to prevent the user from cheating, for example when asking for the @versionnum (since the user could use a non-RestrainedLove viewer and make the RestrainedLove devices believe they run within a RestrainedLove viewer by spoofing the reply to the version command on a positive channel, which they can't do on negative channels). Positive channels are best used for commands that may return large reply strings (@getpath, for example).<br />
<br />
* Parameters "n" and "add" are '''exactly equal''' and are treated '''exactly the same way''', they are just '''synonyms'''. Same goes for "y" and "rem". The only purpose is to distinguish rules ("sendchannel=n") from exceptions ("sendchannel:8=add") in a script for clarity.<br />
<br />
* Rules are stored inside a table linking the [[UUID]] of the emitter to the rule itself. They are '''added''' when receiving a "n"/"add" Param, and '''removed''' when receiving a "y"/"rem" Param.<br />
If '''''UUID1''''' is a collar and '''''UUID2''''' is a gag :<br />
<br />
'''''UUID1''''' => detach, tploc, tplm, tplure, sittp<br />
<br />
'''''UUID2''''' => detach, sendim, sendim:(keyholder)<br />
<br />
Those two rules mean that the user cannot send IMs except to their keyholder, and cannot TP at all. Those two items are not detachable. Now if the collar sends "@sendim=n", the table becomes :<br />
<br />
'''''UUID1''''' => detach, tploc, tplm, tplure, sittp, sendim<br />
<br />
'''''UUID2''''' => detach, sendim, sendim:(keyholder)<br />
<br />
If it sends "@sendim=n" a second time nothing changes, as there is a check for its existence prior to adding it. If the gag is unlocked and detached, either it sends a "@clear" or the garbage collector kicks in so the rules linked to '''''UUID2''''' disappear. However, the avatar is still unable to send IMs even to their keyholder, as the exception has disappeared as well. This is because rules linked to one object don't conflict with rules linked to another one.<br />
<br />
* One-shot commands, on the other hand, are executed on-the-fly and are not stored.<br />
<br />
* When logging on, the avatar stays non-operational (cannot chat, cannot move) for some time, while the user sees the progress bar. However, worn scripted objects [[rez]] in the meantime and start sending rules and commands before the viewer can execute them. Therefore it stores them in a buffer and executes them only when the user is given controls (when the progress bar disappears).<br />
<br />
* The viewer periodically (every N seconds) checks all its rules and removes the ones linked to an [[UUID]] that does not exist around anymore ("garbage collector"). This means that rules issued by an '''unworn''' owned object will be discarded as soon as the avatar [[teleport|teleports]] elsewhere.<br />
<br />
[[Category:Third Party Client]]<br />
[[Category:RestrainedLove]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlSetObjectName&diff=1186860LlSetObjectName2014-01-13T11:32:41Z<p>Felis Darwin: Fix link</p>
<hr />
<div>{{LSL Function<br />
|inject-2={{LSL_Function/limits}}<br />
|func_id=203<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llSetObjectName|sort=SetObjectName<br />
|p1_type=string<br />
|p1_name=name<br />
|func_desc=Sets the prim's name according to the {{LSLP|name}} parameter.<br />
|func_footnote=If this function is called from a child prim in a linked set, it will change the name of the child prim and not the root prim.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* The name is limited to 63 characters. Longer prim names are cut short.<br />
* Names can only consist of the 95 printable characters found in the [http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters ASCII-7] (non-extended) character set.<br />
** Non-ASCII characters will be replaced with two question marks ("??").<br />
* While an object is attached, the script cannot change the name of the object as it appears in the user's inventory.{{Footnote|2=Whether or not this is a bug is still debated, regardless LL has said they will not fix it. This behavior was first recognized as a pain point in 2006 and only confirmed to be lava-flowed more recently (2011).|1=Whether or not this is a bug is still debated, regardless LL has said they will not fix it. This behavior was first recognized as a pain point in [http://forums-archive.secondlife.com/255/ab/84853/1.html 2006] and only confirmed to be lava-flowed [https://jira.secondlife.com/browse/SVC-3429?focusedCommentId=267372&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-267372 more recently (2011)].}}<br />
** Changes to the name of the root prim (with [[llSetObjectName]] for example) will not be saved to inventory; when the attachment is detached (to inventory, not dropped) this name change is discarded and the name in inventory is used instead.<br />
** When attachment is dropped (to the ground) and taking it into inventory, the inventory item will have the new name (not the old).<br />
* Changes to the names of child prims will be saved back to inventory when the object is detached to inventory. They survive detachment.<br />
|constants<br />
|examples=<br />
{{{!}} class="sortable" width="100%" {{Prettytable}}<br />
{{!}}- {{Hl2}}<br />
! '''Set this prim's name'''<br />
! '''Set the root prim's name'''<br />
{{!}}-<br />
{{!!}}<lsl><br />
default<br />
{<br />
state_entry()<br />
{<br />
llSetObjectName("NEW PRIM NAME");<br />
}<br />
}<br />
</lsl><br />
{{!!}}<br />
<lsl><br />
default<br />
{<br />
state_entry()<br />
{<br />
llSetLinkPrimitiveParamsFast(LINK_ROOT,<br />
[PRIM_NAME, "NEW ROOT PRIM NAME"]);<br />
}<br />
}<br />
</lsl><br />
{{!}}}<br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llGetObjectName]]|Get the prims name}}<br />
{{LSL DefineRow||[[llGetLinkName]]|Get a linked prims name}}<br />
{{LSL DefineRow||[[llGetObjectDesc]]|Get the prims description}}<br />
{{LSL DefineRow||[[llSetObjectDesc]]|Set the prims description}}<br />
{{LSL DefineRow||[[llGetObjectDetails]]|Get a list of object details}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Prim<br />
|cat2<br />
|cat3<br />
|cat4<br />
|}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlSetObjectName&diff=1186859LlSetObjectName2014-01-13T11:31:18Z<p>Felis Darwin: Update wiki link to go to proper section</p>
<hr />
<div>{{LSL Function<br />
|inject-2={{LSL_Function/limits}}<br />
|func_id=203<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llSetObjectName|sort=SetObjectName<br />
|p1_type=string<br />
|p1_name=name<br />
|func_desc=Sets the prim's name according to the {{LSLP|name}} parameter.<br />
|func_footnote=If this function is called from a child prim in a linked set, it will change the name of the child prim and not the root prim.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* The name is limited to 63 characters. Longer prim names are cut short.<br />
* Names can only consist of the 95 printable characters found in the {{wikipedia|ASCII#ASCII_printable_characters|ASCII-7}} (non-extended) character set.<br />
** Non-ASCII characters will be replaced with two question marks ("??").<br />
* While an object is attached, the script cannot change the name of the object as it appears in the user's inventory.{{Footnote|2=Whether or not this is a bug is still debated, regardless LL has said they will not fix it. This behavior was first recognized as a pain point in 2006 and only confirmed to be lava-flowed more recently (2011).|1=Whether or not this is a bug is still debated, regardless LL has said they will not fix it. This behavior was first recognized as a pain point in [http://forums-archive.secondlife.com/255/ab/84853/1.html 2006] and only confirmed to be lava-flowed [https://jira.secondlife.com/browse/SVC-3429?focusedCommentId=267372&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-267372 more recently (2011)].}}<br />
** Changes to the name of the root prim (with [[llSetObjectName]] for example) will not be saved to inventory; when the attachment is detached (to inventory, not dropped) this name change is discarded and the name in inventory is used instead.<br />
** When attachment is dropped (to the ground) and taking it into inventory, the inventory item will have the new name (not the old).<br />
* Changes to the names of child prims will be saved back to inventory when the object is detached to inventory. They survive detachment.<br />
|constants<br />
|examples=<br />
{{{!}} class="sortable" width="100%" {{Prettytable}}<br />
{{!}}- {{Hl2}}<br />
! '''Set this prim's name'''<br />
! '''Set the root prim's name'''<br />
{{!}}-<br />
{{!!}}<lsl><br />
default<br />
{<br />
state_entry()<br />
{<br />
llSetObjectName("NEW PRIM NAME");<br />
}<br />
}<br />
</lsl><br />
{{!!}}<br />
<lsl><br />
default<br />
{<br />
state_entry()<br />
{<br />
llSetLinkPrimitiveParamsFast(LINK_ROOT,<br />
[PRIM_NAME, "NEW ROOT PRIM NAME"]);<br />
}<br />
}<br />
</lsl><br />
{{!}}}<br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llGetObjectName]]|Get the prims name}}<br />
{{LSL DefineRow||[[llGetLinkName]]|Get a linked prims name}}<br />
{{LSL DefineRow||[[llGetObjectDesc]]|Get the prims description}}<br />
{{LSL DefineRow||[[llSetObjectDesc]]|Set the prims description}}<br />
{{LSL DefineRow||[[llGetObjectDetails]]|Get a list of object details}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Prim<br />
|cat2<br />
|cat3<br />
|cat4<br />
|}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=User:Felis_Darwin&diff=1177813User:Felis Darwin2013-04-13T18:02:09Z<p>Felis Darwin: </p>
<hr />
<div>If you're looking for me, you're probably interested in something listed below:<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Felis Darwin's Amethyst Plugin]]<br />
<br />
[[User:Felis Darwin/Random Tidbits]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=User:Felis_Darwin/Random_Tidbits&diff=1177812User:Felis Darwin/Random Tidbits2013-04-13T18:01:43Z<p>Felis Darwin: Created page with "This is just a repository for whatever script-related stuff I come up with that is worth noting. -- ~~~ ==Unicode equivalents== Many standard English ASCII characters have Unico…"</p>
<hr />
<div>This is just a repository for whatever script-related stuff I come up with that is worth noting. -- [[User:Felis Darwin|Felis Darwin]]<br />
<br />
==Unicode equivalents==<br />
Many standard English ASCII characters have Unicode characters which appear visually identical. Much of this comes down to the font used when comparing the characters. After comparing visuals on most TPVs and the SecondLife official viewer, I've come up with a list of ASCII characters and Unicode characters which appear visually identical within the SecondLife viewer.<br />
<br />
Because this is taken from scripts I've been working on, the following lists are presented as simple strings. The order is the ASCII character, followed by the Unicode one:<br />
<br />
; Visually identical - "aаAАBВcϲCϹdⅾeеEΕgɡHНiіIΙjϳJЈKΚMМNΝoοOΟpрPРqԛQԚsѕSЅTТwԝWԜxхXΧyуYΥ,‚ !ǃ-‐"<br />
<br />
The above character pairs look ''exactly'' the same when viewed in SecondLife. The only way to tell the difference is to compare hexcodes or examine the text outside of SL.<br />
<br />
; Visually similar - <nowiki>"bḅDḊfḟFḞGḠhḣkḱlḻLḶmḿnṅrṙRṘtṫuṳUṲvṿVṾzẓZẒ.․?⁇:∶00112233445566778899\""##'’&&(❨)❩*⁕++$$%%"</nowiki><br />
<br />
This next set of pairs are very close in appearance to each other, but still have notable differences. For letters this is mostly a matter of phonetic markings added to the character which most residents will ignore, if noticed at all. Punctuation marks and numerals are somewhat more obvious, with many of them having no alias other than the fixed width versions (e.g. 8 and 8, $ and $, etc.). Also note that the single backslash before the quotation mark (\") is present under the assumption that this is a string encased by quotes within an LSL script.</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlEmail&diff=1176709LlEmail2013-02-13T19:43:28Z<p>Felis Darwin: Undo revision 1176706 by Felis Darwin (Talk) Whoops, didn't read carefully enough</p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SVC-23}}{{Issues/SVC-391}}{{Issues/SVC-412}}{{Issues/SCR-499}}<br />
|func_id=119|func_sleep=20.0|func_energy=10.0<br />
|sort=Email|func=llEmail<br />
|p1_type=string|p1_name=address<br />
|p2_type=string|p2_name=subject<br />
|p3_type=string|p3_name=message<br />
|func_footnote=The entire message (including the address, subject and other miscellaneous fields) can't be longer than 4096 bytes combined.<br />
|func_desc=Sends an email to {{LSLP|address}} with {{LSLP|subject}} and {{LSLP|message}}.<br />
|return_text<br />
|spec=The {{LSLP|message}} is prefixed with information about the prim sending the email.<br />
{{{!}}{{Prettytable}}<br />
{{!}}-{{Hl2}}<br />
!Template<br />
!Example<br />
{{!}}-<br />
{{!}}<pre><br />
Object-Name: *prim*<br />
Region: *simname* (*simpos.x*, *simpos.y*)<br />
Local-Position: (*primpos.x*, *primpos.y*, *primpos.z*)<br />
<br />
*message*<br />
</pre><br />
{{!}}<br />
<pre><br />
Object-Name: Object<br />
Region: Gibson (254976, 256000)<br />
Local-Position: (117, 129, 50)<br />
<br />
The real message starts here.<br />
</pre><br />
{{!}}}<br />
|caveats=* There is a limit to the number of email messages an object can send in a given amount of time. <br />
* There is a limit of 500 messages from a single agent's objects in a one hour period.<br />
* The 4096 byte size limit includes the subject line and automatically added text. The practical maximum body size is approximately 3600 bytes.<br />
* (Sept-2008) The Email Throttle was modified slightly, Per {{User|Prospero Linden}}'s comments: "there has long been a throttle that makes a single script sleep for 20 seconds after sending an email. The new throttle is per user... some were using many, many different scripts to send spam. (the new throttle applies) when the destination is outside of Second Life. I know that messages within the same region were not throttled (beyond the 20-second delay), and I *believe* that messages between different sims were not throttled (between the 20-second delay)."<br />
* Due to the bug {{Jira|SVC-23}} (present since 2005), objects may stop receiving emails completely until either the region is restarted or the object crosses a region boundary (resetting the script doesn't help). Emails sent may eventually be received after a restart/region-cross. Hence, don't rely on this function for reliable inter-region messaging. <br />
* Due to the bug {{Jira|SVC-391}} llEmail will silently fail (no mail will arrive) when non-ascii characters are present in the subject. However, non-ascii characters in the message body will be replaced by "?".<br />
|constants<br />
|examples=<br />
<lsl><br />
string emailAddress = "somebody@example.com";<br />
string emailHeader = "Someone touched me!";<br />
<br />
<br />
default<br />
{<br />
touch_start(integer num_detected)<br />
{<br />
// llSay(PUBLIC_CHANNEL, "Sending eMail report now, this will take ~20 seconds.");<br />
<br />
key id = llDetectedKey(0);<br />
string name = llDetectedName(0);<br />
<br />
llEmail(emailAddress, emailHeader,<br />
"I was touched by: '" + name + "' (" + (string)id + ").");<br />
<br />
// llSay(PUBLIC_CHANNEL, "Email has been sent.");<br />
}<br />
}<br />
</lsl><br />
|also_functions=<br />
{{LSL DefineRow||[[llGetNextEmail]]}}<br />
{{LSL DefineRow||[[llMessageLinked]]}}<br />
|also_events=<br />
{{LSL DefineRow||[[email]]}}<br />
{{LSL DefineRow||[[link message]]}}<br />
|also_tests={{LSL DefineRow|[https://osiris.lindenlab.com/mediawiki/index.php/Email_Test internal test]}}<br />
|also_articles={{LSL DefineRow||[[IM to email]]}}<br />
{{LSL DefineRow||[[Postcards]]}}<br />
|notes=* Because of the long delay on this function, it is often called from a second script triggered by [[link_message]].<br />
* If you are sending email to a prim within Second Life, its address is ''[key]''@lsl.secondlife.com<br />
** Which means if the key returned by [[llGetKey]] is "a2e76fcd-9360-4f6d-a924-000000000003", then its email address is "a2e76fcd-9360-4f6d-a924-000000000003@lsl.secondlife.com".<br />
** Agents do not have fixed email addresses, use [[llInstantMessage]] or [[llOwnerSay]].<br />
<br />
===Prim2Prim Email===<br />
<br />
In LSL you can both send email with llEmail and receive it with the [[email]] event.<br />
<br />
<br />
The email event is triggered with 5 pieces of information.<br />
{{{!}}<br />
{{LSL DefineRow|string|time|When the message was sent, in the <code>(string)[[llGetUnixTime]]</code> format}}<br />
{{LSL DefineRow|string|address|Who sent the message}}<br />
{{LSL DefineRow|string|subject|Subject of the message}}<br />
{{LSL DefineRow|string|message|Body of the message}}<br />
{{LSL DefineRow|integer|num_left|The number of emails left in the email queue}}<br />
{{!}}}<br />
<br />
<br />
When receiving a message sent with [[llEmail]] it helps to separate the message from the prefixed header. The header and original message body are separated by "\n\n"<br />
<br />
<lsl>integer divide = llSubStringIndex(message, "\n\n");<br />
string header = llDeleteSubString(message, divide, -1);<br />
message = llDeleteSubString(message, 0, divide + 1);</lsl><br />
<br />
To get just 1 of the header items, do this:<br />
<lsl>list lines = llParseStringKeepNulls(header, ["\n"], []);<br />
string objname_line = llList2String(lines, 0);<br />
string region_line = llList2String(lines, 1);<br />
string localpos_line = llList2String(lines, 2);</lsl><br />
<br />
To get a pure region name, do this:<br />
<lsl>string region_name = llStringTrim(<br />
(string)llDeleteSubList(<br />
llParseStringKeepNulls(<br />
llDeleteSubString(region_line, 0, 12),<br />
[], <br />
["("]<br />
), -2, -1), STRING_TRIM);</lsl><br />
<br />
This application uses email to have objects check with a central server to see if the owner has the latest version. In the objects:<br />
<lsl>string version = "1"; //<br />
string type = "lolcube";<br />
default<br />
{<br />
on_rez(integer start_param)<br />
{<br />
llEmail("5a634b27-f032-283f-2df2-55ead7724b23@lsl.secondlife.com",<br />
version,<br />
(string)llGetOwner() + "," + type);<br />
}<br />
}</lsl><br />
The server:<br />
<lsl>default<br />
{<br />
state_entry()<br />
{<br />
llSetTimerEvent(15.0);<br />
}<br />
<br />
timer()<br />
{<br />
llGetNextEmail("", "");<br />
}<br />
<br />
email( string time, string address, string version, string message, integer num_left )<br />
{ <br />
if ((integer)version < 2)<br />
{<br />
list info = llCSV2List( llDeleteSubString(message, 0, llSubStringIndex(message, "\n\n") + 1));<br />
llGiveInventory(llList2Key(info,0), llList2String(info,1));<br />
}<br />
<br />
if(num_left)<br />
llGetNextEmail("","");<br />
}<br />
}</lsl><br />
|helpers=<br />
<lsl><br />
email( string time, string address, string subj, string message, integer num_left )<br />
{<br />
if(llGetSubString(address, -19, -1) == "@lsl.secondlife.com")//trim the header<br />
message = llDeleteSubString(message, 0, llSubStringIndex(message, "\n\n") + 1);<br />
}<br />
</lsl><br />
|permission<br />
|negative_index<br />
|cat1=Communications<br />
|cat2=Email<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlEmail&diff=1176706LlEmail2013-02-13T19:12:32Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SVC-23}}{{Issues/SVC-391}}{{Issues/SVC-412}}{{Issues/SCR-499}}<br />
|func_id=119|func_sleep=20.0|func_energy=10.0<br />
|sort=Email|func=llEmail<br />
|p1_type=string|p1_name=address<br />
|p2_type=string|p2_name=subject<br />
|p3_type=string|p3_name=message<br />
|func_footnote=The entire message (including the address, subject and other miscellaneous fields) can't be longer than 4096 bytes combined.<br />
|func_desc=Sends an email to {{LSLP|address}} with {{LSLP|subject}} and {{LSLP|message}}.<br />
|return_text<br />
|spec=The {{LSLP|message}} is prefixed with information about the prim sending the email.<br />
{{{!}}{{Prettytable}}<br />
{{!}}-{{Hl2}}<br />
!Template<br />
!Example<br />
{{!}}-<br />
{{!}}<pre><br />
Object-Name: *prim*<br />
Region: *simname* (*simpos.x*, *simpos.y*)<br />
Local-Position: (*primpos.x*, *primpos.y*, *primpos.z*)<br />
<br />
*message*<br />
</pre><br />
{{!}}<br />
<pre><br />
Object-Name: Object<br />
Region: Gibson (254976, 256000)<br />
Local-Position: (117, 129, 50)<br />
<br />
The real message starts here.<br />
</pre><br />
{{!}}}<br />
|caveats=* There is a limit to the number of email messages an object can send in a given amount of time. <br />
* There is a limit of 500 messages from a single agent's objects in a one hour period.<br />
* The 4096 byte size limit includes the subject line and automatically added text. The practical maximum body size is approximately 3600 bytes.<br />
* (Sept-2008) The Email Throttle was modified slightly, Per {{User|Prospero Linden}}'s comments: "there has long been a throttle that makes a single script sleep for 20 seconds after sending an email. The new throttle is per user... some were using many, many different scripts to send spam. (the new throttle applies) when the destination is outside of Second Life. I know that messages within the same region were not throttled (beyond the 20-second delay), and I *believe* that messages between different sims were not throttled (between the 20-second delay)."<br />
* Due to the bug {{Jira|SVC-23}} (present since 2005), objects may stop receiving emails completely until either the region is restarted or the object crosses a region boundary (resetting the script doesn't help). Emails sent may eventually be received after a restart/region-cross. Hence, don't rely on this function for reliable inter-region messaging. <br />
* Due to the bug {{Jira|SVC-391}} llEmail will silently fail (no mail will arrive) when non-ascii characters are present in the subject. However, non-ascii characters in the message body will be replaced by "?".<br />
|constants<br />
|examples=<br />
<lsl><br />
string emailAddress = "somebody@example.com";<br />
string emailHeader = "Someone touched me!";<br />
<br />
<br />
default<br />
{<br />
touch_start(integer num_detected)<br />
{<br />
// llSay(PUBLIC_CHANNEL, "Sending eMail report now, this will take ~20 seconds.");<br />
<br />
key id = llDetectedKey(0);<br />
string name = llDetectedName(0);<br />
<br />
llEmail(emailAddress, emailHeader,<br />
"I was touched by: '" + name + "' (" + (string)id + ").");<br />
<br />
// llSay(PUBLIC_CHANNEL, "Email has been sent.");<br />
}<br />
}<br />
</lsl><br />
|also_functions=<br />
{{LSL DefineRow||[[llGetNextEmail]]}}<br />
{{LSL DefineRow||[[llMessageLinked]]}}<br />
|also_events=<br />
{{LSL DefineRow||[[email]]}}<br />
{{LSL DefineRow||[[link message]]}}<br />
|also_tests={{LSL DefineRow|[https://osiris.lindenlab.com/mediawiki/index.php/Email_Test internal test]}}<br />
|also_articles={{LSL DefineRow||[[IM to email]]}}<br />
{{LSL DefineRow||[[Postcards]]}}<br />
|notes=* Because of the long delay on this function, it is often called from a second script triggered by [[link_message]].<br />
* If you are sending email to a prim within Second Life, its address is ''[key]''@lsl.secondlife.com<br />
** Which means if the key returned by [[llGetKey]] is "a2e76fcd-9360-4f6d-a924-000000000003", then its email address is "a2e76fcd-9360-4f6d-a924-000000000003@lsl.secondlife.com".<br />
** Agents do not have fixed email addresses, use [[llInstantMessage]] or [[llOwnerSay]].<br />
<br />
===Prim2Prim Email===<br />
<br />
In LSL you can both send email with llEmail and receive it with the [[email]] event.<br />
<br />
<br />
The email event is triggered with 5 pieces of information.<br />
{{{!}}<br />
{{LSL DefineRow|string|time|When the message was sent, in the <code>(string)[[llGetUnixTime]]</code> format}}<br />
{{LSL DefineRow|string|address|Who sent the message}}<br />
{{LSL DefineRow|string|subject|Subject of the message}}<br />
{{LSL DefineRow|string|message|Body of the message}}<br />
{{LSL DefineRow|integer|num_left|The number of emails left in the email queue}}<br />
{{!}}}<br />
<br />
<br />
Object-to-object email will have a header added to the beginning of any message sent using [[llEmail]], containing the sending prim's name, the region name and grid offset, and the local position of the sending prim. The message will be of the following format:<br />
<br />
<lsl>Object-Name: Object<br />
Region: Region Name (X, Y)<br />
Local-Position: (X, Y, Z)<br />
<br />
message text<br />
</lsl><br />
<br />
When receiving a message sent with [[llEmail]] it helps to separate the message from the prefixed header. The header and original message body are separated by "\n\n"<br />
<br />
<lsl>integer divide = llSubStringIndex(message, "\n\n");<br />
string header = llDeleteSubString(message, divide, -1);<br />
message = llDeleteSubString(message, 0, divide + 1);</lsl><br />
<br />
To get just 1 of the header items, do this:<br />
<lsl>list lines = llParseStringKeepNulls(header, ["\n"], []);<br />
string objname_line = llList2String(lines, 0);<br />
string region_line = llList2String(lines, 1);<br />
string localpos_line = llList2String(lines, 2);</lsl><br />
<br />
To get a pure region name, do this:<br />
<lsl>string region_name = llStringTrim(<br />
(string)llDeleteSubList(<br />
llParseStringKeepNulls(<br />
llDeleteSubString(region_line, 0, 12),<br />
[], <br />
["("]<br />
), -2, -1), STRING_TRIM);</lsl><br />
<br />
This application uses email to have objects check with a central server to see if the owner has the latest version. In the objects:<br />
<lsl>string version = "1"; //<br />
string type = "lolcube";<br />
default<br />
{<br />
on_rez(integer start_param)<br />
{<br />
llEmail("5a634b27-f032-283f-2df2-55ead7724b23@lsl.secondlife.com",<br />
version,<br />
(string)llGetOwner() + "," + type);<br />
}<br />
}</lsl><br />
The server:<br />
<lsl>default<br />
{<br />
state_entry()<br />
{<br />
llSetTimerEvent(15.0);<br />
}<br />
<br />
timer()<br />
{<br />
llGetNextEmail("", "");<br />
}<br />
<br />
email( string time, string address, string version, string message, integer num_left )<br />
{ <br />
if ((integer)version < 2)<br />
{<br />
list info = llCSV2List( llDeleteSubString(message, 0, llSubStringIndex(message, "\n\n") + 1));<br />
llGiveInventory(llList2Key(info,0), llList2String(info,1));<br />
}<br />
<br />
if(num_left)<br />
llGetNextEmail("","");<br />
}<br />
}</lsl><br />
|helpers=<br />
<lsl><br />
email( string time, string address, string subj, string message, integer num_left )<br />
{<br />
if(llGetSubString(address, -19, -1) == "@lsl.secondlife.com")//trim the header<br />
message = llDeleteSubString(message, 0, llSubStringIndex(message, "\n\n") + 1);<br />
}<br />
</lsl><br />
|permission<br />
|negative_index<br />
|cat1=Communications<br />
|cat2=Email<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlInstantMessage&diff=1176348LlInstantMessage2013-01-25T08:26:35Z<p>Felis Darwin: Move notation of llRegionSayTo up</p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SVC-92}}{{LSL_Function/avatar|user}}{{LSL_Function/chat||message}}<br />
|func_id=118|func_sleep=2.0|func_energy=10.0<br />
|func=llInstantMessage<br />
|p1_type=key|p1_name=user|p1_desc<br />
|p2_type=string|p2_name=message|p2_desc<br />
|func_desc=Sends an Instant Message specified in the string {{LSLP|message}} to the user specified by {{LSLP|user}}.<br />
|func_footer=To send a message directly to an object, use [[llRegionSayTo]].<br />
|return_text<br />
|spec<br />
|caveats=<br />
* All object IM's are throttled at a maximum of 2500 per 30mins, per owner, per region in a rolling window. this includes IM's sent after the throttle is in place<br />
** Throttled IM's are dropped. for implementation see [[llInstantMessage#notes|notes]] below<br />
* Messages longer than 1175 bytes will be truncated to 1175 bytes. This can convey 1175 ASCII characters, or fewer if non-ASCII characters are present.<br />
*{{LSLP|message}} will appear in the chat window and will not logged by the InstantMessage logging facility. (If a the specified user is not signed it, the messages will be delivered to their email just like a regular instant message, if the user has enabled email for their account.)<br />
<br />
|examples=Tell the owner somebody touched the object:<br />
<lsl><br />
default<br />
{<br />
touch_start( integer total_num )<br />
{ <br />
llInstantMessage( llGetOwner(), "Someone touched me" );<br />
}<br />
}</lsl><br />
Send an IM to a detected avatar only<br />
<lsl><br />
default<br />
{<br />
touch_start( integer total_num )<br />
{<br />
llInstantMessage( llDetectedKey(0), "Hands Off!");<br />
}<br />
}<br />
</lsl><br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llOwnerSay]]|Sends chat region wide to owner}}<br />
{{LSL DefineRow||[[llRegionSay]]|Sends chat region wide}}<br />
{{LSL DefineRow||[[llRegionSayTo]]|Sends chat region wide to a specific prim/avatar}}<br />
{{LSL DefineRow||[[llWhisper]]|Sends chat limited to 10 meters}}<br />
{{LSL DefineRow||[[llSay]]|Sends chat limited to 20 meters}}<br />
{{LSL DefineRow||[[llShout]]|Sends chat limited to 100 meters}}<br />
|also_tests<br />
|also_articles<br />
|also_events<br />
|notes=<br />
* [[llRegionSayTo]] may be a better choice if the target is in the same region as the object sending the message, as it has no built-in delay and can communicate directly with objects, as well as with avatars and their attachments.<br />
* Instant Messaging allows communication from an object to an avatar anywhere on the Grid. However, an object cannot receive an Instant Message. <br />
* Using [[llInstantMessage]] from one or more child scripts will avoid delays in the main script. Child scripts will still be subject to delays, [[LSL Event Queue|message queue]] limits, and region throttles.<br />
* Throttling Implementation (Kelly Linden):<br />
** The throttle is on all IMs from the object owner. It does not disable all IMs in the region, but does disable all IMs from the owner of the object.<br />
** The throttle is not per object, but per owner. Splitting the spamming object into multiple objects will not help unless owned by different people. This also means that owning multiple almost too spammy objects will cause you to hit the limit.<br />
** 2500 IMs in 30 minutes will trigger the block.<br />
** IMs that are blocked continue to count against the throttle. The IM count must drop below 2500 before any IMs will be delivered.<br />
** The IM count of the previous window is used to approximate the rolling window. If it is 20% into the current window the IM count will be the current count + 80% of the previous count. This allows us to approximate a rolling average, however it has the behavior that a flood of IMs can have an effect on the throttle for double the window length. This is why in practice the throttle behaves more like 5k in 1hr than 2.5k in 30min.<br />
<br />
|cat1=Communications<br />
|cat2=Instant Message<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Talk:PRIM_POSITION&diff=1171970Talk:PRIM POSITION2012-08-23T04:31:46Z<p>Felis Darwin: /* Avatar movement limit */ new section</p>
<hr />
<div>== Other Tag Duplication ==<br />
<br />
Q: What is this 'other tag duplication' for which 'results are undefined'?<br />
<br />
That is, what is a 'tag' here? Are 'tag's and 'rule's and 'flag's and such meant to be multiple words meaning the same thing, or multiple words meaning different things? Like do we say we have 'duplicated a tag' if we repeat a 'rule' more than once in a call to [[llSetPrimitiveParams]] or [[llGetPrimitiveParams]] and such? <br />
<br />
-- [[User:Ppaatt Lynagh|Ppaatt Lynagh]] 06:03, 1 January 2009 (UTC)<br />
<br />
:I used the term 'tag' here because I couldn't think of a better term to describe a top level PRIM_* flag and it's parameter as a single entity. In this case, the documentation is saying that it is frowned upon to use a top level PRIM_* flag multiple times; that to do so can have undefined results. PRIM_POSITION is the only flag that can be used multiple times reliably, though this is undoubtedly a misfeature. 'rule' is a better term. -- '''[[User:Strife_Onizuka|Strife]]''' <sup><small>([[User talk:Strife_Onizuka|talk]]|[[Special:Contributions/Strife_Onizuka|contribs]])</small></sup> 22:10, 1 January 2009 (UTC)<br />
<br />
== Set or Get, Your Option ==<br />
<br />
We'd rather see:<br />
<br />
<lsl><br />
llSetPrimitiveParams([ ..., PRIM_POSITION, vector position, ... ]<br />
...<br />
llGetPrimitiveParams([ ..., PRIM_POSITION, ... ]);<br />
...<br />
</lsl><br />
<br />
where today in the [[PRIM_POSITION]] article we see:<br />
<br />
<lsl><br />
[ PRIM_POSITION, vector position ]<br />
...<br />
llGetPrimitiveParams([ PRIM_POSITION ]);<br />
...<br />
</lsl><br />
<br />
I think this because I am new, so I still now remember the pain of not knowing:<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;i) that often Lsl defines the same PRIM op to do symmetric things inside both llGetPrimitiveParams and for llSetPrimitiveParams, and<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;ii) that each PRIM op and its args appear in the list of Params catenated together with other ops and args.<br />
<br />
I then think the spirit of wiki is that, since I believe in this change, I should make this change myself, and spark community review that way. However, the wikitext here is much too exotic for me to begin to know how to make and save a change like that myself here. So I ask ...<br />
<br />
Q: Anyone out there both:<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;a) agree with me on how we should improve this page for the newbie and<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;b) know how to change the wikitext to achieve this effect?<br />
<br />
Or please would you hint how I could learn to do this myself? Thanks in advance,<br />
<br />
-- [[User:Ppaatt Lynagh|Ppaatt Lynagh]] 17:59, 24 December 2008 (UTC)<br />
<br />
:I'll see what I can do about supporting this. It looks like a good idea. Give me a bit of time to figure out how to best support this. -- '''[[User:Strife_Onizuka|Strife]]''' <sup><small>([[User talk:Strife_Onizuka|talk]]|[[Special:Contributions/Strife_Onizuka|contribs]])</small></sup> 22:10, 1 January 2009 (UTC)<br />
<br />
::Turns out it's not so hard to support... that said, seeing it in print I now have some misgivings about it. I want to ask on the forums for peoples opinions about it. '''[[User:Strife_Onizuka|Strife]]''' <sup><small>([[User talk:Strife_Onizuka|talk]]|[[Special:Contributions/Strife_Onizuka|contribs]])</small></sup> 22:21, 1 January 2009 (UTC)<br />
<br />
== About Caveats ==<br />
<br />
Q: It seems to be duplicated. Any intention? [[User:Mako Nozaki|Mako Nozaki]] 13:17, 13 April 2010 (UTC)<br />
<br />
A1: Intelectual laziness.<br><br />
A2: Some ideas require a bit of reiteration or people just do not understand. It's a trade off. <br><br />
I don't remember which it is. -- '''[[User:Strife_Onizuka|Strife]]''' <sup><small>([[User talk:Strife_Onizuka|talk]]|[[Special:Contributions/Strife_Onizuka|contribs]])</small></sup> 21:26, 13 April 2010 (UTC)<br />
<br />
:lol I see.. -- [[User:Mako Nozaki|Mako Nozaki]] 10:49, 14 April 2010 (UTC)<br />
<br />
== Avatar movement limit ==<br />
<br />
After some recent tests, it seems that the 54m avatar movement limit is no longer the case. The limit appears to have been raised from 54m to a full 1000(!) meters.<br />
<br />
I will be testing this in detail to confirm. -- [[User:Felis Darwin|Felis Darwin]] 21:31, 22 August 2012 (PDT)</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=User:Toy_Wylie/RLV_Documentation/setrot&diff=1171699User:Toy Wylie/RLV Documentation/setrot2012-08-18T01:25:13Z<p>Felis Darwin: </p>
<hr />
<div>{{Template:RLV_Documentation/Command<br />
|command=@setrot|type=General<br />
|usage=@setrot:<angle>=force<br />
|purpose=Rotates the user's avatar to face the angle given in radians. An angle of 0.0 means "Face North". Keep in mind that an avatar will only rotate if the difference is big enough from the current rotation. The threshold is around 6° to 10°.<br />
|version=1.17<br />
|seealso=getrot<br />
|example=<lsl>default<br />
{<br />
touch_start(integer num)<br />
{<br />
if(llDetectedKey(0)==llGetOwner())<br />
{<br />
float angle=llFrand(TWO_PI);<br />
llOwnerSay("@setrot:"+(string) angle+"=force");<br />
llOwnerSay("You are now facing "+(string) ((integer) (angle*RAD_TO_DEG))+"° from the north.");<br />
}<br />
}<br />
}</lsl><br />
|example_2='''Direction Vector to Angle'''<br />
<br />
It's very easy to turn a direction vector (such as the distance between two points) into an angle that can be used with this command:<br />
<lsl>default<br />
{<br />
touch_start(integer num)<br />
{<br />
//== When the owner touches us, make them face us<br />
if(llDetectedKey(0)==llGetOwner())<br />
{<br />
vector diff=llGetPos() - llDetectedPos(0);<br />
float angle=llAtan2(diff.x, diff.y); // Note that this is X first, then Y, contrary to what documentation would make you think you should use<br />
llOwnerSay("@setrot:"+(string) angle+"=force");<br />
llOwnerSay("You are now facing "+(string) ((integer) (angle*RAD_TO_DEG))+"° from the north.");<br />
}<br />
}<br />
}</lsl><br />
<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Template:RLV_Documentation/Command&diff=1171698Template:RLV Documentation/Command2012-08-18T01:24:05Z<p>Felis Darwin: Close noinclude tag properly</p>
<hr />
<div>__NOTOC__{{#if:{{{lock|}}}|<br />
<div style="background:#ff9999; padding:1em; font-size:12px; text-align:center">'''LOCKED - Locked by {{{lock}}} - Please do not modify! - LOCKED'''</div>}}<br />
<br />
== @{{SUBPAGENAME}} ==<br />
<br />
{{#if:{{{deprecated|}}}|<div id="box" style="margin-top:2em; clear:both; color:red"><br />
<h2>DEPRECATED</h2><br />
<div style="padding: 0.5em">'''This function is DEPRECATED - Do not use it!'''</div><br />
</div>}}<br />
<br />
<div id="box" style="margin-top:2em; float:right"><br />
<h2>Type</h2><br />
<div style="padding: 0.5em">{{{type|<noinclude>"Restriction", "Force", "Exception" or "General"</noinclude>}}}</div><br />
</div><br />
<br />
<div id="box" style="margin-top:2em; margin-right:2em; float:right"><br />
<h2>Implemented</h2><br />
<div style="padding:0.5em">Implemented since RLV version {{{version|<noinclude>NUMBER</noinclude>}}}</div><br />
</div><br />
<br />
<div id="box" style="margin-top:2em; float:left"><br />
<h2>Usage</h2><br />
<div style="padding: 0.5em">{{{usage|<noinclude>Usage here</noinclude>}}}</div><br />
</div><br />
<br />
<div id="box" style="margin-top:2em; clear:both"><br />
<h2>Purpose</h2><br />
<div style="padding: 0.5em">{{{purpose|<noinclude>Purpose of this command here</noinclude>}}}</div><br />
</div><br />
<br />
{{#if:{{{notes|}}}|<div id="box" style="margin-top:2em; clear:both"><br />
<h2>Notes</h2><br />
<div style="padding:0.5em">{{{notes|<noinclude>Additional notes go here</noinclude>}}}</div><br />
</div>}}<br />
<br />
<div id="box" style="margin-top:2em"><br />
<h2>See Also</h2><br />
<div style="padding: 0.5em">{{#if:{{{seealso|}}}|<br />
{{#vardefine:path|User:Toy Wylie/RLV Documentation/}}<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |0}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |1}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |2}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |3}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |4}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |5}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |6}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |7}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |8}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
{{#vardefine:cmd|{{#explode:{{{seealso}}}| |9}}}}{{#if:{{#var:cmd|}}|*[[{{#var:path}}{{#var:cmd|}}|@{{#var:cmd|}}]]<br />
}} }} }} }} }} }} }} }} }} }}<noinclude>|<br />
*List of related commands here</noinclude>}}<br />
{{#if:{{{seealsoalso|}}}|{{{seealsoalso}}}<noinclude>|Further notes here</noinclude>}}<br />
</div><br />
</div><br />
<br />
<div id="box" style="margin-top:2em"><br />
<h2>{{#if:{{{example_2|}}}|Example 1|Example}}</h2><br />
<div style="padding:0.5em">{{{example<noinclude>|Example LSL code here</noinclude>}}}</div><br />
</div><br />
<br />
{{#if:{{{example_2|}}}|<div id="box" style="margin-top:2em"><br />
<h2>Example 2</h2><br />
<div style="padding:0.5em">{{{example_2<noinclude>|Second Example LSL code here</noinclude>}}}</div><br />
</div>}}<br />
<br />
[[Category:RestrainedLove/Documentation/Command<includeonly>|{{SUBPAGENAME}}</includeonly>]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlRegionSayTo&diff=1170725LlRegionSayTo2012-07-18T23:14:12Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SCR-66}}<br />
{{LSL_Function/chat|channel|msg|nd=*|direct=*}}<br />
{{LSL_Function/uuid|target|sim=*}}<br />
|func_id=363<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llRegionSayTo<br />
|sort=RegionSayTo<br />
|p1_type=key|p1_name=target<br />
|p2_type=integer|p2_name=channel<br />
|p3_type=string|p3_name=msg<br />
|func_desc=Says the text supplied in string {{LSLP|msg}} on channel supplied in integer {{LSLP|channel}} to the object or avatar specified by {{LSLP|target}}<br />
|return_text<br />
|spec<br />
|caveats=<br />
*Text is spoken directly to the object or avatar within the same region as the script.<br />
*Scripts in tasks other than {{LSLP|target}} can not listen and receive these text messages, with an exception for attachments described below.<br />
*Text can be a maximum of 1023 bytes.<br />
*A prim cannot hear itself, to prevent problems with recursion.<br />
*Sending text on {{#var:DEBUG_CHANNEL}} is not supported<br />
*Text sent to an avatar's ID on channel zero will be sent to the viewer.<br />
*Text sent to an avatar's ID on non-zero channels can be heard by any attachment on the avatar<br />
|examples=<lsl>default<br />
{<br />
touch_start(integer i)<br />
{<br />
llRegionSayTo(llDetectedKey(0), 0, "You touched this!");<br />
}<br />
}</lsl><br />
<br />
|helpers<br />
|also_events=<br />
{{!}}-<br />
{{!}}{{!}}{{!}} style="width:9em;" {{!}}<br />
{{LSL DefineRow||[[listen]]|Receives chat}}<br />
|also_functions=<br />
{{!}}-<br />
{{!}}{{!}}{{!}} style="width:9em;" {{!}}<br />
{{LSL DefineRow||[[llListen]]|Ask for listen events}}<br />
{{LSL DefineRow||[[llInstantMessage]]|Sends chat to a specific avatar, inside our outside the current region.}}<br />
{{LSL DefineRow||[[llOwnerSay]]|Sends chat to the owner only to avoid spamming the PUBLIC_CHANNEL}}<br />
{{LSL DefineRow||[[llRegionSay]]|Sends chat region wide}}<br />
{{LSL DefineRow||[[llSay]]|Sends chat limited to 20 meters}}<br />
{{LSL DefineRow||[[llShout]]|Sends chat limited to 100 meters}}<br />
{{LSL DefineRow||[[llWhisper]]|Sends chat limited to 10 meters}}<br />
|also_tests<br />
|also_articles={{LSL DefineRow||[[Hello Avatar]]}}<br />
|notes=<br />
* Channel {{HoverText|0|Zero}} is the {{#var:PUBLIC_CHANNEL}}. This should only be used for chat intended to be sent to the viewer.<br />
* If one object 'says' something to another object (''e.g''., a button that, when touched, turns on a lamp), it is a good idea to use a very negative channel, ''e.g.'',<br />
<lsl><br />
llRegionSayTo("55499a64-45c3-4b81-8880-8ffb5a7c251b",-5243212,"turn on");<br />
</lsl><br />
Negative channels are popular for script communications because the standard Second Life client is unable to chat directly on those channels ("/-xxxx message" won't chat "message" on channel "-xxxx", it will chat "/-xxxx message" on channel zero). The only way to do so prior to [[llTextBox]] was to use [[llDialog]], which is limited to 24 characters, or certain third-party viewers.<br />
|cat1<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlRegionSayTo&diff=1170724LlRegionSayTo2012-07-18T23:13:14Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SCR-66}}<br />
{{LSL_Function/chat|channel|msg|nd=*|direct=*}}<br />
{{LSL_Function/uuid|target|sim=*}}<br />
|func_id=363<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llRegionSayTo<br />
|sort=RegionSayTo<br />
|p1_type=key|p1_name=target<br />
|p2_type=integer|p2_name=channel<br />
|p3_type=string|p3_name=msg<br />
|func_desc=Says the text supplied in string {{LSLP|msg}} on channel supplied in integer {{LSLP|channel}} to the object or avatar specified by {{LSLP|target}}<br />
|return_text<br />
|spec<br />
|caveats=<br />
*Text is spoken directly to the object or avatar within the same region as the script.<br />
*Scripts in tasks other than {{LSLP|target}} can not listen and receive these text messages, with an exception for attachments described below.<br />
*Text can be a maximum of 1023 bytes.<br />
*A prim cannot hear itself, to prevent problems with recursion.<br />
*Sending text on {{#var:DEBUG_CHANNEL}} is not supported<br />
*Text sent to an avatar's ID on channel zero will be sent to the viewer.<br />
*Text sent to an avatar's ID on non-zero channels can be heard by any attachment on the avatar<br />
|examples=<lsl>default<br />
{<br />
touch_start(integer i)<br />
{<br />
llRegionSayTo(llDetectedKey(0), 0, "You touched this!");<br />
}<br />
}</lsl><br />
<br />
|helpers<br />
|also_events=<br />
{{!}}-<br />
{{!}}{{!}}{{!}} style="width:9em;" {{!}}<br />
{{LSL DefineRow||[[listen]]|Receives chat}}<br />
|also_functions=<br />
{{!}}-<br />
{{!}}{{!}}{{!}} style="width:9em;" {{!}}<br />
{{LSL DefineRow||[[llListen]]|Ask for listen events}}<br />
{{LSL DefineRow||[[llInstantMessage]]|Sends chat to a specific avatar, inside our outside the current region.<br />
{{LSL DefineRow||[[llOwnerSay]]|Sends chat to the owner only to avoid spamming the PUBLIC_CHANNEL}}<br />
{{LSL DefineRow||[[llRegionSay]]|Sends chat region wide}}<br />
{{LSL DefineRow||[[llSay]]|Sends chat limited to 20 meters}}<br />
{{LSL DefineRow||[[llShout]]|Sends chat limited to 100 meters}}<br />
{{LSL DefineRow||[[llWhisper]]|Sends chat limited to 10 meters}}<br />
|also_tests<br />
|also_articles={{LSL DefineRow||[[Hello Avatar]]}}<br />
|notes=<br />
* Channel {{HoverText|0|Zero}} is the {{#var:PUBLIC_CHANNEL}}. This should only be used for chat intended to be sent to the viewer.<br />
* If one object 'says' something to another object (''e.g''., a button that, when touched, turns on a lamp), it is a good idea to use a very negative channel, ''e.g.'',<br />
<lsl><br />
llRegionSayTo("55499a64-45c3-4b81-8880-8ffb5a7c251b",-5243212,"turn on");<br />
</lsl><br />
Negative channels are popular for script communications because the standard Second Life client is unable to chat directly on those channels ("/-xxxx message" won't chat "message" on channel "-xxxx", it will chat "/-xxxx message" on channel zero). The only way to do so prior to [[llTextBox]] was to use [[llDialog]], which is limited to 24 characters, or certain third-party viewers.<br />
|cat1<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlRegionSayTo&diff=1170723LlRegionSayTo2012-07-18T22:51:43Z<p>Felis Darwin: Clarify that llDialog is limited to 24 CHARACTERS, not bytes. Also, Phoenix can chat on negative channels.</p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SCR-66}}<br />
{{LSL_Function/chat|channel|msg|nd=*|direct=*}}<br />
{{LSL_Function/uuid|target|sim=*}}<br />
|func_id=363<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llRegionSayTo<br />
|sort=RegionSayTo<br />
|p1_type=key|p1_name=target<br />
|p2_type=integer|p2_name=channel<br />
|p3_type=string|p3_name=msg<br />
|func_desc=Says the text supplied in string {{LSLP|msg}} on channel supplied in integer {{LSLP|channel}} to the object or avatar specified by {{LSLP|target}}<br />
|return_text<br />
|spec<br />
|caveats=<br />
*Text is spoken directly to the object or avatar within the same region as the script.<br />
*Scripts in tasks other than {{LSLP|target}} can not listen and receive these text messages, with an exception for attachments described below.<br />
*Text can be a maximum of 1023 bytes.<br />
*A prim cannot hear itself, to prevent problems with recursion.<br />
*Sending text on {{#var:DEBUG_CHANNEL}} is not supported<br />
*Text sent to an avatar's ID on channel zero will be sent to the viewer.<br />
*Text sent to an avatar's ID on non-zero channels can be heard by any attachment on the avatar<br />
|examples=<lsl>default<br />
{<br />
touch_start(integer i)<br />
{<br />
llRegionSayTo(llDetectedKey(0), 0, "You touched this!");<br />
}<br />
}</lsl><br />
<br />
|helpers<br />
|also_events=<br />
{{!}}-<br />
{{!}}{{!}}{{!}} style="width:9em;" {{!}}<br />
{{LSL DefineRow||[[listen]]|Receives chat}}<br />
|also_functions=<br />
{{!}}-<br />
{{!}}{{!}}{{!}} style="width:9em;" {{!}}<br />
{{LSL DefineRow||[[llListen]]|Ask for listen events}}<br />
{{LSL DefineRow||[[llInstantMessage]]|Sends chat to the owner only (or to some other user only) to avoid spamming the {{#var:PUBLIC_CHANNEL}}}}<br />
{{LSL DefineRow||[[llOwnerSay]]|Sends chat to the owner only to avoid spamming the PUBLIC_CHANNEL}}<br />
{{LSL DefineRow||[[llRegionSay]]|Sends chat region wide}}<br />
{{LSL DefineRow||[[llSay]]|Sends chat limited to 20 meters}}<br />
{{LSL DefineRow||[[llShout]]|Sends chat limited to 100 meters}}<br />
{{LSL DefineRow||[[llWhisper]]|Sends chat limited to 10 meters}}<br />
|also_tests<br />
|also_articles={{LSL DefineRow||[[Hello Avatar]]}}<br />
|notes=<br />
* Channel {{HoverText|0|Zero}} is the {{#var:PUBLIC_CHANNEL}}. This should only be used for chat intended to be sent to the viewer.<br />
* If one object 'says' something to another object (''e.g''., a button that, when touched, turns on a lamp), it is a good idea to use a very negative channel, ''e.g.'',<br />
<lsl><br />
llRegionSayTo("55499a64-45c3-4b81-8880-8ffb5a7c251b",-5243212,"turn on");<br />
</lsl><br />
Negative channels are popular for script communications because the standard Second Life client is unable to chat directly on those channels ("/-xxxx message" won't chat "message" on channel "-xxxx", it will chat "/-xxxx message" on channel zero). The only way to do so prior to [[llTextBox]] was to use [[llDialog]], which is limited to 24 characters, or certain third-party viewers.<br />
|cat1<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlInstantMessage&diff=1170722LlInstantMessage2012-07-18T21:35:48Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SVC-92}}{{LSL_Function/avatar|user}}{{LSL_Function/chat||message}}<br />
|func_id=118|func_sleep=2.0|func_energy=10.0<br />
|func=llInstantMessage<br />
|p1_type=key|p1_name=user|p1_desc<br />
|p2_type=string|p2_name=message|p2_desc<br />
|func_desc=Sends an Instant Message specified in the string {{LSLP|message}} to the user specified by {{LSLP|user}}.<br />
|func_footer=To send a message directly to an object, use [[llRegionSayTo]].<br />
|return_text<br />
|spec<br />
|caveats=**For applications where this is problematic, [[llRegionSayTo]] can message the targeted agent directly with no delay, provided they are in the same region as the scripted object. For cross-region applications, the call to llInstantMessage can be placed in a child script, with information passed to that script via [[llMessageLinked]].<br />
* All object IM's are throttled at >2500 per 30mins, per owner, per region in a rolling window. this includes IM's sent after the throttle is in place<br />
** Throttled IM's are dropped. for implementation see [[llInstantMessage#notes|notes]] below<br />
*{{LSLP|message}} will be truncated if it exceeds 1199 bytes.<br />
*{{LSLP|message}} will appear in the chat window and will not logged by the InstantMessage logging facility. (If a the specified user is not signed it, the messages will be delivered to their email just like a regular instant message, if the user has enabled email for their account.)<br />
|examples=Tell the owner somebody touched the object:<br />
<lsl>// This particular example is for demonstration only.<br />
// Use llOwnerSay() for the same functionality, in case the owner is known to be in the same Region.<br />
<br />
key owner;<br />
<br />
default<br />
{<br />
on_rez(integer start_param)<br />
{<br />
owner=llGetOwner(); // get the key of the objects owner.<br />
}<br />
touch_start(integer total_num)<br />
{ <br />
llInstantMessage(owner,llKey2Name(owner)+", " + (string)total_num +" Avatar(s) touched me!");<br />
}<br />
}</lsl><br />
Send a confirmation to the Avatar that touches an object without spamming other Avatars:<br />
<lsl>default<br />
{<br />
touch_start(integer total_num)<br />
{ <br />
llInstantMessage(llDetectedKey(0),"You have been registered!");<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llOwnerSay]]|Sends chat region wide to owner}}<br />
{{LSL DefineRow||[[llRegionSay]]|Sends chat region wide}}<br />
{{LSL DefineRow||[[llRegionSayTo]]|Sends chat region wide to a specific prim/avatar}}<br />
{{LSL DefineRow||[[llWhisper]]|Sends chat limited to 10 meters}}<br />
{{LSL DefineRow||[[llSay]]|Sends chat limited to 20 meters}}<br />
{{LSL DefineRow||[[llShout]]|Sends chat limited to 100 meters}}<br />
|also_tests<br />
|also_articles<br />
|also_events<br />
|notes=<br />
* For many applications [[llRegionSayTo]] may be a better choice, as it has no built-in delay and can communicate directly with objects as well as avatars. The only notable limitation is that it requires the target to be in the same region as the scripted object.<br />
* Instant Messaging has the benefit of allowing communication from an object to an avatar anywhere in the Grid. The downside is that an object cannot receive an Instant Message, therefore an avatar cannot send an Instant Message to an object. It's a one-way communication avenue. Also, the two-second script delay can be considered a downside in some applications.<br />
* Throttling Implementation (Kelly Linden):<br />
** The throttle is on all IMs from the object owner. It does not disable all IMs in the region, but does disable all IMs from the owner of the object.<br />
** The throttle is not per object, but per owner. Splitting the spamming object into multiple objects will not help unless owned by different people. This also means that owning multiple almost too spammy objects will cause you to hit the limit.<br />
** 2500 IMs in 30 minutes will trigger the block.<br />
** IMs that are blocked continue to count against the throttle. The IM count must drop below 2500 before any IMs will be delivered.<br />
** The IM count of the previous window is used to approximate the rolling window. If it is 20% into the current window the IM count will be the current count + 80% of the previous count. This allows us to approximate a rolling average, however it has the behavior that a flood of IMs can have an effect on the throttle for double the window length. This is why in practice the throttle behaves more like 5k in 1hr than 2.5k in 30min.<br />
<br />
|cat1=Communications<br />
|cat2=Instant Message<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlInstantMessage&diff=1170721LlInstantMessage2012-07-18T21:34:42Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SVC-92}}{{LSL_Function/avatar|user}}{{LSL_Function/chat||message}}<br />
|func_id=118|func_sleep=2.0|func_energy=10.0<br />
|func=llInstantMessage<br />
|p1_type=key|p1_name=user|p1_desc<br />
|p2_type=string|p2_name=message|p2_desc<br />
|func_desc=Sends an Instant Message specified in the string {{LSLP|message}} to the user specified by {{LSLP|user}}.<br />
|func_footer=To send a message to a specific prim, use [[llRegionSayTo]].<br />
|return_text<br />
|spec<br />
|caveats=**For applications where this is problematic, [[llRegionSayTo]] can message the targeted agent directly with no delay, provided they are in the same region as the scripted object. For cross-region applications, the call to llInstantMessage can be placed in a child script, with information passed to that script via [[llMessageLinked]].<br />
* All object IM's are throttled at >2500 per 30mins, per owner, per region in a rolling window. this includes IM's sent after the throttle is in place<br />
** Throttled IM's are dropped. for implementation see [[llInstantMessage#notes|notes]] below<br />
*{{LSLP|message}} will be truncated if it exceeds 1199 bytes.<br />
*{{LSLP|message}} will appear in the chat window and will not logged by the InstantMessage logging facility. (If a the specified user is not signed it, the messages will be delivered to their email just like a regular instant message, if the user has enabled email for their account.)<br />
|examples=Tell the owner somebody touched the object:<br />
<lsl>// This particular example is for demonstration only.<br />
// Use llOwnerSay() for the same functionality, in case the owner is known to be in the same Region.<br />
<br />
key owner;<br />
<br />
default<br />
{<br />
on_rez(integer start_param)<br />
{<br />
owner=llGetOwner(); // get the key of the objects owner.<br />
}<br />
touch_start(integer total_num)<br />
{ <br />
llInstantMessage(owner,llKey2Name(owner)+", " + (string)total_num +" Avatar(s) touched me!");<br />
}<br />
}</lsl><br />
Send a confirmation to the Avatar that touches an object without spamming other Avatars:<br />
<lsl>default<br />
{<br />
touch_start(integer total_num)<br />
{ <br />
llInstantMessage(llDetectedKey(0),"You have been registered!");<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llOwnerSay]]|Sends chat region wide to owner}}<br />
{{LSL DefineRow||[[llRegionSay]]|Sends chat region wide}}<br />
{{LSL DefineRow||[[llRegionSayTo]]|Sends chat region wide to a specific prim/avatar}}<br />
{{LSL DefineRow||[[llWhisper]]|Sends chat limited to 10 meters}}<br />
{{LSL DefineRow||[[llSay]]|Sends chat limited to 20 meters}}<br />
{{LSL DefineRow||[[llShout]]|Sends chat limited to 100 meters}}<br />
|also_tests<br />
|also_articles<br />
|also_events<br />
|notes=<br />
* For many applications [[llRegionSayTo]] may be a better choice, as it has no built-in delay and can communicate directly with objects as well as avatars. The only notable limitation is that it requires the target to be in the same region as the scripted object.<br />
* Instant Messaging has the benefit of allowing communication from an object to an avatar anywhere in the Grid. The downside is that an object cannot receive an Instant Message, therefore an avatar cannot send an Instant Message to an object. It's a one-way communication avenue. Also, the two-second script delay can be considered a downside in some applications.<br />
* Throttling Implementation (Kelly Linden):<br />
** The throttle is on all IMs from the object owner. It does not disable all IMs in the region, but does disable all IMs from the owner of the object.<br />
** The throttle is not per object, but per owner. Splitting the spamming object into multiple objects will not help unless owned by different people. This also means that owning multiple almost too spammy objects will cause you to hit the limit.<br />
** 2500 IMs in 30 minutes will trigger the block.<br />
** IMs that are blocked continue to count against the throttle. The IM count must drop below 2500 before any IMs will be delivered.<br />
** The IM count of the previous window is used to approximate the rolling window. If it is 20% into the current window the IM count will be the current count + 80% of the previous count. This allows us to approximate a rolling average, however it has the behavior that a flood of IMs can have an effect on the throttle for double the window length. This is why in practice the throttle behaves more like 5k in 1hr than 2.5k in 30min.<br />
<br />
|cat1=Communications<br />
|cat2=Instant Message<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=User:Toy_Wylie/RLV_Documentation/setrot&diff=1165413User:Toy Wylie/RLV Documentation/setrot2012-04-11T03:32:10Z<p>Felis Darwin: Added an example on how to turn a direction vector into an angle. Sorry to edit your page but you have the best documentation on this</p>
<hr />
<div>{{Template:RLV_Documentation/Command<br />
|command=@setrot|type=General<br />
|usage=@setrot:<angle>=force<br />
|purpose=Rotates the user's avatar to face the angle given in radians. An angle of 0.0 means "Face North". Keep in mind that an avatar will only rotate if the difference is big enough from the current rotation. The threshold is around 6° to 10°.<br />
|version=1.17<br />
|seealso=<br />
|seealsoalso=<br />
|example=<lsl>default<br />
{<br />
touch_start(integer num)<br />
{<br />
if(llDetectedKey(0)==llGetOwner())<br />
{<br />
float angle=llFrand(TWO_PI);<br />
llOwnerSay("@setrot:"+(string) angle+"=force");<br />
llOwnerSay("You are now facing "+(string) ((integer) (angle*RAD_TO_DEG))+"° from the north.");<br />
}<br />
}<br />
}</lsl><br />
<br />
'''Direction Vector to Angle'''<br />
<br />
It's very easy to turn a direction vector (such as the distance between two points) into an angle that can be used with this command:<br />
<lsl>default<br />
{<br />
touch_start(integer num)<br />
{<br />
//== When the owner touches us, make them face us<br />
if(llDetectedKey(0)==llGetOwner())<br />
{<br />
vector diff=llGetPos() - llDetectedPos(0);<br />
float angle=llAtan2(diff.x, diff.y); // Note that this is X first, then Y, contrary to what documentation would make you think you should use<br />
llOwnerSay("@setrot:"+(string) angle+"=force");<br />
llOwnerSay("You are now facing "+(string) ((integer) (angle*RAD_TO_DEG))+"° from the north.");<br />
}<br />
}<br />
}</lsl><br />
<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlSetObjectName&diff=1164596LlSetObjectName2012-03-24T18:16:38Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function/limits}}{{LSL Function<br />
|func_id=203<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llSetObjectName|sort=SetObjectName<br />
|p1_type=string<br />
|p1_name=name<br />
|func_desc=Sets the prim's name according to the '''name''' parameter.<br />
|func_footnote=If this function is called from a child prim in a linked set, it will change the name of the child prim and not the root prim.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* The name is limited to 63 characters. Longer prim names are cut short.<br />
* Names can only consist of the 95 printable characters found in the [http://en.wikipedia.org/wiki/ASCII ASCII-7] (non-extended) character set.<br />
** Non-ASCII characters will be replaced with two question marks ("??").<br />
* While an object is attached, the script cannot change the name of the object as it appears in the user's inventory. This behavior [http://forums-archive.secondlife.com/255/ab/84853/1.html was a bug], but it [https://jira.secondlife.com/browse/SVC-3429?focusedCommentId=267372&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-267372 remained long enough to become permanent].<br />
** Changes to the name of the root prim (with [[llSetObjectName]] for example) will not be saved to inventory; when the attachment is detached (to inventory, not dropped) this name change is discarded and the name in inventory is used instead.<br />
** Dropping an attachment (to the ground) and taking it into inventory will <!--somebody please verify this--> cause the inventory name of the attachment to be the changed name.<br />
* Changes to the names of child prims will be saved back to inventory when the object is detached to inventory. They survive detachment.<br />
|constants<br />
|examples=<lsl>default<br />
{<br />
state_entry()<br />
{<br />
string yyyy1mm1dd = llGetDate();<br />
string name = yyyy1mm1dd + " " + llGetObjectName();<br />
llOwnerSay("llSetObjectName(\"" + name + "\")");<br />
llSetObjectName(name);<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llGetObjectName]]|Get the prims name}}<br />
{{LSL DefineRow||[[llGetLinkName]]|Get a linked prims name}}<br />
{{LSL DefineRow||[[llGetObjectDesc]]|Get the prims description}}<br />
{{LSL DefineRow||[[llSetObjectDesc]]|Set the prims description}}<br />
{{LSL DefineRow||[[llGetObjectDetails]]|Get a list of object details}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Prim<br />
|cat2<br />
|cat3<br />
|cat4<br />
|}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Typecast&diff=1164146Typecast2012-03-16T22:29:37Z<p>Felis Darwin: /* Notes */</p>
<hr />
<div>{{LSL Header|ml=*}}<br />
{{LSLC|Syntax}}<br />
{{RightToc}}<br />
<br />
To convert the type of a value a typecast is required. There are two types of typecasting, explicit and implicit. Explicit typecasts must be provided by the programmer, but implicit typecasts are put in place by the compiler. LSL implicitly typecasts strings to keys and integers to floats where the latter type is required but the former is provided.<br />
{| {{Prettytable|style=float:right; clear:right; margin:0em; line-height:1em; font-size:75%;}}<br />
|-<br />
!colspan="9"| Supported Typecasts<br />
|-<br />
|colspan="2" rowspan="2"|<br />
! colspan="7" {{Hl2}}|To<br />
|- align="center" {{Hl2}}<br />
![[integer]]<br />
![[float]]<br />
![[string]]<br />
![[key]]<br />
![[list]]<br />
![[vector]]<br />
![[rotation]]<br />
|- align="center"<br />
! rowspan="7" {{Hl2}}|From<br />
!{{Hl2}}| [[integer]]<br />
| x<br />
| x<br />
| x<br />
|<br />
| x<br />
| <br />
|<br />
|- align="center"<br />
!{{Hl2}}| [[float]]<br />
| x<br />
| x<br />
| x<br />
|<br />
| x<br />
| <br />
|<br />
|- align="center"<br />
!{{Hl2}}| [[string]]<br />
| x<br />
| x<br />
| x<br />
| x<br />
| x<br />
| x<br />
| x<br />
|- align="center"<br />
!{{Hl2}}| [[key]]<br />
| <br />
| <br />
| x<br />
| x<br />
| x<br />
| <br />
| <br />
|- align="center"<br />
!{{Hl2}}| [[list]]<br />
| <br />
| <br />
| x<br />
| <br />
| x<br />
| <br />
| <br />
|- align="center"<br />
!{{Hl2}}| [[vector]]<br />
| <br />
| <br />
| x<br />
| <br />
| x<br />
| x<br />
| <br />
|- align="center"<br />
!{{Hl2}}| [[rotation]]<br />
| <br />
| <br />
| x<br />
| <br />
| x<br />
| <br />
| x<br />
|}<br />
<br />
{{#vardefine:p_type_desc|variable type<br />
}}{{#vardefine:p_value_desc|expression or constant<br />
}}<br />
<div id="box"><br />
== Syntax: ({{LSL Param|type}}){{LSL Param|value}} ==<br />
<div style="padding: 0.5em;"><br />
Converts '''value''' to '''type'''.<br />
{|<br />
{{LSL DefineRow|expression|type|{{#var:p_type_desc}}}}<br />
{{LSL DefineRow|expression|value|{{#var:p_value_desc}}}}<br />
|}<br />
If {{LSL Param|value}} is a complex expression, it may be beneficial to wrap it in parentheses. ({{LSL Param|type}})({{LSL Param|value}})<br />
</div></div><br />
<br />
<div id="box"><br />
== Examples ==<br />
<div style="padding: 0.5em;"><br />
<lsl><br />
string a = "1.5";<br />
float b = (float)a;<br />
integer c = (integer)a;<br />
<br />
integer i;<br />
i = (integer) 1.23; // 1<br />
i = (integer) -1.23; // -1<br />
i = (integer) "0123"; // 123<br />
i = (integer) "0x12A"; // 298<br />
i = (integer) " -5 "; // -5; leading whitespace is ignored<br />
i = (integer) "105 degrees here, it is a nice day"; // 105; non-numeric text which follows numeric text is ignored<br />
i = (integer) "String with no numbers"; // 0<br />
<br />
float f;<br />
f = (float) "6.2e1"; // 62.0<br />
f = (float) " -16.2°C is seriously cold!"; // -16.2; (float)string also ignores leading whitespace and trailing non-numeric characters<br />
// "6.2e1", "6.2e+1", "6.2E1", "6.2E+1" are all equivalent.<br />
f = (float) "Nancy"; // any string beginning with "nan" (case insensitive) --<br />
// Mono only: NaN (not a number); LSO: causes a math error<br />
f = (float) "infallible LSL!"; // any string beginning with "inf" (case insensitive) --<br />
// Mono only: Infinity; LSO: causes a math error<br />
<br />
string s;<br />
s = (string) [1, 2.3, "a"]; // "12.300000a"<br />
s = (string) <1.0, 2.3, 4.56>; // "<1.00000, 2.30000, 4.56000>"<br />
<br />
list l;<br />
l = (list) ""; // [""]<br />
l = (list) <1.0, 2.3, 4.56>; // ["<1.00000, 2.30000, 4.56000>"]<br />
<br />
vector v;<br />
v = (vector) "<1.0, 2.3, 4.56>"; // <1.0, 2.3, 4.56><br />
v = (vector) "<1.0, 2.3>"; // ZERO_VECTOR (Due to insufficient value)<br />
<br />
rotation r;<br />
r = (rotation) "<1.0, 2.3, 4.56, 1.0>"; // <1.0, 2.3, 4.56, 1.0><br />
r = (rotation) "<1.0, 2.3, 4.56>"; // ZERO_ROTATION (Due to insufficient value)<br />
</lsl><br />
<br />
===Applied===<br />
<lsl>default<br />
{<br />
state_entry()<br />
{<br />
integer iUnixTime = llGetUnixTime(); <br />
string sUnixTime = (string)unixTime;<br />
string sRegionTime = (string)llGetTimeOfDay();<br />
llSetObjectDesc(sUnixTime);<br />
llSetText(sRegionTime, <1.0, 0.0 ,0.0>, 1.0);<br />
}<br />
}</lsl><br />
</div></div><br />
<br />
<div id="box"><br />
== Caveats ==<br />
<div style="padding: 0.5em;"><br />
*The compiler allows explicit typecasting where it is not needed and does not optimize it out. Unnecessary typecasts will bloat code and slow it down.<br />
</div></div><br />
<br />
<div id="box"><br />
== Notes ==<br />
<div style="padding: 0.5em;"><br />
*Typcasting from a float to an integer merely removes the portion of the number following the decimal point. To round to the nearest whole integer, use [[llRound]].<br />
*For getting at the elements of a list use the {{LSLGC|List/Read Element|llList2*}} functions.<br />
</div></div></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Template:LSL_Function/give&diff=1164145Template:LSL Function/give2012-03-16T19:15:15Z<p>Felis Darwin: Fix double "is" for some uses, make what</p>
<hr />
<div><includeonly>{{#if:<br />
<br />
{{#vardefine:target-type|~{{#expr:{{#if:{{{avatar|}}}|1|0}}+{{#if:{{{prim|}}}{{{object|}}}|2|0}}+{{#if:{{{link|}}}|4|0}}}}}}<br />
<br />
{{#vardefine:pcaveats|{{#var:caveats}}}}<br />
{{#vardefine:caveats}}<br />
<br />
{{LSL Function/{{#switch:{{#var:target-type}}|~1=avatar|~2=prim|~4=link|uuid}}|object={{{object|}}}|sim={{{sim|}}}|{{{1|}}}|}}<br />
<br />
{{LSL Function/inventory|{{{2|}}}|type={{{type|}}}|uuid={{{uuid|}}}|full={{{full|}}}|insert={{{insert|}}}|}}<br />
<br />
{{#vardefine:caveats|{{#var:pcaveats}}{{#ifeq:{{#var:target-type}}|~4||<br />
* If '''{{LSL Param|{{{1}}}}}''' is {{#switch:{{#var:target-type}}|~1=not the owner|~2=not owned by the same person|not the owner nor shares the same owner}}, {{#ifeq:{{{uuid|}}}|true|'''{{LSL Param|{{{2}}}}}''' is not a [[UUID]]}} and '''{{LSL Param|{{{2}}}}}''' does not have transfer permissions, an error is shouted on {{#var:DEBUG_CHANNEL}}.<br />
{{#if:{{{copyok|}}}||<br />
* If '''{{LSL Param|{{{2}}}}}''' {{#if:{{{full|}}}|is not {{HoverLink|:Category:LSL_Permissions/Asset|copy, mod and transfer|full permissions}}|permissions do not allow copy}}, the transfer fails and an error is shouted on {{#var:DEBUG_CHANNEL}}.{{#switch:{{#var:target-type}}<br />
|~1=|* If '''{{LSL Param|{{{1}}}}}''' {{#ifeq:{{#var:target-type}}|~2||is a prim that}} is not in the same region an error is shouted on {{#var:DEBUG_CHANNEL}}.}}}}}}<br />
{{#switch:{{{type|}}}|script|=* When scripts are copied or moved between inventories, their state does not survive the transfer. Memory, event queue and execution position are all discarded.|#default=}}<br />
{{#var:caveats}}}}<br />
<br />
{{#vardefine:pcaveats}}<br />
<br />
}}</includeonly><noinclude><br />
{{Multi-lang}}<br />
{{UpdateLink}}<br />
<br />
{| {{Prettytable}} {{#vardefine:caveats}}<br />
|+<code><nowiki>{{</nowiki>{{FULLPAGENAMEE}}<nowiki>|target|item|type=sound|uuid=true}}</nowiki></code>{{{{FULLPAGENAMEE}}|target|item|type=sound|uuid=true}}<br />
|-{{Hl2}}<br />
! #var<br />
! value<br />
|-<br />
{{VarPair|caveats}}<br />
|-<br />
{{VarPair|p_target_desc}}<br />
|-<br />
{{VarPair|p_target_hover}}<br />
|-<br />
{{VarPair|p_item_desc}}<br />
|-<br />
{{VarPair|p_item_hover}}<br />
|}<br />
<br />
{| {{Prettytable}} {{#vardefine:caveats}}<br />
|+<code><nowiki>{{</nowiki>{{FULLPAGENAMEE}}<nowiki>|target|item|type=sound|uuid=false}}</nowiki></code>{{{{FULLPAGENAMEE}}|target|item|type=sound|uuid=false}}<br />
|-{{Hl2}}<br />
! #var<br />
! value<br />
|-<br />
{{VarPair|caveats}}<br />
|-<br />
{{VarPair|p_target_desc}}<br />
|-<br />
{{VarPair|p_target_hover}}<br />
|-<br />
{{VarPair|p_item_desc}}<br />
|-<br />
{{VarPair|p_item_hover}}<br />
|}<br />
<br />
{| {{Prettytable}} {{#vardefine:caveats}}<br />
|+<code><nowiki>{{</nowiki>{{FULLPAGENAMEE}}<nowiki>|target|item|type=script|uuid=true|link=*}}</nowiki></code>{{{{FULLPAGENAMEE}}|target|item|type=script|uuid=true|link=*}}<br />
|-{{Hl2}}<br />
! #var<br />
! value<br />
|-<br />
{{VarPair|caveats}}<br />
|-<br />
{{VarPair|p_target_desc}}<br />
|-<br />
{{VarPair|p_target_hover}}<br />
|-<br />
{{VarPair|p_item_desc}}<br />
|-<br />
{{VarPair|p_item_hover}}<br />
|}<br />
</noinclude></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlSetObjectName&diff=1162476LlSetObjectName2012-02-04T02:11:16Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function/limits}}{{LSL Function<br />
|func_id=203<br />
|func_sleep=0.0<br />
|func_energy=10.0<br />
|func=llSetObjectName|sort=SetObjectName<br />
|p1_type=string<br />
|p1_name=name<br />
|func_desc=Sets the prim's name according to the '''name''' parameter.<br />
|func_footnote=If this function is called from a child prim in a linked set, it will change the name of the child prim and not the root prim.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* The name is limited to 63 characters. Longer prim names are cut short.<br />
* Names can only consist of the 128 characters found in the ASCII-7 (non-extended) character set.<br />
** Non-ASCII characters will be replaced with two question marks ("??").<br />
* While an object is attached, the script cannot change the name of the object as it appears in the user's inventory. This behavior [http://forums-archive.secondlife.com/255/ab/84853/1.html was a bug], but it [https://jira.secondlife.com/browse/SVC-3429?focusedCommentId=267372&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-267372 remained long enough to become permanent].<br />
** Changes to the name of the root prim (with [[llSetObjectName]] for example) will not be saved to inventory; when the attachment is detached (to inventory, not dropped) this name change is discarded and the name in inventory is used instead.<br />
** Dropping an attachment (to the ground) and taking it into inventory will <!--somebody please verify this--> cause the inventory name of the attachment to be the changed name.<br />
* Changes to the names of child prims will be saved back to inventory when the object is detached to inventory. They survive detachment.<br />
|constants<br />
|examples=<lsl>default<br />
{<br />
state_entry()<br />
{<br />
string yyyy1mm1dd = llGetDate();<br />
string name = yyyy1mm1dd + " " + llGetObjectName();<br />
llOwnerSay("llSetObjectName(\"" + name + "\")");<br />
llSetObjectName(name);<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions=<br />
{{LSL DefineRow||[[llGetObjectName]]|Get the prims name}}<br />
{{LSL DefineRow||[[llGetLinkName]]|Get a linked prims name}}<br />
{{LSL DefineRow||[[llGetObjectDesc]]|Get the prims description}}<br />
{{LSL DefineRow||[[llSetObjectDesc]]|Set the prims description}}<br />
{{LSL DefineRow||[[llGetObjectDetails]]|Get a list of object details}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Prim<br />
|cat2<br />
|cat3<br />
|cat4<br />
|}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlMatchGroup&diff=1161778LlMatchGroup2012-01-21T07:03:25Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function<br />
|inject-2={{Issues/SCR-79}}{{Issues/SVC-2818}}{{Issues/SVC-708}}{{LSL_Function/uuid|id|sim=*}}<br />
|func=llMatchGroup<br />
|sort=llMatchGroup<br />
|func_id<br />
|func_sleep0.0<br />
|func_energy=0<br />
|func_desc=This function is a request from several scripters to reach what [[llDetectedGroup]] and [[llSameGroup]] is not able to provide. It would not cause any security and/or privacy issues due the fact it will return a integer as {{HoverText|boolean|TRUE or FALSE}} instead of any information about the group. Furthermore, this function will ''only'' check against an agent's current active group, something which can be readily discovered by most residents using the standard viewer even if the group is marked as hidden.<br />
|func_footnote=<br />
|return_type=integer<br />
|return_text={{HoverText|boolean|TRUE or FALSE}}; [[TRUE]] if the agent has '''group''' as active group, otherwise [[FALSE]]<br />
|spec=* Still unknow.<br />
|p1_type=key|p1_name=avatar|p1_desc=avatar [[UUID]] that is in the same [[region]]|p1_hover=avatar UUID that is in the same region <br />
|p2_type=key|p2_name=group|p2_desc=group UUID to check against avatar's current active group|p2_hover=group UUID to check against avatar's current active group<br />
|p3_type|p3_name|p3_desc|p3_hover<br />
|p4_type|p4_name|p4_desc|p4_hover<br />
|p5_type|p5_name|p5_desc|p5_hover<br />
|p6_type|p6_name|p6_desc|p6_hover<br />
|p7_type|p7_name|p7_desc|p7_hover<br />
|p8_type|p8_name|p8_desc|p8_hover<br />
|p9_type|p9_name|p9_desc|p9_hover<br />
|p10_type|p10_name|p10_desc|p10_hover<br />
|p11_type|p11_name|p11_desc|p11_hover<br />
|p12_type|p12_name|p12_desc|p12_hover<br />
|constants<br />
|caveats=* Can still check the agent's active group even if it is hidden<br />
|examples=<lsl>key group_key = "cd505f45-a33c-dcd5-eb7e-cad27847a4af"; //Key of some group<br />
key toucher; //Key of person who touches the object<br />
<br />
default<br />
{<br />
touch_start(integer total_number)<br />
{<br />
toucher = llGetDetected(0);<br />
<br />
if (llMatchGroup(toucher, group_key) == TRUE) { //It checks if the agent (avatar) has<br />
// the group of "group_key" as active group<br />
llSay(0,"You belong to the group secondlife:///app/group/" + (string)group_key + "/about");<br />
} else {<br />
llSay(0,"You don't belong to the group secondlife:///app/group/" + (string)group_key + "/about");<br />
}<br />
}<br />
}</lsl><br />
|helpers<br />
|also_header<br />
|also_functions={{LSL DefineRow||[[llDetectedGroup]]|Used in conjunction with {{LSLGC|Detected|detection}} events}}<br />
{{LSL DefineRow||[[llSameGroup]]}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|also_footer<br />
|notes=Please go vote if this feature is important to you. [[https://jira.secondlife.com/browse/SCR-79 Jira]]<br />
|comments="A function, tentatively titled llMatchGroup(), which takes two parameters: an agent/object key and a group key, and returns a boolean result. (e.g. integer llMatchGroup(key target, key group); ) The function simply checks "target"'s active group against the key "group", returns a 1 if the two match and returns a 0 if they do not. For the purposes this is intended to cover it would only need to work for targets located within the same sim. (Though it would be nice if it could check targets 96m within adjacent sims, a la llGetObjectDetails().)<br />
<br />
I believe this function would satisfy both needs for the most part; it would allow scripters to set up multi-group authorization systems, but would not allow them to potentially breach resident privacy by in essence "fishing" for group information as implementing SVC-1612 would allow. Any scripts using it would have to know the key of the group in advance to get any meaningful information out of this.<br />
<br />
It should also be noted that this functionality is strongly desired, to the point where designers use rather complex methods to accomplish this despite the heavy overhead required. I've seen a few security/etc. setups where the system relied on multiple objects set to different groups, each using llSameGroup() within intercommunicating scripts in order to allow multiple-group access checking. Implementing this would allow developers to use self-contained scripts to accomplish the same thing."<br />
<br />
"'''IF''' this idea is implemented, I'd much rather see the function called llIsGroupActive or llCheckGroupActive as llMatchGroup is too vague."<br />
<br />
- Text extracted from [https://jira.secondlife.com/browse/SCR-79 Jira]<br />
|mode=request<br />
|deprecated<br />
|location<br />
|cat1<br />
|cat2<br />
|cat3<br />
|cat4<br />
|cat5<br />
|cat6<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations&diff=1160552LSL Protocol/Restrained Love Relay/Other Implementations2012-01-05T02:36:44Z<p>Felis Darwin: /* Amethyst Plugin version by Felis Darwin */</p>
<hr />
<div>{{Restrained Life Relay Specs TOC}}<br />
<br />
Want to share your own relay script?<br />
<br />
Please create a new page and add a link here.<br />
<br />
== Amethyst Plugin version by Felis Darwin ==<br />
An edited version of the Reference Implementation meant to be used as a plugin for Amethyst collars.<br />
<br />
Adds an actual timeout for the ask dialog, periodic pinging, locking support, a whitelist/blacklist feature, and a few other goodies specific to Amethyst collars.<br />
<br />
[[LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin|This version's subpage]]<br />
<br />
== Maike Short's Version ==<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay|Maike Short's Relay]]<br />
<br />
== Dominatech RLV Relay ==<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Dominatech RLV Relay|Julia Banshee's relay]], with multiple device support.<br />
<br />
== THINK KINK's tkPBA (Personal Bondage Assistant) ==<br />
<br />
[[LSL_Protocol/Restrained_Life_Relay/Other_Implementations/Think_Kink_Restrained_Life_PBA|THINK KINK's]], multiple device relay with '''!who''', '''!x-who''' (it is unclear whether !who will be allowed to remain in the standard spec), '''!x-vision''', '''SAFEWORD''' and '''HARDCORE''' support.</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Living_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=1160551LSL Protocol/Restrained Living Relay/Other Implementations/Felis Darwin's Amethyst Plugin2012-01-05T02:35:11Z<p>Felis Darwin: moved LSL Protocol/Restrained Living Relay/Other Implementations/Felis Darwin's Amethyst Plugin to LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin</p>
<hr />
<div>#REDIRECT [[LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=1160550LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2012-01-05T02:35:11Z<p>Felis Darwin: moved LSL Protocol/Restrained Living Relay/Other Implementations/Felis Darwin's Amethyst Plugin to LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst RLV Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. It also adds some nice features for Owners set on the collar, such as automatic exceptions to certain restrictions (IM and TP) and the ability to "lock" the plugin's mode so that the wearer cannot turn it off. It currently complies with Marine's v1.100 spec.<br />
<br />
Finally, this implementation does ''not'' include support for multiple control objects, due to the overhead necessary for the plugin to interact with the Amethyst collar system. If you absolutely need this functionality I recommend using one of the many free Relay HUDs.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.<br />
<br />
:- Changed the release routine so it releases restrictions in reverse order, per the new spec.<br />
<br />
:- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.<br />
<br />
:- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
:- A few other script cleanups that should add a little more working memory to the relay.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1100; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination = nullkey;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") || index == -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number) or empty?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage=nullstr) + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = nullstr;<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != nullstr)<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != nullstr)<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + "\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits, and disregard commands issued with no variable (clear is handled elsewhere)<br />
if(param == nullstr || (~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && ((integer)param <= 0))<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
{<br />
//== If we're seated on the kSource and it's restricting us, it's probably the sit target<br />
if(lastForceSitDestination == nullkey && (command == "@unsit=n" || command == "@unsit=add") && llGetAgentInfo(llGetOwner()) & AGENT_SITTING)<br />
lastForceSitDestination = kSource;<br />
<br />
return;<br />
}<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
//== If this somehow isn't a sit command (or there's no valid target) then return<br />
if(behav != "@sit:" || llStringLength(param) != 36)<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
//== Do this in reverse order because 1) it saves memory, and 2) the spec says so<br />
integer len;<br />
for (len = (llGetListLength(lRestrictions) - 1); len >= 0; len--)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, len)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,len)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, len),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
lastForceSitDestination = nullkey;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName=nullstr;<br />
sPendingMessage=nullstr;<br />
llListen (-1812221819, nullstr, nullstr, nullstr);<br />
llListen (-1812220409, nullstr, llGetOwner(), nullstr);<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit" && lastForceSitDestination != nullkey)<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == nullstr && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == nullstr && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and in that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == nullstr || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
setby = nullkey;<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
setby = nullkey;<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName=nullstr;<br />
sPendingId=nullkey;<br />
sPendingMessage=nullstr;<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl><br />
<br />
[[Category:RestrainedLove]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=1160549LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2012-01-05T02:34:17Z<p>Felis Darwin: </p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst RLV Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. It also adds some nice features for Owners set on the collar, such as automatic exceptions to certain restrictions (IM and TP) and the ability to "lock" the plugin's mode so that the wearer cannot turn it off. It currently complies with Marine's v1.100 spec.<br />
<br />
Finally, this implementation does ''not'' include support for multiple control objects, due to the overhead necessary for the plugin to interact with the Amethyst collar system. If you absolutely need this functionality I recommend using one of the many free Relay HUDs.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.<br />
<br />
:- Changed the release routine so it releases restrictions in reverse order, per the new spec.<br />
<br />
:- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.<br />
<br />
:- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
:- A few other script cleanups that should add a little more working memory to the relay.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1100; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination = nullkey;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") || index == -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number) or empty?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage=nullstr) + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = nullstr;<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != nullstr)<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != nullstr)<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + "\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits, and disregard commands issued with no variable (clear is handled elsewhere)<br />
if(param == nullstr || (~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && ((integer)param <= 0))<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
{<br />
//== If we're seated on the kSource and it's restricting us, it's probably the sit target<br />
if(lastForceSitDestination == nullkey && (command == "@unsit=n" || command == "@unsit=add") && llGetAgentInfo(llGetOwner()) & AGENT_SITTING)<br />
lastForceSitDestination = kSource;<br />
<br />
return;<br />
}<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
//== If this somehow isn't a sit command (or there's no valid target) then return<br />
if(behav != "@sit:" || llStringLength(param) != 36)<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
//== Do this in reverse order because 1) it saves memory, and 2) the spec says so<br />
integer len;<br />
for (len = (llGetListLength(lRestrictions) - 1); len >= 0; len--)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, len)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,len)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, len),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
lastForceSitDestination = nullkey;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName=nullstr;<br />
sPendingMessage=nullstr;<br />
llListen (-1812221819, nullstr, nullstr, nullstr);<br />
llListen (-1812220409, nullstr, llGetOwner(), nullstr);<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit" && lastForceSitDestination != nullkey)<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == nullstr && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == nullstr && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and in that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == nullstr || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
setby = nullkey;<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
setby = nullkey;<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName=nullstr;<br />
sPendingId=nullkey;<br />
sPendingMessage=nullstr;<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl><br />
<br />
[[Category:RestrainedLove]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlList2String&diff=1159073LlList2String2011-12-04T07:17:15Z<p>Felis Darwin: Link to llDumpList2String</p>
<hr />
<div>{{LSL_Function/list/element|src|index|string|nf=*|return={{HoverText|null string|null string: ""}}}}{{LSL_Function<br />
|func_id=188|func_sleep=0.0|func_energy=10.0<br />
|func=llList2String|return_type=string|p1_type=list|p1_name=src|p2_type=integer|p2_name=index<br />
|func_footnote<br />
|func_desc<br />
|return_text=that is at '''index''' in '''src'''.<br />
|spec<br />
|caveats=*When using this function to typecast a list element to a string it will truncated float based types to 6 decimal places.<br />
|constants<br />
|examples=<lsl>//This code demonstrates the differences in typecasting in LSL (and demonstrates how to use the llList2* functions).<br />
// Best viewed in Chat History (ctrl-h)<br />
default<br />
{<br />
state_entry()<br />
{<br />
list my_list = ["a", "0xFF", "0xFF.FF", "1.0e3", 1, 2.0, <1,2,3>, <1,2,3,4>, llGetOwner()];<br />
integer i = 0;<br />
integer end = llGetListLength(my_list);<br />
for (; i<end; ++i)<br />
{<br />
llOwnerSay("string=" + llList2String(my_list,i)<br />
+ "\n integer=" + (string)llList2Integer(my_list,i) + " OR " +(string)((integer)llList2String(my_list,i))<br />
+ "\n float=" + (string)llList2Float(my_list,i) + " OR " +(string)((float)llList2String(my_list,i))<br />
+ "\n vector=" + (string)llList2Vector(my_list,i) + " OR " +(string)((vector)llList2String(my_list,i))<br />
+ "\n rot=" + (string)llList2Rot(my_list,i) + " OR " +(string)((rotation)llList2String(my_list,i))<br />
+ "\n key=" + (string)llList2Key(my_list,i) + " OR " +(string)((key)llList2String(my_list,i)) );<br />
}<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions={{LSL DefineRow||[[llDumpList2String]]|}}<br />
{{LSL DefineRow||[[llGetListEntryType]]|}}<br />
{{LSL DefineRow||[[llList2Float]]|}}<br />
{{LSL DefineRow||[[llList2Integer]]|}}<br />
{{LSL DefineRow||[[llList2Key]]|}}<br />
|also_events<br />
|also_tests<br />
|also_articles<br />
|notes=*If you wish to extract a string from a list that you know will contain only a single item (for example if you extract a single entry from a list using [[llList2List|llList2List()]]), then instead of using <code>llList2String(myList, 0)</code> you may wish to considering using the more efficient <code>(string)myList</code> as it will produce the same result for single-entry lists with less memory usage due to eliminating a function-call.<br />
*To convert a string of hexadecimal notation to integer, call [[llList2Integer]] and it will automatically cast the value as decimal integer. To convert that integer back to a string of hexadecimal notation, use a user function like [[hex]].<br />
|permission<br />
|cat1=List<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Talk:LSL_Protocol/Restrained_Love_Relay/Specification&diff=1155805Talk:LSL Protocol/Restrained Love Relay/Specification2011-10-15T02:38:09Z<p>Felis Darwin: /* llRegionSayTo */</p>
<hr />
<div>== Script name requirement ==<br />
<br />
The current version of the spec contains rules for the name of the relay script. The problem is, the rules seem to preclude the possibility of updating using llRemoteLoadScriptPin, which requires a stable name for the target script in order to properly replace it. If the script is changed to support a new version, the spec requires the script be renamed, but that's impractical for a script subject to automatic updating. I'm also unsure if there's any point to this rule. In the case of the product I'm currently working on, the relay script is buried in a child prim (to prevent relayed rules interfering with its own rules), which makes attempting to check its name a pretty unfriendly way for the end user to determine version information. Having version information easily available to the end user is a good idea, but requiring the script be named a particular way doesn't provide that, it just interferes with auto-update functionality without apparent benefit. Is there some other reason for saying a script must be named a particular way (e.g. the viewer checks the script name for this info), or can that rule be safely ignored (despite the "must" in the spec)? If there's a technical reason for the naming requirement, can it be changed so that the prim containing the script is required to be named a particular way, rather than the script itself?<br />
--[[User:Galatea Gynoid|Galatea Gynoid]] 17:48, 19 August 2008 (PDT)<br />
<br />
: I think there is no technical reason. --[[User:Maike Short|Maike Short]] 12:26, 22 August 2008 (PDT)<br />
<br />
: There is no technical reason indeed, and I think that naming rule may be lifted in the near future, especially if it is a problem for people to update their stuff... however the user 'must' be able to check the version one way or another. --[[User:Marine Kelley|Marine Kelley]] 11:44, 1 September 2008 (PDT)<br />
<br />
::Fair enough -- I'm using a "Version..." command in my menus that prints:<br />
::* Implant Version 0.7<br />
::* Compatibility:<br />
::* Restrained Life Viewer: 1.12.2 +<br />
::* Relay Specification: 1.014<br />
::iz gud? --[[User:Galatea Gynoid|Galatea Gynoid]] 23:44, 1 September 2008 (PDT)<br />
<br />
== !release command from relay ==<br />
<br />
Really just for clarification. But it's mentioned several times that a relay should issue a !release metacommand to force a session to end, however this isn't very clear. I'm assuming that the relay, for whatever reason, chooses to clear an object's commands, and sends a !release command to the object such as <code>CmdName,<object uuid>,!release,ok</code>. Should CmdName be something specific in this case? If not does that require in-world objects to recognise arbitrary command names other than those that it produced itself? This could do with some clarification and/or examples under the !release heading to avoid confusion, as I'm assuming objects should be aware of !release so that they know they no longer have control over an avatar.<br/>-- '''[[User:Haravikk_Mistral|Haravikk]]''' <sup><small>([[User_talk:Haravikk_Mistral|talk]]|[[Special:Contributions/Haravikk_Mistral|contribs]])</small></sup> 20:11, 24 October 2010 (UTC)<br />
:I saw a few devices who send a !pong answer with another CmdName than ping, although the specification says it has to be ping. So... even if the specification was clear for !release, I would not be surprised if some relays still sent another CmdName string. So careful anyway ;-). --[[User:Satomi Ahn|Satomi Ahn]] 14:56, 27 October 2010 (UTC)<br />
<br />
== [[llRegionSayTo]] ==<br />
<br />
This new LSL command is a godsend for RLV relays and furniture. Its purpose is to send channel chat to one target key in the region only. If the key is that of an avatar, then all their attachments (including their RLV relay) will receive the message and no other object. Conversely, the relay can send acknowledgements back to the trap/furniture key, and no other object will hear them and even less process them with LSL.<br />
<br />
In short, this new command helps easing on sim lag, and does not create backward compatibility issues for RLV devices who start using it.<br />
<br />
Maybe this should be stated as an explicit recommendation in the RLV Relay protocol main page?<br />
<br />
--[[User:Satomi Ahn|Satomi Ahn]] 09:21, 1 June 2011 (PDT)<br />
<br />
I strongly support this! I am currently doing an application where I am getting crazy by some relays answering on llSay, some on llShout, some on llWhisper... and I need the responses!<br />
<br />
--[[User:Unya Tigerfish|Unya Tigerfish]] 03:11, 7 July 2011 (PDT)<br />
<br />
Just a few caveats for llRegionSayTo:<br />
* I haven't met any furniture that would not work with a relay using llRegionSayTo, but I can see a possible scenario where it would fail. Imagine the furniture listens and speaks from two different prims. Then if the relay uses llRegionSayTo to speak to the primitive who spoke to it, then the furniture won't be able to hear the message. If this becomes an official recommendation, then it should also be recommended that the same prim is used for communication both ways.<br />
* On the furniture side, the relay should obviously send the llRegionSayTo message to the avatar UUID, since the relay UUID is not known first. llRegionSayTo works in such a way that if an avatar UUID is given (and channel is not 0), then the message will be heard by all their attachments.<br />
--[[User:Satomi Ahn|Satomi Ahn]] 06:51, 11 July 2011 (PDT)<br />
<br />
:I'm not sure when this was added, but if call this function on an Avatar's key using a non-zero channel, every attachment worn by that avatar will hear what is sent. That effectively solves the issues with using this function for issuing relay commands. The relay itself using it is almost as reliable, though I'd still recommend using both transmission methods, since one can extend past sim boundaries and one can't. -- [[User:Felis Darwin|Felis Darwin]] 19:38, 14 October 2011 (PDT)</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=827512LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2010-03-29T03:02:08Z<p>Felis Darwin: /* Current Source Code */</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Though it currently complies with Marine's v1.100 spec, I fully intend for it to have additional features not covered here already.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.<br />
<br />
:- Changed the release routine so it releases restrictions in reverse order, per the new spec.<br />
<br />
:- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.<br />
<br />
:- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
:- A few other script cleanups that should add a little more working memory to the relay.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1100; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination = nullkey;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") || index == -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number) or empty?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage=nullstr) + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = nullstr;<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != nullstr)<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != nullstr)<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + "\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits, and disregard commands issued with no variable (clear is handled elsewhere)<br />
if(param == nullstr || (~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && ((integer)param <= 0))<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
{<br />
//== If we're seated on the kSource and it's restricting us, it's probably the sit target<br />
if(lastForceSitDestination == nullkey && (command == "@unsit=n" || command == "@unsit=add") && llGetAgentInfo(llGetOwner()) & AGENT_SITTING)<br />
lastForceSitDestination = kSource;<br />
<br />
return;<br />
}<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
//== If this somehow isn't a sit command (or there's no valid target) then return<br />
if(behav != "@sit:" || llStringLength(param) != 36)<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
//== Do this in reverse order because 1) it saves memory, and 2) the spec says so<br />
integer len;<br />
for (len = (llGetListLength(lRestrictions) - 1); len >= 0; len--)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, len)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,len)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, len),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
lastForceSitDestination = nullkey;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName=nullstr;<br />
sPendingMessage=nullstr;<br />
llListen (-1812221819, nullstr, nullstr, nullstr);<br />
llListen (-1812220409, nullstr, llGetOwner(), nullstr);<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit" && lastForceSitDestination != nullkey)<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == nullstr && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == nullstr && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and in that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == nullstr || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
setby = nullkey;<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
setby = nullkey;<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName=nullstr;<br />
sPendingId=nullkey;<br />
sPendingMessage=nullstr;<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=827502LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2010-03-29T02:06:29Z<p>Felis Darwin: /* Current Source Code */ new source</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Though it currently complies with Marine's v1.100 spec, I fully intend for it to have additional features not covered here already.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.<br />
<br />
:- Changed the release routine so it releases restrictions in reverse order, per the new spec.<br />
<br />
:- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.<br />
<br />
:- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
:- A few other script cleanups that should add a little more working memory to the relay.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1100; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination = nullkey;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") || index == -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number) or empty?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage=nullstr) + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = nullstr;<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != nullstr)<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != nullstr)<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits, and disregard commands issued with no variable (clear is handled elsewhere)<br />
if(param == nullstr || (~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && ((integer)param <= 0))<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
{<br />
//== If we're seated on the kSource and it's restricting us, it's probably the sit target<br />
if(lastForceSitDestination == nullkey && (command == "@unsit=n" || command == "@unsit=add") && llGetAgentInfo(llGetOwner()) & AGENT_SITTING)<br />
lastForceSitDestination = kSource;<br />
<br />
return;<br />
}<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
//== If this somehow isn't a sit command (or there's no valid target) then return<br />
if(behav != "@sit:" || llStringLength(param) != 36)<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
//== Do this in reverse order because 1) it saves memory, and 2) the spec says so<br />
integer len;<br />
for (len = (llGetListLength(lRestrictions) - 1); len >= 0; len--)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, len)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,len)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, len),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
lastForceSitDestination = nullkey;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName=nullstr;<br />
sPendingMessage=nullstr;<br />
llListen (-1812221819, nullstr, nullstr, nullstr);<br />
llListen (-1812220409, nullstr, llGetOwner(), nullstr);<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit" && lastForceSitDestination != nullkey)<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == nullstr && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == nullstr && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and in that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == nullstr || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
setby = nullkey;<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
setby = nullkey;<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = nullstr;<br />
sPendingMessage = nullstr;<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName=nullstr;<br />
sPendingId=nullkey;<br />
sPendingMessage=nullstr;<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=827492LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2010-03-29T02:04:22Z<p>Felis Darwin: /* v0.6 */</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Though it currently complies with Marine's v1.100 spec, I fully intend for it to have additional features not covered here already.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.<br />
<br />
:- Changed the release routine so it releases restrictions in reverse order, per the new spec.<br />
<br />
:- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.<br />
<br />
:- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
:- A few other script cleanups that should add a little more working memory to the relay.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
return;<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=nullkey;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=727362LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2010-02-13T07:07:44Z<p>Felis Darwin: </p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Though it currently complies with Marine's v1.100 spec, I fully intend for it to have additional features not covered here already.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec. I'm happy to say this is the ONLY thing I had to do to make the relay v1.100 compliant.<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
return;<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=nullkey;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=727352LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2010-02-13T07:00:33Z<p>Felis Darwin: /* Current Source Code */ Update to v0.6</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec. I'm happy to say this is the ONLY thing I had to do to make the relay v1.100 compliant.<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
//== Trim off the _sec part so exceptions can be passed<br />
if(~llSubStringIndex(behav, "_sec"))<br />
behav = llGetSubString(behav, 0, -5);<br />
<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
<br />
//== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
//== Changed to work a little differently<br />
rememberForceSit(string command)<br />
{<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
if (param != "=force")<br />
return;<br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
//== Depreciated but still supported here because it's easy<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=nullkey;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=727342LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2010-02-13T06:59:53Z<p>Felis Darwin: /* Current Changelog */</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
Current version: v0.6<br />
<br />
===v0.6===<br />
<br />
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.<br />
<br />
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec. I'm happy to say this is the ONLY thing I had to do to make the relay v1.100 compliant.<br />
<br />
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.<br />
<br />
:- Removed a few unused variables and irrelevant comments that were lying about.<br />
<br />
===v0.5===<br />
<br />
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.<br />
<br />
===v0.4===<br />
<br />
:- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
::+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
::+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
:- Rewrote the rememberForceSit command to use less resources.<br />
<br />
:- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
:- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
:- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
===v0.3===<br />
<br />
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
:- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
:- Added a means to turn off the "ping" requirement<br />
<br />
<br />
===v0.2 and v0.1===<br />
<br />
No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
integer sentRLV=0;<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
sentRLV=1;<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
rememberForceSit(string command)<br />
{<br />
<br />
// list tokens_command=llParseString2List (command, ["="], []);<br />
// string behav=llList2String (tokens_command, 0); // @sit:<uuid><br />
// string param=llList2String (tokens_command, 1); // force<br />
<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
// if (param != "force")<br />
if (param != "=force")<br />
return;<br />
<br />
// tokens_command=llParseString2List(behav, [":"], []);<br />
// behav=llList2String (tokens_command, 0); // @sit<br />
// param=llList2String (tokens_command, 1); // <uuid><br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
// if (behav != "@sit")<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=nullkey;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations&diff=727332LSL Protocol/Restrained Love Relay/Other Implementations2010-02-13T06:39:00Z<p>Felis Darwin: /* Amethyst Plugin version by Felis Darwin */</p>
<hr />
<div>{{Restrained Life Relay Specs TOC}}<br />
<br />
Want to share your own relay script?<br />
<br />
Please create a new page and add a link here.<br />
<br />
== Amethyst Plugin version by Felis Darwin ==<br />
An edited version of the Reference Implementation meant to be used as a plugin for Amethyst collars.<br />
<br />
Adds an actual timeout for the ask dialog, periodic pinging, locking support, a whitelist/blacklist feature, and a few other goodies specific to Amethyst collars.<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Felis Darwin's Amethyst Plugin|This version's subpage]]<br />
<br />
== Maike Short's Version ==<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Maike Short's Relay|Maike Short's Relay]]<br />
<br />
== Dominatech RLV Relay ==<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Dominatech RLV Relay|Julia Banshee's relay]], with multiple device support.<br />
<br />
== THINK KINK's tkPBA (Personal Bondage Assistant) ==<br />
<br />
[[LSL_Protocol/Restrained_Life_Relay/Other_Implementations/Think_Kink_Restrained_Life_PBA|THINK KINK's]], multiple device relay with '''!who''', '''!x-who''' (it is unclear whether !who will be allowed to remain in the standard spec), '''!x-vision''', '''SAFEWORD''' and '''HARDCORE''' support.</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=Talk:LSL_Protocol/RestrainedLoveAPI&diff=466792Talk:LSL Protocol/RestrainedLoveAPI2009-08-20T06:06:24Z<p>Felis Darwin: /* Curious behavior with exceptions and @sendim/@recvim */ new section</p>
<hr />
<div>===Question regarding @remoutfit and teens===<br />
<br />
Thank you for putting up that API.<br />
<br />
But I got a question regarding the ''@remoutfit[:<part>]=<y/n>''-command:<br />
It says''(underpants and underwear are kept for teens)''. What do you mean with that?<br />
<br />
Is removing underwear disabled only on teengrid? Is it disabled in PG Areas? Or just one of your nice jokes? :-)<br />
<br />
MK: No no this time it was not a joke, the standard viewer actually checks that you're not on the teen grid and if you are, discards removing underwear. I took care of not breaking that check when adding the remoutfit command :)<br />
<br />
== How to use for bondage furniture? ==<br />
<br />
Hi Marine,<br />
<br />
Good stuff, your viewer. I have a question on how to use the API in bondage furniture. <br />
<br />
As a test I did put the @remoutfit=force in the menuhandler of a bondage cross that I have. Now, when I use the menu while not sitting on the cross ... I am suddenly naked. Blush. Nice effect, but not what I expected. I expected the command to work on the AvatarOnSitTarget, if that person would use your viewer. <br />
<br />
Do I do something wrong, or is your viewer focused to impact the Owner of worn attachments? If the former, please let me know how to do it right. If the latter, it would be great if the viewer would work on Owner in case of attachments, and work on AvatarOnSitTarget in case of objects to sit on.<br />
<br />
Looking forward to your answer.<br />
Ciao, Tam<br />
<br />
<br />
The answer came to me in world. Use the RealRestraint Relay<br />
https://wiki.secondlife.com/wiki/LSL_Protocol/RestrainedLifeRelaySpecs<br />
<br />
<br />
== Meaningless or App-Defined Rules ==<br />
<br />
I noticed (accidentially, due to a misspelling) that if you send a rule that doesn't exist, it has no effect, but it DOES remember the rule, and can be retrieved using @getstatus. For example, you can llOwnerSay("@randomtext=n") and later when you call llOwnerSay("@getstatus=x") the rules in the response will include "randomtext" in the list.<br />
<br />
Is this a bug or a feature? The reason I ask is, there are some good uses for this if the behavior can be depended upon, but if it's likely to disappear in a future version, then I'll steer clear of using it. Thanks.<br />
<br />
--[[User:Galatea Gynoid|Galatea Gynoid]] 12:07, 17 August 2008 (PDT)<br />
<br />
<br />
Actually there is no such thing as "a rule that does not exist", the viewer takes whatever it is sent and puts it into its internal list for later. This is because I don't want to duplicate commands (I have to for some of them, for instance when checking whether the inventory is already open when issuing a @showinv). It's by design so it's easier to just poke at the code and add your own command without wondering whether you have to add it to a static table somewhere. So it's neither a bug nor a feature I would say.<br />
<br />
And to answer your actual question, it's very unlikely to change in the future, at least as long as I'm in charge. Yes you're right, it could be a good way to store data in-world for the time of a unique session. However keep in mind that @getstatus gives a regular chat message, that is capped at 1023 characters.<br />
<br />
--[[User:Marine Kelley|Marine Kelley]] 01:47, 18 August 2008 (PDT)<br />
<br />
== @sendchannel needs a bit of clarification ==<br />
<br />
The way it's worded, the explanation of sendchannel is a bit confusing. I ''assume'' the way the restriction works is that the command @sendchannel=n would restrict all alt-channel chat, but the command @sendchannel:3=n would allow chat on that specific channel even if the global restriction is in place. The way it's worded though, it seems like it could be that @sendchannel:3=n would restrict chat ''only'' on channel 3. -- [[User:Felis Darwin|Felis Darwin]] 21:12, 7 December 2008 (UTC)<br />
<br />
Maybe it's poorly worded... @sendchannel:3=n does add an exception to the @sendchannel restriction, in other words you can speak on channel 3 when the former is applied.<br />
<br />
[[User:Marine Kelley|Marine Kelley]] 15:16, 4 April 2009 (UTC)<br />
<br />
== @acceptpermission & @denypermission ==<br />
<br />
Marine, your blog lists these new commands, but I don't see anything on usage in here. Were they overlooked? --[[User:Teyonas Miklos|Teyonas Miklos]] 07:19, 8 February 2009 (UTC)<br />
<br />
By the way, if @acceptpermission would also affect animation permission, that would be awesome! --[[User:Satomi Ahn|Satomi Ahn]] 16:42, 19 February 2009 (UTC)<br />
:Oh it looks like the wiki has been completed now. Thanks Marine. But, may I ask, do @acceptpermission & @denypermission really work for animation permission now? Your blog says it is only for attachements and controls! --[[User:Satomi Ahn|Satomi Ahn]] 11:52, 31 March 2009 (UTC)<br />
<br />
::Yes if I remember correctly it only accepts attach and controls permissions... I am really torn on the animation permission because it is automatically accepted if you're sitting on the prim that contains the animation (like furnitures), or wearing it (like restraints) but it proves to be a vulnerability for griefers to exploit. Granted, griefing with weird animations is lame (as is griefing in general), but it is griefing nonetheless. I should see how to accept animation permission requests from prims you are sitting on, regardless of whether they contain the animation or not. That should help for rezzable poseballs at least. [[User:Marine Kelley|Marine Kelley]] 15:13, 4 April 2009 (UTC)<br />
<br />
::: Is there a way to start an animation which is not in the prim? In case it is possible it would be interesting to extend the relay protocol to allow this. But I thought this was impossible? Another idea is to explicitly specify the key of the prim that should be allowed to start an animation: <code>@acceptpermission:7adf6218-ab26-8566-8387-660133840794=add</code>. I am not sure if the viewer has this information in the permission query function. The scenario I have in mind is a Star Trek like force-field cell door that will give an electroshock on touch, assuming that the cell has an active relay session. Something else to keep in mind is that there is a pyramid scheme vampire game out there which sees accepting animation permission as accepting to be added to the database. --[[User:Maike Short|Maike Short]] 18:54, 4 April 2009 (UTC)<br />
<br />
::: Eh, you actually wrote this, on the wiki: "Force the viewer to automatically accept attach and 'animate' permission requests : @acceptpermission=<rem/add> ". So I guess this is an error. By the way, Kitty might have pointed this to you, but there is actually a way to to get animation permissions by mean of RLV commands, exploiting a loophole in the logic you use for @acceptpermission. Basically, if this behavior is enabled, if you request permission to animate along with either permission to attach or permission to take controls, then you are granted both. --[[User:Satomi Ahn|Satomi Ahn]] 20:35, 14 April 2009 (UTC)<br />
<br />
== A shared root... and what about a protected root? ==<br />
<br />
I mean, if you are tired to put your tatoos back every time someone strips you... it would be great to be able to protect easily some clothes and attachments (hair, tail, ears,...).<br />
You can already somehow do this for clothes with @remoutfit:xxxx=n, and for prims with @detach=n, but this requires scripting, and very often, you will forget to lock them.<br />
<br />
I believe this would be a lot easier if you could have a /#protected folder whose content cannot be force-detached by the mean of rlv commands.<br />
Moreover, it would be even greater if RLV did not report items you are wearing and are in #protected: indeed some scripts will loop until they have successfully detached all they wanted to. If the viewer does not report those items are worn, then those scripts would be happy.<br />
<br />
--[[User:Satomi Ahn|Satomi Ahn]] 23:52, 19 February 2009 (UTC)<br />
<br />
<br />
I like the idea of checking against some kind of viewer-side characteristic of the object, but I'm not sure a folder would do... After all the user could want to protect-unprotect quickly, and moving a large chunk of inventory could reveal to be a hassle. I'll think about it.<br />
<br />
[[User:Marine Kelley|Marine Kelley]] 15:09, 4 April 2009 (UTC)<br />
<br />
<br />
Just a note a posteriori: Kitty Barnett's "nostrip" flag works really well for what I wanted. It would be nice if Marine Kelley's RLV had this feature too. How does it work? Easy: if an item or folder has (nostrip) in its name, then it will be protected against @remoutfit:layer=force or @detach:attachpt=force commands. A lot better than my initial proposal as it is really fine-grained.<br />
<br />
--[[User:Satomi Ahn|Satomi Ahn]] 09:31, 27 July 2009 (UTC)<br />
<br />
== Third party extension @putinv and the SL permission system ==<br />
<br />
In my opinion the SL permission system has a serious) flaw. Objects owned by the land owner do have special rights without a permission dialog:<br />
* manage the ban list<br />
* change the media settings (can be used to get the ip-address of visitors)<br />
* teraform (can be used to destroy the landscape and return rezzed objects to the lost and found folder, thous destroying a lovely place)<br />
<br />
While I think it is an issue that SL automatically grants these rights to objects owned by the land lord, I am concerned that the auto-accept and attach might be used to simplify attacks. So if you implement this inofficial extension (the official RLV viewer does not) please add a warning.<br />
<br />
PS: Oh, it just occured to me that it can be used to write a self replicating worm that jumps across avatars. This would surely have the potential to get the BDSM community some bad PR. --[[User:Maike Short|Maike Short]] 16:18, 8 April 2009 (UTC)<br />
<br />
:Hm there's already a (way too) long discussion about this on the forums.<br />
:Here: http://forums.secondlife.com/showthread.php?t=223796&page=30&pp=15<br />
:And here: http://sldev.free.fr/forum/viewtopic.php?f=7&t=8<br />
:I personnaly don't want to discuss the issue anymore, but I thought I had to point you to this ;-), as I basically share your concerns.<br />
:--[[User:Satomi Ahn|Satomi Ahn]] 20:29, 14 April 2009 (UTC)<br />
<br />
== Protecting attachments against detach ==<br />
<br />
I'm scripting a gizmo to prevent stripping of tattoos, piercings etc. It would be very useful to have a command similar to the "@remoutfit", but for attachments. That way texture clothing and prim clothing can be treated the same way, instead of having to drop a script into each protected attachment.<br />
<br />
My question is: Would it be possible to include commands looking something like this?<br />
* '''''Allow/prevent wearing attachments''''' : @addattach[:<attachpt>]=<y/n><br />
Where attachpt is :<br />
chest|skull|left shoulder|right shoulder|left hand|right hand|left foot|right foot|spine|<br />
pelvis|mouth|chin|left ear|right ear|left eyeball|right eyeball|nose|r upper arm|r forearm|<br />
l upper arm|l forearm|right hip|r upper leg|r lower leg|left hip|l upper leg|l lower leg|stomach|left pec|<br />
right pec|center 2|top right|top|top left|center|bottom left|bottom|bottom right<br />
* '''''Allow/prevent removing attachments''''' : @remattach[:<attachpt>]=<y/n><br />
Where attachpt is :<br />
chest|skull|left shoulder|right shoulder|left hand|right hand|left foot|right foot|spine|<br />
pelvis|mouth|chin|left ear|right ear|left eyeball|right eyeball|nose|r upper arm|r forearm|<br />
l upper arm|l forearm|right hip|r upper leg|r lower leg|left hip|l upper leg|l lower leg|stomach|left pec|<br />
right pec|center 2|top right|top|top left|center|bottom left|bottom|bottom right<br />
<br />
Or is there already an easy way to do this which I've overlooked? --[[User:Scarlett Flores|Scarlett Flores]] 13:58, 13 July 2009 (UTC)<br />
<br />
:There is currently no way to do that in Marine's Viewer, but I can point you to Kitty's nostrip flag [http://rlva.catznip.com/blog/2009/07/feature-nostrip/] which would work well for what you want (currently in Emerald Greenlife Viewer only).<br />
:However there is still a use case for @detach:attachpt=n that cannot be covered by nostrip: for locking an attachement against the wearer's will (but in that case, it is likely the attachment has already been made in the first place for the purpose of being locked, and is scripted with a llOwnerSay("@detach=n")). Still, in my opinion this remains an inconsistency in the current RLV API as @remoutfit:arg can be used both with =n and =force but @detach:arg can only be used with =force...<br />
:--[[User:Satomi Ahn|Satomi Ahn]] 09:39, 27 July 2009 (UTC)<br />
<br />
== Loading a preset is laggy, but still better than loading each parameter by script? ==<br />
<br />
Loading WL presets is laggy, but is it less laggy than loading all parameters by script? Or would a script doing this not cause lag but just lag itself? --[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 21:25, 16 July 2009 (UTC)<br />
<br />
== Version Changelog ==<br />
<br />
Can we have a little section on the API page that briefly lists the additions and changes? --[[User:Ninjafoo Ng|Ninjafoo Ng]] 07:47, 18 July 2009 (UTC)<br />
<br />
== feature suggestion: forcechat and forceIM ==<br />
<br />
How about allowing scripts to make ther person say somthing, emote or otherwise? This woudl serve to, among other things, mind control situations, doing emoting without a choice (like flinching when zapped, blushing when groped etc), having a script send voice commands to another script that usually only obeys voice commands fromt he avatar etc<br />
<br />
ps: is there a better place to make feature suggestions?<br />
<br />
--[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 16:24, 19 July 2009 (UTC)<br />
<br />
<br />
===force chat level (whisper, talk or shout) ===<br />
the idea is to allow scripts to force the target (usually only the owner can be the target) to have all chat messages be either whisper, regular talk or shout, regardless of what keyboard shortcuts or gui buttons were pressed --[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 19:24, 26 July 2009 (UTC)<br />
<br />
==Feature Suggestion: No-Fly/Fly zones ==<br />
<br />
a command to set a range of altitude where flying is allowed or forbidden, operates on top of @fly, defining a zone where the behavior is the opposite of what @fly defined.<br />
<br />
It would also be nice to be able to have both a fly and a no-fly zone defined at once, whatever is defined last will overwrite the other where they overlap (would allow for a zone that has a hole in the middle, a sandwich if you will) --[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 19:13, 28 July 2009 (UTC)<br />
<br />
== Feature sugestion: @Forcefly ==<br />
<br />
This command would force the avatar to start or stop flying, if flying is forbidden by the same object that now is running @Forcefly, the avatar would be unable to change their flying/no flying status --[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 19:21, 28 July 2009 (UTC)<br />
<br />
== Feature suggestion: @Forcepresskeys ==<br />
<br />
This command would result in the client behaving as if the user pressed the keys, including triggering control events in scripts.<br />
<br />
The parameter would be a list of key and time pairs (time as integer miliseconds), so a sequence of keypresses can be defined in single command. Zero, or an special keyword would indicate no key being pressed. Perhaps use two different separators, one to indicate the keys afterward must wait the last key to timeout and another to indicate they should start being pressed already.<br />
<br />
<br />
If possible make all keys avaiable (perhaps except for dangerous ones, like delete, backspace, combinations like alt-F4, control-alt-del (this one isn't as bad as it used to be on Windows, but still better not allow), alt-tab, backspace etc), and for the keypresses must only work inside the client, if another window has focus or somthing, they keys would simply not work, or trigger client behavior event without the client window having focus. --[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 19:34, 28 July 2009 (UTC)<br />
<br />
== Feature suggestion: @allowpay ==<br />
<br />
My goddess brought up a interesting concept of forcing slaves to not being able to spend lindens without the permission of the dom. So my idea is to have a api command to change the status of whether you are allowed to give money or not give money. I may write a patch for it in due time if this function wants to be considered. This would cover all abilities to debit funds from the current balance. [[User:Chibi Pedalo|Chibi Pedalo]] 17:24, 5 August 2009 (UTC)<br />
<br />
== Feature Suggestion: force paint ==<br />
<br />
(Inspired partially by an idea from a friend of mine)<br />
<br />
a command to have the client overlay a texture specified by UUID, over the baked texture, making that the new baked texture, alpha pixels and all<br />
<br />
this would allow scripts to make people get dirty, covered in slime sploches, darkyellow semi-transparent stain between the legs implying the person pissed on their pants, bloody gashes across the face etc<br />
<br />
<br />
there should also be a command to do a clean rebake (a rebake that isntead of being the old rebake, perhaps composited with new textures, would actually do a regular rebake, reading the clothing info and generating the regular baked texture that woudl result form that.<br />
<br />
A copy of dirty baked textures would be kept on the client, when a rebake is done, either by the user request or during default functioning of the client, that saved temporary rebake would be what is sent to the server. Having the dirty rebaked textures be saved would allow for cumulative effects, since each time a new texture is applied the results are saved as the baked texture, each new one would be applied to whatever is there already, be it the normal baked texture, or one with dirt on it.<br />
<br />
There would also of course be a command to lock/unlock the current (re)baked texture, if it's dirty, prevent a clean rebake, and if it isn't prevent, though better would be separated locks, one for clean rebake, and one for getting any more dirty than how it already is<br />
<br />
<br />
--[[User:TigroSpottystripes Katsu|TigroSpottystripes Katsu]] 05:51, 8 August 2009 (UTC)<br />
<br />
== Curious behavior with exceptions and @sendim/@recvim ==<br />
<br />
So I was toying with a "Safety" plugin for my toy's collar, something I wrote after she was locked to something and was unable to tell me or get help from me. I just wrote something simple that obtained the collar's owner, then set TP and IM restriction exceptions for that person on the wearer, thus ensuring that the owner would always be able to Teleport the wearer or Send/Receive IMs from them. Initially this worked fine... I thought. Every device we tested restricted everything covered except for SENDING IMs.<br />
<br />
When we finally were in a situation where a separate restraint (a gag) restricted her IM sending ability she wasn't able to IM me, despite an IM sending exception being set on her collar. The same gag also restricted her ability to receive IMs, but the exception on the collar allowed me to IM her just fine. The same was true for the teleport lure exception. Bottom line, it seems that ONLY for the @sendIM restriction the exception MUST be set on the same object that sets the overall restriction.<br />
<br />
Apparently she is using RLV v1.17 (she said it was based on 1.22.11, so I think that's right), so I don't know if this is something which has been addressed in v1.19. Am I right in assuming this is a glitch? -- [[User:Felis Darwin|Felis Darwin]] 06:06, 20 August 2009 (UTC)</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Life_Relay/who&diff=397782LSL Protocol/Restrained Life Relay/who2009-06-19T04:57:28Z<p>Felis Darwin: /* !who */</p>
<hr />
<div>{{Restrained Life Relay Specs TOC}}<br />
<br />
This is a discussion page for adding support of the controller of an device.<br />
<br />
<br />
<br />
<br />
=== !who ===<br />
<br />
''Implemented in THINK KINK's tkPBA v30i''<br />
<br />
''Partly implemented in the [https://wiki.secondlife.com/wiki/LSL_Protocol/Cool_Hud_Protocol Cool Hud v2.30]''<br />
<br />
''Implemented in [[LSL_Protocol/Restrained_Life_Relay/Other_Implementations/Felis_Darwin's_Amethyst_Plugin|Felis Darwin's Amethyst Plugin version]]''<br />
&nbsp;<br />
<br />
; Description : meta command to pass the UUID of WHO is trying to operate your relay, not just WHAT device and the OWNER of the device (more often than not, WHO's the operator and WHO's the owner are NOT the same avatar). <br />
; Background : we constantly have people coming to our store wanting a way to know WHO is trying to control their relay, it's almost become a mantra "I don't care WHAT it is, I want to know WHO it is!". This is a simple way to pass that information from a device to the relay when the device attempts use the relay.<br />
; Syntax: !who/(key)<br />
:: (key) is the UUID of the AV that you wish to present to the relay.<br />
; Implementation :<br />
:: as THINK KINK is implementing this, we are making a few caveats -<br />
::: We are making the !who as the FIRST entry in a command string, so it can be picked up immediately and used in an ASK dialog (if necessary)<br />
::: IF the !who on a new command from an object is the same as the last !who from the same object, no ASK necessary<br />
::: IF the !who on a new command from an object is DIFFERENT from the last !who from the same object, ASK again<br />
::: IF no !who on a command string, normal object verification rules apply<br />
<br />
:: Cool Hud v2.30 partial implementation:<br />
::: When in "Ask" mode, and !who is used somewhere in the command line (and not necessarily as the first command) the relay presents the permission request menu with info about who is controlling the device.<br />
::: When !who is present in a command line sent to the relay and bears the UUID of a user who was previously banned from the relay, the relay immediately frees the wearer (with !release ok sent to the RL device) and tells them the banned user attempted to control them via the device.<br />
<br />
; Further Points of Implementation :<br />
:: The ASK message will change if a !who command comes in, instead of "Dastardly Device owned by Random Avatar wants to control your relay, ALLOW/DENY?" <br />
:: the message becomes "Crafty Avatar wants to control your relay using Random Avatar's Dastardly Device, ALLOW/DENY?"<br />
<br />
:: if the owner and the operator are the same, then a more succinct message could be "Crafty Avatar wants to control your relay using their Dasterdly Device, ALLOW/DENY?"<br />
<br />
:: further if YOU (the victim) are the 'operator' (ie. by walking into an area effect device) you could say "You have activated Crafty Avatar's Area of Doom and it is attempting to control your relay, ALLOW/DENY?"<br />
<br />
:: addition of a this will make for clearer messages and communication with the victim.<br />
<br />
:: if a !who is present, then should the victim DENY the request, an IM can go back to the 'clicker' "Sitting Duck has denied your attempt to control their relay".<br />
<br />
:: Extension of the ALLOW/DENY dialog to include ALLOW/DENY/ALWAYS ALLOW (effectively give this AV 'Auto' Permission, no matter the relay setting)/ALWAYS ASK (give this AV the 'Ask' requirement no matter the relay setting)/ALWAYS DENY (effectively 'blacklist' this AV from any attempts to control your AV)<br />
<br />
:: Means of saving/restoring these AV lists (allow/ask/ban) to/from the relay as backup/restore of data<br />
<br />
:: This function is currently being implemented in the THINK KINK tkRLV 5IVE relay and THINK KINK devices.<br />
; Compatibility : since this is a metacommand, relays that don't support this should ignore it<br />
<br />
--[[User:Ilana Debevec|Ilana Debevec]] 10:44, 15 February 2009 (UTC)<br />
<br />
EDIT: added some usability message examples --[[User:Ilana Debevec|Ilana Debevec]] 20:22, 15 February 2009 (UTC)<br />
<br />
; Discussion : I came to this page to add something similar to this !who meta-command. But I am not opposed to the proposed mechanism, provided it ensures that there is never an ambiguity on who is the current user (but as you present it, it looks ok).<br />
: Now imagine you are using a multi-device relay and you are under restrictions from several users through the same device. How should the relay interpret a !release? Should it clear every restriction from the device? Or only those that where issued by the user in the latest !who? I believe both should be made possible (but in a way that won't make the older relays go wrong... Should, in this case, a pratial release be ignored or be interpreted as a full release?).<br />
:If we can agree on a good spec, I'll try to help you pushing it into the official protocol ^^. Anyway I put this in the TODO list of my multi-relay. :--[[User:Satomi Ahn|Satomi Ahn]] 16:30, 19 February 2009 (UTC)<br />
<br />
:: I am sorry I am not sure whether I understand you correctly: On the one hand you are talking about a multi-device relay and on the other hand you are talking about multiple doms controlling the same device. !release must be implemented on a per device basis. Imagine you are locked up in a cell. Someone griever could simply free you with an attachment that sends !release otherwise, so spoiling all the fun.<br />
:: For multiple persons controlling the same device, I think the last one should override older settings. It gets way to complicated to understand my non-coders otherwise. The world object should check whether it allows access by another person or not. And the relay can ask the user if she trust the new dom or not.<br />
:: We have to keep in mind that !who can be easily faked by untrusted world objects. --[[User:Maike Short|Maike Short]] 18:36, 19 February 2009 (UTC)<br />
:::Ok, I admit what I have in mind is quite complicated. I was thinking of considering a single device as several virtual devices when controlled by several doms (no risk of faking: if the command comes from another real device, of course I don't want it to allow releasing the commands from another one!). But even without going that far, it would make sense, if restrictions come from several doms on a single device, that in certain cases (to be determined by the device maker), only the restrictions coming from one dom would be cleared. --[[User:Satomi Ahn|Satomi Ahn]] 22:05, 19 February 2009 (UTC)<br />
:: We are only !release'ing by object, not by person. We COULD say "release everything this person has" when they !release one... but that's not very .. er.. realistic... if they have you locked in multiple restrictions (a device in a cage for instance), they can only undo you one-at-a-time... and for the !who being faked.. yes it could be, but a) you have to have an object with the !who command (by default) so you would have to be able to fake the UUID of an object that has you actively controlled and the UUID of a person... --[[User:Ilana Debevec|Ilana Debevec]] 23:51, 19 February 2009 (UTC)<br />
<br />
Other point of discussion: when a bunch of commands prefixed by a !who have been ok'ed, should the device assume that every following bunch of commands from the same user will also be accepted? --[[User:Satomi Ahn|Satomi Ahn]] 14:35, 23 February 2009 (UTC)<br />
<br />
: No, it cannot. Some people do not like to be tpto-ed away or stripped and therefore reject those commands. --[[User:Maike Short|Maike Short]] 20:21, 23 February 2009 (UTC)<br />
:: I don't understand your point. How can you know the next bunch of commands from the same user will tpto you or strip you? Is it because the previous bunch had an unpleasant result? Oh.. or maybe you say that some ppl block every srip or tp command? Ok... but that's not the issue I wanted to point to (and this precisely is not really an issue!). The problem would be if you accepted @behav=n commands and then, later, refused the @behav=y.--[[User:Satomi Ahn|Satomi Ahn]] 22:10, 23 February 2009 (UTC)<br />
<br />
If no, the device should take care of eventually releasing every restriction in a non-prefixed command (which should work, provided the relay keeps the order of execution for commands issued by a same device).<br />
<br />
If yes, this makes the relay more complicated, as you have to keep in memory the fact that a user still has restrictions on the wearer. Then either you allow only one user per device to lock the relay (as older relays did for devices: only one device using a relay at a time), or you have to do some very complicated bookkeeping for users, similar as what you do for devices in multidevices-relay (which would give an over-kill, over-bloated, multi-device and multi-user relay!).<br />
<br />
Well, what is your opinion on this? --[[User:Satomi Ahn|Satomi Ahn]] 14:35, 23 February 2009 (UTC)<br />
<br />
: I strongly advocate only to keep track of one user per device. Things get really complicated if different users have their own restrictions. And I thing that is not how the real world works. Sure, there can be two locks on a door with multi device support. But a second persons operating the same lock as the first person with a completely independent set of restrictions is strange. In the rare case this is really desired, the object can use a second prim or do the book keeping itself. Which is a good idea anyway so that the second person can see the restrictions of the first one.--[[User:Maike Short|Maike Short]] 20:21, 23 February 2009 (UTC)<br />
: Apart from that, I think !who should only be send as first command or on change, but not prefix every command. --[[User:Maike Short|Maike Short]] 20:21, 23 February 2009 (UTC)<br />
::This is not what the current spec says, but why not. In that case, we should also have a "!who/NULL_KEY" which says that from now on the commands do not come from a user. I believe we need at least that the final !release does not belong to a user (who could have been ko'ed). --[[User:Satomi Ahn|Satomi Ahn]] 22:10, 23 February 2009 (UTC)<br />
::Oh and, btw, the answer was in the proposal: a new ask dialog can pop up at every new !who that is different from the latest, which is equivalent to have only one user at a time.--[[User:Satomi Ahn|Satomi Ahn]] 22:13, 23 February 2009 (UTC)<br />
<br />
I'd just like to say that I find this function particularly useful. Some sims have devices in place that attract a special type of a-hole who just tries to randomly restrain whoever they can with whatever they can, where it's the person operating the device that's the problem, not the device itself. With that in mind I set up my relay with a blacklist that checks against the key passed by this command. Having tested it for a while I can safely say that it's VERY useful information to have; it's stopped a good number of potential headaches dead.<br />
<br />
!who at its core is basically informational; I think the spec requirement should just treat it as an informational metacommand, e.g. it should be along the lines of "the relay must list the name of the agent passed by !who in the allow/deny prompt," which in and of itself is a massively useful feature. All other uses of the information would NOT be required by the spec, and be up to the individual programmer. I think this would diffuse a bunch of the issues being raised here, while adding a new and powerful tool to the RLV Relay spec. -- [[User:Felis Darwin|Felis Darwin]] 04:57, 19 June 2009 (UTC)</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Life_Relay/who&diff=397712LSL Protocol/Restrained Life Relay/who2009-06-19T04:41:19Z<p>Felis Darwin: /* !who */ My version has it too</p>
<hr />
<div>{{Restrained Life Relay Specs TOC}}<br />
<br />
This is a discussion page for adding support of the controller of an device.<br />
<br />
<br />
<br />
<br />
=== !who ===<br />
<br />
''Implemented in THINK KINK's tkPBA v30i''<br />
<br />
''Partly implemented in the [https://wiki.secondlife.com/wiki/LSL_Protocol/Cool_Hud_Protocol Cool Hud v2.30]''<br />
<br />
''Implemented in [[LSL_Protocol/Restrained_Life_Relay/Other_Implementations/Felis_Darwin's_Amethyst_Plugin|Felis Darwin's Amethyst Plugin version]]''<br />
&nbsp;<br />
<br />
; Description : meta command to pass the UUID of WHO is trying to operate your relay, not just WHAT device and the OWNER of the device (more often than not, WHO's the operator and WHO's the owner are NOT the same avatar). <br />
; Background : we constantly have people coming to our store wanting a way to know WHO is trying to control their relay, it's almost become a mantra "I don't care WHAT it is, I want to know WHO it is!". This is a simple way to pass that information from a device to the relay when the device attempts use the relay.<br />
; Syntax: !who/(key)<br />
:: (key) is the UUID of the AV that you wish to present to the relay.<br />
; Implementation :<br />
:: as THINK KINK is implementing this, we are making a few caveats -<br />
::: We are making the !who as the FIRST entry in a command string, so it can be picked up immediately and used in an ASK dialog (if necessary)<br />
::: IF the !who on a new command from an object is the same as the last !who from the same object, no ASK necessary<br />
::: IF the !who on a new command from an object is DIFFERENT from the last !who from the same object, ASK again<br />
::: IF no !who on a command string, normal object verification rules apply<br />
<br />
:: Cool Hud v2.30 partial implementation:<br />
::: When in "Ask" mode, and !who is used somewhere in the command line (and not necessarily as the first command) the relay presents the permission request menu with info about who is controlling the device.<br />
::: When !who is present in a command line sent to the relay and bears the UUID of a user who was previously banned from the relay, the relay immediately frees the wearer (with !release ok sent to the RL device) and tells them the banned user attempted to control them via the device.<br />
<br />
; Further Points of Implementation :<br />
:: The ASK message will change if a !who command comes in, instead of "Dastardly Device owned by Random Avatar wants to control your relay, ALLOW/DENY?" <br />
:: the message becomes "Crafty Avatar wants to control your relay using Random Avatar's Dastardly Device, ALLOW/DENY?"<br />
<br />
:: if the owner and the operator are the same, then a more succinct message could be "Crafty Avatar wants to control your relay using their Dasterdly Device, ALLOW/DENY?"<br />
<br />
:: further if YOU (the victim) are the 'operator' (ie. by walking into an area effect device) you could say "You have activated Crafty Avatar's Area of Doom and it is attempting to control your relay, ALLOW/DENY?"<br />
<br />
:: addition of a this will make for clearer messages and communication with the victim.<br />
<br />
:: if a !who is present, then should the victim DENY the request, an IM can go back to the 'clicker' "Sitting Duck has denied your attempt to control their relay".<br />
<br />
:: Extension of the ALLOW/DENY dialog to include ALLOW/DENY/ALWAYS ALLOW (effectively give this AV 'Auto' Permission, no matter the relay setting)/ALWAYS ASK (give this AV the 'Ask' requirement no matter the relay setting)/ALWAYS DENY (effectively 'blacklist' this AV from any attempts to control your AV)<br />
<br />
:: Means of saving/restoring these AV lists (allow/ask/ban) to/from the relay as backup/restore of data<br />
<br />
:: This function is currently being implemented in the THINK KINK tkRLV 5IVE relay and THINK KINK devices.<br />
; Compatibility : since this is a metacommand, relays that don't support this should ignore it<br />
<br />
--[[User:Ilana Debevec|Ilana Debevec]] 10:44, 15 February 2009 (UTC)<br />
<br />
EDIT: added some usability message examples --[[User:Ilana Debevec|Ilana Debevec]] 20:22, 15 February 2009 (UTC)<br />
<br />
; Discussion : I came to this page to add something similar to this !who meta-command. But I am not opposed to the proposed mechanism, provided it ensures that there is never an ambiguity on who is the current user (but as you present it, it looks ok).<br />
: Now imagine you are using a multi-device relay and you are under restrictions from several users through the same device. How should the relay interpret a !release? Should it clear every restriction from the device? Or only those that where issued by the user in the latest !who? I believe both should be made possible (but in a way that won't make the older relays go wrong... Should, in this case, a pratial release be ignored or be interpreted as a full release?).<br />
:If we can agree on a good spec, I'll try to help you pushing it into the official protocol ^^. Anyway I put this in the TODO list of my multi-relay. :--[[User:Satomi Ahn|Satomi Ahn]] 16:30, 19 February 2009 (UTC)<br />
<br />
:: I am sorry I am not sure whether I understand you correctly: On the one hand you are talking about a multi-device relay and on the other hand you are talking about multiple doms controlling the same device. !release must be implemented on a per device basis. Imagine you are locked up in a cell. Someone griever could simply free you with an attachment that sends !release otherwise, so spoiling all the fun.<br />
:: For multiple persons controlling the same device, I think the last one should override older settings. It gets way to complicated to understand my non-coders otherwise. The world object should check whether it allows access by another person or not. And the relay can ask the user if she trust the new dom or not.<br />
:: We have to keep in mind that !who can be easily faked by untrusted world objects. --[[User:Maike Short|Maike Short]] 18:36, 19 February 2009 (UTC)<br />
:::Ok, I admit what I have in mind is quite complicated. I was thinking of considering a single device as several virtual devices when controlled by several doms (no risk of faking: if the command comes from another real device, of course I don't want it to allow releasing the commands from another one!). But even without going that far, it would make sense, if restrictions come from several doms on a single device, that in certain cases (to be determined by the device maker), only the restrictions coming from one dom would be cleared. --[[User:Satomi Ahn|Satomi Ahn]] 22:05, 19 February 2009 (UTC)<br />
:: We are only !release'ing by object, not by person. We COULD say "release everything this person has" when they !release one... but that's not very .. er.. realistic... if they have you locked in multiple restrictions (a device in a cage for instance), they can only undo you one-at-a-time... and for the !who being faked.. yes it could be, but a) you have to have an object with the !who command (by default) so you would have to be able to fake the UUID of an object that has you actively controlled and the UUID of a person... --[[User:Ilana Debevec|Ilana Debevec]] 23:51, 19 February 2009 (UTC)<br />
<br />
Other point of discussion: when a bunch of commands prefixed by a !who have been ok'ed, should the device assume that every following bunch of commands from the same user will also be accepted? --[[User:Satomi Ahn|Satomi Ahn]] 14:35, 23 February 2009 (UTC)<br />
<br />
: No, it cannot. Some people do not like to be tpto-ed away or stripped and therefore reject those commands. --[[User:Maike Short|Maike Short]] 20:21, 23 February 2009 (UTC)<br />
:: I don't understand your point. How can you know the next bunch of commands from the same user will tpto you or strip you? Is it because the previous bunch had an unpleasant result? Oh.. or maybe you say that some ppl block every srip or tp command? Ok... but that's not the issue I wanted to point to (and this precisely is not really an issue!). The problem would be if you accepted @behav=n commands and then, later, refused the @behav=y.--[[User:Satomi Ahn|Satomi Ahn]] 22:10, 23 February 2009 (UTC)<br />
<br />
If no, the device should take care of eventually releasing every restriction in a non-prefixed command (which should work, provided the relay keeps the order of execution for commands issued by a same device).<br />
<br />
If yes, this makes the relay more complicated, as you have to keep in memory the fact that a user still has restrictions on the wearer. Then either you allow only one user per device to lock the relay (as older relays did for devices: only one device using a relay at a time), or you have to do some very complicated bookkeeping for users, similar as what you do for devices in multidevices-relay (which would give an over-kill, over-bloated, multi-device and multi-user relay!).<br />
<br />
Well, what is your opinion on this? --[[User:Satomi Ahn|Satomi Ahn]] 14:35, 23 February 2009 (UTC)<br />
<br />
: I strongly advocate only to keep track of one user per device. Things get really complicated if different users have their own restrictions. And I thing that is not how the real world works. Sure, there can be two locks on a door with multi device support. But a second persons operating the same lock as the first person with a completely independent set of restrictions is strange. In the rare case this is really desired, the object can use a second prim or do the book keeping itself. Which is a good idea anyway so that the second person can see the restrictions of the first one.--[[User:Maike Short|Maike Short]] 20:21, 23 February 2009 (UTC)<br />
: Apart from that, I think !who should only be send as first command or on change, but not prefix every command. --[[User:Maike Short|Maike Short]] 20:21, 23 February 2009 (UTC)<br />
::This is not what the current spec says, but why not. In that case, we should also have a "!who/NULL_KEY" which says that from now on the commands do not come from a user. I believe we need at least that the final !release does not belong to a user (who could have been ko'ed). --[[User:Satomi Ahn|Satomi Ahn]] 22:10, 23 February 2009 (UTC)<br />
::Oh and, btw, the answer was in the proposal: a new ask dialog can pop up at every new !who that is different from the latest, which is equivalent to have only one user at a time.--[[User:Satomi Ahn|Satomi Ahn]] 22:13, 23 February 2009 (UTC)</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=397702LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2009-06-19T04:38:14Z<p>Felis Darwin: </p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
Current version: v0.5<br />
<br />
'''NOTE: NOT COMPLETE. I added a bunch of features like "!who" and "!handover" and forgot to note them.''' Briefly, these are: !who support, !handover support, and a new "owner + whitelist" mode in which the relay ONLY responds to objects owned by the sub's owner(s) and anybody they have on their whitelist.<br />
<br />
<br />
''<br />
v0.4:<br />
<br />
- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
- Rewrote the rememberForceSit command to use less resources.<br />
<br />
- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
v0.3:<br />
<br />
- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
- Added a means to turn off the "ping" requirement''<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
integer sentRLV=0;<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
sentRLV=1;<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
rememberForceSit(string command)<br />
{<br />
<br />
// list tokens_command=llParseString2List (command, ["="], []);<br />
// string behav=llList2String (tokens_command, 0); // @sit:<uuid><br />
// string param=llList2String (tokens_command, 1); // force<br />
<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
// if (param != "force")<br />
if (param != "=force")<br />
return;<br />
<br />
// tokens_command=llParseString2List(behav, [":"], []);<br />
// behav=llList2String (tokens_command, 0); // @sit<br />
// param=llList2String (tokens_command, 1); // <uuid><br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
// if (behav != "@sit")<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=nullkey;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=397692LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2009-06-19T04:37:09Z<p>Felis Darwin: /* Current Source Code */ Update to v0.5</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
'''NOTE: NOT COMPLETE. I added a bunch of features like "!who" and "!handover" and forgot to note them.''' Briefly, these are: !who support, !handover support, and a new "owner + whitelist" mode in which the relay ONLY responds to objects owned by the sub's owner(s) and anybody they have on their whitelist.<br />
<br />
<br />
''<br />
v0.4:<br />
<br />
- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
- Rewrote the rememberForceSit command to use less resources.<br />
<br />
- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
v0.3:<br />
<br />
- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
- Added a means to turn off the "ping" requirement''<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = nullkey; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != nullkey)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)";<br />
if (nMode == 2) return "RLV Relay is ON (auto-accept)"; <br />
return "RLV Relay is ON (owners + whitelist only)";<br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != nullkey) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))<br />
{<br />
kController = nullkey;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
integer sentRLV=0;<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
sentRLV=1;<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
if(ownerkey != nullkey)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == nullkey)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == nullkey)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either nullkey or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
rememberForceSit(string command)<br />
{<br />
<br />
// list tokens_command=llParseString2List (command, ["="], []);<br />
// string behav=llList2String (tokens_command, 0); // @sit:<uuid><br />
// string param=llList2String (tokens_command, 1); // force<br />
<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
// if (param != "force")<br />
if (param != "=force")<br />
return;<br />
<br />
// tokens_command=llParseString2List(behav, [":"], []);<br />
// behav=llList2String (tokens_command, 0); // @sit<br />
// param=llList2String (tokens_command, 1); // <uuid><br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
// if (behav != "@sit")<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = nullkey; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=nullkey;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, nullkey);<br />
<br />
if(kController != nullkey && sPendingId != nullkey)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);<br />
<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = nullkey;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=nullkey;<br />
lRestrictions=[];<br />
sPendingId=nullkey;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != nullkey)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != nullkey && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
if(second == "owner" || second == "wl")<br />
{<br />
nMode = 3;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 3) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = nullkey;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode >= 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == nullkey)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = nullkey;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != nullkey && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == nullkey)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=nullkey)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == nullkey) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=nullkey;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=397682LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2009-06-19T04:35:44Z<p>Felis Darwin: /* Current Changelog */</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
'''NOTE: NOT COMPLETE. I added a bunch of features like "!who" and "!handover" and forgot to note them.''' Briefly, these are: !who support, !handover support, and a new "owner + whitelist" mode in which the relay ONLY responds to objects owned by the sub's owner(s) and anybody they have on their whitelist.<br />
<br />
<br />
''<br />
v0.4:<br />
<br />
- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
- Rewrote the rememberForceSit command to use less resources.<br />
<br />
- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
v0.3:<br />
<br />
- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
- Added a means to turn off the "ping" requirement''<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = NULL_KEY; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != NULL_KEY)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)"; <br />
return "RLV Relay is ON (auto-accept)"; <br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != NULL_KEY && (kSource == id || ((kSource == NULL_KEY) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != NULL_KEY) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != NULL_KEY) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == NULL_KEY && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]))<br />
{<br />
kController = NULL_KEY;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
integer sentRLV=0;<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
sentRLV=1;<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
if(ownerkey != NULL_KEY)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == NULL_KEY)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == NULL_KEY)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either NULL_KEY or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
rememberForceSit(string command)<br />
{<br />
<br />
// list tokens_command=llParseString2List (command, ["="], []);<br />
// string behav=llList2String (tokens_command, 0); // @sit:<uuid><br />
// string param=llList2String (tokens_command, 1); // force<br />
<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
// if (param != "force")<br />
if (param != "=force")<br />
return;<br />
<br />
// tokens_command=llParseString2List(behav, [":"], []);<br />
// behav=llList2String (tokens_command, 0); // @sit<br />
// param=llList2String (tokens_command, 1); // <uuid><br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
// if (behav != "@sit")<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = NULL_KEY; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=NULL_KEY;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, NULL_KEY);<br />
<br />
if(kController != NULL_KEY && sPendingId != NULL_KEY)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);<br />
<br />
sPendingId = NULL_KEY;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = NULL_KEY;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=NULL_KEY;<br />
lRestrictions=[];<br />
sPendingId=NULL_KEY;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != NULL_KEY)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != NULL_KEY && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == NULL_KEY)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 2) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = NULL_KEY;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode == 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == NULL_KEY)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != NULL_KEY && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = NULL_KEY;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != NULL_KEY && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == NULL_KEY)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=NULL_KEY)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == NULL_KEY) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=NULL_KEY;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=339512LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2009-04-29T06:50:51Z<p>Felis Darwin: /* Current Changelog */</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
<br />
''<br />
v0.4:<br />
<br />
- Implemented changes to bring the relay up to 1040 spec:<br />
<br />
+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel<br />
+ Made sure the distance check fails when the controlling object has been de-rezzed/removed<br />
+ Stopped groupless objects from immediately passing the trustworthy check on groupless land<br />
+ Added support for the "!who" metacommand (tells you who is in control of the device), and updated the user dialog messages accordingly<br />
+ Added support fo the "!handover" metacommand (allows devices to "hand over" controlled residents)<br />
<br />
- Rewrote the rememberForceSit command to use less resources.<br />
<br />
- Removed the "!mode" metacommand I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise<br />
<br />
- Went through the script and combined a number of if statements so as to reduce memory useage and overhead<br />
<br />
- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory useage a bit, and has allowed me to cram in more features.<br />
<br />
- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.<br />
<br />
- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.<br />
<br />
- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.<br />
<br />
<br />
v0.3:<br />
<br />
- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)<br />
<br />
- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.<br />
<br />
- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".<br />
<br />
- Removed the "stripping" deny mode, as the above method will handle the same thing.<br />
<br />
- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).<br />
<br />
- Added a means to turn off the "ping" requirement''<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = NULL_KEY; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != NULL_KEY)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)"; <br />
return "RLV Relay is ON (auto-accept)"; <br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != NULL_KEY && (kSource == id || ((kSource == NULL_KEY) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != NULL_KEY) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != NULL_KEY) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == NULL_KEY && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]))<br />
{<br />
kController = NULL_KEY;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
integer sentRLV=0;<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
sentRLV=1;<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
if(ownerkey != NULL_KEY)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == NULL_KEY)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == NULL_KEY)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either NULL_KEY or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
rememberForceSit(string command)<br />
{<br />
<br />
// list tokens_command=llParseString2List (command, ["="], []);<br />
// string behav=llList2String (tokens_command, 0); // @sit:<uuid><br />
// string param=llList2String (tokens_command, 1); // force<br />
<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
// if (param != "force")<br />
if (param != "=force")<br />
return;<br />
<br />
// tokens_command=llParseString2List(behav, [":"], []);<br />
// behav=llList2String (tokens_command, 0); // @sit<br />
// param=llList2String (tokens_command, 1); // <uuid><br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
// if (behav != "@sit")<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = NULL_KEY; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=NULL_KEY;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, NULL_KEY);<br />
<br />
if(kController != NULL_KEY && sPendingId != NULL_KEY)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);<br />
<br />
sPendingId = NULL_KEY;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = NULL_KEY;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=NULL_KEY;<br />
lRestrictions=[];<br />
sPendingId=NULL_KEY;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != NULL_KEY)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != NULL_KEY && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == NULL_KEY)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 2) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = NULL_KEY;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode == 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == NULL_KEY)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != NULL_KEY && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = NULL_KEY;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != NULL_KEY && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == NULL_KEY)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=NULL_KEY)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == NULL_KEY) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=NULL_KEY;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LSL_Protocol/Restrained_Love_Relay/Other_Implementations/Felis_Darwin%27s_Amethyst_Plugin&diff=339502LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin2009-04-29T06:49:32Z<p>Felis Darwin: /* Current Source Code */</p>
<hr />
<div>This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. '''At the moment this plugin may nullify commands issued by the standard Amethyst Restrained Life Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)<br />
<br />
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. '''This is a work in progress.''' Eventually it will have most of the features suggested by Marine in the original Reference Implementation document.<br />
<br />
== Current Changelog ==<br />
<br />
''<br />
v0.2:<br />
<br />
- Added support for new Amethyst v7 menu system. (Plugin Menu messages are handled on link-828 now)<br />
<br />
- Delayed force-sit on relog until the sit target responds to a ping.<br />
<br />
- "!release" now clears any pending restrictions. (e.g. from an unapproved control request in ASK mode)<br />
<br />
- Used an LSL workaround to expand the maximum number of pending commands the relay can store when in ASK mode.<br />
<br />
- Added crash-prevention code to stop adding pending commands if the script is running low on memory.<br />
<br />
- Implemented a version of Maike's "@getstatus" exploit fix. Read the Wiki if you want to know what that is.<br />
<br />
- Added support for a new "!mode" metacommand, which makes the Relay reply with its current mode. (a number)<br />
<br />
- The Relay now issues a link message on release which tells the Amethyst RLV plugin to re-issue its restrictions.<br />
<br />
- Whenever the relay releases restrictions it will issue the "!release,ok" reply to the current or pending control object.<br />
<br />
- Fixed a logic error which could cause the restriction list to grow infinitely, eventually causing a script crash.<br />
<br />
- When restrictions are issued or re-issued the relay counts it as a ping and resets the ping check countdown.''<br />
<br />
== Current Source Code ==<br />
<br />
<lsl><br />
<br />
//== RestrainedLife Viewer Relay Script<br />
//== by Felis Darwin<br />
//== Based on Reference Implementation by Marine Kelley<br />
<br />
integer DEBUG = FALSE;<br />
<br />
// ---------------------------------------------------<br />
// Amethyst Plugin Variables<br />
// ---------------------------------------------------<br />
<br />
key nullkey = NULL_KEY;<br />
string nullstr = "";<br />
<br />
integer secaccess=0; //== Do secondary owners have access to the RL functions?<br />
<br />
// Internal variables<br />
key ownerkey = nullkey;<br />
list secowners = [];<br />
<br />
key setby = NULL_KEY; //== Who set the RLV Relay status?<br />
<br />
integer lockstatus; //== Has the collar been locked by the RLV plugin?<br />
<br />
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from<br />
<br />
// ---------------------------------------------------<br />
// Constants<br />
// ---------------------------------------------------<br />
<br />
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page<br />
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";<br />
<br />
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds<br />
<br />
integer PERMISSION_DIALOG_TIMEOUT = 30;<br />
<br />
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;<br />
<br />
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit<br />
<br />
// ---------------------------------------------------<br />
// Variables<br />
// ---------------------------------------------------<br />
<br />
integer nMode;<br />
<br />
list lRestrictions; // restrictions currently applied (without the "=n" part)<br />
key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not<br />
key kController; // UUID of the person controlling the object, if passed to us by the !who command<br />
<br />
list WhiteBlack = []; //== A combined white/black list of residents. Whitelisting exempts them from ask mode. Blacklisting prevents their objects from even interacting with you.<br />
<br />
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)<br />
key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1)<br />
string sPendingMessage; // message of pending request (first request of a session in mode 1)<br />
integer sPendingTime;<br />
<br />
// used on login<br />
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)<br />
integer loginWaitingForPong;<br />
integer loginPendingForceSit;<br />
<br />
integer noping = 0;<br />
<br />
key lastForceSitDestination;<br />
integer lastForceSitTime;<br />
<br />
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command<br />
<br />
// ---------------------------------------------------<br />
// Low Level Communication<br />
// ---------------------------------------------------<br />
<br />
<br />
debug(string x)<br />
{<br />
if (DEBUG)<br />
{<br />
llOwnerSay("DEBUG: " + x);<br />
}<br />
}<br />
<br />
// acknowledge or reject<br />
ack(string cmd_id, key id, string cmd, string ack)<br />
{<br />
if(id != NULL_KEY)<br />
llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);<br />
}<br />
<br />
<br />
// get current mode as string<br />
string getModeDescription()<br />
{<br />
if (nMode == 0) return "RLV Relay is OFF"; <br />
if (nMode == 1) return "RLV Relay is ON (permission needed)"; <br />
return "RLV Relay is ON (auto-accept)"; <br />
}<br />
<br />
// ---------------------------------------------------<br />
// Permission Handling<br />
// ---------------------------------------------------<br />
<br />
// are we already under command by this object?<br />
integer isObjectKnow(key id)<br />
{<br />
// are we not under command by any object but were we forced to sit on this object recently?<br />
if (id != NULL_KEY && (kSource == id || ((kSource == NULL_KEY) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))<br />
{<br />
return TRUE;<br />
}<br />
<br />
return FALSE;<br />
}<br />
<br />
<br />
// check whether the object is in llSay distance.<br />
// The specification requires llSay instead of llShout or llRegionSay<br />
// to be used to limit the range. But this has to be checked here again<br />
// because the objects are not trustworthy.<br />
integer isObjectNear(key id)<br />
{<br />
vector myPosition = llGetRootPosition();<br />
list temp = llGetObjectDetails(id, ([OBJECT_POS]));<br />
vector objPosition = llList2Vector(temp,0);<br />
if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;<br />
float distance = llVecDist(objPosition, myPosition);<br />
return distance <= 100;<br />
}<br />
<br />
// do a basic check on the identity of the object trying to issue a command<br />
integer isObjectIdentityTrustworthy(key id)<br />
{<br />
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);<br />
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);<br />
key object_owner=llGetOwnerKey(id);<br />
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);<br />
<br />
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);<br />
debug("group= " + (string) parcel_group + " / " + (string) object_group);<br />
<br />
if (object_owner==llGetOwner () // IF I am the owner of the object<br />
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on<br />
|| (object_owner==ownerkey && ownerkey != NULL_KEY) //== Is this my owner's stuff?<br />
|| ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?<br />
|| (object_group==parcel_group && object_group != NULL_KEY) // OR its group is the same as the parcel I'm on<br />
)<br />
{<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
<br />
// Is this a simple request for information or a meta command like !release?<br />
integer isSimpleRequest(list list_of_commands) <br />
{<br />
integer len = llGetListLength(list_of_commands);<br />
integer i;<br />
<br />
debug("Checking simplicity of commands...");<br />
<br />
// now check every single atomic command<br />
for (i=0; i < len; ++i)<br />
{<br />
string command = llList2String(list_of_commands, i);<br />
if (!isSimpleAtomicCommand(command))<br />
{<br />
debug("Command "+ command +" fails simplicity check.");<br />
return FALSE;<br />
}<br />
}<br />
<br />
// all atomic commands passed the test<br />
return TRUE;<br />
}<br />
<br />
// is this a simple atmar command<br />
// (a command which only queries some information or releases restrictions)<br />
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)<br />
integer isSimpleAtomicCommand(string cmd)<br />
{ <br />
// check right hand side of the "=" - sign<br />
integer index = llSubStringIndex (cmd, "=");<br />
// check for a number after the "="<br />
string param = llGetSubString (cmd, index + 1, -1);<br />
if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") && index > -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number)?<br />
{<br />
return TRUE;<br />
}<br />
<br />
// this one is not "simple".<br />
return FALSE;<br />
}<br />
<br />
// If we already have commands from this object pending<br />
// because of a permission request dialog, just add the<br />
// new commands at the end.<br />
// Note: We use a timeout here because the player may<br />
// have "ignored" the dialog.<br />
integer tryToGluePendingCommands(key id, string commands)<br />
{<br />
if (kSource == NULL_KEY && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)<br />
{<br />
debug("Gluing " + sPendingMessage + " with " + commands);<br />
sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;<br />
return TRUE;<br />
}<br />
return FALSE;<br />
}<br />
<br />
// verifies the permission. This includes mode <br />
// (off, permission, auto) of the relay and the<br />
// identity of the object (owned by parcel people).<br />
integer verifyPermission(key id, string name, string message)<br />
{<br />
debug("Verifying permission for command "+ message);<br />
<br />
// extract the commands-part<br />
list tokens = llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]))<br />
{<br />
kController = NULL_KEY;<br />
return FALSE;<br />
}<br />
string commands = llList2String(tokens, 2);<br />
list list_of_commands = llParseString2List(commands, ["|"], []);<br />
<br />
// accept harmless commands silently<br />
if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))<br />
{<br />
debug("simple command or Owner in Whitelist, executing.");<br />
return TRUE;<br />
}<br />
<br />
// if we are already having a pending permission-dialog request for THIS object,<br />
// just add the new commands at the end of the pending command list.<br />
if (tryToGluePendingCommands(id, commands))<br />
{<br />
debug("Appending to store of commands pending approval.");<br />
return FALSE; //== Glue the commands and process them later<br />
}<br />
<br />
// check whether this object belongs here<br />
integer trustworthy = isObjectIdentityTrustworthy(id);<br />
string warning = "";<br />
if (!trustworthy)<br />
{<br />
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";<br />
}<br />
<br />
// ask in permission-request-mode and/OR in case the object identity is suspisous.<br />
if ((nMode == 1 || !trustworthy))<br />
{<br />
sPendingId=id;<br />
sPendingName=name;<br />
sPendingMessage=message;<br />
sPendingTime = llGetUnixTime();<br />
<br />
list opts = ["Yes", "No"];<br />
<br />
llSetTimerEvent(2.0);<br />
<br />
if(llKey2Name(llGetOwnerKey(id)) != "")<br />
{<br />
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";<br />
opts += ["Never", "Always"];<br />
}<br />
<br />
if(llKey2Name(kController) != "")<br />
{<br />
name = llKey2Name(kController) +", using "+ name +",";<br />
opts += ["Never"];<br />
}<br />
<br />
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);<br />
debug("Asking for permission");<br />
return FALSE;<br />
}<br />
return TRUE;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// Executing of commands<br />
// ---------------------------------------------------<br />
<br />
// execute a non-parsed message<br />
// this command could be denied here for policy reasons, (if it were implemenetd)<br />
// but this time there will be an acknowledgement<br />
execute(string name, key id, string message)<br />
{<br />
integer sentRLV=0;<br />
stop = 0;<br />
<br />
list tokens=llParseString2List (message, [","], []);<br />
if (llGetListLength (tokens)==3) // this is a normal command<br />
{<br />
string cmd_id=llList2String (tokens, 0); // CheckAttach<br />
key target=llList2Key (tokens, 1); // UUID<br />
if (target==llGetOwner ()) // talking to me ?<br />
{<br />
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);<br />
integer len=llGetListLength (list_of_commands);<br />
integer i;<br />
string command;<br />
string prefix;<br />
for (i=0; i<len; ++i) // execute every command one by one<br />
{<br />
if(stop) return;<br />
<br />
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'<br />
command=llList2String (list_of_commands, i);<br />
prefix=llGetSubString (command, 0, 0);<br />
<br />
if(command == "@clear")<br />
{<br />
releaseRestrictions();<br />
ack(cmd_id, id, command, "ok"); <br />
}<br />
else if (prefix=="@") // this is a RL command<br />
{<br />
executeRLVCommand(cmd_id, id, command);<br />
sentRLV=1;<br />
}<br />
else if (prefix=="!") // this is a metacommand, aimed at the relay itself<br />
{<br />
executeMetaCommand(cmd_id, id, command);<br />
}<br />
}<br />
}<br />
}<br />
}<br />
<br />
// executes a command for the restrained life viewer <br />
// with some additinal magic like book keeping<br />
executeRLVCommand(string cmd_id, string id, string command)<br />
{<br />
// we need to know whether whether is a rule or a simple command<br />
list tokens_command=llParseString2List (command, ["="], []);<br />
string behav=llList2String (tokens_command, 0); // @getattach:skull<br />
string param=llList2String (tokens_command, 1); // 2222<br />
integer ind=llListFindList (lRestrictions, [behav]);<br />
<br />
debug("behav = "+ behav +"; param=" + param);<br />
<br />
//== Stop the public chat exploits.<br />
if((~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && (integer)param <= 0)<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return; <br />
} <br />
if (param=="n" || param=="add") // add to lRestrictions<br />
{<br />
if (ind<0)<br />
{<br />
if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)<br />
{<br />
llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");<br />
}<br />
else<br />
lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];<br />
<br />
if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions<br />
{<br />
if(ownerkey != NULL_KEY)<br />
llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");<br />
if(secaccess || ownerkey == NULL_KEY)<br />
{<br />
integer i;<br />
for(i = 0; i < llGetListLength(secowners); i++)<br />
llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");<br />
}<br />
}<br />
}<br />
<br />
if(kSource == NULL_KEY)<br />
{<br />
llSetTimerEvent(2.0);<br />
if(!lockstatus)<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
kSource=id; // we know that kSource is either NULL_KEY or id already<br />
}<br />
else if (param=="y" || param=="rem") // remove from lRestrictions<br />
{<br />
if (ind>-1)<br />
lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);<br />
<br />
//== Unlisted Owner Exceptions are NEVER removed, for safety<br />
//== Nor is the public chat exploit fixer<br />
else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")<br />
{<br />
ack(cmd_id, id, command, "ko");<br />
return;<br />
}<br />
<br />
if (llGetListLength (lRestrictions) == 0 && !lockstatus)<br />
llOwnerSay("@detach=y");<br />
<br />
}<br />
<br />
rememberForceSit(command);<br />
if(llGetListLength(lRestrictions) == 1)<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay(command); // execute command<br />
ack(cmd_id, id, command, "ok"); // acknowledge<br />
}<br />
<br />
<br />
// remembers the time and object if this command is a force sit<br />
rememberForceSit(string command)<br />
{<br />
<br />
// list tokens_command=llParseString2List (command, ["="], []);<br />
// string behav=llList2String (tokens_command, 0); // @sit:<uuid><br />
// string param=llList2String (tokens_command, 1); // force<br />
<br />
command = llStringTrim(command, STRING_TRIM);<br />
string param = llGetSubString(command, -6, -1);<br />
<br />
// if (param != "force")<br />
if (param != "=force")<br />
return;<br />
<br />
// tokens_command=llParseString2List(behav, [":"], []);<br />
// behav=llList2String (tokens_command, 0); // @sit<br />
// param=llList2String (tokens_command, 1); // <uuid><br />
<br />
string behav = llGetSubString(command, 0, 4);<br />
param = llGetSubString(command, 5, 40);<br />
<br />
debug("'force'-command:" + behav + "/" + param);<br />
<br />
// if (behav != "@sit")<br />
if(behav != "@sit:")<br />
return;<br />
<br />
<br />
lastForceSitDestination = (key) param;<br />
lastForceSitTime = llGetUnixTime();<br />
debug("remembered force sit");<br />
}<br />
<br />
// executes a meta command which is handled by the relay itself<br />
executeMetaCommand(string cmd_id, string id, string command)<br />
{<br />
if (command=="!version") // checking relay version<br />
{<br />
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);<br />
}<br />
else if (command == "!implversion") // checking relay version<br />
{<br />
ack(cmd_id, id, command, RLVRS_IMPL_VERSION);<br />
}<br />
else if (command=="!release") // release all the restrictions (end session)<br />
{<br />
ack(cmd_id, id, command, "ok");<br />
kSource = NULL_KEY; //== So only one release message is sent<br />
releaseRestrictions();<br />
}<br />
<br />
//== We don't need to do this because any sent restriction automatically does the same thing <br />
// else if (command == "!pong")<br />
// loginWaitingForPong = FALSE;<br />
<br />
if (llGetSubString(command,0,4) == "!who/")<br />
{<br />
kController = (key)llGetSubString(command, 5, -1);<br />
}<br />
if (llGetSubString(command,0,9) == "!handover/")<br />
{<br />
list tokens = llParseString2List(command, ["/"], []);<br />
if(!llList2Integer(tokens, 2))<br />
releaseRestrictions();<br />
kSource = llList2Key(tokens,1);<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
}<br />
<br />
// lift all the restrictions (called by !release and by turning the relay off)<br />
releaseRestrictions ()<br />
{<br />
ack("Relay Release Notification", kSource, "!release", "ok");<br />
<br />
kSource=NULL_KEY;<br />
if(!lockstatus)<br />
llOwnerSay("@detach=y");<br />
integer i;<br />
integer len=llGetListLength (lRestrictions);<br />
for (i=0; i<len; ++i)<br />
{<br />
llOwnerSay(llList2String (lRestrictions, i)+"=y");<br />
<br />
if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))<br />
llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));<br />
}<br />
lRestrictions = [];<br />
<br />
loginPendingForceSit = FALSE;<br />
loginWaitingForPong = FALSE;<br />
llSetTimerEvent(0.0);<br />
ack("Relay Release Notification", sPendingId, "!release", "ok");<br />
<br />
llMessageLinked(LINK_SET, 356, nullstr, NULL_KEY);<br />
<br />
if(kController != NULL_KEY && sPendingId != NULL_KEY)<br />
llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);<br />
<br />
sPendingId = NULL_KEY;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
kController = NULL_KEY;<br />
}<br />
<br />
<br />
// ---------------------------------------------------<br />
// initialisation and login handling<br />
// ---------------------------------------------------<br />
<br />
init() {<br />
debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());<br />
nMode=1;<br />
kSource=NULL_KEY;<br />
lRestrictions=[];<br />
sPendingId=NULL_KEY;<br />
sPendingName="";<br />
sPendingMessage="";<br />
llListen (-1812221819, "", "", "");<br />
llListen (-1812220409, "", llGetOwner(), "");<br />
llOwnerSay (getModeDescription());<br />
}<br />
<br />
// sends the known restrictions (again) to the RL-viewer<br />
// (call this functions on login)<br />
reinforceKnownRestrictions()<br />
{<br />
integer i;<br />
integer len=llGetListLength(lRestrictions);<br />
string restr;<br />
<br />
if(len > 0)<br />
{<br />
llOwnerSay("@a-relay=n");<br />
llOwnerSay("@detach=n");<br />
}<br />
<br />
debug("kSource=" + (string) kSource);<br />
for (i=0; i<len; ++i)<br />
{<br />
restr=llList2String(lRestrictions, i);<br />
debug("restr=" + restr);<br />
llOwnerSay(restr+"=n");<br />
if (restr=="@unsit")<br />
{<br />
loginPendingForceSit = TRUE;<br />
}<br />
}<br />
}<br />
<br />
// send a ping request and start a timer<br />
pingWorldObjectIfUnderRestrictions()<br />
{<br />
loginWaitingForPong = FALSE;<br />
if (kSource != NULL_KEY)<br />
{<br />
ack("ping", kSource, "ping", "ping");<br />
timerTickCounter = 0;<br />
llSetTimerEvent(1.0);<br />
loginWaitingForPong = TRUE;<br />
}<br />
}<br />
<br />
// Handle commands<br />
HandleCommand(string message, key id)<br />
{<br />
list templist = llParseString2List(llToLower(message), [" "], []);<br />
string cmd = llList2String(templist, 0);<br />
<br />
if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))<br />
{<br />
integer change = 0;<br />
<br />
string second = llList2String(templist, 1);<br />
string third = llList2String(templist, 2);<br />
<br />
if(kSource != NULL_KEY && id == llGetOwner())<br />
{<br />
llOwnerSay("You cannot change relay modes while the relay is locked.");<br />
return; <br />
}<br />
<br />
if(id == ownerkey && (second == "secondaries" || second == "sec"))<br />
{<br />
if(third == "on" || third == "auto" || (third == "" && !secaccess))<br />
{<br />
secaccess = 1;<br />
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");<br />
}<br />
else<br />
{<br />
secaccess = 0;<br />
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");<br />
}<br />
}<br />
<br />
else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == NULL_KEY)) && second == "ping")<br />
{<br />
if(third == "off" || (third == "" && !noping))<br />
{<br />
noping = 1;<br />
llWhisper(0,"Restrained Life Relay no longer requires regular object communication. CAUTION: Relay will NOT detect if the control object has crashed or been removed, and that instance will continue to enforce the last known restrictions until the wearer logs off.");<br />
}<br />
else<br />
{<br />
noping = 0;<br />
llWhisper(0,"Restrained Life Relay now requires regular object communication."); <br />
} <br />
}<br />
<br />
if(second == "on" || second == "auto")<br />
{<br />
nMode = 2;<br />
change = 1;<br />
}<br />
if(second == "off")<br />
{<br />
nMode = 0;<br />
change = 1; <br />
}<br />
if(second == "ask")<br />
{<br />
nMode = 1;<br />
change = 1; <br />
}<br />
<br />
if(second == "" || second == "mode")<br />
{<br />
nMode++;<br />
if(nMode > 2) nMode = 0;<br />
change = 1; <br />
}<br />
<br />
if(second == "wbclear")<br />
{<br />
WhiteBlack = [];<br />
llWhisper(0,"Relay Whitelist and Blacklist cleared.");<br />
}<br />
<br />
if(change)<br />
{<br />
setby = NULL_KEY;<br />
if (nMode == 0)<br />
{<br />
llSetTimerEvent(0.0);<br />
releaseRestrictions();<br />
}<br />
else<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
if(nMode == 2) setby = id;<br />
}<br />
if(id == llGetOwner())<br />
llOwnerSay(getModeDescription()); <br />
else<br />
llSay(0, getModeDescription());<br />
<br />
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);<br />
}<br />
}<br />
else if(cmd == "relay" && id == llGetOwner())<br />
{<br />
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); <br />
}<br />
}<br />
<br />
default<br />
{<br />
state_entry()<br />
{<br />
// Request owner list from the collar<br />
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);<br />
// Reset the plugin list<br />
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);<br />
init();<br />
}<br />
<br />
// Handle messages from the collar script<br />
link_message(integer sender, integer num, string str, key id)<br />
{<br />
if(num == 47)<br />
{<br />
list templist = llParseString2List(str, [","], []);<br />
integer x;<br />
integer count = llGetListLength(templist);<br />
<br />
// Handle owner list reply<br />
ownerkey = id;<br />
secowners = [];<br />
for(x=0;x<count;x++)<br />
{<br />
secowners = secowners + [ (key)llList2String(templist, x) ];<br />
}<br />
}<br />
// Prefixless commands<br />
else if(num == 48 || num == 828)<br />
{<br />
if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply<br />
id = (key)(llGetSubString(id,0,35)); <br />
// Handle Commands on the public or alternate channel<br />
HandleCommand(str, id);<br />
}<br />
else if(num == 33 && id != nullkey)<br />
{<br />
// Collar script is giving us an owner<br />
ownerkey = id;<br />
}<br />
else if(num == 34 && id != nullkey)<br />
{<br />
// Collar script is giving us a secondary owner<br />
secowners = secowners + [ id ];<br />
}<br />
else if(num == 35)<br />
{<br />
// Collar script is clearing owners<br />
ownerkey = nullkey;<br />
secowners = [];<br />
}<br />
else if(num == 36)<br />
{<br />
// Collar script is clearing secondary owners<br />
secowners = [];<br />
}<br />
// Handle plugin update<br />
else if(num == 62)<br />
{<br />
string buttons = "Relay Mode";<br />
<br />
if(str == nullstr && (id == nullstr || id == nullkey))<br />
{<br />
// Add for owner and owners (key)<br />
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);<br />
// Add for sub and unowned sub (key)<br />
llMessageLinked(LINK_SET, 63, buttons, nullstr);<br />
}<br />
}<br />
else if(num == 65)<br />
{<br />
lockstatus = (integer)str; <br />
}<br />
else if(num == 66) //== Safeword, unlock<br />
{<br />
releaseRestrictions();<br />
nMode = 0;<br />
llOwnerSay(getModeDescription());<br />
}<br />
else if(num == 355)<br />
reinforceKnownRestrictions();<br />
}<br />
<br />
attach(key id)<br />
{<br />
if(id == NULL_KEY)<br />
llOwnerSay("@clear"); <br />
}<br />
<br />
on_rez(integer start_param)<br />
{<br />
// relogging, we must refresh the viewer and ping the object if any<br />
// if mode is not OFF, fire all the stored restrictions<br />
if (nMode)<br />
{<br />
reinforceKnownRestrictions();<br />
pingWorldObjectIfUnderRestrictions();<br />
}<br />
// remind the current mode to the user<br />
llOwnerSay(getModeDescription());<br />
}<br />
<br />
<br />
timer()<br />
{<br />
timerTickCounter++; <br />
<br />
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);<br />
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");<br />
loginWaitingForPong = FALSE;<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
<br />
if (loginPendingForceSit)<br />
{<br />
integer agentInfo = llGetAgentInfo(llGetOwner());<br />
if (agentInfo & AGENT_SITTING)<br />
{<br />
loginPendingForceSit = FALSE;<br />
debug("is sitting now");<br />
}<br />
else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check<br />
{<br />
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");<br />
loginPendingForceSit = FALSE;<br />
releaseRestrictions();<br />
}<br />
else if(!loginWaitingForPong)<br />
{<br />
llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");<br />
}<br />
}<br />
<br />
if(sPendingId != NULL_KEY && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())<br />
{<br />
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);<br />
sPendingId = NULL_KEY;<br />
sPendingName = "";<br />
sPendingMessage = "";<br />
} <br />
<br />
if(timerTickCounter == 0 && !noping)<br />
pingWorldObjectIfUnderRestrictions(); <br />
<br />
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)<br />
{<br />
timerTickCounter = -1;<br />
if(!noping)<br />
{<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
return;<br />
}<br />
llSetTimerEvent(0.0);<br />
}<br />
}<br />
<br />
listen(integer channel, string name, key id, string message)<br />
{<br />
if (channel==-1812221819)<br />
{<br />
debug("LISTEN: " + message);<br />
<br />
//=== ALWAYS accept a lone "!release" command, no matter the distance<br />
list tokens = llCSV2List(message);<br />
if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))<br />
{<br />
return;<br />
}<br />
tokens = [];<br />
<br />
if (nMode== 0)<br />
{<br />
debug("deactivated - ignoring commands");<br />
return; // mode is 0 (off) => reject<br />
}<br />
<br />
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);<br />
<br />
if (kSource != NULL_KEY && kSource != id)<br />
{<br />
debug("already used by another object => reject");<br />
return;<br />
}<br />
<br />
if(!loginPendingForceSit && sPendingId == NULL_KEY)<br />
{<br />
llSetTimerEvent(0.0);<br />
llSetTimerEvent((float)PING_INTERVAL);<br />
}<br />
<br />
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request<br />
// timerTickCounter = -1;<br />
<br />
if (!isObjectKnow(id))<br />
if(!verifyPermission(id, name, message))<br />
return;<br />
<br />
debug("Executing: " + (string) kSource);<br />
execute(name, id, message);<br />
}<br />
else if (channel==-1812220409 && id == llGetOwner())<br />
{<br />
if (sPendingId!=NULL_KEY)<br />
{ <br />
if (message=="Yes" || message == "Always") // pending request authorized => process it<br />
{<br />
//== Process Whitelist entry<br />
if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];<br />
debug("Got approval of restrictions from wearer");<br />
execute(sPendingName, sPendingId, sPendingMessage);<br />
}<br />
else if(kSource == sPendingId)<br />
releaseRestrictions();<br />
<br />
//== Process Blacklist entry<br />
if(kController == NULL_KEY) kController = llGetOwnerKey(sPendingId);<br />
if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];<br />
<br />
// clear pending request<br />
sPendingName="";<br />
sPendingId=NULL_KEY;<br />
sPendingMessage="";<br />
}<br />
}<br />
}<br />
<br />
changed(integer change)<br />
{<br />
if (change & CHANGED_OWNER) <br />
{<br />
llResetScript();<br />
}<br />
}<br />
}<br />
<br />
</lsl></div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=User:Felis_Darwin&diff=339492User:Felis Darwin2009-04-29T06:48:31Z<p>Felis Darwin: New page: If you're looking for me, you're probably interested in something listed below: LSL Protocol/Restrained Life Relay/Other Implementations/Felis Darwin's Amethyst Plugin</p>
<hr />
<div>If you're looking for me, you're probably interested in something listed below:<br />
<br />
[[LSL Protocol/Restrained Life Relay/Other Implementations/Felis Darwin's Amethyst Plugin]]</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlRemoteLoadScriptPin&diff=218793LlRemoteLoadScriptPin2009-01-31T20:08:42Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function/inventory|name|uuid=false|type=script}}{{Issues/SVC-3321}}<br />
{{LSL_Function<br />
|func_id=253|func_sleep=3.0|func_energy=10.0<br />
|func=llRemoteLoadScriptPin<br />
|p1_type=key|p1_name=target|p1_desc=A prim in the same sim<br />
|p2_type=string|p2_name=name|p2_desc<br />
|p3_type=integer|p3_name=pin|p3_desc=Must match pin set by [[llSetRemoteScriptAccessPin]]<br />
|p4_type=integer|p4_name=running|p4_desc=boolean, if the script is to be set as running.<br />
|p5_type=integer|p5_name=start_param|p5_desc=value returned by [[llGetStartParameter]] in the target script.<br />
|func_desc=Copy script '''name''' into '''target''' and if '''running''' start with '''start_param'''.<br />
|func_footnote=Only works if the owner of the object this script is in can modify '''target'''.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* '''start_param''' only lasts until the script is reset.<br />
* If '''target''' is an {{LSLGC|Attachment|attachment}} owned by a different user, regardless of object modify rights granted, this function will silently (bug?) fail.{{Footnote|{{LSLGC|Attachment|Attachments}} can only be modified by their owner.|Attachments can only be modified by their owner.}}<br />
* If '''pin''' fails to match, the error "Task ~Prim~ trying to illegally load script onto task ~Other_Prim~!" is shouted on [[DEBUG_CHANNEL]]. "~Prim~" and "~Other_Prim~" are substituted with the applicable prim names.<br />
* If '''target''' is invalid then "Unable to add item!" is shouted on [[DEBUG_CHANNEL]]. '''target''' is invalid when...<br />
** it equals the value returned by [[llGetKey]].<br />
* In SL 1.25.4 this function will not copy/move any script into an attachment unless the script is full perm. ([https://jira.secondlife.com/browse/SVC-3725 SVC-3725])<br />
** SL 1.25.5 will allow this function to copy/move a script into an attachment so long as the target has matching (or more restrictive) copy and transfer permissions. ([https://jira.secondlife.com/browse/SVC-3738 SVC-3738])<br />
|constants<br />
|examples=<br />
<lsl>//Child Prim PIN setter<br />
integer PIN=1341134;<br />
<br />
default {<br />
state_entry() {<br />
llOwnerSay(llGetObjectName()+" : "+(string)llGetKey()+" is ready to accept a describer script using the agreed upon PIN.");<br />
llSetRemoteScriptAccessPin(PIN);<br />
<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions={{LSL DefineRow||[[llSetRemoteScriptAccessPin]]|Used to setup a prim for remote loading}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Script<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlRemoteLoadScriptPin&diff=218783LlRemoteLoadScriptPin2009-01-31T20:08:15Z<p>Felis Darwin: </p>
<hr />
<div>{{LSL_Function/inventory|name|uuid=false|type=script}}{{Issues/SVC-3321}}<br />
{{LSL_Function<br />
|func_id=253|func_sleep=3.0|func_energy=10.0<br />
|func=llRemoteLoadScriptPin<br />
|p1_type=key|p1_name=target|p1_desc=A prim in the same sim<br />
|p2_type=string|p2_name=name|p2_desc<br />
|p3_type=integer|p3_name=pin|p3_desc=Must match pin set by [[llSetRemoteScriptAccessPin]]<br />
|p4_type=integer|p4_name=running|p4_desc=boolean, if the script is to be set as running.<br />
|p5_type=integer|p5_name=start_param|p5_desc=value returned by [[llGetStartParameter]] in the target script.<br />
|func_desc=Copy script '''name''' into '''target''' and if '''running''' start with '''start_param'''.<br />
|func_footnote=Only works if the owner of the object this script is in can modify '''target'''.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* '''start_param''' only lasts until the script is reset.<br />
* If '''target''' is an {{LSLGC|Attachment|attachment}} owned by a different user, regardless of object modify rights granted, this function will silently (bug?) fail.{{Footnote|{{LSLGC|Attachment|Attachments}} can only be modified by their owner.|Attachments can only be modified by their owner.}}<br />
* If '''pin''' fails to match, the error "Task ~Prim~ trying to illegally load script onto task ~Other_Prim~!" is shouted on [[DEBUG_CHANNEL]]. "~Prim~" and "~Other_Prim~" are substituted with the applicable prim names.<br />
* If '''target''' is invalid then "Unable to add item!" is shouted on [[DEBUG_CHANNEL]]. '''target''' is invalid when...<br />
** it equals the value returned by [[llGetKey]].<br />
* In SL 1.25.4 this function will not copy/move any script into an attachment unless the script is full perm. ([https://jira.secondlife.com/browse/SVC-3725 SVC-3725])<br />
** SL 1.25.5 will allow this function to copy/move a script into an attachment so long as the target has matching (or more restrictive) copy and transfer permissions. ([https://jira.secondlife.com/browse/SVC-3738 SVC-3738]<br />
|constants<br />
|examples=<br />
<lsl>//Child Prim PIN setter<br />
integer PIN=1341134;<br />
<br />
default {<br />
state_entry() {<br />
llOwnerSay(llGetObjectName()+" : "+(string)llGetKey()+" is ready to accept a describer script using the agreed upon PIN.");<br />
llSetRemoteScriptAccessPin(PIN);<br />
<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions={{LSL DefineRow||[[llSetRemoteScriptAccessPin]]|Used to setup a prim for remote loading}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Script<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwinhttps://wiki.secondlife.com/w/index.php?title=LlRemoteLoadScriptPin&diff=209983LlRemoteLoadScriptPin2009-01-25T20:44:28Z<p>Felis Darwin: Add caveat regarding SVC-3725</p>
<hr />
<div>{{LSL_Function/inventory|name|uuid=false|type=script}}{{Issues/SVC-3321}}<br />
{{LSL_Function<br />
|func_id=253|func_sleep=3.0|func_energy=10.0<br />
|func=llRemoteLoadScriptPin<br />
|p1_type=key|p1_name=target|p1_desc=A prim in the same sim<br />
|p2_type=string|p2_name=name|p2_desc<br />
|p3_type=integer|p3_name=pin|p3_desc=Must match pin set by [[llSetRemoteScriptAccessPin]]<br />
|p4_type=integer|p4_name=running|p4_desc=boolean, if the script is to be set as running.<br />
|p5_type=integer|p5_name=start_param|p5_desc=value returned by [[llGetStartParameter]] in the target script.<br />
|func_desc=Copy script '''name''' into '''target''' and if '''running''' start with '''start_param'''.<br />
|func_footnote=Only works if the owner of the object this script is in can modify '''target'''.<br />
|return_text<br />
|spec<br />
|caveats=<br />
* '''start_param''' only lasts until the script is reset.<br />
* If '''target''' is an {{LSLGC|Attachment|attachment}} owned by a different user, regardless of object modify rights granted, this function will silently (bug?) fail.{{Footnote|{{LSLGC|Attachment|Attachments}} can only be modified by their owner.|Attachments can only be modified by their owner.}}<br />
* If '''pin''' fails to match, the error "Task ~Prim~ trying to illegally load script onto task ~Other_Prim~!" is shouted on [[DEBUG_CHANNEL]]. "~Prim~" and "~Other_Prim~" are substituted with the applicable prim names.<br />
* If '''target''' is invalid then "Unable to add item!" is shouted on [[DEBUG_CHANNEL]]. '''target''' is invalid when...<br />
** it equals the value returned by [[llGetKey]].<br />
* Starting with SL 1.25 this function will not copy/move any script into a worn attachment unless the script is full perm. ([https://jira.secondlife.com/browse/SVC-3725 SVC-3725])<br />
|constants<br />
|examples=<br />
<lsl>//Child Prim PIN setter<br />
integer PIN=1341134;<br />
<br />
default {<br />
state_entry() {<br />
llOwnerSay(llGetObjectName()+" : "+(string)llGetKey()+" is ready to accept a describer script using the agreed upon PIN.");<br />
llSetRemoteScriptAccessPin(PIN);<br />
<br />
}<br />
}</lsl><br />
|helpers<br />
|also_functions={{LSL DefineRow||[[llSetRemoteScriptAccessPin]]|Used to setup a prim for remote loading}}<br />
|also_tests<br />
|also_events<br />
|also_articles<br />
|notes<br />
|cat1=Script<br />
|cat2<br />
|cat3<br />
|cat4<br />
}}</div>Felis Darwin