open-toontown/toontown/ai/WelcomeValleyManagerAI.py

489 lines
19 KiB
Python

from pandac.PandaModules import *
from direct.distributed import DistributedObjectAI
from direct.directnotify import DirectNotifyGlobal
from toontown.toonbase import ToontownGlobals
from toontown.hood import ZoneUtil
from toontown.hood import TTHoodDataAI
from toontown.hood import GSHoodDataAI
from direct.task import Task
import random
# These are the population thresholds we use to balance our automatic
# creation of WelcomeValleys.
# The minimum number of people that should be in the playground before
# we start to phase out the hood.
PGminimum = 1
# The "stable" watermark; the first time we reach this number of
# people in the playground, we consider the hood to be in a stable
# state.
PGstable = 15
# The maximum number of people in the playground before we stop adding
# people to the hood.
PGmaximum = 20
# How often, in seconds, to report the current WelcomeValley state to
# the log.
LogInterval = 300
# The basic balancing algorithm for creating and destroying
# WelcomeValley hoods is as follows.
# There are three classes of hoods: New, Stable, and Removing.
# At any given time, there may be either zero or one New hoods, any
# number of Stable hoods, and any number of Removing hoods.
# Initially there are no hoods.
# When a new avatar arrives, it will be assigned to the New hood if
# there is one; otherwise, it will be assigned to the Stable hood with
# the smallest playground population, unless there are no Stable hoods
# with a population less than PGmaximum (in which case a New hood will
# be created).
# When a New hood is created, it will continue to be considered New
# (and thus receive all newly arriving avatars), until its playground
# population reaches PGstable, at which point the hood is moved into
# the Stable pool.
# If at any point the playground population of a hood in the Stable
# pool decreases below PGminimum (and it is not the only hood
# remaining), it is moved to the Removing pool. A suitable
# replacement hood is chosen using the algorithm for a new avatar,
# above, and this new hood is associated with the Removing hood. Any
# avatar that requests a zone change to or within a Removing hood is
# instead redirected the hood's replacement. (Note that clients who
# are teleporting to a friend in a Removing hood do not request this
# zone change via the AI, and so will not be redirected to a different
# hood--they will still arrive in the same hood with their friend.)
# For the purposes of balancing, we consider the replacement hood
# immediately contains its population plus that of the Removing
# hood.
# Finally, if the total population of any hood reaches zero, the hood
# is completely removed.
# There are a few considerations that have led to this algorithm.
# Firstly, we want to minimize the amount of time a playground is
# nearly empty; you should always be able to enter the game and see
# several people in the playground. Thus, we aggressively fill a New
# hood up quickly, instead of distributing new avatars evenly among
# all available hoods; and once we decide to remove a hood we
# aggressively move avatars off it at the first opportunity.
# Secondly, we would like to keep avatars together whenever possible;
# thus, when we decide to remove a hood, we choose only one hood to be
# its replacement, and all avatars are moved from the source hood to
# the same replacement hood (even if this will result in an overfull
# replacement hood).
class WelcomeValleyManagerAI(DistributedObjectAI.DistributedObjectAI):
notify = DirectNotifyGlobal.directNotify.newCategory("WelcomeValleyManagerAI")
def __init__(self, air):
DistributedObjectAI.DistributedObjectAI.__init__(self, air)
self.welcomeValleyAllocator = UniqueIdAllocator(
ToontownGlobals.WelcomeValleyBegin // 2000,
ToontownGlobals.WelcomeValleyEnd // 2000 - 1)
self.welcomeValleys = {}
self.avatarZones = {}
self.newHood = None
self.stableHoods = []
self.removingHoods = []
def generate(self):
DistributedObjectAI.DistributedObjectAI.generate(self)
if simbase.config.GetBool('report-welcome-valleys', 0):
self.doReportLater()
def delete(self):
name = self.taskName("WelcomeValleyLog")
taskMgr.remove(name)
self.ignoreAll()
DistributedObjectAI.DistributedObjectAI.delete(self)
# now done locally on the AI
## def clientSetZone(self, zoneId):
## """
## This is used by the client to inform the AI which zone he is
## going to. It is mainly used to balance the WelcomeValley zones,
## so the AI can know how many avatars are in each WelcomeValley.
## """
## avId = self.air.getAvatarIdFromSender()
## lastZoneId = self.avatarSetZone(avId, zoneId)
## # Temporary kludge to ensure ghost mode doesn't remain on
## # longer than it should--that's probably the result of an
## # unintended bug. If ghost mode is on, we turn it off
## # whenever the client switches zones. Note that this is not a
## # real solution, since a hacked client can simply not send the
## # clientSetZone messages.
## avatar = self.air.doId2do.get(avId)
## if avatar and avatar.ghostMode:
## self.air.writeServerEvent('suspicious', avId, 'has ghost mode %s transitioning from zone %s to %s' % (avatar.ghostMode, lastZoneId, zoneId))
## if avatar.ghostMode == 1:
## avatar.b_setGhostMode(0)
## if avatar and avatar.cogIndex >= 0:
## if (lastZoneId != 11100 and lastZoneId != 12100 and lastZoneId != 13100 ) or \
## (zoneId < 61000 and zoneId != 11000 and zoneId != 12000 and zoneId != 13000):
## self.air.writeServerEvent('suspicious', avId, 'has cogIndex %s transitioning from zone %s to %s' % (avatar.cogIndex, lastZoneId, zoneId))
## avatar.b_setCogIndex(-1)
def toonSetZone(self, avId, zoneId):
lastZoneId = self.avatarSetZone(avId, zoneId)
# Temporary kludge to ensure ghost mode doesn't remain on
# longer than it should--that's probably the result of an
# unintended bug. If ghost mode is on, we turn it off
# whenever the client switches zones. Note that this is not a
# real solution, since a hacked client can simply not send the
# clientSetZone messages.
avatar = self.air.doId2do.get(avId)
if avatar and avatar.ghostMode:
if avatar.ghostMode == 1:
avatar.b_setGhostMode(0)
if avatar and avatar.cogIndex >= 0:
if (lastZoneId != 11100 and lastZoneId != 12100 and lastZoneId != 13100 ) or \
(zoneId < 61000 and zoneId != 11000 and zoneId != 12000 and zoneId != 13000):
avatar.b_setCogIndex(-1)
def avatarSetZone(self, avId, zoneId):
lastZoneId = self.avatarZones.get(avId)
if lastZoneId == zoneId:
return lastZoneId
if zoneId == None:
self.notify.debug("Avatar %s has left the shard." % (avId))
del self.avatarZones[avId]
self.ignore(self.air.getAvatarExitEvent(avId))
else:
self.notify.debug("Avatar %s is now in zone %s." % (avId, zoneId))
# First, add the avatar into his/her new zone. We must do
# this first so we don't risk momentarily bringing the
# hood population to 0.
hoodId = ZoneUtil.getHoodId(zoneId)
# if this is a GS hoodId, just grab the TT hood
if (hoodId % 2000) < 1000:
hood = self.welcomeValleys.get(hoodId)
else:
hood = self.welcomeValleys.get(hoodId - 1000)
if hood:
hood[0].incrementPopulation(zoneId, 1)
if (hood == self.newHood) and hood[0].getPgPopulation() >= PGstable:
# This is now a stable hood.
self.__newToStable(hood)
self.avatarZones[avId] = zoneId
if lastZoneId == None:
# This is the first time we have heard from this avatar.
self.acceptOnce(self.air.getAvatarExitEvent(avId),
self.avatarLogout, extraArgs=[avId])
else:
# Now, remove the avatar from his/her previous zone.
lastHoodId = ZoneUtil.getHoodId(lastZoneId)
# if this is a GS hoodId, just grab the TT hood
if (lastHoodId % 2000) < 1000:
hood = self.welcomeValleys.get(lastHoodId)
else:
hood = self.welcomeValleys.get(lastHoodId - 1000)
if hood:
hood[0].incrementPopulation(lastZoneId, -1)
if hood[0].getHoodPopulation() == 0:
self.__hoodIsEmpty(hood)
elif (hood != self.newHood) and not hood[0].hasRedirect() and \
hood[0].getPgPopulation() < PGminimum:
self.__stableToRemoving(hood)
return lastZoneId
def avatarLogout(self, avId):
self.avatarSetZone(avId, None)
def makeNew(self, hoodId):
# Makes the indicated hoodId new, if possible. Used for magic
# word purposes only. Returns a string describing the action.
hood = self.welcomeValleys.get(hoodId)
if hood == None:
return "Hood %s is unknown." % (hoodId)
if hood == self.newHood:
return "Hood %s is already new." % (hoodId)
if self.newHood != None:
self.__newToStable(self.newHood)
if hood in self.removingHoods:
self.removingHoods.remove(hood)
hood[0].setRedirect(None)
else:
self.stableHoods.remove(hood)
self.newHood = hood
return "Hood %s is now New." % (hoodId)
def makeStable(self, hoodId):
# Moves the indicated hoodId to the Stable pool, if possible.
# Used for magic word purposes only. Returns a string
# describing the action.
hood = self.welcomeValleys.get(hoodId)
if hood == None:
return "Hood %s is unknown." % (hoodId)
if hood in self.stableHoods:
return "Hood %s is already Stable." % (hoodId)
if hood == self.newHood:
self.__newToStable(hood)
else:
self.removingHoods.remove(hood)
hood[0].setRedirect(None)
self.stableHoods.append(hood)
return "Hood %s is now Stable." % (hoodId)
def makeRemoving(self, hoodId):
# Moves the indicated hoodId to the Removing pool, if possible.
# Used for magic word purposes only. Returns a string
# describing the action.
hood = self.welcomeValleys.get(hoodId)
if hood == None:
return "Hood %s is unknown." % (hoodId)
if hood in self.removingHoods:
return "Hood %s is already Removing." % (hoodId)
if hood == self.newHood:
self.__newToStable(hood)
self.__stableToRemoving(hood)
return "Hood %s is now Removing." % (hoodId)
def checkAvatars(self):
# Checks that all of the avatars recorded as being logged in
# are actually still in the doId2do map, and logs out any that
# are not. Returns a list of the removed avId's. This is
# normally used for magic word purposes only.
removed = []
for avId in self.avatarZones.keys():
if avId not in self.air.doId2do:
# Here's one for removing.
removed.append(avId)
self.avatarLogout(avId)
return removed
def __newToStable(self, hood):
# This New hood's population has reached the stable limit;
# mark it as a Stable hood.
self.notify.info("Hood %s moved to Stable." % (hood[0].zoneId))
assert(hood == self.newHood)
self.newHood = None
self.stableHoods.append(hood)
def __stableToRemoving(self, hood):
# This hood's population has dropped too low;
# schedule it for removal.
self.notify.info("Hood %s moved to Removing." % (hood[0].zoneId))
assert(hood in self.stableHoods)
self.stableHoods.remove(hood)
replacementHood = self.chooseWelcomeValley(allowCreateNew = 0)
if replacementHood == None:
# Hmm, we couldn't find a suitable
# replacement, so just keep this one.
self.stableHoods.append(hood)
else:
hood[0].setRedirect(replacementHood)
self.removingHoods.append(hood)
def __hoodIsEmpty(self, hood):
self.notify.info("Hood %s is now empty." % (hood[0].zoneId))
replacementHood = hood[0].replacementHood
self.destroyWelcomeValley(hood)
# Also check the hood this one is redirecting to; we might
# have just emptied it too.
if replacementHood and replacementHood[0].getHoodPopulation() == 0:
self.__hoodIsEmpty(replacementHood)
def avatarRequestZone(self, avId, origZoneId):
# This services a redirect-zone request for a particular
# avatar. The client is requesting to go to the indicated
# zoneId, which should be a WelcomeValley zoneId; the AI
# should choose which particular WelcomeValley to direct the
# client to.
if not ZoneUtil.isWelcomeValley(origZoneId):
# All requests for static zones are returned unchanged.
return origZoneId
origHoodId = ZoneUtil.getHoodId(origZoneId)
hood = self.welcomeValleys.get(origHoodId)
if not hood:
# If we don't know this hood, choose a new one.
hood = self.chooseWelcomeValley()
if not hood:
self.notify.warning("Could not create new WelcomeValley hood for avatar %s." % (avId))
zoneId = ZoneUtil.getCanonicalZoneId(origZoneId)
else:
# use TTC hoodId
hoodId = hood[0].getRedirect().zoneId
zoneId = ZoneUtil.getTrueZoneId(origZoneId, hoodId)
# Even though the client might choose not to go to the
# indicated zoneId for some reason, we will consider the
# client as having gone there immediately, for the purposes of
# balancing. If the client goes somewhere else instead, it
# will tell us that and we can correct this.
self.avatarSetZone(avId, zoneId)
return zoneId
def requestZoneIdMessage(self, origZoneId, context):
"""
This message is sent from the client to request a new zoneId
for a transition to WelcomeValley.
"""
avId = self.air.getAvatarIdFromSender()
zoneId = self.avatarRequestZone(avId, origZoneId)
self.sendUpdateToAvatarId(avId, "requestZoneIdResponse",
[zoneId, context])
def chooseWelcomeValley(self, allowCreateNew = 1):
# Picks a hood to assign a new avatar to. If allowCreateNew
# is 1, a new hood may be created if necessary.
# First, if we have a New hood, prefer that one.
if self.newHood:
return self.newHood
# Next, choose the Stable hood with the smallest playground
# population.
bestHood = None
bestPopulation = None
for hood in self.stableHoods:
population = hood[0].getPgPopulation()
if bestHood == None or population < bestPopulation:
bestHood = hood
bestPopulation = population
# Unless there are no hoods with a small-enough population, in
# which case we create another New hood.
if allowCreateNew and (bestHood == None or bestPopulation >= PGmaximum):
self.newHood = self.createWelcomeValley()
if self.newHood:
self.notify.info("Hood %s is New." % self.newHood[0].zoneId)
return self.newHood
return bestHood
def createWelcomeValley(self):
# Creates new copy of ToontownCentral and Goofy Speedway and returns
# thier HoodDataAI. Returns None if no new hoods can be created.
index = self.welcomeValleyAllocator.allocate()
if index == -1:
return None
# TTC
ttHoodId = index * 2000
ttHood = TTHoodDataAI.TTHoodDataAI(self.air, ttHoodId)
self.air.startupHood(ttHood)
# GS
gsHoodId = index * 2000 + 1000
gsHood = GSHoodDataAI.GSHoodDataAI(self.air, gsHoodId)
self.air.startupHood(gsHood)
# both hoods are stored in a tuple and referenced by the TTC hoodId
self.welcomeValleys[ttHoodId] = (ttHood, gsHood)
# create a pond bingo manager ai for the new WV
if simbase.wantBingo:
self.notify.info('creating bingo mgr for Welcome Valley %s' % ttHoodId)
self.air.createPondBingoMgrAI(ttHood)
return (ttHood, gsHood)
def destroyWelcomeValley(self, hood):
hoodId = hood[0].zoneId
assert((hoodId % 2000) == 0)
del self.welcomeValleys[hoodId]
self.welcomeValleyAllocator.free(hoodId // 2000)
self.air.shutdownHood(hood[0])
self.air.shutdownHood(hood[1])
if self.newHood == hood:
self.newHood = None
elif hood in self.removingHoods:
self.removingHoods.remove(hood)
elif hood in self.stableHoods:
self.stableHoods.remove(hood)
def doReportLater(self):
name = self.taskName("WelcomeValleyLog")
taskMgr.remove(name)
taskMgr.doMethodLater(LogInterval, self.doReportTask, name)
def doReportTask(self, task):
self.reportWelcomeValleys()
self.doReportLater()
return Task.done
def getAvatarCount(self):
# Players
# the Welcome Valley hoods.
if simbase.fakeDistrictPopulations:
return 0
answer = 0
hoodIds = self.welcomeValleys.keys()
for hoodId in hoodIds:
hood = self.welcomeValleys[hoodId]
answer += hood[0].getHoodPopulation()
return answer
def reportWelcomeValleys(self):
# Writes a message to the log file showing the current state
# of the Welcome Valley hoods.
self.notify.info("Current Welcome Valleys:")
hoodIds = self.welcomeValleys.keys()
hoodIds.sort()
for hoodId in hoodIds:
hood = self.welcomeValleys[hoodId]
if hood == self.newHood:
flag = "N"
elif hood in self.removingHoods:
flag = "R"
else:
flag = " "
self.notify.info("%s %s %s/%s" % (
hood[0].zoneId, flag,
hood[0].getPgPopulation(), hood[0].getHoodPopulation()))