The Stash Bank
LSL Portal | Functions | Events | Types | Operators | Constants | Flow Control | Script Library | Categorized Library | Tutorials |
Created by Kira Komarov.
ChangeLog
- 22-November-2011
Done. The script now should send money properly as well. Cleaned up the tips.
- 21-November-2011
llKey2Name() obviously fails when you click [ Send ] if the avatar keys in the BOOKMARK list are not all in the current region. Will be fixed soon...
About
From experience, I know (and so you should find out if you do not know), that it is most wise to create a secondary account to hold all your money. Running about in Second Life with all your cash on you is a bad idea. However, creating a secondary account is a bit cumbersome when it comes to sending money. If you run out, you will have to log-in the secondary account and transfer the money. Thanks to the feature of not have a log-out option in all the viewers, that either means you run two viewers at the same time (which is not possible on all machines), or you hotswap your accounts. This is not very convenient.
For that, I have created a bank that would allow you to link a secondary account to a primitive in world containing a script. You add yourself (hard-coded) to a list of people able to retrieve money, you grant the script permissions, you set a limit and then every time you want some money from your stash, you visit your primitive and retrieve the money.
There are also some tricks used in this script which might be interesting to developers.
Features
- Set an upper limit on how much to retrieve.
- Logs all the people who take money from the stash and pay money into the stash.
- Withdraw money.
- Deposit money.
- Show the current total holdings.
- Send money to a list of bookmarked avatars.
Setup
These steps have to be done with your alt:
- Chose a wise place to place your primitive. Visible, inconspicuous and somewhere where it will not be returned.
- Create a new primitive and drop the script below inside it.
- Edit the script and configure the parameters in the configuration section:
list STASH_USERS = [ "5552f71f-60c9-4564-b861-55b200e12176" ];
This is a list of keys of the avatars that will be able to retrieve money from the stash. You should change this to the avatar key of your main account, for example. Or add other avatars if you wish to share the stash.
list SEND_BOOKMARKS = [ "5552f71f-60c9-4564-b861-55b200e12176", "a2e76fcd-9360-4f6d-a924-000000000003" ];
This is a list of keys to whom somebody in the STASH_USERS list will be able to send money to. For example, this could be your landlord.
- Touch the primitive to start the set-up procedure (text will be displayed above the primitive to guide you).
- You will be prompted to allow taking / depositing money. Accept.
- Now it will ask you to pay a certain amount. Triple check that you are the owner and the creator of the primitive! If you are, then if you pay money into your own object, that money will go back to you. Why do we do this? By doing this we are setting the upper limit (explained more in developers section) of money that the avatars in your STASH_USERS will be able to retrieve. The script measures how much you paid in, hands you the money back and stores that amount as the upper limit the stash will contain.
- Now the script will wait for a few seconds and go to a run state, discarding the text message above to appear inconspicuous.
These steps may be done by your alt and anybody in your STASH_USERS list:
- You simply touch the object and follow the menus.
Tricks used in this Script (Developers)
There are a number of problems that I got around while writing this script:
- There is no way to get the amount of L$ your avatar has on itself.
One way around that, is to pay an amount into an object and count the money using money(). Not only will the script now be aware of the money, but you can thereby implicitly set a limit. Neat!
- Dang! I already used up the timer() event.
Use llSensorRepeat() with some ridiculous parameters (initially I was searching for Philip Linden's key in a 0.1 range) and a repeat time of your desired time. Since the llSensorRepeat() is crafted to fail and trigger no_sensor() all the time it runs, you can use no_sensor() as your timer() equivalent. In fact with llSensorRemove() you can have your llSetTimerEvent(0) call. However, you should keep in mind that different from llSetTimerEvent(), the llSensorRepeat() call immediately proceeds to scan which would make the no_sensor() event fire right away before it is rescheduled.
To get around that problem, just use some global variable that will make no_sensor() return just on the first entry. Something like this might be appropriate:
integer flag;
default
{
state_entry() {
flag = 1;
llSensorRepeat("", NULL_KEY, AGENT, 0.1, 0.1, 60);
}
no_sensor() {
if(flag) { // <--- tests flag, which will be 1 for the first run and then brings it back to 0, making it fail on the next entry of no_sensor().
--flag;
return;
}
// this area will be hit 60 seconds after the llSensorRepeat() has been executed.
}
}
Here is a equivalence table (NUMBER is the number of seconds, replace with a value):
llSetTimerEvent(NUMBER) <=> llSensorRepeat("", NULL_KEY, AGENT, 0.1, 0.1, NUMBER) timer() <=> no_sensor() llSetTimerEvent(0) <=> llSensorRemove()
again, keeping in mind that llSensorRepeat() will immediately trigger no_sensor().
Now you have freed up the timer() event, effectively having two timers. Neat!
- Heap protection. Adding and adding stuff to lists will eventually make the stack collide with the heap.
Ideally, you could measure the amount of free memory and flush those lists when it becomes critically low. Since we are unable to do that reliably, you can just flush them after a certain period of time and size. For example, this script will check every 60 seconds if the DONORS and RETRIEVERS lists are over 25 elements and flush them if they are.
This should be kept in mind for logging scripts which continuously add to the heap. Eventually, if there is no upper limit on the data, the script will crash.
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 //
//////////////////////////////////////////////////////////
// //
// You can add to this list, the keys of the avatars
// that will be able to retrieve money from the stash.
// Please change these to something... Sensible.
list STASH_USERS = [ "5552f71f-60c9-4564-b861-55b200e12176" ];
// Add all the avatar keys you wish to send money to.
// This is even called "bookmarks" suggestively because
// it will contain the people whom you frequently pay.
list SEND_BOOKMARKS = [ "5552f71f-60c9-4564-b861-55b200e12176", "a2e76fcd-9360-4f6d-a924-000000000003" ];
// //
// END CONFIGURATION //
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// INTERNALS //
//////////////////////////////////////////////////////////
list bookmarks2name = [];
integer current_holdings = 0;
integer comHandle = 0;
integer comChannel = 0;
key cUser = NULL_KEY;
key sendToAv = NULL_KEY;
list DONORS = [];
list DONORS_AMOUNTS = [];
list RETRIEVERS = [];
list RETRIEVERS_AMOUNTS = [];
list menu_items = [];
list cList = [];
integer mitra = 0;
list mFwd() {
if(mitra+1>llGetListLength(menu_items)) return cList;
cList = llListInsertList(llListInsertList(llList2List(menu_items, ++mitra, (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
return cList;
}
list mBwd() {
if(mitra-19<0) return cList;
cList = llListInsertList(llListInsertList(llList2List(menu_items, (mitra-=19), (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
return cList;
}
init() {
if(llSubStringIndex(llDumpList2String(STASH_USERS, " "), llGetOwner()) == -1) {
STASH_USERS += llGetOwner();
}
cUser = NULL_KEY;
current_holdings = 0;
llSetText(llKey2Name(llGetOwnerKey(llGetKey())) + "\nPlease touch the primitive to configure.", <1,1,1>, 1.0);
}
default
{
state_entry() {
init();
}
on_rez(integer param) {
init();
}
touch_start(integer total_number) {
if(llDetectedKey(0) != llGetOwner()) return;
sendToAv = llRequestAgentData(llList2Key(SEND_BOOKMARKS, current_holdings++), DATA_NAME);
}
dataserver(key queryid, string data) {
if(queryid == sendToAv) {
bookmarks2name += data;
}
if(llGetListLength(bookmarks2name) == llGetListLength(SEND_BOOKMARKS)) {
current_holdings = 0;
llRequestPermissions(llGetOwner(), PERMISSION_DEBIT);
return;
}
sendToAv = llRequestAgentData(llList2Key(SEND_BOOKMARKS, current_holdings++), DATA_NAME);
}
run_time_permissions(integer perm) {
if(perm & PERMISSION_DEBIT)
state configure;
}
}
state configure
{
state_entry() {
llSetText(llKey2Name(llGetOwnerKey(llGetKey())) + " please make sure that this primitive and the script belongs to you.\n Then, pay all the money you wish this stash to hold.", <1,1,1>, 1.0);
}
money(key id, integer amount) {
if(id == llGetOwner()) {
current_holdings = amount;
llSetText(llKey2Name(llGetOwnerKey(llGetKey())) + " those will be your current holdings.\nSwitching to running state in 10 seconds.\nPlease wait...", <1,1,1>, 1.0);
llSetTimerEvent(10);
}
}
timer() {
llSetTimerEvent(0);
llSetText("", <1,1,1>, 1.0);
state stash;
}
}
state stash
{
state_entry() {
llSensorRepeat("", NULL_KEY, AGENT, 0.1, 0.1, 60);
}
no_sensor() {
if(llGetListLength(DONORS) > 25) {
DONORS = [];
DONORS_AMOUNTS = [];
}
if(llGetListLength(RETRIEVERS) > 25) {
RETRIEVERS = [];
RETRIEVERS_AMOUNTS = [];
}
}
touch_start(integer total_number) {
if(llSubStringIndex(llDumpList2String(STASH_USERS, " "), llDetectedKey(0)) == -1) return;
cUser = llDetectedKey(0);
comChannel = ((integer)("0x"+llGetSubString((string)cUser,-8,-1)) & 0x3FFFFFFF) ^ 0xBFFFFFFF;
llSetTimerEvent(10);
comHandle = llListen(comChannel, "", cUser, "");
llDialog(cUser, "Please select a transaction:\n ", [ "[ Take ]", "[ Reset ]", "[ Holdings ]", "[ Log ]", "[ Send ]" ], comChannel);
}
timer() {
llSetTimerEvent(0);
llInstantMessage(cUser, "Menu timeout.");
llListenRemove(comHandle);
}
listen(integer channel, string name, key id, string message) {
if(cUser != id && llSubStringIndex(llDumpList2String(STASH_USERS, " "), id) == -1) return;
if(message == "[ Take ]") {
llSetTimerEvent(10);
llListenRemove(comHandle);
comHandle = llListen(comChannel+1, "", id, "");
llTextBox(id, "\nThis stash currently holds: L$" + (string)current_holdings + "\n\nPlease enter an amount that you would like to retrieve from this stash.", comChannel+1);
return;
}
if(message == "[ Send ]") {
llSetTimerEvent(10);
llListenRemove(comHandle);
comHandle = llListen(comChannel+2, "", id, "");
integer itra;
for(itra=0, mitra=0, menu_items=[]; itra<llGetListLength(bookmarks2name); ++itra) {
menu_items += llList2String(bookmarks2name, itra);
}
cList = llListInsertList(llListInsertList(llList2List(menu_items, mitra, (mitra+=9)), ["<= Back"], 0), ["Next =>"], 2);
llDialog(id, "\nPlease select the name of the avatar to send money to:\n", cList, comChannel+2);
return;
}
if(channel == comChannel+2) {
llSetTimerEvent(10);
llListenRemove(comHandle);
sendToAv = llList2Key(SEND_BOOKMARKS, llListFindList(bookmarks2name, (list)message));
comHandle = llListen(comChannel+3, "", id, "");
llTextBox(id, "\nThe money will be sent to: " + message + "\nIf this is corrent, please type an amount of money that should be sent.", comChannel+3);
return;
}
if(channel == comChannel+3 && current_holdings - (integer)message >= 0 && (integer)message != 0) {
llSetTimerEvent(0);
llListenRemove(comHandle);
llGiveMoney(sendToAv, (integer)message);
RETRIEVERS += llKey2Name(id);
RETRIEVERS_AMOUNTS += (integer)message;
current_holdings -= (integer)message;
return;
}
if(message == "[ Holdings ]") {
llSetTimerEvent(0);
llListenRemove(comHandle);
llInstantMessage(cUser, "Current holdings: " + (string)current_holdings);
return;
}
if(id == llGetOwner() && message == "[ Reset ]") {
llSetTimerEvent(0);
llListenRemove(comHandle);
state default;
return;
}
if(id == llGetOwner() && message == "[ Log ]") {
llSetTimerEvent(0);
llListenRemove(comHandle);
integer itra;
llSleep(llGetRegionTimeDilation());
llOwnerSay("--------------- START RETRIEVERS ---------------");
for(itra=0; itra<llGetListLength(RETRIEVERS); ++itra) {
llOwnerSay(llList2String(RETRIEVERS, itra) + " took L$" + llList2String(RETRIEVERS_AMOUNTS, itra));
llSleep(llGetRegionTimeDilation());
}
llOwnerSay("---------------- END RETRIEVERS ----------------");
llSleep(llGetRegionTimeDilation());
llOwnerSay("--------------- START DONORS ---------------");
for(itra=0; itra<llGetListLength(DONORS); ++itra) {
llOwnerSay(llList2String(DONORS, itra) + " paid L$" + llList2String(DONORS_AMOUNTS, itra));
llSleep(llGetRegionTimeDilation());
}
llOwnerSay("---------------- END DONORS ----------------");
return;
}
if(channel == comChannel+1 && current_holdings - (integer)message >= 0 && (integer)message != 0) {
llSetTimerEvent(0);
llListenRemove(comHandle);
llGiveMoney(id, (integer)message);
RETRIEVERS += llKey2Name(id);
RETRIEVERS_AMOUNTS += (integer)message;
current_holdings -= (integer)message;
return;
}
}
money(key id, integer amount) {
current_holdings += amount;
DONORS += llKey2Name(id);
DONORS_AMOUNTS += amount;
}
}