Merge branch 'beta-wip' into 'master'
7-21 update mostly bugfixes and recovery from double access weekend
This commit is contained in:
commit
9030daccf9
9 changed files with 189 additions and 46 deletions
|
@ -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"]
|
||||
|
|
69
otp/ai/DiagnosticMagicWords.py
Normal file
69
otp/ai/DiagnosticMagicWords.py
Normal 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...'
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue