Tipjar

From Second Life Wiki
Jump to: navigation, search

Created by Kira Komarov.

Introduction

I wanted to recreate all the features of a commercial tipjar as well as implement some of my own twists. Here are the features of this tipjar:

  • Does NOT require to be deeded to group, however it implements the same functionality of shared access using notecards and LSL.
  • Supports split profits based on percentages, a notecard called Tipjar Spoils should be added to the same primitive in which this script resides.
  • Has customisable messages for all the output, also supporting markers to replace to be replaced dynamically with the current avatar using the tipjar, and a tipper.
  • Optionally offers tippers to join a group and hands them a gift is one is present in the tipjar.

And an uncommon and personal one:

  • After a configurable amount of time, the tipjar will start to move around the room from avatar to avatar and then go back to its initial position.

Setup

  • Create a notecard called Tipjar Access. This notecard will contain a list of people who will be able to log into the tipjar and receive tips. The format of the card is the following:
<avatar name_1>#<avatar key_1>
<avatar name_2>#<avatar key_2>

For example, if two people are allowed to log into the system, the Tipjar Access notecard would look like this:

Kira Komarov#1ad33407-a792-416d-a5e3-06007c0802bf
William Riker#82c73d6d-facf-4322-b17b-06d8cd66a116
  • Next, create another notecard called Tipjar Spoils. This notecard will contain a list of people, their key names and the percentage they will get from the total pot of money. The format of the card is the following:
<avatar name_1>#<avatar key_1>#<percentage>
<avatar name_2>#<avatar key_2>#<percentage>

So, following our previous example, for two people, the notecard would look like this:

Tasha Yar#1ad33417-a792-476d-a5e3-86007c7802bf#75
Lance Lenoirre#82c73d7d-facf-4332-b17b-06d8cd66a116#25

That means, that after a tipjar user, either Kira Komarov or William Riker logout, the tipjar will distribute 75% of all profits to Tasha Yar and 25% to Lance Lenoirre.

  • Now create a script, and copy and paste the code below in the code section and drop it in a primitive. If your settings are right, the users on the Tipjar Access list will be able to log into the tipjar and use it. To start distributing the profits after several tips, any of the people in the Tipjar Spoils notecard will be able to log out, as well as the user currently using the tipjar.

Configuration

There are several options you can configure in the CONFIGURATION section:

string THANKS_MESSAGE = "Thank you for your tip %n%! %m% greatly appreciates it!";
string TIPPER_TOUCH_MESSAGE = "Hello %n%! This tipjar belongs to %m%. If you like my work, please consider giving me a tip. To tip me, please right-click my tipjar and select Pay and enter the ammount you would like to tip me with. Thank you!";
string TIPPER_DIALOG_MESSAGE = "Hello, %n%! Please choose an option from the ones below:\nJoin - will send you a group invite.\nGift - will send you a free gift!";
string OVERHEAD_MESSAGE = "%m%'s Tipjar, any tip is welcome!";
string LOGOUT_MESSAGE = "%m%'s Tipjar is logging off, please standby to receive your share %n%...";
list TIPPER_TOUCH_MENU = [ "◆ Join ◆", "◆ Gift ◆" ];
key INVITE_GROUP_KEY = "004079ff-e1b0-c671-dd5e-eb4f6e361684";
string INVITE_GROUP_MESSAGE = "Please join the group %n%! To do so, please click the link in your history window (ctrl+h):";
list PAY_BUTTONS = [ "20", "40", "60", "80" ];
integer EXCLUDE_ACCESS_FROM_SPOILS = 1;
integer TIPJAR_ROAMING = 1;
integer ROAM_INTERVAL = 30;
integer ROAM_RANGE = 10;

Here is a description of what they are and what they are meant for:

  • The string THANKS_MESSAGE is a message that will be sent to a tipper after they have tipped the avatar currently using the tipjar. As you can see, this string contains some keywords like %n% and %m%. They are expanded dynamically depending on the situation. The keyword %n% will be dynamically replaced by the person tipping the user currently using the avatar. The keyword %m% will be dynamically replaced by the avatar's name who is currently using the tipjar.
  • The TIPPER_TOUCH_MESSAGE is a message that will be sent to a tipper when they touch the tipjar.
  • The TIPPER_DIALOG_MESSAGE is the message in the dialog that pops up when a tipper touch the tipjar.
  • The OVERHEAD_MESSAGE is a message that will be displayed while an avatar is using and logged into the tipjar.
  • The LOGOUT_MESSAGE is a message that will be sent to all the people in the Tipjar Spoils notecard informing them that they are about to receive their share of the spoils.
  • The TIPPER_TOUCH_MENU represents the buttons that a potential tipper will get when they touch the tipjar. You can remove, for example, "◆ Gift ◆" if you do not wish to give the people who click your tipjar a gift. If you chose not to give a gift, then that line would look like:
list TIPPER_TOUCH_MENU = [ "◆ Join ◆" ];

If somebody clicks the "◆ Join ◆" button, they will be sent an invite to a group of your choosing (see below). You can disable both of these options, enable both, or choose one or the other. Please note, that in order to give a gift, an object should be placed in the tipjar contents tab.

  • The INVITE_GROUP_KEY is the key of the group that potential tippers will be invited to when they press "◆ Join ◆". You should replace this key with the key of your group of choosing.
  • The INVITE_GROUP_MESSAGE is the message that a potential tipper will get after pressing the "◆ Join ◆" button.
  • The EXCLUDE_ACCESS_FROM_SPOILS is a special option. For example, suppose that you put some employees in the Tipjar Access notecard and assign a certain percentage of the spoils to them. When an employee logs into the tipjar, if this option is set to 1, then all the other employees in the Tipjar Access notecard will be excluded from the current session. This is fairly understandable because they are not currently working and logged into the tipjar. This will allow you to have multiple employees in the notecard and not change their names in the Tipjar Access notecard when one of them is working.
  • TIPJAR_ROAMING is an option that enables the tipjar to move from avatar to avatar in the room in a given interval. Set this to 1 to enable and 0 to disable.
  • ROAM_INTERVAL this is the amount of time between which the tipjar starts roaming from avatar to avatar in the room.
  • The final option ROAM_RANGE is the maximum range that the tipjar will see when it chooses to move from avatar to avatar.

Code

//////////////////////////////////////////////////////////
// [K] Kira Komarov - 2011, License: GPLv3              //
// Please see: http://www.gnu.org/licenses/gpl.html     //
// for legal details, rights of fair usage and          //
// the disclaimer and warranty conditions.              //
//////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////
//                   CONFIGURATION                      //
//////////////////////////////////////////////////////////
//                                                      //
string THANKS_MESSAGE = "Thank you for your tip %n%! %m% greatly appreciates it!";
string TIPPER_TOUCH_MESSAGE = "Hello %n%! This tipjar belongs to %m%. If you like my work, please consider giving me a tip. To tip me, please right-click my tipjar and select Pay and enter the ammount you would like to tip me with. Thank you!";
string TIPPER_DIALOG_MESSAGE = "Hello, %n%! Please choose an option from the ones below:\nJoin - will send you a group invite.\nGift - will send you a free gift!";
string OVERHEAD_MESSAGE = "%m%'s Tipjar, any tip is welcome!";
string LOGOUT_MESSAGE = "%m%'s Tipjar is logging off, please standby to receive your share %n%...";
list TIPPER_TOUCH_MENU = [ "◆ Join ◆", "◆ Gift ◆" ];
key INVITE_GROUP_KEY = "004079ff-e1b0-c671-dd5e-eb4f6e361684";
string INVITE_GROUP_MESSAGE = "Please join the group %n%! To do so, please click the link in your history window (ctrl+h):";
list PAY_BUTTONS = [ "20", "40", "60", "80" ];
integer EXCLUDE_ACCESS_FROM_SPOILS = 1;
integer TIPJAR_ROAMING = 1;
integer ROAM_INTERVAL = 30;
integer ROAM_RANGE = 10;
//                                                      //
//                  END CONFIGURATION                   //
//////////////////////////////////////////////////////////
 
//////////////////////////////////////////////////////////
//                     INTERNALS                        //
//////////////////////////////////////////////////////////
string tokenSubstitute(string input, key id)
{
    list kSubst = llParseString2List(input, ["%"], [""]);
    integer itra;
    for(itra = 0; itra < llGetListLength(kSubst); ++itra)
    {
        if(llList2String(kSubst, itra) == "n") kSubst = llListReplaceList(kSubst, (list)llKey2Name(id), itra, itra);
        if(llList2String(kSubst, itra) == "m") kSubst = llListReplaceList(kSubst, (list)activeAvatarName, itra, itra);
    }
    return llDumpList2String(kSubst, " ");
}
 
moveTo(vector position)
{
    llTargetRemove(targetID);
    targetID = llTarget(position, 0.8);
    llLookAt(position, 0.6, 0.6);
    llMoveToTarget(position, 3.0);
}
 
physics(integer bool)
{
    if(bool) llSetForce(<0,0,9.81> * llGetMass(), 0);
    llSetStatus(STATUS_PHYSICS, bool);
    llSetStatus(STATUS_PHANTOM, bool);
}
 
list spoilMemberNames;
list spoilMemberKeys;
list spoilPercents;
 
list tippers;
list tipperAmounts;
 
list accessListNames;
list accessListKeys;
 
string activeAvatarName;
key activeAvatarKey;
 
key sQuery;
integer sLine;
integer readNotecard;
integer comHandleTiper;
integer comHandleSpoiler;
 
integer allSpoils;
 
vector landingPoint;
rotation landingRotation;
list avPositions;
integer positionRoam;
integer targetID;
integer roaming;
 
default
{
    state_entry()
    {
        physics(FALSE);
        accessListNames = [];
        accessListKeys = [];
        sLine = 0;
        readNotecard = 0;
        activeAvatarName = "";
        activeAvatarKey = NULL_KEY;
        llSetText("Tipjar loading access, please wait...", <1.0,1.0,1.0>, 1.0);
        integer itra;
        for(itra = 0; itra < llGetInventoryNumber(INVENTORY_NOTECARD); ++itra)
        {
            if(llGetInventoryName(INVENTORY_NOTECARD, itra) == "Tipjar Access")
                jump found_access;
        }
        llSetText("No access list. Please revise your configuration.", <1.0,1.0,1.0>, 1.0);
        llInstantMessage(llGetOwner(), "Failed to find Tipjar Access card. Please add a notecard called Tipjar Access and configure it apropriately.");
        return;
@found_access;
        sQuery = llGetNotecardLine("Tipjar Access", sLine);
        llSetTimerEvent(5.0);
    }
 
    changed(integer change)
    {
        if(change & CHANGED_INVENTORY)
            llResetScript();
    }
 
    on_rez(integer num)
    {
        physics(FALSE);
    }
 
    timer()
    {
        if(readNotecard)
        {
            llSetTimerEvent(0.0);
            llSetText("Tipjar idle. Please click me to activate.", <1.0,1.0,1.0>, 1.0);
            return;
        }
        llSetTimerEvent(5.0);
    }
 
    dataserver(key id, string data)
    {
        if(id != sQuery) return;
        if(data == EOF)
        {
            readNotecard = 1;
            return;
        }
        if(data == "") jump next_line;
        list accessParse = llParseString2List(data, ["#"], [""]);
        accessListNames += llList2String(accessParse, 0);
        accessListKeys += llList2Key(accessParse, 1);
@next_line;
        sQuery = llGetNotecardLine("Tipjar Access", ++sLine);
    }
 
    touch_start(integer num)
    {
        if(~llListFindList(accessListKeys, (list)llDetectedKey(0)))
        {
            activeAvatarName = llDetectedName(0);
            activeAvatarKey = llDetectedKey(0);
            state init;
        }
    }
}
 
state init
{
    state_entry()
    {
        allSpoils = 0;
        tippers = [];
        tipperAmounts = [];
        spoilMemberNames = [];
        spoilMemberKeys = [];
        spoilPercents = [];
        readNotecard = 0;
        sLine = 0;
        llSetText("Tipjar initalizing, please wait...", <1.0,1.0,1.0>, 1.0);
        integer itra;
        for(itra = 0; itra < llGetInventoryNumber(INVENTORY_NOTECARD); ++itra)
        {
            if(llGetInventoryName(INVENTORY_NOTECARD, itra) == "Tipjar Spoils")
                jump found_spoils;
        }
        llSetText("Falied! Please check Tipjar Spoils notecard.", <1.0,1.0,1.0>, 1.0);
        llInstantMessage(llGetOwner(), "Failed to find Tipjar Spoils card. Please add a notecard called Tipjar Spoils and configure it apropriately.");
        return;
@found_spoils;
        sQuery = llGetNotecardLine("Tipjar Spoils", sLine);
        landingPoint = llGetPos();
        landingRotation = llGetRot();
        llSetTimerEvent(5.0);
    }
 
    changed(integer change)
    {
        if(change & CHANGED_INVENTORY)
            llResetScript();
    }
 
    timer()
    {
        if(readNotecard)
        {
            llSetTimerEvent(0.0);
            integer itra;
            integer percents = 0;
            for(itra = 0; itra < llGetListLength(spoilPercents); ++itra)
            {
                percents += llList2Integer(spoilPercents, itra);
            }
            if(percents > 100)
            {
                llOwnerSay("The percents in your Tipjar Spoils notecard add up to " + (string)percents + "%. They should add up to 100%. Please check your setup again.");
                state default;
            }
            llRequestPermissions(llGetOwner(), PERMISSION_DEBIT);
            return;
        }
        llSetTimerEvent(5.0);
    }
 
    listen(integer channel, string name, key id, string message)
    {
        if(id != activeAvatarKey) return;
 
        if(message == "[ Confirm ]")
        {
            llListenRemove(comHandleSpoiler);
            state tipjar;
        }
        llResetScript();
    }
 
    dataserver(key id, string data)
    {
        if(id != sQuery) return;
        if(data == EOF)
        {
            readNotecard = 1;
            return;
        }
        if(data == "") jump next_line;
        list spoilList = llParseString2List(data, ["#"], [""]);
        if(EXCLUDE_ACCESS_FROM_SPOILS && ~llListFindList(accessListNames, (list)llList2String(spoilList, 0))) jump next_line;
        spoilMemberNames += llList2String(spoilList, 0);
        spoilMemberKeys += llList2Key(spoilList, 1);
        spoilPercents += llList2String(spoilList, 2);
@next_line;
        sQuery = llGetNotecardLine("Tipjar Spoils", ++sLine);
    }
 
    run_time_permissions(integer perm)
    {
        if(perm & PERMISSION_DEBIT)
        {
            integer comChannel = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
            comHandleSpoiler = llListen(comChannel, "", activeAvatarKey, "");
            integer itra;
            string confirmText;
            for(itra = 0; itra < llGetListLength(spoilMemberNames); ++itra)
            {
                confirmText += llList2String(spoilMemberNames, itra) + " gets " + llList2String(spoilPercents, itra) + "%.\n";
            }
            confirmText += "\n\n";
            llDialog(activeAvatarKey, "Tipjar: Please revise and confirm the spoils distribution:\n\n" + confirmText, [ "[ Confirm ]", "[ Reject ]" ], comChannel);
        }
    }
 
}
 
state tipjar
{
    state_entry()
    {
        llOwnerSay("Tipjar initialized and ready to be tipped. Good Luck " + activeAvatarName + "!");
        llSetPayPrice(100, PAY_BUTTONS);
        llSetText(tokenSubstitute(OVERHEAD_MESSAGE, activeAvatarKey), <1.0,1.0,1.0>, 1.0);
        if(TIPJAR_ROAMING) llSensorRepeat("", "", AGENT, ROAM_RANGE, PI, ROAM_INTERVAL);
    }
 
    sensor (integer num)
    {
        if(roaming) return;
        roaming = 1;
        integer itra;
        for(itra = 0, avPositions = [], positionRoam = 0; itra < num; ++itra)
        {
            avPositions += llDetectedPos(itra);
        }
        avPositions += landingPoint;
        physics(TRUE);
        moveTo(llList2Vector(avPositions, positionRoam++));
    }
 
    at_target(integer tnum, vector targetpos, vector ourpos)
    {
        if(tnum != targetID) return;
        if(positionRoam == llGetListLength(avPositions))
        {
            physics(FALSE);
            llSetPos(landingPoint);
            llSetRot(landingRotation);
            roaming = 0;
            return;
        }
        moveTo(llList2Vector(avPositions, positionRoam++));
    }
 
    touch_start(integer num)
    {
        key id = llDetectedKey(0);
 
        integer comChannel;
        if(~llListFindList(spoilMemberKeys, (list)id) || id == activeAvatarKey)
        {
            jump spoiler_touch;
        }
        llInstantMessage(id, tokenSubstitute(TIPPER_TOUCH_MESSAGE, id));
        comChannel = ((integer)("0x"+llGetSubString((string)llGetKey(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comHandleTiper = llListen(comChannel, "", id, "");
        llDialog(id, tokenSubstitute(TIPPER_DIALOG_MESSAGE, id), TIPPER_TOUCH_MENU, comChannel);
        return;
@spoiler_touch;
        comChannel = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comHandleSpoiler = llListen(comChannel, "", id, "");
        llDialog(id, "Tipjar: Please choose an option:\n", [ "◆ LogOut ◆", "◆ Tipers ◆", "◆ Tops ◆", "◆ Total ◆" ], comChannel);
    }
 
    listen(integer channel, string name, key id, string message)
    {
        if(~llListFindList(spoilMemberNames, (list)name) || name == activeAvatarName)
        {
            jump spoiler_com;
        }
        if(message == "◆ Join ◆")
        {
            llInstantMessage(id, tokenSubstitute(INVITE_GROUP_MESSAGE, id) + "\n secondlife:///app/group/" + (string)INVITE_GROUP_KEY + "/about");
        }
        if(message == "◆ Gift ◆")
        {
            integer itra;
            list gifts;
            for(itra = 0; itra < llGetInventoryNumber(INVENTORY_OBJECT); ++itra)
            {
                gifts += llGetInventoryName(INVENTORY_OBJECT, itra);
            }
            for(itra = 0; itra < llGetListLength(gifts); ++itra)
            {
                llGiveInventory(id, llList2String(gifts, itra));
            }
        }
        llListenRemove(comHandleTiper);
        return;
@spoiler_com;
        if(message == "◆ LogOut ◆")
        {
            llListenRemove(comHandleTiper);
            llListenRemove(comHandleSpoiler);
            if(TIPJAR_ROAMING) llSensorRemove();
            if(TIPJAR_ROAMING && roaming)
            {
                physics(FALSE);
                state gohome;
            }
            state payments;
        }
        if(message == "◆ Tipers ◆")
        {
            integer itra;
            llInstantMessage(id, "---------- BEGIN TIPPERS ----------");
            for(itra = 0; itra < llGetListLength(tippers); ++itra)
            {
                llInstantMessage(id, llKey2Name(llList2Key(tippers, itra)) + " has tipped you: l$" + llList2String(tipperAmounts, itra));
            }
            llInstantMessage(id, "----------- END TIPPERS -----------");
        }
        if(message == "◆ Tops ◆")
        {
            integer itra;
            llInstantMessage(id, "---------- BEGIN TOP TIPPERS ----------");
            integer topNum;
            for(itra = 0; itra < llGetListLength(tippers); ++itra)
            {
                if(itra == 3)
                {
                    jump end_tippers;
                }
                integer tip = llList2Integer(llListSort(tipperAmounts, 1, 0), itra);
                llInstantMessage(id, llKey2Name(llList2Key(tippers, llListFindList(tippers, (list)tip))) + " has tipped you: l$" + (string)tip);
            }
@end_tippers;
            llInstantMessage(id, "----------- END TOP TIPPERS -----------");
        }
        if(message == "◆ Total ◆")
        {
            llInstantMessage(id, "So far, " + activeAvatarName + " has made l$" + (string)allSpoils);
        }
        llListenRemove(comHandleSpoiler);
    }
 
    money(key id, integer amount)
    {
        if(~llListFindList(tippers, (list)id))
        {
            integer tip = amount + llList2Integer(tipperAmounts, llListFindList(tippers, (list)id));
            tipperAmounts = llListReplaceList(tipperAmounts, (list)tip, llListFindList(tippers, (list)id), llListFindList(tippers, (list)id));
            jump tippers_updated;
        }
        tippers += id;
        tipperAmounts += amount;
@tippers_updated;
        allSpoils += amount;
        llShout(PUBLIC_CHANNEL, tokenSubstitute(THANKS_MESSAGE, id));
        llInstantMessage(activeAvatarKey, llKey2Name(id) + " has just tipped you: l$" + (string)amount + ".");
    }
}
 
state gohome
{
    state_entry()
    {
        llSetText("Please wait, returning home...", <1.0,1.0,1.0>, 1.0);
        physics(TRUE);
        moveTo(landingPoint);
    }
 
    at_target(integer tnum, vector targetpos, vector ourpos)
    {
        if(tnum != targetID) return;
        physics(FALSE);
        llSetPos(landingPoint);
        llSetRot(landingRotation);
        state payments;
    }
}
 
state payments
{
    state_entry()
    {
        llSetText("Loging out...", <1.0,1.0,1.0>, 1.0);
        physics(FALSE);
        integer remSpoils = allSpoils;
        integer itra;
        for(itra = 0; itra < llGetListLength(spoilMemberKeys) && remSpoils; ++itra)
        {
            llInstantMessage(llList2Key(spoilMemberKeys, itra), tokenSubstitute(LOGOUT_MESSAGE, llList2Key(spoilMemberKeys, itra)));
            integer share = (integer)((llList2Float(spoilPercents, itra)/100.0) * (float)allSpoils);
            if(share) llGiveMoney(llList2Key(spoilMemberKeys, itra), share);
            remSpoils -= (integer)((llList2Float(spoilPercents, itra)/100.0) * (float)allSpoils);
        }
        state default;
    }
}