ParcelChatRelay

==Parcel Chat Relay==
===Description:===
Communicate on your land in general chat regardless of distance between avatars. Useful when building, in clubs, for roleplay, etc.
Messages will be intelligently relayed - a listener will never hear a message twice! It will either be heard normally, or relayed. Using the new llRegionSayTo() api, chat messages are only relayed to avatars that are out of range.
===How to use:===
* Rez 1 or more relays on your parcel, in a way that you could hear each avatar (<20m). Do not worry overlapping, but don't overdo it either - less relays will still be less lag.
* If you rez a relay on its' final position, it will immediately be 'tuned' and part of the network.
* If you move or remove a relay, it might take up to 10 minutes for the network to catch up.
===Features:===
* Muliple relays can be rezzed to cover each part of your parcel.
* Dedoubling without any overhead
* Low lag - relays do not have to communicate to dedouble messages
===Limitations:===
* Works only per parcel (by design). Agents on neighboring land will not receive relayed messages.
* Receivers have to be within radar range (96m) of the receiving relay
* Scripts cannot distinguish normal say, whispering and shouting. Normal say is assumed. Shouts may be heard double by the receiver
* Moving avatars might experience 'border' cases - where a message sometimes accidentally is not relayed or double relayed. Due to the fast response time unlikely
* The displayed ('faked') name is the official linden name, and chat is colored like objects, not avatars. Also depends on viewer settings.
Remarks on the parcel limitation.. Other options (sim wide, range based) would be easily possible, but i like the item to be zero-config ('just works'). Feel free to mod the script at your demands.
===License:===
Modified BSD / public domain
This script is also available at marketplace for your convenience: https://marketplace.secondlife.com/p/Parcel-Chat-Relay/3157725
===Notes:===
Possible easy-to-adjust modifications: do not use parcel key but owner key as handle, allowing cross parcel-but-same-owner chat, or specifying a fixed key for region-wide that.
===VERSION 2.0===
Recommended and most efficient version
=Code=
//PCR Version 2//
//changes:
//* No longer uses radar but llGetAgentList to detect agents
//* Removed hover text and replaced by ownersay spam on updates
//* Added configurable option for coverage (parcel, parcels by same owner, entire region)
//* Made timer faster running in first 10 minutes after rez, to update faster on repositions
//* Some adjustments to allow cross-parcel and region-wide chat; replaced parcel key by owner key in those situations.
//* Overall, this version is more efficient and causes less lag, especially when idling, since agent list is not unneccesary updated by sensor events. It also provides better coverage and automatic support for skyboxes.
//2011-2012 Tano Toll - TSL License//
//Short summorization:
//Relays will know of eachothers existance
//Relays listen to public chat
//The relay _closest_ to sending agent is responsible for relaying the message
//Messages will only be relayed to agents further than 20 meter away from the speaker
//Messages will only be relayed to agents on the same parcel
//USAGE:
//* Edit coverage parameter below to suit your needs
//* place this script in a objects, placed in hearing range of avatars
//* If you do NOT want to relay a certain place, like a skybox, simply place no relay object. However, agents in the skybox will still hear what is said near other relays. If you do not want that, please use version 1 of this script that uses radar to detect agents.
// CONFIGURATION
//choose coverage:
//integer coverage=AGENT_LIST_PARCEL;
integer coverage=AGENT_LIST_PARCEL_OWNER;
//integer coverage=AGENT_LIST_REGION;
//Maximum range from listener object to avatar
integer maxRange=6000; //sim wide, reaches all skyboxes
//integer maxRange=290; //sim wide, ground level
//integer maxRange=100; //radar range, like version 1
//just a hash. change to use several systems on a single parcel.
string tag="+-/";
// NO NEED TO EDIT BELOW THIS LINE. CHANGES AT OWN RISK //
//list of detected nearby agents:
list insim;
//timestamp of last check - to reduce sim load
integer stamp;
//key of the parcel where we are rezzed
key parcelkey;
//key based for handle, depending on setting parcel or owner
key handlebase; 
//chat channel handle made with key as base
integer handle;
//list of detected nearby relays
list repeaters;
//for less confusing setup, fast timer first minutes after rez, then slow down to appropiate interval
integer timercount;
announce() {
 llRegionSay (handle, tag+(string)handlebase); 
}
default
{
 state_entry()
 {
 //llSetText("Chat Relay started...",ZERO_VECTOR,1);
 llOwnerSay("Chat Relay started...");
 if (llGetAttached()) {
 //owner relay only mode
 //redundant in this script.
 //llListen (0, "", "", "");
 } else {
 //relay all agents
 llListen (0, "", "", ""); 
 }
 
 //scan for nearby agents
 //llSensorRepeat ("", "", AGENT, 65, TWO_PI, 20);
 
 //make up a inter-relay channel, so they can find eachother
 parcelkey=llList2Key(llGetParcelDetails(llGetPos(), [PARCEL_DETAILS_ID]),0); 
 
 //adjust key based on coverage. for single parcel, use parcel key. else use owner key.
 //notice that on region-wide coverage, all relays must be rezzed by same owner.
 
 if (coverage==AGENT_LIST_PARCEL)
 handlebase=parcelkey;
 else
 handlebase=llGetOwner();
 
 handle=-10909-(integer)("0x"+llGetSubString(handlebase,1,5));
 llListen(handle, "", "", tag+(string)handlebase);
 
 //broadcast our existance about every 5 minutes, but starting with 10 second interval to avoid setup confusion
 //llSetTimerEvent(290.0+llFrand (20.0)); //about each 300 seconds, not all at once
 llSetTimerEvent (10.0);
 announce();
 }
 
 on_rez(integer n) {
 //simply reset
 llResetScript(); 
 }
 timer() { 
 //announce self
 announce();
 
 //check interval
 timercount++;
 if (timercount==60) //after about 10 minutes
 llSetTimerEvent(290.0+llFrand (20.0)); //change to 5 minute interval 
 
 //and clear list. repeaters with an too old timestamp will be removed.
 integer n=llGetListLength(repeaters)-1;
 integer t=llGetUnixTime();
 while (n>0) {
 if ((t-llList2Integer(repeaters, n))>390) { //time out
 llOwnerSay ("Lost connection to node at "+(string)llList2Vector(repeaters, n-1));
 repeaters=llDeleteSubList(repeaters, n-2, n);
 }
 n-=3;
 } 
 //llSetText ("Node(s): "+(string)(1+llGetListLength(repeaters)/3), <1,1,1>, 1);
 }
 
 listen (integer channel, string name, key id, string m) { 
 
 if (!channel) { //channel 0
 //only relay agents, not objects
 if (llGetAgentSize(id));else
 return;
 
 //see if another relay is more nearby
 integer n=0;
 vector senderpos=llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0);
 float d=llVecDist(senderpos, llGetPos()); 
 while (n<llGetListLength(repeaters)) {
 if (d>llVecDist(senderpos, llList2Vector(repeaters, n+1)))
 return;
 n+=3;
 }
 //We are nearest repeater. Our job to relay.
 
 //see if we need to refresh our list - cache it for 15 seconds
 if ((llGetUnixTime()-stamp) > 15.0) {
 stamp=llGetUnixTime();
 insim=llGetAgentList (coverage, []);
 }
 
 //relay to everyone in range. that's fixed to minimum 20 mins maximum 60.
 //get position of chatter
 vector o=llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0);
 
 n=llGetListLength(insim);
 string oname=llGetObjectName();
 llSetObjectName(name);
 while (~--n) {
 vector t=llList2Vector(llGetObjectDetails(llList2Key(insim, n), [OBJECT_POS]), 0); 
 float d=llVecDist (o,t);
 if (d>=20 && d<=maxRange) {
 //same parcel? - edit - check skipped.
 //if (parcelkey==llList2Key(llGetParcelDetails(llList2Vector(llGetObjectDetails(llList2Key(insim,n), [OBJECT_POS]),0), [PARCEL_DETAILS_ID]),0))
 llRegionSayTo (llList2Key(insim,n), 0, m); 
 }
 } 
 llSetObjectName(oname);
 }
 else
 if (channel==handle) {
 //there's another repeater..
 vector pos=llList2Vector(llGetObjectDetails(id, [OBJECT_POS]),0);
 
 integer p=llListFindList(repeaters, [id, pos]);
 if (~p)
 repeaters=llDeleteSubList(repeaters, p, p+2);
 else
 llOwnerSay ("New node found at "+(string)pos);
 repeaters += [id, pos, llGetUnixTime()];
 }
 
 }
}
===VERSION 1.0===
In some cases, you might want to use the original radar-based version. Usually not recommended, listed for completeness.
=Code=
//2011-2012 Tano Toll - TSL License//
//Short summorization:
//Relays will know of eachothers existance
//Relays listen to public chat
//The relay _closest_ to sending agent is responsible for relaying the message
//Messages will only be relayed to agents further than 20 meter away from the speaker
//Messages will only be relayed to agents on the same parcel
//list of detected nearby agents:
list insim;
//just a hash. change to use several systems on a single parcel.
string tag="+-/";
//key of the parcel where we are rezzed
key parcelkey;
//chat channel handle made with key as base
integer parcelhandle;
//list of detected nearby relays
list repeaters;
announce() {
 llRegionSay (parcelhandle, tag+(string)parcelkey); 
}
default
{
 state_entry()
 {
 llSetText("Chat Relay started...",ZERO_VECTOR,1);
 if (llGetAttached()) {
 //owner relay only mode
 //redundant in this script.
 //llListen (0, "", "", "");
 } else {
 //relay all agents
 llListen (0, "", "", ""); 
 }
 
 //scan for nearby agents
 llSensorRepeat ("", "", AGENT, 65, TWO_PI, 20);
 
 //make up a inter-relay channel, so they can find eachother
 parcelkey=llList2Key(llGetParcelDetails(llGetPos(), [PARCEL_DETAILS_ID]),0);
 parcelhandle=-10909-(integer)("0x"+llGetSubString(parcelkey,1,5));
 llListen(parcelhandle, "", "", tag+(string)parcelkey);
 
 //broadcast our existance about every 5 minutes
 llSetTimerEvent(290.0+llFrand (20.0)); //about each 300 seconds, not all at once
 announce();
 }
 
 on_rez(integer n) {
 //simply reset
 llResetScript(); 
 }
 timer() { 
 //announce self
 announce();
 
 //and clear list. repeaters with an too old timestamp will be removed.
 integer n=llGetListLength(repeaters)-1;
 integer t=llGetUnixTime();
 while (n>0) {
 if ((t-llList2Integer(repeaters, n))>390) { //time out
 repeaters=llDeleteSubList(repeaters, n-2, n);
 }
 n-=3;
 } 
 llSetText ("Node(s): "+(string)(1+llGetListLength(repeaters)/3), <1,1,1>, 1);
 }
 
 sensor (integer n) {
 //add detected agents to our list
 while (~--n) {
 key id=llDetectedKey(n);
 if (!~llListFindList(insim, [id]))
 insim+=id; 
 }
 
 //clean up agents that are no longer in sim
 n=llGetListLength(insim);
 while (~--n)
 if (llGetAgentSize(llList2Key(insim,n))); else
 insim=llDeleteSubList(insim,n,n); 
 }
 
 no_sensor() {
 insim=[];
 /*
 //clean up agents that are no longer in sim
 integer n=llGetListLength(insim);
 while (~--n)
 if (llGetAgentSize(llList2Key(insim,n))); else
 insim=llDeleteSubList(insim,n,n); 
 */
 }
 
 listen (integer channel, string name, key id, string m) {
 
 
 if (!channel) { //channel 0
 //only relay agents, not objects
 if (llGetAgentSize(id));else
 return;
 
 //see if another relay is more nearby
 integer n=0;
 vector senderpos=llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0);
 float d=llVecDist(senderpos, llGetPos()); 
 while (n<llGetListLength(repeaters)) {
 if (d>llVecDist(senderpos, llList2Vector(repeaters, n+1)))
 return;
 n+=3;
 }
 //We are nearest repeater. Our job to relay.
 
 //relay to everyone in range. that's fixed to minimum 20 mins maximum 60.
 //get position of chatter
 vector o=llList2Vector(llGetObjectDetails(id, [OBJECT_POS]), 0);
 
 n=llGetListLength(insim);
 string oname=llGetObjectName();
 llSetObjectName(name);
 while (~--n) {
 vector t=llList2Vector(llGetObjectDetails(llList2Key(insim, n), [OBJECT_POS]), 0); 
 float d=llVecDist (o,t);
 if (d>=20/* && d<=60*/) {
 //same parcel?
 if (parcelkey==llList2Key(llGetParcelDetails(llList2Vector(llGetObjectDetails(llList2Key(insim,n), [OBJECT_POS]),0), [PARCEL_DETAILS_ID]),0))
 llRegionSayTo (llList2Key(insim,n), 0, m); 
 }
 } 
 llSetObjectName(oname);
 }
 else
 if (channel==parcelhandle) {
 //there's another repeater..
 vector pos=llList2Vector(llGetObjectDetails(id, [OBJECT_POS]),0);
 
 integer p=llListFindList(repeaters, [id, pos]);
 if (~p)
 repeaters=llDeleteSubList(repeaters, p, p+2);
 repeaters += [id, pos, llGetUnixTime()];
 }
 
 }
}