// -*- 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