corrade-lsl-templates – Rev 29

Subversion Repositories:
Rev:
///////////////////////////////////////////////////////////////////////////
//  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 = llParseString2List(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;
    }
}