# ClockDelta provides the ability to use clock synchronization for
# distributed objects
from panda3d.core import ClockObject, ConfigVariableBool
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
[docs]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')
[docs] 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)
[docs] def getDelta(self):
return self.delta
[docs] 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 is None:
return None
now = self.globalClock.getRealTime()
elapsed = now - self.lastResync
return self.uncertainty + elapsed * ClockDriftPerSecond
[docs] 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
[docs] def clear(self):
"""
Throws away any previous synchronization information.
"""
self.delta = 0
self.uncertainty = None
self.lastResync = 0.0
[docs] 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)
[docs] 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
[docs] 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 is not 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 ###
[docs] 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 is 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 \
ConfigVariableBool('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
[docs] 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 ###
[docs] 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)
[docs] 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)
[docs] 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()