Table of Contents

About

The script is a countdown script that displays the number of years, months, days, hours and minutes till a certain set term using a configuration notecard.

Exports

Setup

To set-up the object, you will need a 5-pieces xyzzytext text board. In the root primitive, you can add the script below along with a notecard called configuration.

A sample configuration notecard contains the following:

#### START CONFIGURATION ####

# goal in llGetTimestamp format
# YYYY-MM-DDTHH:MM:SS.MSZ
alarm = 2014-07-27T06:00:00.000000Z

# repeat every: yearly, monthly, weekly, daily, hourly
# this is an optional parameter and can be commented out
repeat = weekly

# the text to display when the countdown reaches the alarm
alarm_text = GOAL REACHED!

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

The alarm has to be set in UTC time in the format YYYY-MM-DDTHH:MM:SS.MS where:

The repeat can be set to either: yearly, monthly, weekly, daily or hourly.

For example, you could set the alarm to:

alarm = 2014-07-29T13:37:52.270346Z

and with a repeat of yearly:

repeat = yearly

which will make the countdown trigger the alarm every year on the 29th July at time 13:37:52.

Code

countdown.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2021 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueEncode(list data) {
    list k = llList2ListStrided(data, 0, -1, 2);
    list v = llList2ListStrided(llDeleteSubList(data, 0, 0), 0, -1, 2);
    data = [];
    do {
        data += llList2String(k, 0) + "=" + llList2String(v, 0);
        k = llDeleteSubList(k, 0, 0);
        v = llDeleteSubList(v, 0, 0);
    } while(llGetListLength(k) != 0);
    return llDumpList2String(data, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
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: GNU GPLv3    //
//     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 ];
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
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: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer getYearDays(integer year) {
    integer leap = (year % 4 == 0 && year % 100 != 0) || 
            (year % 400 == 0);
    if(leap == TRUE) {
        return 366;
    }
    return 365;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
integer getMonthDays(integer month, integer year) {
    if (month == 4 || month == 6 || month == 9 || month == 11) {
        return 30;
    }
    if(month == 2) {
        integer leap = (year % 4 == 0 && year % 100 != 0) || 
            (year % 400 == 0);
        if(leap == TRUE) {
            return 29;
        }
        return 28;
    }
    return 31;
}
 
// for notecard reading
integer line = 0;
 
// key-value data will be read into this list
list tuples = [];
 
default {
    state_entry() {
        if(llGetInventoryType(llGetInventoryName(INVENTORY_NOTECARD, 0)) != INVENTORY_NOTECARD) {
            llSay(DEBUG_CHANNEL, "Sorry, could not find an inventory notecard.");
            return;
        }
        llGetNotecardLine(
            llGetInventoryName(
                INVENTORY_NOTECARD, 
                0
            ),
            line
        );
    }
    dataserver(key id, string data) {
        if(data == EOF) state countdown; // invariant, length(tuples) % 2 == 0
        if(data == "") {
            llGetNotecardLine(
                llGetInventoryName(
                    INVENTORY_NOTECARD,
                    0
                ), 
                ++line
            );
            return;
        }
        integer i = llSubStringIndex(data, "#");
        if(i != -1) data = llDeleteSubString(data, i, -1);
        list o = llParseString2List(data, ["="], []);
        string k = llStringTrim(llList2String(o, 0), STRING_TRIM);
        string v = llStringTrim(llList2String(o, 1), STRING_TRIM);
        if(k == "" || v == "") {
            llGetNotecardLine(
                llGetInventoryName(
                    INVENTORY_NOTECARD,
                    0
                ), 
                ++line
            );
            return;
        }
        tuples += k;
        tuples += v;
        llGetNotecardLine(
            llGetInventoryName(
                INVENTORY_NOTECARD,
                0
            ), 
            ++line
        );
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}
 
state countdown {
    state_entry() {
        llSetTimerEvent(1);
    }
    timer() {
        list alarm = llList2List(
            llParseString2List(
                wasKeyValueGet("alarm", wasKeyValueEncode(tuples)), 
                ["-",":","T", "."],[""]
            ), 
            0, 5
        );
        list stamp = llList2List(
            llParseString2List(
                llGetTimestamp(),
                ["-",":","T", "."],[""]
            ),
            0, 5
        );
 
        integer delta = wasDateTimeToStamp(
            llList2Integer(alarm, 0),
            llList2Integer(alarm, 1),
            llList2Integer(alarm, 2),
            llList2Integer(alarm, 3),
            llList2Integer(alarm, 4),
            llList2Integer(alarm, 5)
        ) - wasDateTimeToStamp(
            llList2Integer(stamp, 0),
            llList2Integer(stamp, 1),
            llList2Integer(stamp, 2),
            llList2Integer(stamp, 3),
            llList2Integer(stamp, 4),
            llList2Integer(stamp, 5)
        );
 
        list count = [];
 
        // handle repeat
        if(delta < 0) {
            string repeat = wasKeyValueGet("repeat", wasKeyValueEncode(tuples));
            if(repeat == "") {
                count += wasKeyValueGet("alarm_text", wasKeyValueEncode(tuples));
                jump display;
            }
            if(repeat == "yearly") {
                delta = (getYearDays(llList2Integer(stamp, 0)) * 86400)-(-delta % (getYearDays(llList2Integer(stamp, 0)) * 86400));
                jump set;
            }
            if(repeat == "monthly") {
                delta = (getMonthDays(llList2Integer(stamp, 1), llList2Integer(stamp, 0)) * 86400) - (-delta % (getMonthDays(llList2Integer(stamp, 1), llList2Integer(stamp, 0)) * 86400));
                jump set;
            }
            if(repeat == "weekly") {
                delta = 7*86400-(-delta % (7*86400));
                jump set;
            }
            if(repeat == "daily") {
                delta = 86400-(-delta % 86400);
                jump set;
            }
            if(repeat == "hourly") {
                delta = 3600-(-delta % 3600);
                jump set;
            }
            if(repeat == "minutely") {
                delta = 60-(-delta % 60);
                jump set;
            }
        }
@set;
        // get date and remove epoch 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);
 
        // start display
        integer Y = llList2Integer(remaining, 0);
        if(Y == 0) jump MO;
        count += (string)Y;
        if(Y > 1) {
            count += "years";
            jump MO;
        }
        count += "year";
@MO;
        integer M = llList2Integer(remaining, 1);
        if(M == 0) jump DA;
        count += (string)M;
        if(M > 1) {
            count += "months";
            jump DA;
        }
        count += "month";
@DA;
        integer D = llList2Integer(remaining, 2);
        if(D == 0) jump HO;
        count += (string)D;
        if(D > 1) {
            count += "days";
            jump HO;
        }
        count += "day";
@HO;
        integer H = llList2Integer(remaining, 3);
        if(H == 0) jump MI;
        count += (string)H;
        if(H > 1) {
            count += "hours";
            jump MI;
        }
        count += "hour";
@MI;
        integer I = llList2Integer(remaining, 4);
        if(I == 0) jump done;
        count += (string)I;
        if(I > 1) {
            count += "minutes";
            jump done;
        }
        count += "minute";  
@done;
        if(llGetListLength(count) == 0) {
            count += wasKeyValueGet("alarm_text", wasKeyValueEncode(tuples));
            jump display;
        }
        count += "left";
 
@display;
 
        // pad with spaces so the message appears centered
        string result = llDumpList2String(count, " ");
        integer spaces = (60 - llStringLength(result))/2;
        string space = "";
        do {
            space += " ";
        } while(--spaces >-1);
 
        // send the spaces and result to xyz text
        llMessageLinked(LINK_ROOT, 204000, space + result, "0");
 
        // reschedule every minute
        llSetTimerEvent(60);
    }
    changed(integer change) {
        if(change & CHANGED_INVENTORY) llResetScript();
    }
    on_rez(integer num) {
        llResetScript();
    }
}