User:Yumi Murakami/Golden Vendor

From Second Life Wiki
Jump to navigation Jump to search

This is a vendor which has an additional function: an avie can click on the vendor frame and pay a small fee to "claim" the vendor. For a certain period of time, they then get commission on all sales. The idea was to combine a vendor and a game by allowing people to earn L$ by selling a product to others. Didn't take off very much, but I know some people still liked it, in part because of my super paranoid validation that the vendor is set up properly (largely because I was burned by badly set ones several times myself..).

This is in two parts: the vendor and the frame. The frame needs to be a separate object near the vendor, but not linked. Originally the frame was just that - a frame - and the vendor was just a box, but nothing in the script really requires that.

Original Documentation Notecard


WHAT IS THE GOLDEN VENDOR? The Golden vendor is a standard single item vendor with an added special feature. Any person who wants to can pay a certain amount of money to "claim" the vendor for a certain period of time. During that period of time, every time the vendor makes a sale, they get a share of money from the sale. The idea is that during that time they'll be encouraged to help attract people's attention to your vendor, and sell your product to others. And if they don't manage to do that, then the shares they earn won't repay the amount they paid to claim the vendor anyway - so you make extra money either way!

HOW TO SET UP THE GOLDEN VENDOR In order to overcome some awkward restrictions on Second Life's user interface, the Golden Vendor is provided as TWO unlinked objects, the vendor itself and the golden frame. DO NOT LINK THESE OBJECTS TOGETHER. You should always keep these two near to each other when using the vendor. All of the actual important script and configuration is stored in the vendor part - so if the frame happens to come apart from the vendor, you can rez another one (a "spare frame" object is included); then pick up the vendor and drop it again (to reset it) and connect it to the new frame.

To set up the vendor: 1 - Rez the vendor and place it where you want it. (It may complain about having nothing to sell - ignore it for the moment.) 2 - Place the item you want to sell into the vendor. Also, texture the vendor with an appropriate texture (you do this in the same way you would texture any other prim) 3 - Edit the configuration notecard in the vendor to describe how you want it to behave. There's help for doing this in the notecard, and you can also read the information below. 4 - Touch the vendor panel itself to reset it. 5 - When prompted to do so, touch the vendor's frame (this establishes a secure connection between the two, ensuring that no other object can pretend to be the vendor or frame) 6 - Grant debit permissions to the vendor when asked (this enables it to pay change and shares) 7 - You're all done! :)

SETTING UP THE NOTECARD Each line of the notecard that starts with "//" is a comment and is ignored. The notecard provided has a lot of comments in it to help you set up, but it will speed up the startup of the vendor if you remove the comments once you're done with them. Once the vendor is running the notecard has already been read and its speed from that point onward is not affected by the comments in the notecard.

Every line that isn't a comment consists of a first word (the command) and the rest of the line (the data). The command tells the vendor what the data represents. The commands are:

 item - name of the item to be sold.  The item must be placed in the vendor.   Must be case-exact.
 itemPrice - price of the item in L$.
 notecard - name of the information notecard for the item.  This is given to customers when they click on the vendor.  This isn't
   mandatory but is highly recommended as customers value them a lot.
 
 partnerKey, partnerSplit - if you have business partners who you want to take a share of the money from the vendor, you can 
   specify them here.  partnerKey and partnerSplit must always be used together; you can use as many pairs as you like, one 
   for each partner.  partnerKey specifies the key or UUID of the partner (a unique serial code which identifies them to scripts in 
   Second Life - to find this, drop the "Key Getter" object in the world and ask your partner to click on it), and partnerSplit 
   specifies the share they recieve AS A PERCENTAGE.  Partners recieve a share of money paid for sales, but NOT of claim costs -
   this is to protect you from losing money if a claimer doesn't sell any items.  Also, money paid to claimers is paid before 
   partners' shares are calculated.
   
 claimPrice - the number of L$ it costs to claim the vendor.  You can set this to "auto" to use automatic adjustment (see below)
 claimTime - how long a claim should last.
 claimAmount - how many L$ a claimer recieves if a sale is made while the vendor is claimed.  
  

There are two other commands, claimMultiply and claimMinimum, used only for automatic claim price adjustment. If you don't use claim price adjustment, you don't have to worry about them. See below for more information about them.

AUTO CLAIM PRICE ADJUST The vendor also features a mode where it automatically adjusts the price of claiming based on your current sales.

Here's how it works: the key value used by the vendor is the "SCP". That stands for Sales in Claim Period. The vendor will use the record of sales so far to work out how many sales you would make in a claim period. Then, it will adjust the claim price so that in order to make a profit, the claimer will have to make more sales in their claim period than on average you would have done otherwise.

Here's an example: suppose you have an item that sells for L$100. You pay claimers L$25 per sale during a claim period and the claim period is an hour. On average, you sell 50 of this item a day.

If you sell 50 of the item a day, that means that on average, you sell 2 every hour - this is your average SCP. By default when auto claim price adjust is on, the vendor assumes that a claimer should be "rewarded" - by making a profit - if they manage to *double* your SCP during their claim period. So the vendor will set the claim price to L$75. That way, if they manage to sell 4 items they'll make a profit, but if they sell only 2, they'll make a loss - but that's good, because based on the averages you probably would have sold 2 anyway, so obviously their sales efforts didn't do very much.

But what if you haven't sold any yet? That means that your average SCP is zero. So double of that is still zero.. but in that case, the vendor detects it and sets the target value to a particular minimum that you specify. So if the minimum value was set to 2, that would be used in this case, and the claim price would be set to L$25. The vendor will always use this low value for the claim price for an amount of time equal to the claim time after it's first rezzed - this enables it to get a fair sample.

If you want to use auto claim price adjustment, put the following line in the notecard in place of your claim price setting:

     claimPrice auto

Then, you can specify your settings with the following commands:

claimMultiply - How much better a claimer should have to make your sales in order to make a profit on their claim. For

 example if it's set to 2.0, the claim price will be set so that they make a profit if twice as many goods are sold during a 
 claim period as "normally" would have been.  If it's set to 4.0, it needs to be four times, and so on.
 

claimMinimum - The minimum number of sales a claimer should have to make during their claim period to make a profit.

 It is highly recommended that you leave this set to at least 4, unless your product is really popular and routinely sells 
 more than that in a claim period.  **DO NOT SET THIS TO ZERO**.
 

DISCLAIMER: The Golden Vendor has been extensively tested to ensure that it should not misbehave, but you must acknowledge that testing cannot be perfect and that Second Life is not a 100% stable platform. Neither Yumi Murakami nor anyone else is liable to you if this object winds up losing you money, sales or anything else. If you do not agree with this, return the vendor to Yumi and you will recieve a refund.

Scripts

For the main vendor..

key currentClaimer = NULL_KEY;
string currentClaimerName;

string itemToSell;
string infocardName;
integer itemPrice;
integer claimAmount;
integer claimPrice;
integer claimTime;

list percentSplits;

key myFrame;

integer notecardLine;
integer maxLine;
string notecardName = "Configuration Notecard";

key partnerKey;
string partnerName;
integer partnerSplit;
integer doingPartnerRequest;
integer totalPartnerSplits;
integer claimStartTime;
key lastFrameClick;
integer startUpTime;

integer totalSales;
integer totalNotecards;
integer totalClaims;
integer totalClaimedSales;

integer autoAdjustClaim;
float aaClaimMultiplier = 2.0;
integer aaClaimMinimum = 4;

init() {
    llSay(-593927,"reset");
    partnerKey = NULL_KEY;
    partnerSplit = 0;
    totalPartnerSplits = 0;
    doingPartnerRequest = FALSE;
    autoAdjustClaim = FALSE;
    llOwnerSay("Reading notecard.. (this may take a few moments)");
    if (llGetInventoryType(notecardName) != INVENTORY_NOTECARD) {
        llOwnerSay("Error: Configuration Notecard is missing!");
        state setupError;
    }
    doingPartnerRequest = FALSE;
    notecardLine = -1;
    llGetNumberOfNotecardLines(notecardName);
}

adjustAmount() {
    integer upTime = (llGetUnixTime() - startUpTime) / 60;
    float spm;
    if (upTime < claimTime) {
        spm = 0;
    } else {
        spm = totalSales / upTime;
    }
    integer salesInTime = (integer)( spm * ((float)claimTime) );
    integer targetSales;
    targetSales = (integer) ( (float)salesInTime * aaClaimMultiplier);
    if (targetSales < aaClaimMinimum) targetSales = aaClaimMinimum;
    integer suggestPrice = claimAmount * (targetSales - 1);
    if (suggestPrice != claimPrice) {
        claimPrice = suggestPrice;   
        llInstantMessage(llGetOwner(),"Claim price auto-adjusted to L$" + (string)claimPrice + " (actual average SCP " + (string)salesInTime + ", target SCP " + (string)targetSales +")");
    }     
}

default
{
    state_entry() {
        init();
    }
    on_rez(integer junk) {
        init();
    }
    changed(integer theChange) {
        llOwnerSay("My inventory changed!  Starting again...");
        llResetScript();
    }
    dataserver(key request, string result) {
        if (doingPartnerRequest) {
            llSetTimerEvent(0);
            doingPartnerRequest = FALSE;
            partnerName = result;
        } else {
            
        if (notecardLine == -1) {
            maxLine = ((integer)result) - 1;
        } else {
            if (llGetSubString(result,0,2) != "//") {
                integer commandBreak = llSubStringIndex(result," ");
                string command = llToLower(llGetSubString(result,0,commandBreak - 1));
                string rest = llGetSubString(result,commandBreak+1,-1);
                if (command == "item") {
                    itemToSell = rest;
                    if (llGetInventoryType(itemToSell) == INVENTORY_NONE) {
                        llOwnerSay("Error: Item to sell (" + itemToSell + ") not found in inventory.");
                        state setupError;
                        return;
                    }
                    if (! ((llGetInventoryPermMask(itemToSell,MASK_OWNER)) & (PERM_COPY | PERM_TRANSFER))) {
                        llOwnerSay("Error: Can't sell this item, I don't have copy and transfer permission.");
                        state setupError;
                        return;
                    }
                    if (! ((llGetInventoryPermMask(itemToSell,MASK_NEXT)) & (PERM_COPY | PERM_TRANSFER))) {
                        llOwnerSay("Warning: Customer will get copy and transfer permission!");
                    }
                } 
                if (command == "notecard") {
                    infocardName = rest;
                    if (llGetInventoryType(infocardName) != INVENTORY_NOTECARD) {
                        llOwnerSay("Error: Information notecard (" + infocardName + ") not found in inventory.");
                        state setupError;
                        return;
                    }
                    if (! ((llGetInventoryPermMask(infocardName,MASK_OWNER)) & (PERM_COPY | PERM_TRANSFER))) {
                        llOwnerSay("Error: Can't give out this information notecard, I don't have copy and transfer permission.");
                        state setupError;
                        return;
                    }
                    if (! ((llGetInventoryPermMask(infocardName,MASK_NEXT)) & (PERM_COPY | PERM_TRANSFER))) {
                        llOwnerSay("Error: Customer won't be able to read information notecard, you need to give them copy and transfer permission.");
                        state setupError;
                        return;
                    }
                }       
                if (command == "itemprice") {
                    itemPrice = (integer)rest;
                    if (itemPrice <= 0) {
                        llOwnerSay("Error: Price is not valid, or is zero.  (This vendor is not suitable for free items.)");
                        state setupError;
                        return;
                    }
                }
                if (command == "claimprice") {
                    if (rest == "auto") {
                        autoAdjustClaim = TRUE;
                    } else {
                        autoAdjustClaim = FALSE;
                        claimPrice = (integer)rest;
                        if ((claimPrice <= 0) && (result != "0")) {
                            llOwnerSay("Error: Claim price is not valid.");
                            state setupError;
                            return;
                        }
                    }
                }
                if (command == "claimamount") {
                    claimAmount = (integer)rest;
                    if ((claimAmount <= 0)) {
                        llOwnerSay("Error: Claim amount is not valid or is zero.");
                        state setupError;
                        return;
                    }   
                }
                if (command == "claimtime") {
                    claimTime = (integer)rest;
                    if ((claimTime <= 0)) {
                        llOwnerSay("Error: Claim time is not valid or is zero.");
                        state setupError;
                        return;
                    }   
                }  
                if (command == "claimminimum") {
                    aaClaimMinimum = (integer)rest;
                    if ((aaClaimMinimum <= 0)) {
                        llOwnerSay("Error: Claim minimum is not valid or is zero.");
                        state setupError;
                        return;
                    }   
                }  
                if (command == "claimmultiply") {
                    aaClaimMultiplier = (float)rest;
                    if ((aaClaimMinimum <= 0.0)) {
                        llOwnerSay("Error: Claim multiplier is not valid or is zero.");
                        state setupError;
                        return;
                    }   
                }  
                
                if (command == "partnerkey") {
                    if (partnerKey != NULL_KEY){
                        llOwnerSay("Error: Two partnerkeys in a row with no partnersplit in between.");
                        state setupError;
                        return;
                    }
                    partnerKey = (key)rest;
                    llSetTimerEvent(5);
                    doingPartnerRequest = TRUE;
                    llRequestAgentData(partnerKey,DATA_NAME);
                    return;
                }  
                if (command == "partnersplit") {
                    if (partnerKey == NULL_KEY) {
                        llOwnerSay("Error: Partnersplit needs to be preceded by a partnerkey.");
                        state setupError;
                        return;
                    }
                    partnerSplit = (integer)rest;
                    if ((partnerSplit <= 0)) {
                        llOwnerSay("Error: Partner split amount is not valid or is zero.");
                        state setupError;
                        return;
                    }   
                    totalPartnerSplits += partnerSplit;
                    llOwnerSay("Partner: " + partnerName + " recieves " + (string)partnerSplit + "%.");
                    percentSplits += [partnerKey, partnerSplit];
                    partnerKey = NULL_KEY;
                }
                     
            }
        }
        }
        if (notecardLine == maxLine) {
            // finalValidate();
            if (claimAmount >= itemPrice) {
                llOwnerSay("Error: Claim amount is greater than item price!");
                state setupError;
                return;
            }
            if (totalPartnerSplits > 100) {
                llOwnerSay("Error: Partner splits add up to greater than 100%!");
                state setupError;
                return;
            }
            if ((itemPrice == 0) || (itemToSell == "") || ((claimPrice == 0) && (autoAdjustClaim == FALSE)) || (claimTime == 0) || (claimAmount == 0)) {
                llOwnerSay("Error: Essential information is missing.");
                llOwnerSay((string)autoAdjustClaim);
                state setupError;
                return;
            }
            state setupStage;   
            return;
        }
        notecardLine++;
        llGetNotecardLine(notecardName,notecardLine);
    }
       
    timer() {
        llSetTimerEvent(0);
        llOwnerSay("Error: Dataserver timed out while checking information for partner key " + (string)partnerKey + ".");
        llOwnerSay("This can be caused by lag, but more usually means that there's a mistake in the key.");
        state setupError;
        return;
    }
}

state setupError {
    state_entry() {
        llOwnerSay("An error occured in setup.  Please click me to try again.");
    }
    touch_start(integer junk) {
        if (llDetectedKey(0) == llGetOwner()) llResetScript();
    }
}

state setupStage {
    state_entry() {
        llListen(-593827,"",NULL_KEY,"frame");
        llOwnerSay("Setting up linkage.  Please click on my frame.");
    }
    
    listen(integer channel, string name, key speaker, string message) {
        if (llGetOwnerKey(speaker) != llGetOwner()) return;
        if (message != "frame") return;
        myFrame = speaker;
        llOwnerSay("Linked to frame " + (string)speaker);
        llWhisper(-593827,myFrame);
        llSleep(1);
        llRequestPermissions(llGetOwner(),PERMISSION_DEBIT);
    }
    run_time_permissions(integer perms) {
        if (perms & PERMISSION_DEBIT) {
            state gotFrame;
        } else {
            llOwnerSay("Debit permissions are required for this vendor to work.");
            llOwnerSay("Please click on my frame to try again.");
            llWhisper(-593927,"reset");
        }
    }
    changed(integer thechange) {
        if (thechange && CHANGED_INVENTORY) {
            state maint;
        }
    }
    
}

state gotFrame {
    state_entry() {
        llOwnerSay("Vendor setup complete and ready to sell.");
        llListen(-593927,"",myFrame,"");
        llSetPayPrice(PAY_HIDE,[itemPrice,PAY_HIDE,PAY_HIDE,PAY_HIDE]);
        totalSales = 0;
        totalNotecards = 0;
        totalClaims = 0;
        totalClaimedSales = 0;
        startUpTime = llGetUnixTime();
        if (autoAdjustClaim) adjustAmount();
        llWhisper(-593927,(string)claimPrice);
        llSetTimerEvent(60);
    }
    on_rez(integer junk) {
        llResetScript();
    }
    touch_start(integer junk) {
        if (llDetectedKey(0) != llGetOwner()) {
            totalNotecards++;
            if (infocardName != "") llGiveInventory(llDetectedKey(0),infocardName);
            return;
        } else {
            llOwnerSay("I have made " + (string)totalSales + " sales (" + (string)totalClaimedSales + " while claimed), given out " + (string)totalNotecards + " notecards, and been claimed " + (string)totalClaims + " times.");
        }
    }
    
    money(key giver, integer amount) {
        if (amount != itemPrice) {
            llGiveMoney(giver,amount);
            llSay(0,llKey2Name(giver) + ", you paid the wrong amount.  The price is L$" + (string)itemPrice + ".");
            return;
        }
        totalSales++;
        integer gotMoney = itemPrice;
        llGiveInventory(giver,itemToSell);
        if (currentClaimer != NULL_KEY) {
            totalClaimedSales++;
            llGiveMoney(currentClaimer,claimAmount);
            gotMoney -= claimAmount;
        }
        integer totalSplits = 0;
        if (percentSplits != []) {
            integer t;
            for (t=0; t<llGetListLength(percentSplits); t+=2) {
                key benefit = llList2Key(percentSplits,t);
                float percent = llList2Float(percentSplits,t+1);
                integer share = (integer) ( ((float)gotMoney / 100.0) * percent);
                llGiveMoney(benefit,share);
                totalSplits += share;
            }
        }
        string message = llKey2Name(giver) + " (" + (string)giver + ") bought a " + itemToSell + " from " + llGetRegionName() + " for L$" + (string)itemPrice + ".";
 
        if (currentClaimer != NULL_KEY) {
            message += "  Vendor was claimed by " + currentClaimerName + " who recieved L$" + (string)claimAmount +".";
        }
        if (totalSplits != 0) {
            message += "  L$" + (string)totalSplits + " was paid to partners.";
        }
        llInstantMessage(llGetOwner(),message);
        if (currentClaimer != NULL_KEY) {
            llInstantMessage(currentClaimer,"A " + itemToSell + " was sold from a vendor you claimed; you win L$" + (string)claimAmount + "!");
        }
        
    }
            
    listen(integer channel, string name, key speaker, string message) {
        if (speaker != myFrame) return;
        list msgFromFrame = llParseString2List(message,["|"],[]);
        integer amount = (integer)(llList2String(msgFromFrame,1));
        key giver = (key)(llList2String(msgFromFrame,0));
        string giverName = llKey2Name(giver);
        if (amount == -1) {
            if (currentClaimer != NULL_KEY) {
                integer passed = llGetUnixTime() - claimStartTime;
                passed /= 60;
                integer left = claimTime - passed;
                llSay(0,"This vendor is already claimed.  It will be available to claim in " + (string)left + " minutes.");
            } else {
                llSay(0,"This vendor is available to be claimed right now!");
            }
            llSay(0,"Cost to claim: L$" + (string)claimPrice + ".  You earn L$" + (string)claimAmount + " per sale for " + (string)claimTime + " minute.");
          
            llSay(0,"If you've never played a Golden Vendor before, click the frame again for an information notecard.");
            llSay(0,"Note - if you just want to buy the product, ignore this message and click the white poster part itself."); 
            if (giver == lastFrameClick) {
                llGiveInventory(giver,"About Golden Vendors");
            }
            lastFrameClick = giver;
            return;
        }
        if (amount != claimPrice) {
            llGiveMoney(giver,amount);
            llSay(0,giverName + ", you paid the wrong amount.  Price to claim the vendor is " + (string)claimPrice + ".");
            return;
        }
        if (currentClaimer != NULL_KEY) {
            llGiveMoney(giver,amount);
            llSay(0,giverName + ", sorry, this vendor is already claimed.");
            return;
        }
        totalClaims++;
        llWhisper(-593927,"noclaim");
        currentClaimer = giver;
        currentClaimerName = giverName;
        claimStartTime = llGetUnixTime();
        llSay(0,currentClaimerName + ", you have claimed this vendor for " + (string)claimTime + " minutes.");
        llSay(0,"Get as many other people to buy this item as you can and win money!");
        
    }
    changed(integer thechange) {
        if (thechange && CHANGED_INVENTORY) {
            state maint;
        }
    }
    
    timer() {
        if (currentClaimer != NULL_KEY) {
            if (llGetUnixTime() >= (claimStartTime + (claimTime * 60))) {
                 llInstantMessage(currentClaimer,"Your claim time is over!");
                 currentClaimer = NULL_KEY;
                 llWhisper(-593927,"claim");
            }
        }
        if (autoAdjustClaim) adjustAmount();
    }
        
    
}

state maint {
    state_entry() {
        llWhisper(-593927,"noclaim");
        llOwnerSay("My inventory changed.  Please click me to reset..");
    }
    
    touch_start(integer count) {
        if (llDetectedKey(0) == llGetOwner()) llResetScript();
    }
    
}

And the script for the frame..

key master;

init() {
    llListen(-593827,"",NULL_KEY,"");
}

default
{
    state_entry() { init(); }
    on_rez(integer junk) { init();}
     
    touch_start(integer total_number)
    {
        if (llDetectedKey(0) != llGetOwner()) return;
        llSay(-593827,"frame");        
    }
    
    listen(integer channel, string name, key speaker, string message) {
        if (llGetOwnerKey(speaker) != llGetOwner()) return;
        if (((key)message) != llGetKey()) return;
        master = speaker;
        llOwnerSay("Frame linked to vendor key " + (string)master);
        state running;
    }
}

state running {
    state_entry() {
        llListen(-593927,"",master,"");
    }
    
    on_rez(integer junk) {
        llResetScript();
    }
    
    touch_start(integer total_number) {
        llSay(-593927,(string)llDetectedKey(0) + "|-1");
    }
    
    listen(integer channel, string name, key speaker, string message) {
        if (speaker != master) return;
        if (message == "reset") llResetScript();
        if (message == "noclaim") state noClaim;
        llSetPayPrice(PAY_HIDE,[(integer)message,PAY_HIDE,PAY_HIDE,PAY_HIDE]);
    }
     
    money(key giver, integer amount) {
        llSay(-593927,(string)giver + "|" + (string)amount);
    }      
    
}

state noClaim {
    state_entry() {
        llListen(-593927,"",master,"");
    }
    
    on_rez(integer junk) {
        llResetScript();
    }
    
    touch_start(integer total_number) {
        llSay(-593927,(string)llDetectedKey(0) + "|-1");
    }
    
    listen(integer channel, string name, key speaker, string message) {
        if (speaker != master) return;
        if (message == "reset") llResetScript();
        if (message == "claim") state running;
        llOwnerSay(message);
        llSetPayPrice(PAY_HIDE,[(integer)message,PAY_HIDE,PAY_HIDE,PAY_HIDE]);
    }
}