// -*- Mode: LSL -*- // // Jean Zee's polite_group_parcel version 1 (2022-01-20) // https://cybertiggyr.com/alriv/polite_group_parcel.txt // // Copyright (C) 2022 Jean Zee (username CmpZ) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // //////////////////////////////////////////////////////////////////////// // // Number of seconds between checks. // I fear that less than 1 second is too demanding on the sim. // We get the value from an "Interval:" line in the configuration // notecard if there is one. // float Interval; // // Toons above this height pass the access check (just like // toons in our group pass it). Height defaults to a very // high value so that toons are unlikely to pass the check // that way. // You can alter height with a "Height:" line in the configuration // notecard. // float Height; // // When true, the Trace(...) function prints messages // to owner. Otherwise, that function prints nothing. // You can set the value with a "Trace:" line in the // configuration notecard. // integer Is_Trace; // // When true, we Eject toons before banning them to // get rid of them immediately. Otherwise, this is false & // we don't call Eject at all. // // You can control this from the configuration notecard. // If the notecard contains a line that says "Eject: true", // we'll call Eject. Otherwise, we don't call Eject. // // By default, we call Eject. // integer Is_Eject; // // List of avatars on this parcel. // We populate this in the Load_Avatars() function. // We clear it in the Unload_Avatars() function. // Things work fine if you forget to call Unload_Avatars(), // but try to call it to free some memory when possible. // list Avatars; // // When we are reading the configuration notecard, // this is the line number. // integer Config_Line_Number; // // // key Query_Key; // // Constants for Act_On_Avatar() // The exact values are meaningless as long as they are distinguishable. // I selected (3, 4) because (0, 1) and (1, 2) both feel to predictable. // integer ACTION_EJECT = 3; integer ACTION_BAN = 4; // // Name of the configuration notecard // string CONFIG_NAME = "Config"; // // // float DEFAULT_HEIGHT = 10000.0; float DEFAULT_INTERVAL = 3.0; // // // vector YELLOW = <1.0, 1.0, 0.0>; // // // float ALPHA_OPAQUE = 1.0; //////////////////////////////////////////////////////////////////////// // // // Trace( string msg ) { if (Is_Trace) { llOwnerSay( msg ); } } Warn( string msg ) { llOwnerSay( "WARNING: " + msg ); } Error( string msg ) { llOwnerSay( "ERROR: " + msg ); } // // Get list of Avatars on this parcel. Save it in global Avatars. // Return length of the list. // integer Load_Avatars() { integer scope; list options; scope = AGENT_LIST_PARCEL; options = []; // unused Avatars = llGetAgentList( scope, options ); return llGetListLength( Avatars ); } // // Save a little memory. // Unload_Avatars() { Avatars = []; } // // True if a group member is present on the parcel. // Otherwise, false. // integer Is_Group_Present() { integer b; // boolean integer count; integer i; key avatar; b = FALSE; count = llGetListLength( Avatars ); i = 0; while (!b && i < count) { avatar = llList2Key( Avatars, i ); b = llSameGroup( avatar ); i = i + 1; } return b; } // // Eject or Ban the avatar // Act_On_Avatar( integer action, key avatar ) { float hours; if (action == ACTION_EJECT) { Trace( "Eject" ); llEjectFromLand( avatar ); } else if (action == ACTION_BAN) { Trace( "Ban" ); hours = 72.0; // three days llAddToLandBanList( avatar, hours ); } else { Warn( "Unexpected action: " + ((string) action) ); } } // // Return the elevation (height above z = 0.0) of the toon. // float Toon_Elevation( key toon ) { list params; list deets; vector pos; float elevation; params = [OBJECT_POS]; deets = llGetObjectDetails( toon, params ); pos = llList2Vector( deets, 0 ); elevation = pos.z; return elevation; } // // Apply ACTION to all the avatars that aren't in the group. // Assumes you've already loaded the global Avatars list. // Loop_Over_Avatars( integer action ) { integer count; integer i; key avatar; count = llGetListLength( Avatars ); for( i = 0; i < count; ++i) { avatar = llList2Key( Avatars, i ); if (llSameGroup( avatar )) { // This avatar is allowed. } else if (Height <= Toon_Elevation( avatar )) { // Avatar is high enough in the air that we // let it pass. } else { // This avatar is not allowed. Act_On_Avatar( action, avatar ); } } } // // Eject anyone who isn't a group member // Eject() { Loop_Over_Avatars( ACTION_EJECT ); } // // Ban anyone who isn't a group member // Ban() { Loop_Over_Avatars( ACTION_BAN ); } // // Return the key for my (this object's) group. // Purpose of that is mostly to verify that this object // is in a group. // key My_Group() { key id; list params; list deets; id = llGetKey(); params = [OBJECT_GROUP]; deets = llGetObjectDetails( id, params ); return llList2Key( deets, 0 ); } // // Return key of the parcel's group. // key Parcel_Group() { vector pos; list params; list deets; pos = llGetPos(); params = [PARCEL_DETAILS_GROUP]; deets = llGetParcelDetails( pos, params ); return llList2Key( deets, 0 ); } // // Return key of the parcel's owner // key Parcel_Owner() { vector pos; list params; list deets; pos = llGetPos(); params = [PARCEL_DETAILS_OWNER]; deets = llGetParcelDetails( pos, params ); return llList2Key( deets, 0 ); } // // Request the next line from the configuration notecard. // Increments the notecard line counter & saves the query's // key. // Next_Config() { string text; vector color; float alpha; Config_Line_Number = Config_Line_Number + 1; Query_Key = llGetNotecardLine( CONFIG_NAME, Config_Line_Number ); text = "Config line #" + ((string) Config_Line_Number); color = YELLOW; alpha = ALPHA_OPAQUE; llSetText( text, color, alpha ); } // // If the string is all white-space, return true. // integer Try_Empty( string s ) { s = llStringTrim( s, STRING_TRIM ); return llStringLength( s ) == 0; } // // If the string is a comment, return true. // It's a comment if it begins with "#". // integer Try_Comment( string s ) { return 1<= llStringLength( s ) && "#" == llGetSubString( s, 0, 0 ); } // // // integer Try_Eject( string s ) { string Prefix = "Eject:"; integer found; if (llStringLength( s ) <= llStringLength( Prefix )) { // String is too short to contain both the prefix & // something after it. So it's not a trace. found = FALSE; } else if (Prefix != llGetSubString( s, 0, llStringLength(Prefix) - 1 )) { // String does not begin with Prefix. found = FALSE; } else { found = TRUE; // Just the value s = llGetSubString( s, llStringLength(Prefix), -1 ); // Remove space chars s = llStringTrim( s, STRING_TRIM ); if (s == "1" || s == "true") { Is_Eject = TRUE; } else if (s == "0" || s == "false") { Is_Eject = FALSE; } else { Is_Eject = TRUE; Warn( "Don't know what to do when the value for Trace" + " is \"" + s + "\". Assuming you meant true." ); } if (Is_Eject) { Trace( Prefix + " true" ); } } return found; } // // If the configuration line tells us a High, // extract the height & save that, then return true. // Otherwise, return false. // integer Try_Height( string s ) { string Prefix = "Height:"; integer is_height; if (llStringLength( s ) <= llStringLength( Prefix )) { // String is too short to contain both the prefix & // something after it. So it's not a height. is_height = FALSE; } else if (Prefix != llGetSubString( s, 0, llStringLength(Prefix) - 1 )) { // String does not begin with Prefix. is_height = FALSE; } else { Height = (float) llGetSubString( s, llStringLength(Prefix), -1 ); if (Height <= 0.0) { Height = DEFAULT_HEIGHT; } else if (DEFAULT_HEIGHT < Height) { Height = DEFAULT_HEIGHT; } Trace( Prefix + " " + ((string) Height) ); is_height = TRUE; } return is_height; } // // If the configuration line tells us an Interval, // extract the seconds & save that, then return true. // Otherwise, return false. // integer Try_Interval( string s ) { string Prefix = "Interval:"; integer is_interval; if (llStringLength( s ) <= llStringLength( Prefix )) { // String is too short to contain both the prefix & // something after it. So it's not an interval. is_interval = FALSE; } else if (Prefix != llGetSubString( s, 0, llStringLength(Prefix) - 1 )) { // String does not begin with Prefix. is_interval = FALSE; } else { Interval = (float) llGetSubString( s, llStringLength(Prefix), -1 ); if (Interval <= 0.0) { Interval = DEFAULT_INTERVAL; } else if (DEFAULT_INTERVAL < Interval) { Interval = DEFAULT_INTERVAL; } Trace( Prefix + " " + ((string) Interval) ); is_interval = TRUE; } return is_interval; } // // If the configuration line tells us a Trace, // extract the value, then return true. // Otherwise, return false. // The value for "Trace:" can be 1 or true to enable trace // statements. It can be 0 or false to disable them. // Other values enable trace & also earn you a warning // message. // integer Try_Trace( string s ) { string Prefix = "Trace:"; integer found; if (llStringLength( s ) <= llStringLength( Prefix )) { // String is too short to contain both the prefix & // something after it. So it's not a trace. found = FALSE; } else if (Prefix != llGetSubString( s, 0, llStringLength(Prefix) - 1 )) { // String does not begin with Prefix. found = FALSE; } else { found = TRUE; // Just the value s = llGetSubString( s, llStringLength(Prefix), -1 ); // Remove space chars s = llStringTrim( s, STRING_TRIM ); if (s == "1" || s == "true") { Is_Trace = TRUE; } else if (s == "0" || s == "false") { Is_Trace = FALSE; } else { Is_Trace = TRUE; Warn( "Don't know what to do when the value for Trace" + " is \"" + s + "\". Assuming you meant true." ); } if (Is_Trace) { Trace( Prefix + " true" ); } } return found; } //////////////////////////////////////////////////////////////////////// On_Changed( integer change ) { if (change & CHANGED_INVENTORY) { Trace( "Reset script due to changed inventory." ); llResetScript(); } else if (change & CHANGED_OWNER) { Trace( "Reset script due to changed owner." ); llResetScript(); } else if (change & CHANGED_REGION) { Trace( "Reset script due to changed region." ); llResetScript(); } } //////////////////////////////////////////////////////////////////////// // // // default { state_entry() { string s; Trace( "default's state_entry" ); // // Check that we have a group. if (My_Group() == NULL_KEY) { s = "\n" + "I am not in a group so cannot determine" + " whether someone is in the same group.\n" + " me from your viewer & put me in a group."; Error( s ); state Error_Bucket; } // // Check owner. if (llGetOwner() != Parcel_Owner()) { s = "My owner is not the parcel's owner. " + " I might be able to ban users" + " & everything works, but it's suspicious."; Warn( s ); } // // Check that our group is same as parcel's // group. if (My_Group() != Parcel_Group()) { s = "My group is not the same as the" + " parcel's group. I might be able to ban users" + " & everything works, but it's suspicious. You" + " might want to double-check that both my group &" + " the parcel's group are correct."; Warn( s ); } state Load_Config; } on_rez( integer start_param ) { Trace( "Reset script due to on_rez(...)" ); llResetScript(); } } // // If there is a configuration notecard, read the lines from it. // state Load_Config { state_entry() { string s; Trace( "Load_Config's state_entry" ); Interval = DEFAULT_INTERVAL; Height = DEFAULT_HEIGHT; Is_Eject = TRUE; Is_Trace = FALSE; Config_Line_Number = -1; if (llGetInventoryType( CONFIG_NAME ) == INVENTORY_NOTECARD) { Next_Config(); llSetTimerEvent( 10.0 ); } else { s = "There is no notecard called " + CONFIG_NAME + ". I'll rely on defaults."; Warn( s ); state Loop_With_Group_Members; } } dataserver( key queryid, string data ) { string s; if (queryid != Query_Key) { // We don't recognize this query. Ignore. } else if (data == EOF) { // No more lines. state Loop_With_Group_Members; } else if (Try_Empty( data )) { Next_Config(); } else if (Try_Comment( data )) { Next_Config(); } else if (Try_Eject( data )) { Next_Config(); } else if (Try_Height( data )) { Next_Config(); } else if (Try_Interval( data )) { Next_Config(); } else if (Try_Trace( data )) { Next_Config(); } else { s = "Don't know what to do with configuration notecard's" + " line number " + ((string) Config_Line_Number) + ", \"" + data + "\"."; Warn( s ); Next_Config(); } } timer() { string s; s = "Timeout while reading the configuration notecard. " + " This probably indicates a programming error."; Error( s ); llSetTimerEvent( 0.0 ); state Loop_With_Group_Members; } // // These next events detect situations in which we want to // reset the script. The other states have similar // detectors. It's unlikely that we'll be in this, the // configuration-reading state, long enough for these to // occur, but I guess it's better to be safe than sorry. // changed( integer change ) { On_Changed( change ); } moving_end() { Trace( "Reset script due to moving_end(...)" ); llResetScript(); } on_rez( integer start_param ) { Trace( "Reset script due to on_rez(...)" ); llResetScript(); } } // // Periodically get a list of all toons on the parcel. // // If none of them are in the group, we head to another // state. // // If at least one person is in the group, then we Eject & // Ban any who aren't. // // As in other states, there are some events that cause us // to reset & take it from the top; // state Loop_With_Group_Members { state_entry() { Trace( "Loop_With_Group_Members's state_entry" ); llSetText( "group members", YELLOW, ALPHA_OPAQUE ); Unload_Avatars(); llSetTimerEvent( Interval ); } timer() { Load_Avatars(); if (Is_Group_Present()) { if (Is_Eject) { Eject(); } Ban(); Unload_Avatars(); } else { // Group members are not present, go to the // more permissive loop llSetTimerEvent( 0.0 ); // end the timer state Loop_Without_Group_Members; } } changed( integer change ) { On_Changed( change ); } moving_end() { Trace( "Reset script due to moving_end(...)" ); llResetScript(); } on_rez( integer start_param ) { Trace( "Reset script due to on_rez(...)" ); llResetScript(); } } // // In this state, no group members are present, so we allow // anyone to access the parcel. // // For starters, we clear out the ban list. // // Periodically get a list of all toons on the parcel. // // If none of them are in the group, we stay here. // // If at least one person is in the group, we head to the // previous state, where we Eject & Ban people who aren't // in the group. // // As in other states, there are some events that cause us // to reset & take it from the top; // state Loop_Without_Group_Members { state_entry() { Trace( "Loop_Without_Group_Members's state_entry" ); llSetText( "anyone", YELLOW, ALPHA_OPAQUE ); llResetLandBanList(); Unload_Avatars(); llSetTimerEvent( Interval ); } timer() { Load_Avatars(); if (Is_Group_Present()) { llSetTimerEvent( 0.0 ); // end the timer state Loop_With_Group_Members; } else { // No group members present. Stay here. Unload_Avatars(); } } changed( integer change ) { On_Changed( change ); } moving_end() { Trace( "Reset script due to moving_end(...)" ); llResetScript(); } on_rez( integer start_param ) { Trace( "Reset script due to on_rez(...)" ); llResetScript(); } } // // Stay here until an event justifies resetting the script. // state Error_Bucket { state_entry() { string s; Trace( "Error_Bucket's state_entry" ); s = "Entering error state. I'll remain here" + " unless you move me, change owner, change" + " group, or change my inventory. If you do" + " any of those, I'll restart & try again."; llSay( 0, s ); llSetText( "error", YELLOW, ALPHA_OPAQUE ); Unload_Avatars(); } changed( integer change ) { On_Changed( change ); } moving_end() { Trace( "Reset script due to moving_end(...)" ); llResetScript(); } on_rez( integer start_param ) { Trace( "Reset script due to on_rez(...)" ); llResetScript(); } } // end of file