Table of Contents

About

This variation adds the capability for donation jars placed on the same simulator to synchronise the donation amounts and user-data.

Screenshot

Configuration

The script uses a notecard named configuration that must be placed in the same primitive as the script:

#### START CONFIGURATION ####

# the title to show in hoover-text above
# the progress bar and the statistics.
title = "Synchronized Donation Jar"

# this is the goal you want to attain in L$
goal = 500

# this is an LSL color vector which will 
# be used for the overhead text
color = <0.5, 0.5, 1>

# this is a list of pay buttons displayed 
# when an avatar click the donation jar
buttons = 10 ,100, 500, 1000

# the synchronization password must
# is the shared secret between all the
# donation jars that will synchronize.
password = "feelin'blu!"

##### END CONFIGURATON ######

Code

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2021 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasProgress(integer percent, integer length, list symbols) {
    percent /= (integer)((float)100.0/(length));
    string p = llList2String(symbols,0);
    integer itra = 0;
    do {
        if(itra>percent-1) p += llList2String(symbols,2);
        else p += llList2String(symbols,1);
    } while(++itra<length);
    return p + llList2String(symbols,3);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string k, string v, string data) {
    if(llStringLength(data) == 0) return k + "=" + v;
    if(llStringLength(k) == 0) return "";
    if(llStringLength(v) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llListReplaceList(
            llParseString2List(data, ["&"], []), 
            [ k + "=" + v ], 
        i, i), 
    "&");
    return data + "&" + k + "=" + v;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueEncode(list data) {
    list k = llList2ListStrided(data, 0, -1, 2);
    list v = llList2ListStrided(llDeleteSubList(data, 0, 0), 0, -1, 2);
    data = [];
    do {
        data += llList2String(k, 0) + "=" + llList2String(v, 0);
        k = llDeleteSubList(k, 0, 0);
        v = llDeleteSubList(v, 0, 0);
    } while(llGetListLength(k) != 0);
    return llDumpList2String(data, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueDelete(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llDeleteSubList(
            llParseString2List(data, ["&"], []),
        i, i), 
    "&");
    return data;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasCompoundToList(integer T, list compound) {
    if(llGetListLength(compound) == 0) return [];
    if(T == TYPE_FLOAT) return llList2Float(compound, 0) + 
        wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
    if(T == TYPE_INTEGER) return llList2Integer(compound, 0) + 
        wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
    if(T == TYPE_STRING) return llList2String(compound, 0) + 
        wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
    if(T == TYPE_KEY) return llList2Key(compound, 0) + 
        wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
    if(T == TYPE_VECTOR) return llList2Vector(compound, 0) + 
        wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
    if(T == TYPE_ROTATION) return llList2Rot(compound, 0) + 
        wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
    return wasCompoundToList(T, llDeleteSubList(compound, 0, 0));
}
 
integer line = 0;
list settings = [];
integer pipe = 0;
 
default {
    state_entry() {
        // generate synchronization channel
        pipe = (integer)("0x8" + llGetSubString(llGetOwner(), 0, 6));
        if(llGetInventoryType(llGetInventoryName(INVENTORY_NOTECARD, 0)) != INVENTORY_NOTECARD) {
            llSay(DEBUG_CHANNEL, "Sorry, could not find an inventory notecard.");
            return;
        }
        llGetNotecardLine(
            llGetInventoryName(
                INVENTORY_NOTECARD, 
                0
            ), 
            line
        );
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            // invariant, length(tuples) % 2 == 0
            if(llGetListLength(settings) % 2 == 0) state update;
            llSay(DEBUG_CHANNEL, "Error in configuration notecard.");
            return;
        } 
        if(data == "") {
            llGetNotecardLine(
                llGetInventoryName(
                    INVENTORY_NOTECARD, 
                    0
                ), 
                ++line
            );
            return;
        }
        integer i = llSubStringIndex(data, "#");
        if(i != -1) data = llDeleteSubString(data, i, -1);
        list o = llParseString2List(data, ["="], []);
        // get rid of starting and ending quotes
        string k = llDumpList2String(
            llParseString2List(
                llStringTrim(
                    llList2String(
                        o, 
                        0
                    ), 
                STRING_TRIM), 
            ["\""], []
        ), "\"");
        string v = llDumpList2String(
            llParseString2List(
                llStringTrim(
                    llList2String(
                        o, 
                        1
                    ), 
                STRING_TRIM), 
            ["\""], []
        ), "\"");
        if(k == "" || v == "") {
            llGetNotecardLine(
                llGetInventoryName(
                    INVENTORY_NOTECARD, 
                    0
                ), 
                ++line
            );
            return;
        }
        settings += k;
        settings += v;
        llGetNotecardLine(
            llGetInventoryName(
                INVENTORY_NOTECARD, 
                0
            ), 
            ++line
        );
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
}
 
state donations {
    state_entry() {
        // Listen on the synchronization channels.
        llListen(pipe, "", "", "");
        // Set the pay buttons and click action.
        llSetPayPrice(100, 
            wasCompoundToList(TYPE_INTEGER, 
                llParseString2List(
                    wasKeyValueGet("buttons", 
                        wasKeyValueEncode(settings)
                    ), 
                [",", " "], [])
            )
        );
        llSetClickAction(CLICK_ACTION_PAY);
    }
    listen(integer channel, string name, key id, string message) {
        string password = wasKeyValueGet("password", message);
        // no password was sent bail - are you tickling my channels?
        if(password == "") return;
        // bail if the password does not match our settings
        if(password != wasKeyValueGet("password", wasKeyValueEncode(settings))) {
            return;
        }
        // overload message with the password filtered
        message = wasKeyValueDelete("password", message);
        // this should not happen at this point
        if(message == "") return;
        // and commit the rest
        llSetObjectDesc(message);
        // switch to update
        state update;
    }
    money(key id, integer amount) {
        llSetObjectDesc(
            wasKeyValueSet(
                "lastname", 
                llList2String(llParseString2List(llKey2Name(id), [" "], [""]), 0), 
                llGetObjectDesc()
            )
        );
        llSetObjectDesc(
            wasKeyValueSet(
                "lastamount", 
                (string)amount, 
                llGetObjectDesc()
            )
        );
        llShout(0, "Thank you, " + llKey2Name(id) + " for your generous donation!");
        llSetObjectDesc(
            wasKeyValueSet("donated", 
                (string)((integer)wasKeyValueGet(
                    "donated", 
                    llGetObjectDesc()
                )+amount),
                llGetObjectDesc()
            )
        );
        // Overload the description with the password from the
        // settings and then pass the message through the pipe.
        llRegionSay(
            pipe, 
            wasKeyValueSet(
                "password", 
                wasKeyValueGet(
                    "password", 
                    wasKeyValueEncode(settings)
                ), 
                llGetObjectDesc()
            )
        );
        state update;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
}
 
state update {
    state_entry() {
        // Update the text with the current progress.
        integer percent = 100*(integer)wasKeyValueGet(
            "donated", llGetObjectDesc()
        )/(integer)wasKeyValueGet("goal", wasKeyValueEncode(settings));
        llSetText(
            wasKeyValueGet("title", 
                wasKeyValueEncode(settings)
            ) + "\nGoal: " + 
            wasProgress(percent, 5, ["[", "█", "░", "]"]) + 
            " %\n" + 
            (string)percent + "% of L$" + wasKeyValueGet("goal", wasKeyValueEncode(settings)), 
        (vector)wasKeyValueGet("color", wasKeyValueEncode(settings)), 1);
        // If the goal hasn't been reached, jump to the doner section.
        if(percent < 100) {
            // If somebody tipped and this is not a script restart,
            // then get the display text and add a thank you message 
            // for the last tipper. We grab just the first name of 
            // the tipper to make the donation jar more personal.
            string lastname = wasKeyValueGet("lastname", llGetObjectDesc());
            integer lastamount = (integer)wasKeyValueGet("lastamount", llGetObjectDesc());
            if(lastname == "") {
                // Go back to accepting donations.
                state donations;
            }
            if(lastamount == 0) {
                // Go back to accepting donations.
                state donations;
            }
            llSetText(llList2String(
                llGetPrimitiveParams(
                    [ PRIM_TEXT ]
                ), 0) + 
                "\nLast donation: " +
                "L$" +  
                (string)lastamount + 
                " from " + lastname + 
                "!" + 
                "\nTotal donations: " +
                "L$" + 
                wasKeyValueGet(
                    "donated", llGetObjectDesc()
                ), 
            (vector)wasKeyValueGet("color", wasKeyValueEncode(settings)), 1);
            state donations;
        }
        // If the goal has been attained or overstepped,
        // let the owner know, schedule a reset and return.
        llInstantMessage(llGetOwner(), "Donation goal attained at " + llGetRegionName() + "!");
        llSetObjectDesc(
            wasKeyValueSet("donated", 
                (string)0,
                llGetObjectDesc()
            )
        );
        llSetObjectDesc(
            wasKeyValueDelete(
                "lastname", 
                llGetObjectDesc()
            )
        );
        llSetObjectDesc(
            wasKeyValueDelete(
                "lastamount", 
                llGetObjectDesc()
            )
        );
        llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        llResetScript();
    }
}