forked from panda3d/panda3d
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathClockDelta.py
More file actions
334 lines (274 loc) · 12.8 KB
/
ClockDelta.py
File metadata and controls
334 lines (274 loc) · 12.8 KB
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
# ClockDelta provides the ability to use clock synchronization for
# distributed objects
from panda3d.core import ClockObject
from direct.directnotify import DirectNotifyGlobal
from direct.showbase import DirectObject
import math
# The following two parameters, NetworkTimeBits and
# NetworkTimePrecision, define the number of bits required to store a
# network time, and the number of ticks per second it represents,
# respectively. The tradeoff is the longest period of elapsed time we
# can measure, vs. the precision with which we can measure it.
# 16 and 100 give us precision to 1/100th of a second, with a range of
# +/- 5 minutes in a 16-bit integer. These are eminently tweakable,
# but the parameter types in toon.dc must match the number of bits
# specified here (i.e. int16 if NetworkTimeBits is 16; int32 if
# NetworkTimeBits is 32).
NetworkTimeBits = 16
NetworkTimePrecision = 100.0
# These values are derived from the above.
NetworkTimeMask = (1 << NetworkTimeBits) - 1
NetworkTimeSignedMask = NetworkTimeMask >> 1 # the max absolute value bits.
NetworkTimeTopBits = 32 - NetworkTimeBits
MaxTimeDelta = NetworkTimeSignedMask / NetworkTimePrecision
# This is the maximum number of seconds by which we expect our clock
# (or the server's clock) to drift over an hour.
ClockDriftPerHour = 1.0 # Is this generous enough?
# And the above, scaled into a per-second value.
ClockDriftPerSecond = ClockDriftPerHour / 3600.0
# How many seconds to insist on waiting before accepting a second
# resync request from another client.
P2PResyncDelay = 10.0
class ClockDelta(DirectObject.DirectObject):
"""
The ClockDelta object converts between universal ("network") time,
which is used for all network traffic, and local time (e.g. as
returned by getFrameTime() or getRealTime()), which is used for
everything else.
"""
notify = DirectNotifyGlobal.directNotify.newCategory('ClockDelta')
def __init__(self):
self.globalClock = ClockObject.getGlobalClock()
# self.delta is the relative delta from our clock to the
# server's clock.
self.delta = 0
# self.uncertainty represents the number of seconds plus or
# minus in which we are confident our delta matches the
# server's actual time. The initial value, None, represents
# infinity--we have no idea.
self.uncertainty = None
# self.lastResync is the time at which self.uncertainty
# was measured. It is important to remember because our
# uncertainty increases over time (due to relative clock
# drift).
self.lastResync = 0.0
self.accept("resetClock", self.__resetClock)
def getDelta(self):
return self.delta
def getUncertainty(self):
# Returns our current uncertainty with our clock measurement,
# as a number of seconds plus or minus. Returns None,
# representing infinite uncertainty, if we have never received
# a time measurement.
if self.uncertainty == None:
return None
now = self.globalClock.getRealTime()
elapsed = now - self.lastResync
return self.uncertainty + elapsed * ClockDriftPerSecond
def getLastResync(self):
# Returns the local time at which we last resynchronized the
# clock delta.
return self.lastResync
def __resetClock(self, timeDelta):
"""
this is called when the global clock gets adjusted
timeDelta is equal to the amount of time, in seconds,
that has been added to the global clock
"""
assert self.notify.debug(
"adjusting timebase by %f seconds" % timeDelta)
# adjust our timebase by the same amount
self.delta += timeDelta
def clear(self):
"""
Throws away any previous synchronization information.
"""
self.delta = 0
self.uncertainty = None
self.lastResync = 0.0
def resynchronize(self, localTime, networkTime, newUncertainty,
trustNew = 1):
"""resynchronize(self, float localTime, int32 networkTime,
float newUncertainty)
Accepts a new networkTime value, which is understood to
represent the same moment as localTime, plus or minus
uncertainty seconds. Improves our current notion of the time
delta accordingly.
"""
newDelta = (float(localTime) -
(float(networkTime) / NetworkTimePrecision))
self.newDelta(
localTime, newDelta, newUncertainty, trustNew = trustNew)
def peerToPeerResync(self, avId, timestamp, serverTime, uncertainty):
"""
Accepts an AI time and uncertainty value from another client,
along with a local timestamp value of the message from this
client which prompted the other client to send us its delta
information.
The return value is true if the other client's measurement was
reasonably close to our own, or false if the other client's
time estimate was wildly divergent from our own; the return
value is negative if the test was not even considered (because
it happened too soon after another recent request).
"""
now = self.globalClock.getRealTime()
if now - self.lastResync < P2PResyncDelay:
# We can't process this request; it came in on the heels
# of some other request, and our local timestamp may have
# been resynced since then: ergo, the timestamp in this
# request is meaningless.
assert self.notify.debug(
"Ignoring request for resync from %s within %.3f s." %
(avId, now - self.lastResync))
return -1
# The timestamp value will be a timestamp that we sent out
# previously, echoed back to us. Therefore we can confidently
# convert it back into our local time, even though we suspect
# our clock delta might be off.
local = self.networkToLocalTime(timestamp, now)
elapsed = now - local
delta = (local + now) / 2.0 - serverTime
gotSync = 0
if elapsed <= 0 or elapsed > P2PResyncDelay:
# The elapsed time must be positive (the local timestamp
# must be in the past), and shouldn't be more than
# P2PResyncDelay. If it does not meet these requirements,
# it must be very old indeed, or someone is playing tricks
# on us.
self.notify.info(
"Ignoring old request for resync from %s." % (avId))
else:
# Now the other client has told us his delta and uncertainty
# information, which was generated somewhere in the range
# [-elapsed, 0] seconds ago. That means our complete window
# is wider by that amount.
self.notify.info(
"Got sync +/- %.3f s, elapsed %.3f s, from %s." %
(uncertainty, elapsed, avId))
delta -= elapsed / 2.0
uncertainty += elapsed / 2.0
gotSync = self.newDelta(local, delta, uncertainty, trustNew = 0)
return gotSync
def newDelta(self, localTime, newDelta, newUncertainty,
trustNew = 1):
"""
Accepts a new delta and uncertainty pair, understood to
represent time as of localTime. Improves our current notion
of the time delta accordingly. The return value is true if
the new measurement was used, false if it was discarded.
"""
oldUncertainty = self.getUncertainty()
if oldUncertainty != None:
self.notify.info(
'previous delta at %.3f s, +/- %.3f s.' %
(self.delta, oldUncertainty))
self.notify.info(
'new delta at %.3f s, +/- %.3f s.' %
(newDelta, newUncertainty))
# Our previous measurement was self.delta +/- oldUncertainty;
# our new measurement is newDelta +/- newUncertainty. Take
# the intersection of both.
oldLow = self.delta - oldUncertainty
oldHigh = self.delta + oldUncertainty
newLow = newDelta - newUncertainty
newHigh = newDelta + newUncertainty
low = max(oldLow, newLow)
high = min(oldHigh, newHigh)
# If there is no intersection, whoops! Either the old
# measurement or the new measurement is completely wrong.
if low > high:
if not trustNew:
self.notify.info('discarding new delta.')
return 0
self.notify.info('discarding previous delta.')
else:
newDelta = (low + high) / 2.0
newUncertainty = (high - low) / 2.0
self.notify.info(
'intersection at %.3f s, +/- %.3f s.' %
(newDelta, newUncertainty))
self.delta = newDelta
self.uncertainty = newUncertainty
self.lastResync = localTime
return 1
### Primary interface functions ###
def networkToLocalTime(self, networkTime, now = None, bits = 16,
ticksPerSec=NetworkTimePrecision):
"""networkToLocalTime(self, int networkTime)
Converts the indicated networkTime to the corresponding
localTime value. The time is assumed to be within +/- 5
minutes of the current local time given in now, or
getRealTime() if now is not specified.
"""
if now == None:
now = self.globalClock.getRealTime()
# Are we in non-real-time mode (i.e. filming a movie)? If you
# set movie-network-time 1, then we'll circumvent this logic
# and always return now.
if self.globalClock.getMode() == ClockObject.MNonRealTime and \
base.config.GetBool('movie-network-time', False):
return now
# First, determine what network time we have for 'now'.
ntime = int(math.floor(((now - self.delta) * ticksPerSec) + 0.5))
# The signed difference between these is the number of ticks
# by which the network time differs from 'now'.
if bits == 16:
diff = self.__signExtend(networkTime - ntime)
else:
# Assume the bits is either 16 or 32. If it's 32, no need
# to sign-extend. 32 bits gives us about 227 days of
# continuous timestamp.
diff = networkTime - ntime
return now + float(diff) / ticksPerSec
def localToNetworkTime(self, localTime, bits = 16,
ticksPerSec=NetworkTimePrecision):
"""localToNetworkTime(self, float localTime)
Converts the indicated localTime to the corresponding
networkTime value.
"""
ntime = int(math.floor(((localTime - self.delta) * ticksPerSec) + 0.5))
if bits == 16:
return self.__signExtend(ntime)
else:
# Assume the bits is either 16 or 32. If it's 32, no need
# to sign-extend. 32 bits gives us about 227 days of
# continuous timestamp.
return ntime
### Convenience functions ###
def getRealNetworkTime(self, bits=16,
ticksPerSec=NetworkTimePrecision):
"""
Returns the current getRealTime() expressed as a network time.
"""
return self.localToNetworkTime(self.globalClock.getRealTime(),
bits=bits,
ticksPerSec=ticksPerSec)
def getFrameNetworkTime(self, bits=16,
ticksPerSec=NetworkTimePrecision):
"""
Returns the current getFrameTime() expressed as a network time.
"""
return self.localToNetworkTime(self.globalClock.getFrameTime(),
bits=bits,
ticksPerSec=ticksPerSec)
def localElapsedTime(self, networkTime, bits=16,
ticksPerSec=NetworkTimePrecision):
"""localElapsedTime(self, int networkTime)
Returns the amount of time elapsed (in seconds) on the client
since the server message was sent. Negative values are
clamped to zero.
"""
now = self.globalClock.getFrameTime()
dt = now - self.networkToLocalTime(networkTime, now, bits=bits,
ticksPerSec=ticksPerSec)
return max(dt, 0.0)
### Private functions ###
def __signExtend(self, networkTime):
"""__signExtend(self, int networkTime)
Preserves the lower NetworkTimeBits of the networkTime value,
and extends the sign bit all the way up.
"""
r = ((networkTime+32768) & NetworkTimeMask) - 32768
assert -32768 <= r <= 32767
return r
globalClockDelta = ClockDelta()