Table of Contents

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:

And an uncommon and personal one:

Setup

<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:

Morgan LeFay#1ad33407-a792-416d-a5e3-06007c0802bf
William Riker#82c73d6d-facf-4322-b17b-06d8cd66a116
<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
Frank Black#82c73d7d-facf-4332-b17b-06d8cd66a116#25

That means, that after a tipjar user, either Morgan LeFay or William Riker logout, the tipjar will distribute 75% of all profits to Tasha Yar and 25% to Frank Black.

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:

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.

Code

tipjar.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2011 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, 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, .8);
    llLookAt(position, .6, .6);
    llMoveToTarget(position, 3);    
}
 
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 = NULL_KEY;
 
key sQuery = NULL_KEY;
integer sLine = 0;
integer readNotecard = 0;
integer comHandleTiper = 0;
integer comHandleSpoiler = 0;
 
integer allSpoils = 0;
 
vector landingPoint;
rotation landingRotation;
list avPositions = [];
integer positionRoam = 0;
integer targetID = 0;
integer roaming = 0;
 
default
{
    state_entry() {
        physics(FALSE);
        accessListNames = [];
        accessListKeys = [];
        sLine = 0;
        readNotecard = 0;
        activeAvatarName = "";
        activeAvatarKey = NULL_KEY;
        llSetText("Tipjar loading access, please wait...", <1,1,1>, 1);
        if(llGetInventoryType("Tipjar Access") == INVENTORY_NOTECARD) jump found_access;
        llSetText("No access list. Please revise your configuration.", <1,1,1>, 1);
        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);
            llSetText("Tipjar idle. Please click me to activate.", <1,1,1>, 1);
            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,1,1>, 1);
        if(llGetInventoryType("Tipjar Spoils") == INVENTORY_NOTECARD) jump found_spoils;
        llSetText("Falied! Please check Tipjar Spoils notecard.", <1,1,1>, 1);
        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);
            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,1,1>, 1);
        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) {
        integer comChannel;
        if(~llListFindList(spoilMemberKeys, (list)llDetectedKey(0)) || llDetectedKey(0) == activeAvatarKey) jump spoiler_touch;
        llInstantMessage(llDetectedKey(0), tokenSubstitute(TIPPER_TOUCH_MESSAGE, llDetectedKey(0)));
        comChannel = ((integer)("0x"+llGetSubString((string)llGetKey(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comHandleTiper = llListen(comChannel, "", llDetectedKey(0), "");
        llDialog(llDetectedKey(0), tokenSubstitute(TIPPER_DIALOG_MESSAGE, llDetectedKey(0)), TIPPER_TOUCH_MENU, comChannel);
        return;
@spoiler_touch;
        comChannel = ((integer)("0x"+llGetSubString((string)llGetOwner(),-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
        comHandleSpoiler = llListen(comChannel, "", llDetectedKey(0), "");
        llDialog(llDetectedKey(0), "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(0, 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,1,1>, 1);
        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,1,1>, 1);
        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;        
    }
}