historical/toontown-classic.git/otp/uberdog/GameServicesManagerUD.py

794 lines
37 KiB
Python
Raw Normal View History

2024-01-16 17:20:27 +00:00
import anydbm
import dumbdbm
import sys
import time
from datetime import datetime
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD
from direct.distributed.PyDatagram import *
from direct.fsm.FSM import FSM
from otp.distributed import OtpDoGlobals
from otp.otpbase import OTPGlobals
import base64
admins = ["TTBuM3IwRlRXIQ=="] #put admins here as a string containing their encoded token, seperated by ",". :-)
# --- ACCOUNT DATABASES ---
# These classes make up the available account database interfaces for Toontown Online.
# At the moment, we have two functional account database interfaces: DeveloperAccountDB, and LocalAccountDB.
# These will be explained further in their respective class definition.
class AccountDB:
"""
AccountDB is the base class for all account database interface implementations. Inherit from this class when
creating new account database interfaces, but DO NOT try to use this class on its own; you'll have a bad time!
"""
notify = DirectNotifyGlobal.directNotify.newCategory('AccountDB')
def __init__(self, gameServicesManager):
self.gameServicesManager = gameServicesManager
# This uses dbm, so we open the DB file:
accountDbFile = simbase.config.GetString('accountdb-local-file', 'astron/databases/accounts.db')
if sys.platform == 'darwin':
dbm = dumbdbm
else:
dbm = anydbm
self.dbm = dbm.open(accountDbFile, 'c')
def lookup(self, playToken, callback):
raise NotImplementedError('lookup') # Must be overridden by subclass.
def storeAccountID(self, databaseId, accountId, callback):
self.dbm[databaseId] = str(accountId)
if getattr(self.dbm, 'sync', None):
self.dbm.sync()
callback(True)
else:
self.notify.warning('Unable to associate user %s with account %d!' % (databaseId, accountId))
callback(False)
class DeveloperAccountDB(AccountDB):
"""
DeveloperAccountDB is a special account database interface implementation designed for use on developer builds of
the game. This is the default account database interface when running the server locally via source code, which is
assumed to be a development environment. DeveloperAccountDB accepts a username, and assigns each new user with
"TTOFF_DEVELOPER" access automatically upon login.
"""
notify = DirectNotifyGlobal.directNotify.newCategory('DeveloperAccountDB')
def lookup(self, playToken, callback):
encodedToken = base64.b64encode(playToken.encode('utf-8'))
print "user joined with encoded token: " + encodedToken + " (" + playToken + ")"
# Check if this play token exists in the dbm:
if str(playToken) not in self.dbm:
# It is not, so we'll associate them with a brand new account object.
if encodedToken in admins:
callback({'success': True,
'accountId': 0,
'databaseId': playToken,
'accessLevel': "TTOFF_DEVELOPER"})
print encodedToken + " is a dev!"
else:
callback({'success': True,
'accountId': 0,
'databaseId': playToken,
'accessLevel': "NO_ACCESS"})
print encodedToken + " is a user!"
else:
def handleAccount(dclass, fields):
if dclass != self.gameServicesManager.air.dclassesByName['AccountUD']:
result = {'success': False,
'reason': 'Your account object (%s) was not found in the database!' % dclass}
else:
# We already have an account object, so we'll just return what we have.
if encodedToken in admins:
result = {'success': True,
'accountId': int(self.dbm[playToken]),
'databaseId': playToken,
'accessLevel': "TTOFF_DEVELOPER"}
print encodedToken + " is a DEV!"
else:
result = {'success': True,
'accountId': int(self.dbm[playToken]),
'databaseId': playToken,
'accessLevel': "NO_ACCESS"}
print encodedToken + " is a user!"
callback(result)
self.gameServicesManager.air.dbInterface.queryObject(self.gameServicesManager.air.dbId,
int(self.dbm[playToken]), handleAccount)
class GameOperation(FSM):
"""
GameOperation is the base class for all other operations used by the GameServicesManager.
"""
notify = DirectNotifyGlobal.directNotify.newCategory('GameOperation')
targetConnection = False
def __init__(self, gameServicesManager, target):
FSM.__init__(self, self.__class__.__name__)
self.gameServicesManager = gameServicesManager
self.target = target
def enterOff(self):
# Deletes the target from either connection2fsm or account2fsm
# depending on the value of self.targetConnection.
if self.targetConnection:
del self.gameServicesManager.connection2fsm[self.target]
else:
del self.gameServicesManager.account2fsm[self.target]
def enterKill(self, reason=''):
# Kills either the target connection or the target account
# depending on the value of self.targetConnection, and then
# sets this FSM's state to Off.
if self.targetConnection:
self.gameServicesManager.killConnection(self.target, reason)
else:
self.gameServicesManager.killAccount(self.target, reason)
self.demand('Off')
class LoginOperation(GameOperation):
notify = DirectNotifyGlobal.directNotify.newCategory('LoginOperation')
targetConnection = True
def __init__(self, gameServicesManager, target):
GameOperation.__init__(self, gameServicesManager, target)
self.playToken = None
self.account = None
def enterStart(self, playToken):
# Sets self.playToken, then enters the QueryAccountDB state.
self.playToken = playToken
self.demand('QueryAccountDB')
def enterQueryAccountDB(self):
# Calls the lookup function on the GameServicesManager's defined account DB interface.
self.gameServicesManager.accountDb.lookup(self.playToken, self.__handleLookup)
def __handleLookup(self, result):
# This is a callback function that will be called by the lookup function
# of the GameServicesManager's account DB interface. It processes the
# lookup function's result & determines which operation should run next.
if not result.get('success'):
# The play token was rejected! Kill the connection.
self.gameServicesManager.air.writeServerEvent('play-token-rejected', self.target, self.playToken)
self.demand('Kill', result.get('reason', 'The accounts database rejected your play token.'))
return
# Grab the databaseId, accessLevel, and accountId from the result.
self.databaseId = result.get('databaseId', 0)
self.accessLevel = result.get('accessLevel', 'NO_ACCESS')
accountId = result.get('accountId', 0)
if accountId:
# There is an account ID, so let's retrieve the associated account.
self.accountId = accountId
self.demand('RetrieveAccount')
else:
# There is no account ID, so let's create a new account.
self.demand('CreateAccount')
def enterCreateAccount(self):
# Creates a brand new account & stores it in the database.
self.account = {'ACCOUNT_AV_SET': [0] * 6,
'ESTATE_ID': 0,
'ACCOUNT_AV_SET_DEL': [],
'CREATED': time.ctime(),
'LAST_LOGIN': time.ctime(),
'ACCOUNT_ID': str(self.databaseId),
'ACCESS_LEVEL': self.accessLevel}
# Create the account object in the database using the data from self.account.
# self.__handleCreate is the callback which will be called after createObject has completed.
self.gameServicesManager.air.dbInterface.createObject(self.gameServicesManager.air.dbId,
self.gameServicesManager.air.dclassesByName['AccountUD'],
self.account, self.__handleCreate)
def __handleCreate(self, accountId):
# This function handles successful & unsuccessful account creations.
if self.state != 'CreateAccount':
# If we're not in the CreateAccount state, this request is invalid.
self.notify.warning('Received CreateAccount response outside of the CreateAccount state.')
return
if not accountId:
# If we don't have an accountId, then that means the database was unable
# to create an account object for us, for whatever reason. Kill the connection.
self.notify.warning('Database failed to create an account object!')
self.demand('Kill', 'Your account object could not be created in the game database.')
return
# Otherwise, the account object was created successfully!
self.gameServicesManager.air.writeServerEvent('account-created', accountId)
# We can now enter the StoreAccountID state.
self.accountId = accountId
self.demand('StoreAccountID')
def enterStoreAccountID(self):
# Stores the account ID in the account bridge.
# self.__handleStored is the callback which
# will be called after storeAccountID has completed.
self.gameServicesManager.accountDb.storeAccountID(self.databaseId, self.accountId, self.__handleStored)
def __handleStored(self, success=True):
if not success:
# The account bridge was unable to store the account ID,
# for whatever reason. Kill the connection.
self.demand('Kill', 'The account server could not save your account DB ID!')
return
# We are all set with account creation now! It's time to enter the SetAccount state.
self.demand('SetAccount')
def enterRetrieveAccount(self):
# Query the database object associated with self.accountId.
# self.__handleRetrieve is the callback which will be called
# after queryObject has completed.
self.gameServicesManager.air.dbInterface.queryObject(self.gameServicesManager.air.dbId, self.accountId,
self.__handleRetrieve)
def __handleRetrieve(self, dclass, fields):
# Checks if the queried object is valid and if it is, enters
# the SetAccount state. Otherwise, the connection is killed.
if dclass != self.gameServicesManager.air.dclassesByName['AccountUD']:
# This is not an account object! Kill the connection.
self.demand('Kill', 'Your account object (%s) was not found in the database!' % dclass)
return
# We can now enter the SetAccount state.
self.account = fields
self.demand('SetAccount')
def enterSetAccount(self):
# If somebody's already logged into this account, disconnect them.
datagram = PyDatagram()
datagram.addServerHeader(self.gameServicesManager.GetAccountConnectionChannel(self.accountId),
self.gameServicesManager.air.ourChannel, CLIENTAGENT_EJECT)
datagram.addUint16(OTPGlobals.BootedLoggedInElsewhere)
datagram.addString('This account has been logged into elsewhere.')
self.gameServicesManager.air.send(datagram)
# Now we'll add this connection to the account channel.
datagram = PyDatagram()
datagram.addServerHeader(self.target, self.gameServicesManager.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL)
datagram.addChannel(self.gameServicesManager.GetAccountConnectionChannel(self.accountId))
self.gameServicesManager.air.send(datagram)
# Set their sender channel to represent their account affiliation.
datagram = PyDatagram()
datagram.addServerHeader(self.target, self.gameServicesManager.air.ourChannel, CLIENTAGENT_SET_CLIENT_ID)
datagram.addChannel(self.accountId << 32) # accountId in high 32 bits, 0 in low (no avatar).
self.gameServicesManager.air.send(datagram)
# We can now un-sandbox the sender.
self.gameServicesManager.air.setClientState(self.target, 2) # ESTABLISHED state.
# Update the last login timestamp.
self.gameServicesManager.air.dbInterface.updateObject(self.gameServicesManager.air.dbId, self.accountId,
self.gameServicesManager.air.dclassesByName['AccountUD'],
{'LAST_LOGIN': time.ctime(),
'ACCOUNT_ID': str(self.databaseId),
'ACCESS_LEVEL': self.accessLevel})
# We're done.
self.gameServicesManager.air.writeServerEvent('account-login', clientId=self.target, accId=self.accountId,
dbId=self.databaseId, playToken=self.playToken)
# Send the acceptLogin update through the GameServicesManager & set this operation's state to Off.
self.gameServicesManager.sendUpdateToChannel(self.target, 'acceptLogin', [])
self.demand('Off')
class AvatarOperation(GameOperation):
notify = DirectNotifyGlobal.directNotify.newCategory('AvatarOperation')
postAccountState = 'Off' # Must be overridden by subclass.
def enterRetrieveAccount(self):
# Query the account. self.__handleRetrieve is the callback
# which will be called after queryObject has completed.
self.gameServicesManager.air.dbInterface.queryObject(self.gameServicesManager.air.dbId, self.target,
self.__handleRetrieve)
def __handleRetrieve(self, dclass, fields):
if dclass != self.gameServicesManager.air.dclassesByName['AccountUD']:
# This is not an account object! Kill the connection.
self.demand('Kill', 'Your account object (%s) was not found in the database!' % dclass)
return
# Set the account & avList.
self.account = fields
self.avList = self.account['ACCOUNT_AV_SET']
# Sanitize the avList, just in case it is too long/short.
self.avList = self.avList[:6]
self.avList += [0] * (6 - len(self.avList))
# We're done; enter the postAccountState.
self.demand(self.postAccountState)
class GetAvatarsOperation(AvatarOperation):
notify = DirectNotifyGlobal.directNotify.newCategory('GetAvatarsOperation')
postAccountState = 'QueryAvatars'
def __init__(self, gameServicesManager, target):
AvatarOperation.__init__(self, gameServicesManager, target)
self.pendingAvatars = None
self.avatarFields = None
def enterStart(self):
# First, retrieve the account.
self.demand('RetrieveAccount')
def enterQueryAvatars(self):
# Now, we will query the avatars that exist in the account.
self.pendingAvatars = set()
self.avatarFields = {}
# Loop through the list of avatars:
for avId in self.avList:
if avId:
# This index contains an avatar! Add it to the pending avatars.
self.pendingAvatars.add(avId)
# This is our callback function that queryObject
# will call when done querying each avatar object.
def response(dclass, fields, avId=avId):
if self.state != 'QueryAvatars':
# We're not in the QueryAvatars state, so this request is invalid.
return
if dclass != self.gameServicesManager.air.dclassesByName[self.gameServicesManager.avatarDclass]:
# The dclass is invalid! Kill the connection.
self.demand('Kill', 'One of the account\'s avatars is invalid! dclass = %s, expected = %s' % (
dclass, self.gameServicesManager.avatarDclass))
return
# Otherwise, we're all set! Add the queried avatar fields to the
# avatarFields array, remove from the pending list, and set our
# state to SendAvatars.
self.avatarFields[avId] = fields
self.pendingAvatars.remove(avId)
if not self.pendingAvatars:
self.demand('SendAvatars')
# Query the avatar object.
self.gameServicesManager.air.dbInterface.queryObject(self.gameServicesManager.air.dbId, avId, response)
if not self.pendingAvatars:
# No pending avatars! Set our state to SendAvatars.
self.demand('SendAvatars')
def enterSendAvatars(self):
# Here is where we'll construct a list of potential avatars,
# given the data from self.avatarFields, and send that to the client.
potentialAvatars = []
# Loop through the avatarFields array:
for avId, fields in self.avatarFields.items():
# Get the appropriate values.
index = self.avList.index(avId)
wishNameState = fields.get('WishNameState', [''])[0]
name = fields['setName'][0]
nameState = 0
if wishNameState == 'OPEN':
nameState = 1
elif wishNameState == 'PENDING':
nameState = 2
elif wishNameState == 'APPROVED':
nameState = 3
name = fields['WishName'][0]
elif wishNameState == 'REJECTED':
nameState = 4
elif wishNameState == 'LOCKED':
nameState = 0
else:
self.gameServicesManager.notify.warning(
'Avatar %s is in unknown name state %s.' % (avId, wishNameState))
nameState = 0
# Add those to potentialAvatars.
potentialAvatars.append([avId, name, fields['setDNAString'][0], index, nameState])
# We're done; send the avatarListResponse update through the
# GameServicesManager, then we can set this operation's
# state to Off.
self.gameServicesManager.sendUpdateToAccountId(self.target, 'avatarListResponse', [potentialAvatars])
self.demand('Off')
# n.b.: We inherit from GetAvatarsOperation here as the remove
# operation ends in a setAvatars message being sent to the client.
class RemoveAvatarOperation(GetAvatarsOperation):
notify = DirectNotifyGlobal.directNotify.newCategory('RemoveAvatarOperation')
postAccountState = 'ProcessRemove'
def __init__(self, gameServicesManager, target):
GetAvatarsOperation.__init__(self, gameServicesManager, target)
self.avId = None
def enterStart(self, avId):
# Store this value & call the base function.
self.avId = avId
GetAvatarsOperation.enterStart(self)
def enterProcessRemove(self):
# Make sure that the target avatar is part of the account:
if self.avId not in self.avList:
# The sender tried to remove an avatar not on the account! Kill the connection.
self.demand('Kill', 'Tried to remove an avatar not on the account!')
return
# Get the index of this avatar.
index = self.avList.index(self.avId)
self.avList[index] = 0
# We will now add this avatar to ACCOUNT_AV_SET_DEL.
avatarsRemoved = list(self.account.get('ACCOUNT_AV_SET_DEL', []))
avatarsRemoved.append([self.avId, int(time.time())])
# Get the estate ID of this account.
estateId = self.account.get('ESTATE_ID', 0)
if estateId != 0:
# The following will assume that the house already exists,
# however it shouldn't be a problem if it doesn't.
self.gameServicesManager.air.dbInterface.updateObject(self.gameServicesManager.air.dbId, estateId,
self.gameServicesManager.air.dclassesByName[
'DistributedEstateAI'],
{'setSlot%sToonId' % index: [0],
'setSlot%sItems' % index: [[]]})
if self.gameServicesManager.air.ttoffFriendsManager:
self.gameServicesManager.air.ttoffFriendsManager.clearList(self.avId)
else:
friendsManagerDoId = OtpDoGlobals.OTP_DO_ID_TTOFF_FRIENDS_MANAGER
friendsManagerDclass = self.gameServicesManager.air.dclassesByName['TTOffFriendsManagerUD']
datagram = friendsManagerDclass.aiFormatUpdate('clearList', friendsManagerDoId, friendsManagerDoId,
self.gameServicesManager.air.ourChannel, [self.avId])
self.gameServicesManager.air.send(datagram)
# We can now update the account with the new data. self.__handleRemove is the
# callback which will be called upon completion of updateObject.
self.gameServicesManager.air.dbInterface.updateObject(self.gameServicesManager.air.dbId, self.target,
self.gameServicesManager.air.dclassesByName['AccountUD'],
{'ACCOUNT_AV_SET': self.avList,
'ACCOUNT_AV_SET_DEL': avatarsRemoved},
{'ACCOUNT_AV_SET': self.account['ACCOUNT_AV_SET'],
'ACCOUNT_AV_SET_DEL': self.account[
'ACCOUNT_AV_SET_DEL']},
self.__handleRemove)
def __handleRemove(self, fields):
if fields:
# The avatar was unable to be removed from the account! Kill the account.
self.demand('Kill', 'Database failed to mark the avatar as removed!')
return
# Otherwise, we're done! We'll enter the QueryAvatars state now so that
# the user is sent back to the avatar chooser.
self.gameServicesManager.air.writeServerEvent('avatar-deleted', self.avId, self.target)
self.demand('QueryAvatars')
class LoadAvatarOperation(AvatarOperation):
notify = DirectNotifyGlobal.directNotify.newCategory('LoadAvatarOperation')
postAccountState = 'GetTargetAvatar'
def __init__(self, gameServicesManager, target):
AvatarOperation.__init__(self, gameServicesManager, target)
self.avId = None
def enterStart(self, avId):
# Store this value & move on to RetrieveAccount
self.avId = avId
self.demand('RetrieveAccount')
def enterGetTargetAvatar(self):
# Make sure that the target avatar is part of the account:
if self.avId not in self.avList:
# The sender tried to play on an avatar not on the account! Kill the connection.
self.demand('Kill', 'Tried to play on an avatar not on the account!')
return
# Query the database for the avatar. self.__handleAvatar is
# our callback which will be called upon queryObject's completion.
self.gameServicesManager.air.dbInterface.queryObject(self.gameServicesManager.air.dbId, self.avId,
self.__handleAvatar)
def __handleAvatar(self, dclass, fields):
if dclass != self.gameServicesManager.air.dclassesByName[self.gameServicesManager.avatarDclass]:
# This dclass is not a valid avatar! Kill the connection.
self.demand('Kill', 'One of the account\'s avatars is invalid!')
return
# Store the avatar & move on to SetAvatar.
self.avatar = fields
self.demand('SetAvatar')
def enterSetAvatar(self):
# Get the client channel.
channel = self.gameServicesManager.GetAccountConnectionChannel(self.target)
# We will first assign a POST_REMOVE that will unload the
# avatar in the event of them disconnecting while we are working.
cleanupDatagram = PyDatagram()
cleanupDatagram.addServerHeader(self.avId, channel, STATESERVER_OBJECT_DELETE_RAM)
cleanupDatagram.addUint32(self.avId)
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_ADD_POST_REMOVE)
datagram.addString(cleanupDatagram.getMessage())
self.gameServicesManager.air.send(datagram)
# We will now set the account's days since creation on the client.
creationDate = self.getCreationDate()
accountDays = -1
if creationDate:
now = datetime.fromtimestamp(time.mktime(time.strptime(time.ctime())))
accountDays = abs((now - creationDate).days)
if accountDays < 0 or accountDays > 4294967295:
accountDays = 100000
self.gameServicesManager.sendUpdateToAccountId(self.target, 'receiveAccountDays', [accountDays])
# Get the avatar's "true" access (that is, the integer value that corresponds to the assigned string value).
accessLevel = self.account.get('ACCESS_LEVEL', 'NO_ACCESS')
accessLevel = OTPGlobals.accessLevelValues.get(accessLevel, 0)
# We will now activate the avatar on the DBSS.
self.gameServicesManager.air.sendActivate(self.avId, 0, 0, self.gameServicesManager.air.dclassesByName[
self.gameServicesManager.avatarDclass], {'setAccessLevel': [accessLevel]})
# Next, we will add them to the avatar channel.
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL)
datagram.addChannel(self.gameServicesManager.GetPuppetConnectionChannel(self.avId))
self.gameServicesManager.air.send(datagram)
# We will now set the avatar as the client's session object.
self.gameServicesManager.air.clientAddSessionObject(channel, self.avId)
# Now we need to set their sender channel to represent their account affiliation.
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_SET_CLIENT_ID)
datagram.addChannel(self.target << 32 | self.avId) # accountId in high 32 bits, avatar in low.
self.gameServicesManager.air.send(datagram)
# We will now grant ownership.
self.gameServicesManager.air.setOwner(self.avId, channel)
# Tell the friends manager that an avatar is coming online.
friendsList = [x for x, y in self.avatar['setFriendsList'][0]]
self.gameServicesManager.air.ttoffFriendsManager.comingOnline(self.avId, friendsList)
# Now we'll assign a POST_REMOVE that will tell the friends manager
# that an avatar has gone offline, in the event that they disconnect
# unexpectedly.
if self.gameServicesManager.air.ttoffFriendsManager:
friendsManagerDclass = self.gameServicesManager.air.ttoffFriendsManager.dclass
cleanupDatagram = friendsManagerDclass.aiFormatUpdate('goingOffline',
self.gameServicesManager.air.ttoffFriendsManager.doId,
self.gameServicesManager.air.ttoffFriendsManager.doId,
self.gameServicesManager.air.ourChannel, [self.avId])
else:
friendsManagerDoId = OtpDoGlobals.OTP_DO_ID_TTOFF_FRIENDS_MANAGER
friendsManagerDclass = self.gameServicesManager.air.dclassesByName['TTOffFriendsManagerUD']
cleanupDatagram = friendsManagerDclass.aiFormatUpdate('goingOffline', friendsManagerDoId,
friendsManagerDoId,
self.gameServicesManager.air.ourChannel, [self.avId])
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_ADD_POST_REMOVE)
datagram.addString(cleanupDatagram.getMessage())
self.gameServicesManager.air.send(datagram)
# We can now finally shut down this operation.
self.gameServicesManager.air.writeServerEvent('avatar-chosen', avId=self.avId, accId=self.target)
self.demand('Off')
def getCreationDate(self):
# Based on game creation date:
creationDate = self.account.get('CREATED', '')
try:
creationDate = datetime.fromtimestamp(time.mktime(time.strptime(creationDate)))
except ValueError:
creationDate = ''
return creationDate
class UnloadAvatarOperation(GameOperation):
notify = DirectNotifyGlobal.directNotify.newCategory('UnloadAvatarOperation')
def __init__(self, gameServicesManager, target):
GameOperation.__init__(self, gameServicesManager, target)
self.avId = None
def enterStart(self, avId):
# Store the avId.
self.avId = avId
# We actually don't even need to query the account, as we know
# that the avatar is being played, so let's just unload the avatar.
self.demand('UnloadAvatar')
def enterUnloadAvatar(self):
# Get the client channel.
channel = self.gameServicesManager.GetAccountConnectionChannel(self.target)
# Tell the friends manager that we're logging off.
self.gameServicesManager.air.ttoffFriendsManager.goingOffline(self.avId)
# First, remove our POST_REMOVES.
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_CLEAR_POST_REMOVES)
self.gameServicesManager.air.send(datagram)
# Next, remove the avatar channel.
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_CLOSE_CHANNEL)
datagram.addChannel(self.gameServicesManager.GetPuppetConnectionChannel(self.avId))
self.gameServicesManager.air.send(datagram)
# Next, reset the sender channel.
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_SET_CLIENT_ID)
datagram.addChannel(self.target << 32) # accountId in high 32 bits, no avatar in low.
self.gameServicesManager.air.send(datagram)
# Reset the session object.
datagram = PyDatagram()
datagram.addServerHeader(channel, self.gameServicesManager.air.ourChannel, CLIENTAGENT_REMOVE_SESSION_OBJECT)
datagram.addUint32(self.avId)
self.gameServicesManager.air.send(datagram)
# Unload the avatar object.
datagram = PyDatagram()
datagram.addServerHeader(self.avId, channel, STATESERVER_OBJECT_DELETE_RAM)
datagram.addUint32(self.avId)
self.gameServicesManager.air.send(datagram)
# We're done! We can now shut down this operation.
self.gameServicesManager.air.writeServerEvent('avatar-unloaded', avId=self.avId)
self.demand('Off')
class GameServicesManagerUD(DistributedObjectGlobalUD):
notify = DirectNotifyGlobal.directNotify.newCategory('GameServicesManagerUD')
avatarDclass = None # Must be overridden by subclass.
def __init__(self, air):
DistributedObjectGlobalUD.__init__(self, air)
self.connection2fsm = {}
self.account2fsm = {}
self.accountDb = None
def announceGenerate(self):
DistributedObjectGlobalUD.announceGenerate(self)
# The purpose of connection2fsm & account2fsm are to
# keep track of the connection & account IDs that are
# currently running an operation on the GameServicesManager.
# Ideally, this will help us prevent clients from running
# more than one operation at a time which could potentially
# lead to race conditions & the exploitation of them.
self.connection2fsm = {}
self.account2fsm = {}
# Instantiate the account database interface.
# TODO: In the future, add more database interfaces & make this configurable.
self.accountDb = DeveloperAccountDB(self)
def login(self, playToken):
# Get the connection ID.
sender = self.air.getMsgSender()
self.notify.debug('Play token %s received from %s' % (playToken, sender))
if sender >> 32:
# This account is already logged in.
self.killConnection(sender, 'This account is already logged in.')
return
if sender in self.connection2fsm:
# This account is already currently running an operation. Kill this connection.
self.killConnectionFSM(sender)
return
# Run the login operation.
self.connection2fsm[sender] = LoginOperation(self, sender)
self.connection2fsm[sender].request('Start', playToken)
def killConnection(self, connectionId, reason):
# Sends CLIENTAGENT_EJECT to the given connectionId with the given reason.
datagram = PyDatagram()
datagram.addServerHeader(connectionId, self.air.ourChannel, CLIENTAGENT_EJECT)
datagram.addUint16(OTPGlobals.BootedConnectionKilled)
datagram.addString(reason)
self.air.send(datagram)
def killConnectionFSM(self, connectionId):
# Kills the connection for duplicate FSMs.
fsm = self.connection2fsm.get(connectionId)
if not fsm:
self.notify.warning('Tried to kill connection %s for duplicate FSMs, but none exist!' % connectionId)
return
self.killConnection(connectionId, 'An operation is already running: %s' % fsm.name)
def killAccount(self, accountId, reason):
# Kills the account's connection given an accountId & a reason.
self.killConnection(self.GetAccountConnectionChannel(accountId), reason=reason)
def killAccountFSM(self, accountId):
# Kills the account for duplicate FSMs.
fsm = self.account2fsm.get(accountId)
if not fsm:
self.notify.warning('Tried to kill account %s for duplicate FSMs, but none exist!' % accountId)
return
self.killAccount(accountId, 'An operation is already running: %s' % fsm.name)
def runOperation(self, operationType, *args):
# Runs an operation on the sender. First, get the sender.
sender = self.air.getAccountIdFromSender()
if not sender:
# If the sender doesn't exist, they're not
# logged in, so kill the connection.
self.killAccount(sender, 'Client is not logged in.')
if sender in self.account2fsm:
# This account is already currently running an operation. Kill this connection.
self.killAccountFSM(sender)
return
self.account2fsm[sender] = operationType(self, sender)
self.account2fsm[sender].request('Start', *args)
def requestAvatarList(self):
# An account is requesting their avatar list;
# let's run a GetAvatarsOperation.
self.runOperation(GetAvatarsOperation)
def requestRemoveAvatar(self, avId):
# Someone is requesting to have an avatar removed; run a RemoveAvatarOperation.
self.runOperation(RemoveAvatarOperation, avId)
def requestPlayAvatar(self, avId):
# Someone is requesting to play on an avatar.
# First, let's get the senders avId & accId.
currentAvId = self.air.getAvatarIdFromSender()
accountId = self.air.getAccountIdFromSender()
if currentAvId and avId:
# An avatar has already been chosen! Kill the account.
self.killAccount(accountId, 'An avatar is already chosen!')
return
elif not currentAvId and not avId:
# The client is likely making sure that none of its
# avatars are active, so this isn't really an error.
return
if avId:
# If the avId is not a NoneType, that means the client wants
# to load an avatar; run a LoadAvatarOperation.
self.runOperation(LoadAvatarOperation, avId)
else:
# Otherwise, the client wants to unload the avatar; run an UnloadAvatarOperation.
self.runOperation(UnloadAvatarOperation, currentAvId)