Merge branch 'beta-wip' into 'master'

7-21 update

mostly bugfixes and recovery from double access weekend
This commit is contained in:
jjkoletar 2014-07-21 19:44:48 -06:00
commit 9030daccf9
9 changed files with 189 additions and 46 deletions

View file

@ -1,6 +1,6 @@
{
"__fyi__": "If you use anything other than the first 7 characters of the git hash, you just broke everything",
"astron": "a0608a9",
"astron": "1ebbf32",
"panda3d": "d048f43",
"version-prefix": "ttr-beta-",
"server-resources": ["dna", "xml", "txt", "dat", "bam"]

View file

@ -0,0 +1,69 @@
from MagicWordGlobal import *
from direct.showbase.GarbageReport import GarbageLogger
from direct.showbase.ContainerReport import ContainerReport
from direct.directnotify.DirectNotifyGlobal import *
import gc
category = CATEGORY_SYSADMIN if game.process == 'server' else CATEGORY_DEBUG
notify = directNotify.newCategory('DiagnosticMagicWords')
def aiPrefix(func):
"""Prefixes `func`'s name with 'ai' if this is an AI server."""
if game.process == 'server':
func.func_name = 'ai' + func.func_name
return func
@magicWord(category=category)
@aiPrefix
def garbage(arg=''):
"""Reports the total garbage use for this process."""
flags = arg.split()
GarbageLogger('~garbage', fullReport=('full' in flags), threaded=True,
safeMode=('safe' in flags), delOnly=('delonly' in flags))
return 'Garbage report is now being written to log...'
@magicWord(category=category)
@aiPrefix
def heap():
"""Counts the number of objects in Python's object memory."""
return '%d active objects (%d garbage)' % (len(gc.get_objects()),
len(gc.garbage))
@magicWord(category=category, types=[int])
@aiPrefix
def objects(minimum=30):
"""Write the objects down to log."""
cls_counts = {}
objs = gc.get_objects()
for obj in objs:
cls = getattr(obj, '__class__', None) or type(obj)
cls_counts[cls] = cls_counts.get(cls, 0) + 1
classes = cls_counts.keys()
classes.sort(key=lambda x: cls_counts[x], reverse=True)
notify.info('=== OBJECT TYPES REPORT: ===')
for cls in classes:
if cls_counts[cls] < minimum: continue # Not notable enough...
notify.info('%s: %s' % (repr(cls), cls_counts[cls]))
notify.info('============================')
return 'Wrote object types to log.'
@magicWord(category=category, types=[int])
@aiPrefix
def containers(limit=30):
"""Write the container report to log."""
ContainerReport('~containers', log=True, limit=limit, threaded=True)
return 'Writing container report to log...'

View file

@ -59,6 +59,7 @@ from toontown.tutorial.TutorialManagerAI import TutorialManagerAI
# Magic Words!
from panda3d.core import PStatClient
from otp.ai.MagicWordGlobal import *
import otp.ai.DiagnosticMagicWords
class ToontownAIRepository(ToontownInternalRepository):
def __init__(self, baseChannel, serverId, districtName):
@ -121,10 +122,11 @@ class ToontownAIRepository(ToontownInternalRepository):
self.createGlobals()
self.createZones()
self.distributedDistrict.b_setAvailable(1)
self.statusSender.start()
self.distributedDistrict.b_setAvailable(1)
self.notify.info('District is now ready.')
def incrementPopulation(self):
self.districtStats.b_setAvatarCount(self.districtStats.getAvatarCount() + 1)
self.statusSender.sendStatus()

View file

@ -2,6 +2,8 @@ import time
from panda3d.core import *
from toontown.toonbase import TTLocalizer
from toontown.battle import SuitBattleGlobals
import gc
import thread
# If we don't have PSUTIL, don't return system statistics.
try:
@ -14,6 +16,10 @@ shard_status_interval = ConfigVariableInt(
'shard-status-interval', 20,
'How often to send shard status update messages.')
shard_heap_interval = ConfigVariableInt(
'shard-status-interval', 60,
'How often to recount objects on the heap (and in garbage).')
shard_status_timeout = ConfigVariableInt(
'shard-status-timeout', 30,
'The maximum time between receiving shard status update messages before'
@ -26,6 +32,20 @@ class ShardStatusSender:
self.interval = None
# This is updated from a separate thread periodically:
self.heap_status = {'objects': 0, 'garbage': 0}
def update_heap(self):
lastUpdate = 0
while taskMgr.running:
if lastUpdate < time.time() - shard_heap_interval.getValue():
self.heap_status = {'objects': len(gc.get_objects()),
'garbage': len(gc.garbage)}
lastUpdate = time.time()
# Don't consume CPU, but don't lay dormant for a long time either:
time.sleep(1.0)
def start(self):
# Set the average frame rate interval to match shard status interval.
globalClock.setAverageFrameRateInterval(shard_status_interval.getValue())
@ -37,6 +57,8 @@ class ShardStatusSender:
dg = self.air.netMessenger.prepare('shardStatus', [offlineStatus])
self.air.addPostRemove(dg)
thread.start_new_thread(self.update_heap, ())
# Fire off the first status, which also starts the interval:
self.sendStatus()
@ -61,7 +83,8 @@ class ShardStatusSender:
'districtName': self.air.distributedDistrict.name,
'population': self.air.districtStats.getAvatarCount(),
'avg-frame-rate': round(globalClock.getAverageFrameRate(), 5),
'invasion': invasion
'invasion': invasion,
'heap': self.heap_status
}
if HAS_PSUTIL:
status['cpu-usage'] = cpu_percent(interval=None, percpu=True)

View file

@ -1037,7 +1037,7 @@ class ToontownClientRepository(OTPClientRepository.OTPClientRepository):
return True
def sendQuietZoneRequest(self):
self.sendSetZoneMsg(OTPGlobals.QuietZone)
self.sendSetZoneMsg(OTPGlobals.QuietZone, [])
def handleQuietZoneGenerateWithRequired(self, di):
doId = di.getUint32()

View file

@ -5,12 +5,21 @@ from direct.distributed.PyDatagram import PyDatagram
from direct.distributed.MsgTypes import *
from panda3d.core import *
import pymongo, urlparse
import signal
mongodb_url = ConfigVariableString('mongodb-url', 'mongodb://localhost',
'Specifies the URL of the MongoDB server that'
'stores all gameserver data.')
' stores all gameserver data.')
mongodb_replicaset = ConfigVariableString('mongodb-replicaset', '', 'Specifies the replica set of the gameserver data DB.')
ai_watchdog = ConfigVariableInt('ai-watchdog', 15,
'Specifies the maximum amount of time that a'
' frame may take before the process kills itself.')
class WatchdogError(Exception): pass
def watchdogExhausted(signum, frame):
raise WatchdogError('The process has stalled!')
class ToontownInternalRepository(AstronInternalRepository):
GameGlobalsId = OTP_DO_ID_TOONTOWN
dbId = 4003
@ -39,6 +48,15 @@ class ToontownInternalRepository(AstronInternalRepository):
self.netMessenger.register(3, 'avatarOffline')
self.netMessenger.register(4, 'enableLogins')
if hasattr(signal, 'alarm'):
signal.signal(signal.SIGALRM, watchdogExhausted)
self.__watchdog = taskMgr.add(self.__resetWatchdog, 'watchdog')
def __resetWatchdog(self, task):
signal.alarm(ai_watchdog.getValue())
return task.cont
def getAvatarIdFromSender(self):
return self.getMsgSender() & 0xFFFFFFFF

View file

@ -1,8 +1,8 @@
from DNAParser import *
import DNAStoreSuitPoint
from collections import deque
import ctypes
class DNASuitGraph:
class DNASuitGraph(object):
def __init__(self, points, edges):
self.points = points
self.edges = edges
@ -11,8 +11,16 @@ class DNASuitGraph:
self._point2outboundEdges = {}
self._point2inboundEdges = {}
for point in points:
self._table = (ctypes.c_uint16*4 * len(points)*len(points))()
ctypes.memset(self._table, 0xFF, ctypes.sizeof(self._table))
if points:
highestId = max(point.id for point in points)
self._id2index = (ctypes.c_uint16 * (highestId+1))()
for i,point in enumerate(points):
self._pointId2point[point.id] = point
self._id2index[point.id] = i
for edge in edges:
try:
@ -24,6 +32,44 @@ class DNASuitGraph:
self._point2outboundEdges.setdefault(a, []).append(edge)
self._point2inboundEdges.setdefault(b, []).append(edge)
visited = (ctypes.c_uint16*len(points))()
for i, point in enumerate(points):
for neighbor in self.getOriginPoints(point):
self.addLink(neighbor, point, 1, point, False, visited)
def addLink(self, point, neighbor, distance, destination, unbounded, visited):
pointIndex = self._id2index[point.id]
neighborIndex = self._id2index[neighbor.id]
destinationIndex = self._id2index[destination.id]
if visited[pointIndex]:
# Loop detected! Modify the unbounded route:
unbounded = True
visited[pointIndex] += 1
entry = self._table[pointIndex][destinationIndex]
existingDistance = entry[3] if unbounded else entry[1]
if distance < existingDistance:
if not unbounded:
entry[0] = neighborIndex
entry[1] = distance
else:
entry[2] = neighborIndex
entry[3] = distance
# We've just updated our link. If we're traversable, announce the
# new route to all of our neighbors:
traversable = point.type in (DNAStoreSuitPoint.STREETPOINT,
DNAStoreSuitPoint.COGHQINPOINT,
DNAStoreSuitPoint.COGHQOUTPOINT)
if traversable:
for neighbor in self.getOriginPoints(point):
self.addLink(neighbor, point, distance+1, destination, unbounded, visited)
visited[pointIndex] -= 1
def getEdgeEndpoints(self, edge):
return self._pointId2point[edge.a], self._pointId2point[edge.b]
@ -44,51 +90,35 @@ class DNASuitGraph:
return self.getEdgeZone(edges[0])
def getSuitPath(self, startPoint, endPoint, minPathLen, maxPathLen):
# Performs a BFS in order to find a path from startPoint to endPoint,
# the minimum length will be minPathLen, and the maximum will be
# maxPathLen. N.B. these values indicate the length in edges, not
# vertices, so the returned list will be:
# minPathLen+1 <= len(list) <= maxPathLen+1
start = self._id2index[startPoint.id]
end = self._id2index[endPoint.id]
# The queue of paths to consider:
# The format is a tuple: (prevPath, depth, point)
pathDeque = deque()
pathDeque.append((None, 0, startPoint))
while pathDeque:
path = pathDeque.popleft()
prevPath, depth, point = path
at = start
path = [startPoint]
newDepth = depth + 1
if newDepth > maxPathLen:
# This path has grown too long, prune it.
continue
while at != end or minPathLen > 0:
boundedNext, boundedDistance, unboundedNext, unboundedDistance = self._table[at][end]
for adj in self.getAdjacentPoints(point):
if adj == endPoint and newDepth >= minPathLen:
# Hey, we found the end! Let's return it:
points = deque()
points.appendleft(adj)
while path:
points.appendleft(path[-1])
path, _, _ = path
return list(points)
if minPathLen <= boundedDistance <= maxPathLen:
at = boundedNext
elif unboundedDistance <= maxPathLen:
at = unboundedNext
else:
# No path exists!
return None
# We're not at the end yet... Let's see if we can traverse this
# point:
if adj.type not in (DNAStoreSuitPoint.STREETPOINT,
DNAStoreSuitPoint.COGHQINPOINT,
DNAStoreSuitPoint.COGHQOUTPOINT):
# This is some other special point that we cannot walk
# across.
continue
# Append this point to the paths we are considering:
pathDeque.append((path, newDepth, adj))
path.append(self.points[at])
minPathLen -= 1
maxPathLen -= 1
return path
def getAdjacentPoints(self, point):
return [self.getPointFromIndex(edge.b) for edge in self.getEdgesFrom(point)]
def getOriginPoints(self, point):
return [self.getPointFromIndex(edge.a) for edge in self.getEdgesTo(point)]
def getConnectingEdge(self, pointA, pointB):
assert isinstance(pointA, DNAStoreSuitPoint.DNAStoreSuitPoint)
assert isinstance(pointB, DNAStoreSuitPoint.DNAStoreSuitPoint)

View file

@ -26,6 +26,7 @@ from toontown.launcher import ToontownDownloadWatcher
from toontown.toontowngui import TTDialog
from sys import platform
from DisplayOptions import DisplayOptions
import otp.ai.DiagnosticMagicWords
class ToonBase(OTPBase.OTPBase):
notify = DirectNotifyGlobal.directNotify.newCategory('ToonBase')

View file

@ -59,7 +59,7 @@ UnpaidMaxSkills = [Levels[0][1] - 1,
Levels[4][4] - 1,
Levels[5][4] - 1,
Levels[6][1] - 1]
ExperienceCap = 200
ExperienceCap = 300
def gagIsPaidOnly(track, level):
return Levels[track][level] > UnpaidMaxSkills[track]