Table of Contents

About

The following project is a rental system written in one single script that uses Corrade to invite renters to the group and place them in a renter role within that group. When paid, the rental system will calculate the amount of time that is attributed to the renter and then demotes the renter in case they fail to pay the rent for the next time slice. When the renter renews, they are allowed to pay any amount of money and the rental system will automatically compute the allowed time based on what the renter paid and add that to the allowed rent time.

Marketplace Item

Setup

The script uses a notecard named configuration which is placed in the same primitive as the script in the code section. It contains the following sample configuration:

####################### START CONFIGURATION ###############################

# The UUID of the owner of the rental box.
owner = "e08a444e-d415-494c-81c2-71dcdd0ef4fe"

##
# Corrade settings.
##
##
# All these settings must correspond to the settings in Corrade.ini.
##
# This is the UUID of the Corrade bot.
corrade = "c2ad6fdd-4a10-48fa-9448-0da05368804f"
# The name of the group - it can also be the UUID of the group.
group = "My Group"
# The password for the group.
password = "mypassword"
##

##
# Rental settings.
##
# The rental settings define a price for an amount of time such that the 
# renter may pay any amount and the script is designed to compute the time
# that will be added to their rent.
##
# The price for renting this place in L$.
price = 100
# The time that this place can be rented for in seconds.
# Cheatsheet:
# 1 day is 86400 seconds (default)
# 1 week is 604800 seconds
# 1 month is on average 2629744 seconds
rent = 86400
# The name of the role to invite renters to.
role = "Renters"
##

####################### END CONFIGURATION #################################

In order to use the rental system, you would need to perform the following steps:

You will then need to grant Corrade the following in-world group abilities:

Furthermore, your scripted agent will require the group Corrade permission (meant for group operations).

When renters will want to lease the property, they will pay an amount of money (by default, the value configured in configuration). After they have paid, the script will check if they are in the group using Corrade - if they are not, the script will use Corrade to invite them to the group and place them in the configured role (by default, the role parameter in the configuration notecard). If on the other hand they are already in the group, the script will assign them to the role specified in the configuration notecard (by default, it is called Renters).

The script will then display some information regarding the rent using overhead text and will check every minute whether the rent has expired. Note that the precision (grace) of this script is of 1 minute. If the rent has expired, the script will then demote them from the role specified in the configuration notecard and they will thereby lose access to the property.

Developer Notes

The script makes use of Corrade's sifting ability, which comes in handy when retrieving large lists such as the list of group members. Instead of returning the entire list of members to the LSL script, the script instructs Corrade to perform a sift and retrieve only the UUID of the renter thereby sparing a lot of memory. The same technique applies to getting the roles that a group member is in.

In order to preserve memory, the script uses a trampoline to provide state re-entry and thereby eliminates the need for code duplication in most cases.

Code

rentalsystem.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2015 - License: CC BY 2.0      //
//  Please see: https://creativecommons.org/licenses/by/2.0 for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
// escapes a string in conformance with RFC1738
string wasURLEscape(string i) {
    string o = "";
    do {
        string c = llGetSubString(i, 0, 0);
        i = llDeleteSubString(i, 0, 0);
        if(c == "") jump continue;
        if(c == " ") {
            o += "+";
            jump continue;
        }
        if(c == "\n") {
            o += "%0D" + llEscapeURL(c);
            jump continue;
        }
        o += llEscapeURL(c);
@continue;
    } while(i != "");
    return o;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
// unescapes a string in conformance with RFC1738
string wasURLUnescape(string i) {
    return llUnescapeURL(
        llDumpList2String(
            llParseString2List(
                llDumpList2String(
                    llParseString2List(
                        i, 
                        ["+"], 
                        []
                    ), 
                    " "
                ), 
                ["%0D%0A"], 
                []
            ), 
            "\n"
        )
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
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) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
list wasCSVToList(string csv) {
    list l = [];
    list s = [];
    string m = "";
    do {
        string a = llGetSubString(csv, 0, 0);
        csv = llDeleteSubString(csv, 0, 0);
        if(a == ",") {
            if(llList2String(s, -1) != "\"") {
                l += m;
                m = "";
                jump continue;
            }
            m += a;
            jump continue;
        }
        if(a == "\"" && llGetSubString(csv, 0, 0) == a) {
            m += a;
            csv = llDeleteSubString(csv, 0, 0);
            jump continue;
        }
        if(a == "\"") {
            if(llList2String(s, -1) != a) {
                s += a;
                jump continue;
            }
            s = llDeleteSubList(s, -1, -1);
            jump continue;
        }
        m += a;
@continue;
    } while(csv != "");
    // postcondition: length(s) = 0
    return l + m;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
integer wasDateTimeToStamp(
    integer year,
    integer month,
    integer day,
    integer hour,
    integer minute,
    integer second
    ) {
    month -= 2;
    if (month <= 0) {
        month += 12;
        --year;
    }
    return 
    (
        (((year / 4 - year / 100 + year / 400 + (367 * month) / 12 + day) +
                year * 365 - 719499
            ) * 24 + hour
        ) * 60 + minute
    ) * 60 + second;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
float wasFmod(float a, float p) {
    if(p == 0) return (float)"nan";
    return a - ((integer)(a/p) * p);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0    //
//     Original: Clive Page, Leicester University, UK.   1995-MAY-2      //
///////////////////////////////////////////////////////////////////////////
list wasUnixTimeToDateTime(integer seconds) {
    integer mjday = (integer)(seconds/86400 + 40587);
    integer dateYear = 1858 + (integer)( (mjday + 321.51) / 365.25);
    float day = (integer)( wasFmod(mjday + 262.25, 365.25) ) + 0.5;
    integer dateMonth = 1 + (integer)(wasFmod(day / 30.6 + 2.0, 12.0) );
    integer dateDay = 1 + (integer)(wasFmod(day,30.6));
    float nsecs = wasFmod(seconds, 86400);
    integer dateSeconds = (integer)wasFmod(nsecs, 60);
    nsecs = nsecs / 60;
    integer dateMinutes = (integer)wasFmod(nsecs, 60);
    integer dateHour = (integer)(nsecs / 60);
    return [ dateYear, 
        dateMonth, dateDay, dateHour, dateMinutes, dateSeconds ];
}
 
// for changing states
string nextstate = "";
 
// notecard reading
integer line = 0;
list tuples = [];
 
// corrade data
key CORRADE = NULL_KEY;
string GROUP = "";
string PASSWORD = "";
// the owner of the rental system
// the dude or dudette that gets paid
key OWNER = NULL_KEY;
// the price of the rent
integer PRICE = 0;
// the time for the rent in seconds
integer RENT = 0;
string URL = "";
// the role to invite rentants to
string ROLE = "";
 
default {
    state_entry() {
        if(llGetInventoryType("configuration") != INVENTORY_NOTECARD) {
            llSetText("Sorry, could not find an inventory notecard.", <1, 0, 0>, 1.0);
            return;
        }
        llSetText("Reading configuration notecard...", <1, 1, 0>, 1.0);
        llGetNotecardLine("configuration", line);
    }
    dataserver(key id, string data) {
        if(data == EOF) {
            // invariant, length(tuples) % 2 == 0
            if(llGetListLength(tuples) % 2 != 0) {
                llSetText("Error in configuration notecard.", <1, 0, 0>, 1.0);
                return;
            }
            CORRADE = (key)llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "corrade"
                              ]
                          )
                      +1);
            if(CORRADE == NULL_KEY) {
                llSetText("Error in configuration notecard: corrade", <1, 0, 0>, 1.0);
                return;
            }
            GROUP = llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "group"
                              ]
                          )
                      +1);
            if(GROUP == "") {
                llSetText("Error in configuration notecard: group", <1, 0, 0>, 1.0);
                return;
            }
            PASSWORD = llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "password"
                              ]
                          )
                      +1);
            if(PASSWORD == "") {
                llSetText("Error in configuration notecard: password", <1, 0, 0>, 1.0);
                return;
            }
            OWNER = (key)llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "owner"
                              ]
                          )
                      +1);
            if(OWNER == NULL_KEY) {
                llSetText("Error in configuration notecard: owner", <1, 0, 0>, 1.0);
                return;
            }
            PRICE = (integer)llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "price"
                              ]
                          )
                      +1);
            if(PRICE == 0) {
                llSetText("Error in configuration notecard: price", <1, 0, 0>, 1.0);
                return;
            }
            RENT = (integer)llList2String(
                          tuples,
                          llListFindList(
                              tuples, 
                              [
                                  "rent"
                              ]
                          )
                      +1);
            if(RENT == 0) {
                llSetText("Error in configuration notecard: rent", <1, 0, 0>, 1.0);
                return;
            }
            ROLE = llList2String(
                tuples,
                llListFindList(
                    tuples, 
                    [
                        "role"
                    ]
                )
                +1
            );
            if(ROLE == "") {
                llSetText("Error in configuration notecard: role", <1, 0, 0>, 1.0);
                return;
            }
 
            llSetText("Read configuration notecard.", <0, 1, 0>, 1.0);
 
            // if data is set switch to rented state
            if(llGetObjectDesc() != "") state rented;
            // otherwise switch to the payment state
            state payment;
        }
        if(data == "") jump continue;
        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 == "") jump continue;
        tuples += k;
        tuples += v;
@continue;
        llGetNotecardLine("configuration", ++line);
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state trampoline {
    state_entry() {
        llSetTimerEvent(5);
    }
    timer() {
        llSetTimerEvent(0);
        // State jump table
        if(nextstate == "url") state url;
        if(nextstate == "getmembers") state getmembers;
        if(nextstate == "getrolemembers") state getrolemembers;
        if(nextstate == "addtorole") state addtorole;
        if(nextstate == "invite") state invite;
        if(nextstate == "rented") state rented;
        if(nextstate == "demote") state demote;
        // automata in invalid state
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state payment {
    state_entry() {
        llSetPayPrice(PRICE, [PRICE]);
        llSetClickAction(CLICK_ACTION_PAY);
        llSetText("☀ Touch me to rent this place! ☀", <0, 1, 0>, 1.0);
    }
    money(key id, integer amount) {
        // Get the current time stamp.
        list stamp = llList2List(
            llParseString2List(
                llGetTimestamp(),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
        // convert to seconds and add the rent
        integer delta = wasDateTimeToStamp(
            llList2Integer(stamp, 0),
            llList2Integer(stamp, 1),
            llList2Integer(stamp, 2),
            llList2Integer(stamp, 3),
            llList2Integer(stamp, 4),
            llList2Integer(stamp, 5)
        ) + 
        // the amount of time is the amount paid
        // times the rent time divided by the price
        amount * (integer)(
            (float)RENT / 
            (float)PRICE
        );
        // convert back to a timestamp
        stamp = wasUnixTimeToDateTime(delta);
        // and set the renter and the eviction date
        llSetObjectDesc(
            wasKeyValueEncode(
                [
                    "rentantUUID", id,
                    "rentantName", llKey2Name(id),
                    "expiresDate", llList2String(stamp, 0) +
                        "-" + llList2String(stamp, 1) + 
                        "-" + llList2String(stamp, 2) + 
                        "T" + llList2String(stamp, 3) +
                        ":" + llList2String(stamp, 4) +
                        ":" + llList2String(stamp, 5)
                ]
            )
        );
        nextstate = "getmembers";
        state url;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state url {
    state_entry() {
        // release any previous URL
        llReleaseURL(URL);
        // request a new URL
        llRequestURL();
    }
    http_request(key id, string method, string body) {
        if(method != URL_REQUEST_GRANTED) {
            llSetText("☀ Unable to get an URL! ☀", <1, 0, 0>, 1.0);
            nextstate = "url";
            state trampoline;
        }
        URL = body;
        // state URL jump table
        if(nextstate == "getmembers") state getmembers;
        if(nextstate == "demote") state demote;
        // automata in invalid state
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state getmembers {
    state_entry() {
        llSetText("☀ Getting group members... ☀", <1, 1, 0>, 1.0);
        llInstantMessage(CORRADE,
            wasKeyValueEncode(
                [
                    "command", "getmembers",
                    "group", wasURLEscape(GROUP),
                    "password", wasURLEscape(PASSWORD),
                    // we just care if the agent is in the group
                    // so we use Corrade's sifting ability in order
                    // to reduce the script memory usage
                    "sift", wasURLEscape(
                        "(" +
                            wasKeyValueGet(
                                "rentantUUID",
                                llGetObjectDesc()
                            ) +
                        ")*"
                    ),
                    "callback", wasURLEscape(URL)
                ]
            )
        );
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        if(wasKeyValueGet("success", body) != "True") {
            llSetText("☀ Could not get group members! ☀", <1, 0, 0>, 1.0);
            nextstate = "getmembers";
            state trampoline;
        }
        // check that the payer is part of the role
        integer i = 
            llListFindList(
                wasCSVToList(
                    wasURLUnescape(
                        wasKeyValueGet(
                            "data", 
                            body
                        )
                    )
                ), 
                (list)wasKeyValueGet(
                    "rentantUUID", 
                    llGetObjectDesc()
                )
            );
        llSetTimerEvent(0);
        // if they are in the group then check roles.
        if(i != -1) state getrolemembers;
        // otherwise invite them to the group role.
        state invite;
    }
    timer() {
        llSetTimerEvent(0);
        nextstate = "getmembers";
        state trampoline;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }    
}
 
state getrolemembers {
    state_entry() {
        llSetText("☀ Getting role members... ☀", <1, 1, 0>, 1.0);
        llInstantMessage(CORRADE,
            wasKeyValueEncode(
                [
                    "command", "getrolemembers",
                    "group", wasURLEscape(GROUP),
                    "password", wasURLEscape(PASSWORD),
                    "role", wasURLEscape(ROLE),
                    // we just care if the agent is in the renter role
                    // so we use Corrade's sifting ability in order
                    // to reduce the script memory usage
                    "sift", wasURLEscape(
                        "(" +
                            wasKeyValueGet(
                                "rentantUUID",
                                llGetObjectDesc()
                            ) +
                        ")*"
                    ),
                    "callback", wasURLEscape(URL)
                ]
            )
        );
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        if(wasKeyValueGet("success", body) != "True") {
            llSetText("☀ Could not get role members! ☀", <1, 0, 0>, 1.0);
            nextstate = "getrolemembers";
            state trampoline;
        }
        // check that the payer is part of the role
        integer i = 
            llListFindList(
                wasCSVToList(
                    wasURLUnescape(
                        wasKeyValueGet(
                            "data", 
                            body
                        )
                    )
                ), 
                (list)wasKeyValueGet(
                    "rentantUUID", 
                    llGetObjectDesc()
                )
            );
        llSetTimerEvent(0);
        // if they are in the role then skip inviting them.
        if(i != -1) state rented;
        // otherwise add them to the land role.
        state addtorole;
    }
    timer() {
        llSetTimerEvent(0);
        nextstate = "getrolemembers";
        state trampoline;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state addtorole {
    state_entry() {
        llSetText("☀ Adding to role... ☀", <1, 1, 0>, 1.0);
        llInstantMessage(CORRADE,
            wasKeyValueEncode(
                [
                    "command", "addtorole",
                    "group", wasURLEscape(GROUP),
                    "password", wasURLEscape(PASSWORD),
                    "agent", wasURLEscape(
                        wasKeyValueGet(
                            "rentantUUID",
                            llGetObjectDesc()
                        )
                    ),
                    "role", wasURLEscape(ROLE),
                    "callback", wasURLEscape(URL)
                ]
            )
        );
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        if(wasKeyValueGet("success", body) != "True") {
            llSetText("☀ Could not add to role! ☀", <1, 0, 0>, 1.0);
            nextstate = "addtorole";
            state trampoline;
        }
        // otherwise invite them to the group role.
        state rented;
    }
    timer() {
        llSetTimerEvent(0);
        nextstate = "addtorole";
        state trampoline;
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state invite {
    state_entry() {
        llSetText("☀ Please accept the group invite! ☀", <1, 1, 0>, 1.0);
        // invite the agent to the land group
        llInstantMessage(CORRADE,
            wasKeyValueEncode(
                [
                    "command", "invite",
                    "group", wasURLEscape(GROUP),
                    "password", wasURLEscape(PASSWORD),
                    "agent", wasURLEscape(
                        wasKeyValueGet(
                            "rentantUUID", 
                            llGetObjectDesc()
                        )
                    ),
                    "role", wasURLEscape(ROLE),
                    "callback", wasURLEscape(URL)
                ]
            )
        );
        // handle any Corrade timeouts
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        llHTTPResponse(id, 200, "OK");
        // Checks if the invite was sent successfully or if that fails but the 
        // agent is already in the group (status 15345) then continue.
        // Otherwise, jump to the trampoline and send the invite again.
        // Status codes:
        // http://grimore.org/secondlife/scripted_agents/corrade/status_codes/progressive
        if(wasKeyValueGet("success", body) != "True" && 
            wasKeyValueGet("status", body) != "15345") {
            llSetText("☀ Group invite could not be sent! ☀", <1, 0, 0>, 1.0);
            nextstate = "invite";
            state trampoline;
        }
        llSetText("☀ Group invitation sent! ☀", <1, 1, 0>, 1.0);
        llSetTimerEvent(0);
        state rented;
    }
    timer() {
        llSetTimerEvent(0);
        nextstate = "invite";
        state trampoline;
    }
}
 
state rented {
    state_entry() {
        // Get the expiration date
        list expires = llList2List(
            llParseString2List(
                wasKeyValueGet(
                    "expiresDate", 
                    llGetObjectDesc()
                ),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
        // Get the current date
        list stamp = llList2List(
            llParseString2List(
                llGetTimestamp(),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
 
        integer delta = wasDateTimeToStamp(
            llList2Integer(expires, 0),
            llList2Integer(expires, 1),
            llList2Integer(expires, 2),
            llList2Integer(expires, 3),
            llList2Integer(expires, 4),
            llList2Integer(expires, 5)
        ) - wasDateTimeToStamp(
            llList2Integer(stamp, 0),
            llList2Integer(stamp, 1),
            llList2Integer(stamp, 2),
            llList2Integer(stamp, 3),
            llList2Integer(stamp, 4),
            llList2Integer(stamp, 5)
        );
 
        // the rent has expired, now evict the rentant
        if(delta <= 0) {
            llSetTimerEvent(0);
            nextstate = "demote";
            state url;
        }
 
        // otherwise, update the remaining time
        list remaining = wasUnixTimeToDateTime(delta);
        remaining = llListReplaceList(remaining, [ llList2Integer(remaining, 0)-1970 ], 0, 0);
        remaining = llListReplaceList(remaining, [ llList2Integer(remaining, 1)-1 ], 1, 1);
        remaining = llListReplaceList(remaining, [ llList2Integer(remaining, 2)-1 ], 2, 2);
 
        llSetText(
            "☀ Private property! ☀" 
            + "\n" +
            "Rented by: " + 
                wasKeyValueGet(
                    "rentantName", 
                    llGetObjectDesc()
                ) 
            + "\n" + 
            "Expires on: " + 
                wasKeyValueGet(
                    "expiresDate", 
                    llGetObjectDesc()
                ) 
            + "\n" + 
            "Remaining: " + 
                llList2String(remaining, 0) + "-" + 
                llList2String(remaining, 1) + "-" +
                llList2String(remaining, 2) + " " +
                llList2String(remaining, 3) + ":" + 
                llList2String(remaining, 4)
            + "\n" +
            "Touch to extend rent.", 
            <0, 1, 1>, 
            1.0
        );
        llSetPayPrice(PRICE, [PRICE]);
        llSetClickAction(CLICK_ACTION_PAY);
        // set the countdown every minute
        llSetTimerEvent(60);
    }
    money(key id, integer amount) {
        // Get the expiration date
        list stamp = llList2List(
            llParseString2List(
                wasKeyValueGet(
                    "expiresDate", 
                    llGetObjectDesc()
                ),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
        // convert to seconds and add the extended rent
        integer delta = wasDateTimeToStamp(
            llList2Integer(stamp, 0),
            llList2Integer(stamp, 1),
            llList2Integer(stamp, 2),
            llList2Integer(stamp, 3),
            llList2Integer(stamp, 4),
            llList2Integer(stamp, 5)
        ) + 
        // the amount of time to extend is the amount 
        // paid times the rent time divided by the price
        amount * (integer)(
            (float)RENT / 
            (float)PRICE
        );
        // convert back to a timestamp
        stamp = wasUnixTimeToDateTime(delta);
        // and set the renter and the eviction date
        llSetObjectDesc(
            wasKeyValueEncode(
                [
                    "rentantUUID", wasKeyValueGet(
                        "rentantUUID", 
                        llGetObjectDesc()
                    ),
                    "rentantName", wasKeyValueGet(
                        "rentantName", 
                        llGetObjectDesc()
                    ),
                    "expiresDate", llList2String(stamp, 0) +
                        "-" + llList2String(stamp, 1) + 
                        "-" + llList2String(stamp, 2) + 
                        "T" + llList2String(stamp, 3) +
                        ":" + llList2String(stamp, 4) +
                        ":" + llList2String(stamp, 5)
                ]
            )
        );
        llSetText("☀ Updating... ☀", <1, 1, 0>, 1.0);
        llSetTimerEvent(0);
        nextstate = "rented";
        state trampoline;
    }
    timer() {
        // Get the expiration date
        list expires = llList2List(
            llParseString2List(
                wasKeyValueGet(
                    "expiresDate", 
                    llGetObjectDesc()
                ),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
        // Get the current date
        list stamp = llList2List(
            llParseString2List(
                llGetTimestamp(),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
 
        integer delta = wasDateTimeToStamp(
            llList2Integer(expires, 0),
            llList2Integer(expires, 1),
            llList2Integer(expires, 2),
            llList2Integer(expires, 3),
            llList2Integer(expires, 4),
            llList2Integer(expires, 5)
        ) - wasDateTimeToStamp(
            llList2Integer(stamp, 0),
            llList2Integer(stamp, 1),
            llList2Integer(stamp, 2),
            llList2Integer(stamp, 3),
            llList2Integer(stamp, 4),
            llList2Integer(stamp, 5)
        );
 
        // the rent has expired, now evict the rentant
        if(delta <= 0) {
            llSetTimerEvent(0);
            nextstate = "demote";
            state url;
        }
 
        // otherwise, update the remaining time
        list remaining = wasUnixTimeToDateTime(delta);
        remaining = llListReplaceList(remaining, [ llList2Integer(remaining, 0)-1970 ], 0, 0);
        remaining = llListReplaceList(remaining, [ llList2Integer(remaining, 1)-1 ], 1, 1);
        remaining = llListReplaceList(remaining, [ llList2Integer(remaining, 2)-1 ], 2, 2);
 
        llSetText(
            "☀ Private property! ☀" 
            + "\n" +
            "Rented by: " + 
                wasKeyValueGet(
                    "rentantName", 
                    llGetObjectDesc()
                ) 
            + "\n" + 
            "Expires on: " + 
                wasKeyValueGet(
                    "expiresDate", 
                    llGetObjectDesc()
                ) 
            + "\n" + 
            "Remaining: " + 
                llList2String(remaining, 0) + "-" + 
                llList2String(remaining, 1) + "-" +
                llList2String(remaining, 2) + " " +
                llList2String(remaining, 3) + ":" + 
                llList2String(remaining, 4)
            + "\n" +
            "Touch to extend rent.", 
            <0, 1, 1>, 
            1.0
        );
 
    }
    on_rez(integer num) {
        llResetScript();
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) {
            llResetScript();
        }
    }
}
 
state demote {
    state_entry() {
        llSetText("☀ Rent has expired! ☀", <1, 0, 0>, 1.0);
        // demote the agent from the renter role
        llInstantMessage(CORRADE,
            wasKeyValueEncode(
                [
                    "command", "deletefromrole",
                    "group", wasURLEscape(GROUP),
                    "password", wasURLEscape(PASSWORD),
                    "agent", wasURLEscape(
                        wasKeyValueGet(
                            "rentantUUID", 
                            llGetObjectDesc()
                        )
                    ),
                    "role", wasURLEscape(ROLE),
                    "callback", wasURLEscape(URL)
                ]
            )
        );
        // handle any Corrade timeouts
        llSetTimerEvent(60);
    }
    http_request(key id, string method, string body) {
        llHTTPResponse(id, 200, "OK");
        // Checks if the demote was sent successfully or if that fails but the 
        // agent has already left the group (status 11502) then continue.
        // Otherwise, jump to the trampoline and send the demote again.
        // Status codes:
        // http://grimore.org/secondlife/scripted_agents/corrade/status_codes/progressive
        if(wasKeyValueGet("success", body) != "True" && 
            wasKeyValueGet("status", body) != "11502") {
            llSetText("☀ Could not demote! ☀", <1, 0, 0>, 1.0);
            nextstate = "demote";
            state trampoline;
        }
        llSetText("☀ Renter demoted! ☀", <1, 1, 0>, 1.0);
        llSetTimerEvent(0);
 
        // Now clean up the rental and restart.
        llSetObjectDesc("");
        llResetScript();
    }
    timer() {
        llSetTimerEvent(0);
        nextstate = "demote";
        state trampoline;
    }
}