-- Time: An object representing a particular time of day, independent of time zone
Time = class(nil);
-- Constants
Time.lengthOfHour = 60 * 60;
Time.lengthOfDay = 24 * Time.lengthOfHour;
Time.lengthOfWeek = 7 * Time.lengthOfDay;
-- Class variables -- shared by all instances; updated by Time.SetLocalTimeOffset()
Time.localTimeOffset = 0; -- number that must be added to the server time to get the local time
Time.gameTimeOffset = 0 -- number that must be added to the game-time to get the local-time
Time.gameDayZeroLocalTime = 0; -- Local-time at which Game Day 0 began (which was at 3 am server time on that day)
Time.serverResetLocalTime = 0; -- local-time of a random occurrence of 3 am server time (i.e. server reset time)
Time.serverMidnightLocalTime = 0; -- local-time of a random occurrence of 12 am server time (i.e. midnight on the server)
Time.serverWeekStartLocalTime = 0; -- local-time of a random occurrence of 12 am Sunday morning server time (i.e. the start of a week)
-- Class functions -- do not require an instance
-- Sets the local time zone. Must be called before working with Time objects.
-- 'offsetHours' is the number of hours (possible noninteger) that must be added to the server time
-- to get the time in the local timezone.
function Time.SetLocalTimeOffset(offsetHours)
Time.localTimeOffset = offsetHours * Time.lengthOfHour;
local currentLocalTime = Turbine.Engine.GetLocalTime();
Time.gameTimeOffset = currentLocalTime - Turbine.Engine.GetGameTime();
Time.serverResetLocalTime = Time():GetNextServerTime(3, 0, 0, nil, false):GetLocalTime();
Time.serverMidnightLocalTime = Time.serverResetLocalTime - 3 * Time.lengthOfHour;
Time.serverWeekStartLocalTime = Time():GetNextServerTime(0, 0, 0, 1, false):GetLocalTime();
-- Calculate amount to be subtracted from local-time values such that server reset times occur at integer numbers of game days.
local approxLocalTimeLotroCreated = 1179817200;
local timeSinceBeginning = Time.serverResetLocalTime - approxLocalTimeLotroCreated;
local nextGameDay = round(timeSinceBeginning / Time.lengthOfDay);
Time.gameDayZeroLocalTime = Time.serverResetLocalTime - (nextGameDay * Time.lengthOfDay);
end
-- Converts a number of seconds to a display string such as "1d 2h 30m 45s"
-- If 'canonical' is true, then all components will be returned; if false,
-- approximations for long delays will be returned, such as "145 days".
-- If the argument is <= 0, returns "Now" (unless 'showNegativeTimes' is true).
function Time.GetDelayStr(delay, canonical, showNegativeTimes)
if (type(delay) ~= "number") then
if (IsA(delay, Time)) then
error("Time.GetDelayStr(): static function called with ':'", 2);
else
error("Time.GetDelayStr(): first argument must be a number", 2);
end
end
-- Format the time display string
local text;
delay = math.ceil(delay);
local s = math.floor(delay % 60);
local m = math.floor((delay % 3600) / 60);
local h = math.floor((delay % 86400) / 3600);
local d = math.floor(delay / 86400);
if (canonical) then
text = L:GetText("/Time/TimeDisplay/dhms");
text = string.gsub(string.gsub(string.gsub(string.gsub(text, "<d>", d), "<h>", h), "<m>", m), "<s>", s);
else
if (delay <= 0.5) then
if (showNegativeTimes and (delay <= -0.5)) then
local delayStr = Time.GetDelayStr(-delay, canonical, false);
text = L:GetText("/Time/TimeDisplay/NegativeTime");
text = string.gsub(text, "<delay>", delayStr);
else
text = L:GetText("/Time/TimeDisplay/Now");
end
elseif (delay < 60) then
text = L:GetText("/Time/TimeDisplay/s");
text = string.gsub(text, "<s>", s);
elseif (delay < 10 * 60) then
text = L:GetText("/Time/TimeDisplay/ms");
text = string.gsub(string.gsub(text, "<m>", m), "<s>", s);
elseif (delay < 60 * 60) then
text = L:GetText("/Time/TimeDisplay/m");
text = string.gsub(text, "<m>", m);
elseif (delay < 24 * 60 * 60) then
text = L:GetText("/Time/TimeDisplay/hm");
text = string.gsub(string.gsub(text, "<h>", h), "<m>", m);
elseif (delay < 36 * 60 * 60) then
text = L:GetText("/Time/TimeDisplay/dhm");
text = string.gsub(string.gsub(string.gsub(text, "<d>", d), "<h>", h), "<m>", m);
elseif (delay < 3 * 24 * 60 * 60) then
text = L:GetText("/Time/TimeDisplay/dh");
text = string.gsub(string.gsub(text, "<d>", d), "<h>", h);
else
if (h >= 12) then
return Time.GetDelayStr(delay + 12 * 60 * 60);
else
text = L:GetText("/Time/TimeDisplay/d");
text = string.gsub(text, "<d>", d);
end
end
end
return text;
end
-- Converts a delay string such as "1d 2h 30m 45s" into a number of seconds
function Time.GetNumericDelay(str)
local delay = 0;
local regexp = L:GetText("/Time/TimeInput/Regexp");
local size = L:GetText("/Time/TimeInput/Components");
for a, b in string.gmatch(str, regexp) do
delay = delay + a * size[b];
end
return delay;
end
-- Converts hours, minutes into "12:34" or "12h34"
function Time.GetTimeOfDayStr(hour, minute)
hour = string.format("%02d", tonumber(hour));
minute = string.format("%02d", tonumber(minute));
local str = L:GetText("/Time/TimeOfDay");
str = string.gsub(str, "<h>", hour);
str = string.gsub(str, "<m>", minute);
return str;
end
-- Extracts hours, minutes from "12:34" or "12h34"
function Time.GetNumericTimeOfDay(str)
local regexp = L:GetText("/Time/TimeOfDayRegexp");
hour, minute = string.match(str, regexp);
return hour, minute;
end
-- Returns a clone of the earlier of two Times
function Time.min(a, b)
if (a:IsBefore(b)) then
return a:Clone();
else
return b:Clone();
end
end
-- Returns a clone of the later of two Times
function Time.max(a, b)
if (a:IsAfter(b)) then
return a:Clone();
else
return b:Clone();
end
end
-- Object functions -- called on a specific Time instance
-- Constructor: If 'constant' is true, then attempting to modify the object later will generate errors.
function Time:Constructor(localTime, constant)
self.localTime = localTime or Turbine.Engine.GetLocalTime();
self.constant = constant;
end
function Time:_CheckConstant()
if (self.constant) then
error("Attempted to modify constant Time object. Clone it first.", 3);
end
end
function Time:SetLocalTime(localTime)
self:_CheckConstant();
self.localTime = localTime;
return self;
end
function Time:GetLocalTime()
return self.localTime;
end
function Time:GetGameDay()
local gameDay = (self.localTime - Time.gameDayZeroLocalTime) / Time.lengthOfDay;
return math.floor(gameDay);
end
function Time:GetGameTime()
return self.localTime - Time.gameTimeOffset;
end
function Time:SetGameTime(gameTime)
self:_CheckConstant();
self.localTime = gameTime + Time.gameTimeOffset;
return self;
end
-- Accepts a local time of day (specified as hours, minutes, seconds) and finds the next
-- Time at which that time of day will occur locally.
-- If 'dayOfWeek' is specified, then it is an integer (1...7 => Sunday...Saturday) and the
-- result will also be restricted to that particular weekday.
-- If 'excludeSelf' is true, this function will return a Time strictly greater than self.
-- If 'excludeSelf' is false, this function will return a Time >= self.
function Time:GetNextLocalTime(hour, minute, second, dayOfWeek, excludeSelf)
local currentLocalTime = Turbine.Engine.GetLocalTime();
local localDateInfo = Turbine.Engine.GetDate();
local nextLocalTime = currentLocalTime + (hour - localDateInfo.Hour) * Time.lengthOfHour;
nextLocalTime = nextLocalTime + (minute - localDateInfo.Minute) * 60;
nextLocalTime = nextLocalTime + (second - localDateInfo.Second);
local windowSize = Time.lengthOfDay;
if (dayOfWeek) then
nextLocalTime = nextLocalTime + (dayOfWeek - localDateInfo.DayOfWeek) * Time.lengthOfDay;
windowSize = Time.lengthOfWeek;
end
local nextTime = Time(nextLocalTime);
if (excludeSelf) then
return nextTime:RestrictToWindow(self:JustAfter(), windowSize);
else
return nextTime:RestrictToWindow(self, windowSize);
end
end
-- Accepts a server time of day (specified as hours, minutes, seconds) and finds the next
-- Time at which that time of day will occur on the server.
-- If 'dayOfWeek' is specified, then it is an integer (1...7 => Sunday...Saturday) and the
-- result will also be restricted to that particular weekday.
-- If 'excludeSelf' is true, this function will return a Time strictly greater than self.
-- If 'excludeSelf' is false, this function will return a Time >= self.
function Time:GetNextServerTime(hour, minute, second, dayOfWeek, excludeSelf)
local nextTime = self:GetNextLocalTime(hour, minute, second, dayOfWeek, false):AddSeconds(Time.localTimeOffset);
local windowSize = Time.lengthOfDay;
if (dayOfWeek) then
windowSize = Time.lengthOfWeek;
end
if (excludeSelf) then
return nextTime:RestrictToWindow(self:JustAfter(), windowSize);
else
return nextTime:RestrictToWindow(self, windowSize);
end
end
-- Returns a new Time corresponding to the next time it will be 03:00 server time.
-- If 'excludeSelf' is true, this function will return a Time strictly greater than self.
-- If 'excludeSelf' is false, this function will return a Time >= self.
function Time:GetNextServerResetTime(excludeSelf)
local serverResetTime = Time(Time.serverResetLocalTime);
if (excludeSelf) then
return serverResetTime:RestrictToWindow(self:JustAfter(), Time.lengthOfDay);
else
return serverResetTime:RestrictToWindow(self, Time.lengthOfDay);
end
end
-- Returns a new Time corresponding to the last time it was 03:00 server time.
-- If 'excludeSelf' is true, this function will return a Time strictly earlier than self.
-- If 'excludeSelf' is false, this function will return a Time <= self.
function Time:GetPrevServerResetTime(excludeSelf)
local serverResetTime = Time(Time.serverResetLocalTime);
local oneDayAgo = self:Clone():SubtractDays(1);
if (excludeSelf) then
return serverResetTime:RestrictToWindow(oneDayAgo, Time.lengthOfDay);
else
return serverResetTime:RestrictToWindow(oneDayAgo:JustAfter(), Time.lengthOfDay);
end
end
-- Returns a new Time corresponding to the next time it will be midnight server time.
-- If 'excludeSelf' is true, this function will return a Time strictly greater than self.
-- If 'excludeSelf' is false, this function will return a Time >= self.
function Time:GetNextServerMidnight(excludeSelf)
local serverMidnight = Time(Time.serverMidnightLocalTime);
if (excludeSelf) then
return serverMidnight:RestrictToWindow(self:JustAfter(), Time.lengthOfDay);
else
return serverMidnight:RestrictToWindow(self, Time.lengthOfDay);
end
end
-- Returns a new Time corresponding to the last time it was midnight server time.
-- If the Time is midnight server time, this function returns self:Clone():SubtractDays(1).
function Time:GetPrevServerMidnight()
local serverMidnight = Time(Time.serverMidnightLocalTime);
local oneDayAgo = self:Clone():SubtractDays(1);
if (excludeSelf) then
return serverMidnight:RestrictToWindow(oneDayAgo, Time.lengthOfDay);
else
return serverMidnight:RestrictToWindow(oneDayAgo:JustAfter(), Time.lengthOfDay);
end
end
-- Modifies the Time object by subtracting the indicated number of seconds. Returns self.
function Time:SubtractSeconds(sec)
self:_CheckConstant();
self.localTime = self.localTime - sec;
return self;
end
-- Modifies the Time object by subtracting the indicated number of hours. Returns self.
function Time:SubtractHours(hours)
self:_CheckConstant();
self.localTime = self.localTime - hours * Time.lengthOfHour;
return self;
end
-- Modifies the Time object by subtracting the indicated number of days. Returns self.
function Time:SubtractDays(days)
self:_CheckConstant();
self.localTime = self.localTime - days * Time.lengthOfDay;
return self;
end
-- Modifies the Time object by adding the indicated number of seconds. Returns self.
function Time:AddSeconds(sec)
self:_CheckConstant();
self.localTime = self.localTime + sec;
return self;
end
-- Modifies the Time object by adding the indicated number of hours. Returns self.
function Time:AddHours(hours)
self:_CheckConstant();
self.localTime = self.localTime + hours * Time.lengthOfHour;
return self;
end
-- Modifies the Time object by adding the indicated number of days. Returns self.
function Time:AddDays(days)
self:_CheckConstant();
self.localTime = self.localTime + days * Time.lengthOfDay;
return self;
end
-- Modifies the Time object by adding or subtracting an integer multiple of the specified increment,
-- such that the resulting time is >= 'startTime' and < one increment later.
-- 'incrementSize' -- the increment, in seconds
function Time:RestrictToWindow(startTime, incrementSize)
self:_CheckConstant();
startDifference = self:GetSecondsSince(startTime);
if (startDifference < 0) then
local incrementsNeeded = math.ceil(-startDifference / incrementSize);
self:AddSeconds(incrementsNeeded * incrementSize);
end
local endTime = startTime:Clone():AddSeconds(incrementSize);
endDifference = endTime:GetSecondsSince(self);
if (endDifference <= 0) then
local excessIncrements = 1 + math.floor(-endDifference / incrementSize);
self:SubtractSeconds(excessIncrements * incrementSize);
end
return self;
end
-- Subtracts another Time and returns the result in seconds.
function Time:GetSecondsSince(otherTime)
return (self.localTime - otherTime.localTime);
end
-- Returns a Time representing the time at which the current server week started,
-- i.e., the last time that server time was 0:00 on a Sunday morning (which could be now,
-- if the Time is 0:00 on a Sunday morning).
function Time:GetStartOfWeek()
return Time(Time.serverWeekStartLocalTime):RestrictToWindow(self:JustAfter(), Time.lengthOfWeek):SubtractDays(7);
end
-- Returns an integer, 1-7, representing Sunday..Saturday, indicating the server's current day of the week.
function Time:GetCurrentDayInWeek()
local startOfWeek = self:GetStartOfWeek();
local elapsedTime = self:GetSecondsSince(startOfWeek);
local day = 1 + math.floor(elapsedTime / Time.lengthOfDay);
return day;
end
-- Returns a Time representing the time at which the current n-day rotation started.
-- This will correspond to a Time in the recent past (or could be the current Time).
-- 'numDays' is n
-- 'changeHour' (optional) indicates the time of day on the server at which the rotation day changes (default = 3.0)
function Time:GetStartOfRotation(numDays, changeHour)
local changeHour = changeHour or 3.0;
local firstRotationStart = Time(Time.gameDayZeroLocalTime):AddHours(changeHour - 3.0);
local rotationLength = numDays * Time.lengthOfDay;
return firstRotationStart:RestrictToWindow(self:JustAfter(), rotationLength):SubtractDays(numDays);
end
-- Returns an integer 1...n, representing the server's current day in an n-day rotation.
-- 'numDays' is n
-- 'changeHour' (optional) indicates the time of day on the server at which the rotation day changes (default = 3.0)
function Time:GetDayInRotation(numDays, changeHour)
local startOfRotation = self:GetStartOfRotation(numDays, changeHour);
local elapsedTime = self:GetSecondsSince(startOfRotation);
local day = 1 + math.floor(elapsedTime / Time.lengthOfDay);
return day;
end
-- Modifies the Time object, advancing it to the the next occurrence of 'day' in an n-day rotation.
-- 'numDays' is n
-- 'day' is an integer 1...n
-- 'changeHour' (optional) indicates the time of day on the server at which the rotation day changes (default = 3.0)
function Time:SetDayInRotation(numDays, day, changeHour)
self:_CheckConstant();
local currentDay = self:GetDayInRotation(numDays, changeHour);
local daysNeeded = (numDays + day - currentDay) % numDays;
return self:AddDays(daysNeeded);
end
-- Time object comparison function
function Time:IsBefore(otherTime)
return (self.localTime < otherTime.localTime);
end
-- Time object comparison function
function Time:IsAfter(otherTime)
return (self.localTime > otherTime.localTime);
end
-- Time object comparison function
function Time:Equals(otherTime)
return (self.localTime == otherTime.localTime);
end
-- True if and only if the Time is 3 am server time.
function Time:IsServerResetTime()
local remainder = (self.localTime - Time.serverResetLocalTime) % Time.lengthOfDay;
return (remainder == 0);
end
function Time:Clone()
return Time(self.localTime);
end
-- Without modifying the Time object, returns a new Time that is just after (1 second after) it.
function Time:JustAfter()
return self:Clone():AddSeconds(1);
end
-- A special instance indicating the latest Time possible.
Time.huge = Time(math.huge, true);