aboutsummaryrefslogtreecommitdiff
path: root/advtrains_line_automation/railwaytime.lua
blob: bc32ed435f515a63e94d2b24124b9390b51d3555 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
-- railwaytime.lua
-- Advtrains uses a desynchronized time for train movement. Everything is counted relative to this time counter.
-- The advtrains-internal time is in no way synchronized to the real-life time, due to:
-- - Lag
-- - Server stops/restarts
-- However, this means that implementing a "timetable" system using the "real time" is not practical. Therefore,
-- we introduce a custom time system, the RWT(Railway Time), which has nothing to do with RLT(Real-Life Time)
-- RWT has a time cycle of 1 day. This should be sufficient for most train lines that will ever be built in Minetest.
-- A RWT looks like this:    14;37;25
-- The ; is to distinguish it from a normal RLT (which has colons e.g. 12:34:56). Left number is hours, middle number is minutes, right number is seconds.
-- The minimum RWT is 00;00;00, the maximum is 23;59;59.
-- There is an "adapt mode", which was proposed by gpcf, and results in RWT automatically adapting itself to real-world time.
-- It works by shifting the hour/minute/second after the realtime hour/minute/second, adjusting the cycle value as needed.
--[[
There is an important distinction between:

## Time (absolute time) ##
Represents a time instant.
It is identified by the 4 values cycle (day), hour, minute and second. These values are never negative.

In string form, it is written as C;HH;MM;SS.
The following short forms are allowed to display a RWT (but they do not uniquely identify a RWT):
HH;MM;SS
HH;MM
It is discouraged to use the string form to store RWTs, the table form or seconds form is better.

In table form:
{ s=45, m=23, h=12, c=1 }

In seconds form:
c*24*60*60 + h*60*60 + m*60 + s

Absolute time wraps around at a boundary of 7 cycles (valid cycle values are 0 to 6). If the difference between instants A and B
is < 3.5 days, B is considered in the future, otherwise B is considered in the past.
The RWT functions take this wrapping into account.

## Interval (relative time) ##
Represents a difference between times.

It is commonly represented in seconds form as a number of seconds.
It can be formatted as a string e.g.
 90 seconds = "+01;30"
-90 seconds = "-01;30"
When converting to a string, a + or - must be prepended.
When parsing a string it is permissible to leave out the + if it is clear from the context that
an interval is parsed.
The meaning of string short forms differ from the time instant case:
[+-]MM;SS (meaning when interval is parsed FROM a string, and the default when converting to string and HH==0 and C==0)
[+-]HH;MM;SS (when HH!=0 and C==0)
[+-]C;HH;MM;SS (when all !=0)
[+-]HH;MM (permissible only when converting TO a string, to be used when it is displayed alongside an instant e.g. 05;30+00;04)


]]--


local rwt = {}

local CYCLE = 24*60*60
local NCYC = 7
local MODULUS = CYCLE*NCYC
local MODULUS_H = MODULUS / 2

rwt.CYCLE = CYCLE
rwt.NCYC = NCYC
rwt.MODULUS = MODULUS

-- adjust so that it is in range [-MODULUS/2, MODULUS/2)
local function i_adjust(sec)
	return ((sec + MODULUS_H) % MODULUS) - MODULUS_H
end

--Time Stamp (Seconds since start of world)
local e_time = 0
local e_has_loaded = false

local setting_rwt_real = minetest.settings:get("advtrains_lines_rwt_realtime")
if setting_rwt_real=="" then
	setting_rwt_real = "independent"
end

local e_last_epoch -- last real-time timestamp

-- Advance RWT to match minute/second to the current real-world time
-- only accounts for the minute/second part, leaves hour/cycle untouched
local function adapt_real_time()
	local datetab = os.date("*t")
	local real_sectotal = 60*datetab.min + datetab.sec
	
	local rwttab = rwt.now()
	local rwt_sectotal = 60*rwttab.m + rwttab.s
	
	--calculate the difference and take it %3600 (seconds/hour) to always move forward
	local secsfwd = (real_sectotal - rwt_sectotal) % 3600
	
	atlog("[lines][rwt] Skipping",secsfwd,"seconds forward to sync rwt (",rwt.to_string(rwttab),") to real time (",os.date("%H:%M:%S"),")")
	
	e_time = (e_time + secsfwd) % MODULUS
end

function rwt.set_time(t)
	e_time = t or 0
	if setting_rwt_real == "adapt_real" then
		adapt_real_time()
	end
	atlog("[lines][rwt] Initialized railway time: ",rwt.to_string(e_time))
	e_last_epoch = os.time()
	
	e_has_loaded = true
end

function rwt.get_time()
	return e_time
end

function rwt.step(dt)
	if not e_has_loaded then
		rwt.set_time(0)
	end

	if setting_rwt_real=="independent" then
		-- Regular stepping with dtime
		e_time = (e_time + dt) % MODULUS
	else
		-- advance with real-world time
		local diff = os.time() - e_last_epoch
		e_last_epoch = os.time()
		
		if diff>0 then
			e_time = (e_time + diff) % MODULUS
		end
	end
end

function rwt.now()
	return rwt.to_table(e_time)
end

function rwt.new_t(c, h, m, s)
	assert(c>=0 and c<7 and c==math.floor(c))
	assert(h>=0 and h<24 and h==math.floor(h))
	assert(m>=0 and m<60 and m==math.floor(m))
	assert(s>=0 and s<60)
	return {
		c = c or 0,
		h = h or 0,
		m = m or 0,
		s = s or 0
	}
end

function rwt.new_i(sign, c, h, m, s)
	assert(math.abs(sign)==1)
	assert(c>=0 and c<3 and c==math.floor(c)) -- interval may be max. +-3 days to avoid overflow!
	assert(h>=0 and h<24 and h==math.floor(h))
	assert(m>=0 and m<60 and m==math.floor(m))
	assert(s>=0 and s<60)
	
	return sign * (c*24*60*60 + h*60*60 + m*60 + s)
end


function rwt.copy(rwtime)
	local rwtimet = rwt.to_table(rwtime)
	return {
		c = rwtimet.c or 0,
		h = rwtimet.h or 0,
		m = rwtimet.m or 0,
		s = rwtimet.s or 0
	}
end

function rwt.to_table(rwtime)
	if type(rwtime) == "table" then
		return rwtime
	elseif type(rwtime) == "string" then
		return rwt.parse_t(rwtime)
	elseif type(rwtime) == "number" then
		local res = {}
		local seconds = atfloor(rwtime)
		res.s = seconds % 60
		local minutes = atfloor(seconds/60)
		res.m = minutes % 60
		local hours = atfloor(minutes/60)
		res.h = hours % 24
		res.c = atfloor(hours/24)
		return res
	end
end

local function intv_to_table(rwintp)
	if type(rwintp) == "number" then
		local res = {}
		local rwint = math.abs(rwintp)
		if rwint~=0 then
			res.sign = rwintp/rwint -- 1 or -1
		else
			res.sign = 1
		end
		local seconds = atfloor(rwint)
		res.s = seconds % 60
		local minutes = atfloor(seconds/60)
		res.m = minutes % 60
		local hours = atfloor(minutes/60)
		res.h = hours % 24
		res.c = atfloor(hours/24)
		return res
	else
		error("intv_to_table needs number")
	end
end

-- time instant to seconds
function rwt.t_sec(rwtime)
	local res = rwtime
	if type(rwtime) == "string" then
		res = rwt.parse_t(rwtime)
	elseif type(rwtime) == "number" then
		return rwtime
	end
	if type(res)=="table" then
		return res.c*60*60*24 + res.h*60*60 + res.m*60 + res.s
	end
end

-- interval to seconds
function rwt.i_sec(rwint)
	if type(rwint) == "string" then
		return rwt.parse_i(rwint)
	elseif type(rwint) == "number" then
		return rwint
	elseif type(rwint)=="table" then
		return res.sign * (res.c*60*60*24 + res.h*60*60 + res.m*60 + res.s)
	end
end
rwt.to_secs = rwt.i_sec -- deprecated alias

-- time to string
-- fmt:
-- 0, nil or false: full format C;HH;MM;SS
-- 1 or true: format HH;MM;SS
-- 2: format MM;SS
-- 3: format HH;MM
function rwt.t_str(rwtime_p, fmt)
	local rwtime = rwt.to_table(rwtime_p)
	if fmt==0 or not fmt then
		return string.format("%d;%02d;%02d;%02d", rwtime.c, rwtime.h, rwtime.m, rwtime.s)
	elseif fmt==1 or fmt==true then
		return string.format("%02d;%02d;%02d", rwtime.h, rwtime.m, rwtime.s)
	elseif fmt==2 then
		return string.format("%02d;%02d", rwtime.m, rwtime.s)
	elseif fmt==3 then
		return string.format("%02d;%02d", rwtime.h, rwtime.m)
	end
end
-- compatibility alias
rwt.to_string = rwt.t_str

-- interval to string
function rwt.i_str(rwint_p, omit_plus, omit_second)
	local rwint = intv_to_table(rwint_p)
	-- sign
	local sic = rwint.sign==1 and (omit_plus and "" or "+") or "-"
	-- find out formatting
	if omit_second then
		if rwint.c~=0 then
			return string.format("%s%d;%02d;%02d", sic, rwint.c, rwint.h, rwint.m)
		else
			return string.format("%s%02d;%02d", sic, rwint.h, rwint.m)
		end
	else
		if rwint.c~=0 then
			return string.format("%s%d;%02d;%02d;%02d", sic, rwint.c, rwint.h, rwint.m, rwint.s)
		elseif rwint.h~=0 then
			return string.format("%s%02d;%02d;%02d", sic, rwint.h, rwint.m, rwint.s)
		else
			return string.format("%s%02d;%02d", sic, rwint.m, rwint.s)
		end
	end
end

---

local function v_n(str)
	if str == "" then
		return 0
	end
	return tonumber(str)
end

-- parse an interval (the default case - parsing an instant is rare)
function rwt.parse(str)
	--atdebug("parse",str)
	--4-value form
	local sic, str_c, str_h, str_m, str_s = string.match(str, "^([%-%+]?)(%d?%d?);(%d%d);(%d%d);(%d?%d?)$")
	if sic and str_c and str_h and str_m and str_s then
		--atdebug("4v", sic, str_c, str_h, str_m, str_s)
		local c, h, m, s = v_n(str_c), v_n(str_h), v_n(str_m), v_n(str_s)
		if c and h and m and s then
			return rwt.new_i(sic=="-" and -1 or 1, c,h,m,s)
		end
	end
	--3-value form
	local sic, str_h, str_m, str_s = string.match(str, "^([%-%+]?)(%d?%d?);(%d%d);(%d?%d?)$")
	if sic and str_h and str_m and str_s then
		--atdebug("3v", sic, str_h, str_m, str_s)
		local c, h, m, s = 0, v_n(str_h), v_n(str_m), v_n(str_s)
		if c and h and m and s then
			return rwt.new_i(sic=="-" and -1 or 1, c,h,m,s)
		end
	end
	--2-value form
	local sic, str_m, str_s = string.match(str, "^([%-%+]?)(%d?%d?);(%d?%d?)$")
	if sic and str_m and str_s then
		--atdebug("2v", sic, str_m, str_s)
		local c, h, m, s = 0, 0, v_n(str_m), v_n(str_s)
		if c and h and m and s then
			return rwt.new_i(sic=="-" and -1 or 1, c,h,m,s)
		end
	end
end
rwt.parse_i = rwt.parse

-- parse a time instant - only the 4-value form is allowed!
function rwt.parse_t(str)
	--atdebug("parse",str)
	--4-value form
	local str_c, str_h, str_m, str_s = string.match(str, "^(%d?%d?);(%d%d);(%d%d);(%d?%d?)$")
	if str_c and str_h and str_m and str_s then
		--atdebug("4v", str_c, str_h, str_m, str_s)
		local c, h, m, s = v_n(str_c), v_n(str_h), v_n(str_m), v_n(str_s)
		if c and h and m and s then
			return rwt.new_t(c,h,m,s)
		end
	end
end

---

-- add a time and an interval. The result is a time (table form)
function rwt.add(t1, i)
	local t1s = rwt.t_sec(t1)
	local t2s = rwt.i_sec(i)
	return rwt.to_table( (t1s + t2s) % MODULUS )
end

-- subtract a time and an interval. The result is a time (table form)
function rwt.sub(t1, i)
	local t1s = rwt.t_sec(t1)
	local t2s = rwt.i_sec(i)
	return rwt.to_table( (t1s - t2s) % MODULUS )
end


-- How many seconds FROM t1 TO t2 (two time instants, result is an interval)
function rwt.diff(t1, t2)
	local t1s = rwt.t_sec(t1)
	local t2s = rwt.t_sec(t2)
	return i_adjust(t2s - t1s)
end

-- Whether t1 is (strictly) after t2
function rwt.is_after(t1, t2)
	return rwt.diff(t2,t1) > 0
end

-- Whether t1 is (strictly) before t2
function rwt.is_before(t1, t2)
	return rwt.diff(t1,t2) > 0
end

-- Helper functions for handling "repeating times" (rpt)
-- Those are generic declarations for time intervals like "every 5 minutes", with an optional offset
-- Note the following:
-- - rwtime is a time instant, rpt_interval and rpt_offset are intervals
-- - repeating times are hard-capped at cycle (24-hour) boundary
-- - if rpt_interval is given as nil, there is exactly 1 match per cycle and it is at X;00;00;00 + offset ( equals a time-of-day)
-- - the offset determines where the "reference cycle" starts e.g. if the off=05;00;00 and int=03;45;00, starting from 0;05;00 the times would be
--   05;00, 08;45, ..., 20;00, 23;45, 1;03;30, then next cycle starts 1;05;00
-- - if the cycle is not a multiple of the interval like in the previous example, the very last instance is not considered when it is closer than interval/2 to the cycle boundary
--   in the example above the distance between 1;03;30 and 1;05;00 is only 90s, which is smaller than interval/2
--   It is preferred to therefore have a longer interval
--   and the actual sequence will be ..., 20;00, 23;45, 1;05;00

-- helper func to return a table of {
-- rwtime_s, rpti_s, rpto_s (rwtime, interval and offset in seconds)
-- ctime (time into current relative cycle [rpto; rpto+CYCLE) )
-- rtime (last/next occurence of the rpt (time into cycle))
-- cycle ( number of relative cycle = cycle no of the current cycle start time)
-- inst ( instance number within the cycle )
-- }
function rwt.rpt_getinfo(rwtime, rpt_interval, rpt_offset, next)
	if not rpt_interval then rpt_interval = CYCLE end
	local t = {}
	t.rwtime = rwt.t_sec(rwtime)
	t.rpti   = rwt.i_sec(rpt_interval)
	t.rpto   = rwt.i_sec(rpt_offset)
	-- reduce to cycle
	local trel = (t.rwtime - t.rpto) % MODULUS
	t.cycle = atfloor(trel / CYCLE)
	t.ctime = trel % CYCLE -- time into current cycle, offset by the rpt offset
	t.rtime = t.ctime - (t.ctime % t.rpti) -- last rpt occurence time into cycle
	if next then
		-- next requested, cutoff to cycle
		t.rtime = t.rtime + t.rpti -- next rpt occurence time into cycle
		if t.rtime > (CYCLE - t.rpti/2) then -- last time cutoff rule, and overflow of cycle
			t.rtime = CYCLE
			t.cycle = (t.cycle + 1) % NCYC
		end
	else
		if t.rtime > (CYCLE - t.rpti/2) then -- last time cutoff rule
			t.rtime = t.rtime - t.rpti
		end
	end
	-- determine instance
	if t.rtime == CYCLE then -- next rounded up case
		t.inst = 0
	else
		t.inst = atfloor(t.rtime / t.rpti) -- this should be exact, theoretically no need for floor
	end
	return t
end

-- Get the time (in seconds) until the next time this rpt occurs
function rwt.time_to_next_rpt(rwtime, rpt_interval, rpt_offset)
	local t = rwt.rpt_getinfo(rwtime, rpt_interval, rpt_offset, true)
	return t.rtime - t.ctime, t.cycle, t.inst
end


-- Get the time (in seconds) since the last time this rpt occured
function rwt.time_from_last_rpt(rwtime, rpt_interval, rpt_offset)
	local t = rwt.rpt_getinfo(rwtime, rpt_interval, rpt_offset, false)
	return t.ctime - t.rtime, t.cycle, t.inst
end

-- From rwtime, get the next time that this rpt matches
function rwt.next_rpt(rwtime, rpt_interval, rpt_offset)
	local time_to_next, c, i = rwt.time_to_next_rpt(rwtime, rpt_interval, rpt_offset)
	return rwt.add(rwtime, time_to_next), c, i
end

-- from rwtime, get the last time that this rpt matched
function rwt.last_rpt(rwtime, rpt_interval, rpt_offset)
	local time_from_last, c, i = rwt.time_from_last_rpt(rwtime, rpt_interval, rpt_offset)
	return rwt.sub(rwtime, time_from_last), c, i
end

advtrains.lines.rwt = rwt