open-toontown/otp/friends/FriendManagerAI.py

489 lines
19 KiB
Python

from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectGlobalAI import DistributedObjectGlobalAI
from direct.distributed.PyDatagram import *
from otp.otpbase import OTPGlobals
class FriendManagerAI(DistributedObjectGlobalAI):
notify = DirectNotifyGlobal.directNotify.newCategory('FriendManagerAI')
# These structures record the invitations currently being handled.
nextContext = 0
invites = {}
inviters = {}
invitees = {}
# This is the length of time that should elapse before we start to
# forget who has declined friendships from whom.
DeclineFriendshipTimeout = 600.0
# This subclass is used to record currently outstanding
# in-the-game invitation requests.
class Invite:
def __init__(self, context, inviterId, inviteeId):
self.context = context
self.inviterId = inviterId
self.inviteeId = inviteeId
self.inviter = None
self.invitee = None
self.inviteeKnows = 0
self.sendSpecialResponse = 0
def __init__(self, air):
DistributedObjectGlobalAI.__init__(self, air)
# We maintain two maps of toons who have declined
# friendships. We add entries to map1, and every ten
# minutes, we roll map1 into map2. This provides a
# timeout of ten to twenty minutes for a particular
# rejection, and also prevents the maps from growing very
# large in memory.
self.declineFriends1 = {}
self.declineFriends2 = {}
self.lastRollTime = 0
### Messages sent from inviter client to AI
def friendQuery(self, inviteeId):
"""friendQuery(self, int inviterId, int inviteeId)
Sent by the inviter to the AI to initiate a friendship
request.
"""
inviterId = self.air.getAvatarIdFromSender()
invitee = self.air.doId2do.get(inviteeId)
# see if the inviteeId is valid
if not invitee:
self.air.writeServerEvent('suspicious', inviteeId, 'FriendManagerAI.friendQuery not on list')
return
self.notify.debug("AI: friendQuery(%d, %d)" % (inviterId, inviteeId))
self.newInvite(inviterId, inviteeId)
def cancelFriendQuery(self, context):
"""cancelFriendQuery(self, int context)
Sent by the inviter to the AI to cancel a pending friendship
request.
"""
avId = self.air.getAvatarIdFromSender()
self.notify.debug("AI: cancelFriendQuery(%d)" % (context))
try:
invite = FriendManagerAI.invites[context]
except:
# The client might legitimately try to cancel a context
# that has already been cancelled.
#self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.cancelFriendQuery unknown context')
#FriendManagerAI.notify.warning('Message for unknown context ' + repr(context))
return
self.cancelInvite(invite)
### Messages sent from invitee client to AI
def inviteeFriendConsidering(self, response, context):
"""inviteeFriendConsidering(self, int response, int context)
Sent by the invitee to the AI to indicate whether the invitee
is able to consider the request right now.
The responses are:
0 - no
1 - yes
4 - the invitee is ignoring you.
"""
self.notify.debug("AI: inviteeFriendConsidering(%d, %d)" % (response, context))
avId = self.air.getAvatarIdFromSender()
try:
invite = FriendManagerAI.invites[context]
except:
self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.inviteeFriendConsidering unknown context')
FriendManagerAI.notify.warning('Message for unknown context ' + repr(context))
return
if response == 1:
self.inviteeAvailable(invite)
else:
self.inviteeUnavailable(invite, response)
def inviteeFriendResponse(self, yesNoMaybe, context):
"""inviteeFriendResponse(self, int yesNoMaybe, int context)
Sent by the invitee to the AI, following an affirmative
response in inviteeFriendConsidering, to indicate whether or
not the user decided to accept the friendship.
"""
self.notify.debug("AI: inviteeFriendResponse(%d, %d)" % (yesNoMaybe, context))
avId = self.air.getAvatarIdFromSender()
try:
invite = FriendManagerAI.invites[context]
except:
self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.inviteeFriendResponse unknown context')
FriendManagerAI.notify.warning('Message for unknown context ' + repr(context))
return
if yesNoMaybe == 1:
self.makeFriends(invite)
else:
self.noFriends(invite, yesNoMaybe)
### Messages sent from AI to invitee client
def down_inviteeFriendQuery(self, recipient, inviterId, inviterName,
inviterDna, context):
"""inviteeFriendQuery(self, DistributedObject recipient,
int inviterId, string inviterName,
AvatarDNA inviterDna, int context)
Sent by the AI to the invitee client to initiate a friendship
request from the indiciated inviter. The invitee client
should respond immediately with inviteeFriendConsidering, to
indicate whether the invitee is able to consider the
invitation right now.
"""
self.sendUpdateToAvatarId(recipient, "inviteeFriendQuery",
[inviterId, inviterName,
inviterDna.makeNetString(), context])
self.notify.debug("AI: inviteeFriendQuery(%d, %s, dna, %d)" % (inviterId, inviterName, context))
def down_inviteeCancelFriendQuery(self, recipient, context):
"""inviteeCancelFriendQuery(self, DistributedObject recipient,
int context)
Sent by the AI to the invitee client to initiate that the
inviter has rescinded his/her previous invitation by clicking
the cancel button.
"""
self.sendUpdateToAvatarId(recipient, "inviteeCancelFriendQuery", [context])
self.notify.debug("AI: inviteeCancelFriendQuery(%d)" % (context))
### Messages involving secrets
def requestSecret(self):
"""requestSecret(self)
Sent by the client to the AI to request a new "secret" for the
user.
"""
# TODO
self.notify.info('TODO: requestSecret')
### Messages sent from AI to inviter client
def down_friendConsidering(self, recipient, yesNoAlready, context):
"""friendConsidering(self, DistributedObject recipient,
int yesNoAlready, int context)
Sent by the AI to the inviter client to indicate whether the
invitee is able to consider the request right now.
The responses are:
# 0 - the invitee is busy
# 2 - the invitee is already your friend
# 3 - the invitee is yourself
# 4 - the invitee is ignoring you.
# 6 - the invitee not accepting friends
"""
self.sendUpdateToAvatarId(recipient, "friendConsidering", [yesNoAlready, context])
self.notify.debug("AI: friendConsidering(%d, %d)" % (yesNoAlready, context))
def down_friendResponse(self, recipient, yesNoMaybe, context):
"""friendResponse(self, DistributedOBject recipient,
int yesNoMaybe, int context)
Sent by the AI to the inviter client, following an affirmitive
response in friendConsidering, to indicate whether or not the
user decided to accept the friendship.
"""
self.sendUpdateToAvatarId(recipient, "friendResponse", [yesNoMaybe, context])
self.notify.debug("AI: friendResponse(%d, %d)" % (yesNoMaybe, context))
### Support methods
def newInvite(self, inviterId, inviteeId):
context = FriendManagerAI.nextContext
FriendManagerAI.nextContext += 1
invite = FriendManagerAI.Invite(context, inviterId, inviteeId)
FriendManagerAI.invites[context] = invite
# If the invitee has previously (recently) declined a
# friendship from this inviter, don't ask again.
previous = self.__previousResponse(inviteeId, inviterId)
if previous != None:
self.inviteeUnavailable(invite, previous + 10)
return
# If the invitee is presently being invited by someone else,
# we don't even have to bother him.
if inviteeId in FriendManagerAI.invitees:
self.inviteeUnavailable(invite, 0)
return
if invite.inviterId == invite.inviteeId:
# You can't be friends with yourself.
self.inviteeUnavailable(invite, 3)
return
# If the inviter is already involved in some other context,
# that one is now void.
if inviterId in FriendManagerAI.inviters:
self.cancelInvite(FriendManagerAI.inviters[inviterId])
FriendManagerAI.inviters[inviterId] = invite
FriendManagerAI.invitees[inviteeId] = invite
#self.air.queryObject(inviteeId, self.gotInvitee, invite)
#self.air.queryObject(inviterId, self.gotInviter, invite)
invite.inviter = self.air.doId2do.get(inviterId)
invite.invitee = self.air.doId2do.get(inviteeId)
if invite.inviter and invite.invitee:
self.beginInvite(invite)
def beginInvite(self, invite):
# Ask the invitee if he is available to consider being
# someone's friend--that is, that he's not busy playing a
# minigame or something.
invite.inviteeKnows = 1
self.down_inviteeFriendQuery(invite.inviteeId, invite.inviterId,
invite.inviter.getName(),
invite.inviter.dna, invite.context)
def inviteeUnavailable(self, invite, code):
# Cannot make the request for one of these reasons:
#
# 0 - the invitee is busy
# 2 - the invitee is already your friend
# 3 - the invitee is yourself
# 4 - the invitee is ignoring you.
# 6 - the invitee not accepting friends
self.down_friendConsidering(invite.inviterId, code, invite.context)
# That ends the invitation.
self.clearInvite(invite)
def inviteeAvailable(self, invite):
# The invitee is considering our friendship request.
self.down_friendConsidering(invite.inviterId, 1, invite.context)
def noFriends(self, invite, yesNoMaybe):
# The invitee declined to make friends.
#
# 0 - no
# 2 - unable to answer; e.g. entered a minigame or something.
# 3 - the invitee has too many friends already.
if yesNoMaybe == 0 or yesNoMaybe == 3:
# The user explictly said no or has too many friends.
# Disallow this guy from asking again for the next ten
# minutes or so.
if invite.inviteeId not in self.declineFriends1:
self.declineFriends1[invite.inviteeId] = {}
self.declineFriends1[invite.inviteeId][invite.inviterId] = yesNoMaybe
self.down_friendResponse(invite.inviterId, yesNoMaybe, invite.context)
self.clearInvite(invite)
def makeFriends(self, invite):
# The invitee agreed to make friends.
self.__handleMakeFriends(invite.inviteeId, invite.inviterId, 0,
invite.context)
self.down_friendResponse(invite.inviterId, 1, invite.context)
# The reply will clear the context out when it comes in.
def __handleMakeFriends(self, avatarAId, avatarBId, flags, context):
# And now the magic begins.
avatarA = self.air.doId2do.get(avatarAId) # invitee
avatarB = self.air.doId2do.get(avatarBId) # inviter
if avatarA and avatarB:
datagram = PyDatagram()
datagram.addServerHeader(self.GetPuppetConnectionChannel(avatarA.getDoId()), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
datagram.addUint32(avatarB.getDoId())
datagram.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(datagram)
datagram = PyDatagram()
datagram.addServerHeader(self.GetPuppetConnectionChannel(avatarB.getDoId()), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
datagram.addUint32(avatarA.getDoId())
datagram.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(datagram)
if avatarA:
avatarAFriendsList = avatarA.getFriendsList()
if len(avatarAFriendsList) >= OTPGlobals.MaxFriends:
self.makeFriendsReply(0, context)
return
avatarANewFriend = (avatarBId, flags)
if avatarANewFriend in avatarAFriendsList:
self.makeFriendsReply(0, context)
return
avatarAFriendsList.append(avatarANewFriend)
avatarA.d_setFriendsList(avatarAFriendsList)
else:
def handleAvatarA(dclass, fields):
if dclass != self.air.dclassesByName['DistributedToonAI']:
return
avatarAFriendsList = fields['setFriendsList'][0]
if len(avatarAFriendsList) >= OTPGlobals.MaxFriends:
self.makeFriendsReply(0, context)
return
avatarANewFriend = (avatarBId, flags)
if avatarANewFriend in avatarAFriendsList:
self.makeFriendsReply(0, context)
return
avatarAFriendsList.append(avatarANewFriend)
self.air.dbInterface.updateObject(self.air.dbId, avatarAId, self.air.dclassesByName['DistributedToonAI'],
{'setFriendsList': [avatarAFriendsList]})
self.air.dbInterface.queryObject(self.air.dbId, avatarAId, handleAvatarA)
if avatarB:
avatarBFriendsList = avatarB.getFriendsList()
if len(avatarBFriendsList) >= OTPGlobals.MaxFriends:
self.makeFriendsReply(0, context)
return
avatarBNewFriend = (avatarAId, flags)
if avatarBNewFriend in avatarBFriendsList:
self.makeFriendsReply(0, context)
return
avatarBFriendsList.append(avatarBNewFriend)
avatarB.d_setFriendsList(avatarBFriendsList)
else:
def handleAvatarB(dclass, fields):
if dclass != self.air.dclassesByName['DistributedToonAI']:
return
avatarBFriendsList = fields['setFriendsList'][0]
if len(avatarBFriendsList) >= OTPGlobals.MaxFriends:
self.makeFriendsReply(0, context)
return
avatarBNewFriend = (avatarAId, flags)
if avatarBNewFriend in avatarBFriendsList:
self.makeFriendsReply(0, context)
return
avatarBFriendsList.append(avatarBNewFriend)
self.air.dbInterface.updateObject(self.air.dbId, avatarBId, self.air.dclassesByName['DistributedToonAI'],
{'setFriendsList': [avatarBFriendsList]})
self.air.dbInterface.queryObject(self.air.dbId, avatarBId, handleAvatarB)
self.makeFriendsReply(1, context)
def __previousResponse(self, inviteeId, inviterId):
# Return the previous rejection code if this invitee has
# previously (recently) declined a friendship from this
# inviter, or None if there was no previous rejection.
now = globalClock.getRealTime()
if now - self.lastRollTime >= self.DeclineFriendshipTimeout:
self.declineFriends2 = self.declineFriends1
self.declineFriends1 = {}
# Now, is the invitee/inviter combination present in either
# map?
previous = None
if inviteeId in self.declineFriends1:
previous = self.declineFriends1[inviteeId].get(inviterId)
if previous != None:
return previous
if inviteeId in self.declineFriends2:
previous = self.declineFriends2[inviteeId].get(inviterId)
if previous != None:
return previous
# Nope, go ahead and ask.
return None
def clearInvite(self, invite):
try:
del FriendManagerAI.invites[invite.context]
except:
pass
try:
del FriendManagerAI.inviters[invite.inviterId]
except:
pass
try:
del FriendManagerAI.invitees[invite.inviteeId]
except:
pass
def cancelInvite(self, invite):
if invite.inviteeKnows:
self.down_inviteeCancelFriendQuery(invite.inviteeId, invite.context)
invite.inviteeKnows = 0
def makeFriendsReply(self, result, context):
try:
invite = FriendManagerAI.invites[context]
except:
FriendManagerAI.notify.warning('Message for unknown context ' + repr(context))
return
if result:
# By now, the server has OK'ed the friends transaction.
# Update our internal bookkeeping so we remember who's
# friends with whom. This is mainly useful for correct
# accounting of the make-a-friend quest.
invitee = self.air.doId2do.get(invite.inviteeId)
inviter = self.air.doId2do.get(invite.inviterId)
if invitee != None:
invitee.extendFriendsList(invite.inviterId, invite.sendSpecialResponse)
self.air.questManager.toonMadeFriend(invitee, inviter)
#inviter = self.air.doId2do.get(invite.inviterId)
if inviter != None:
inviter.extendFriendsList(invite.inviteeId, invite.sendSpecialResponse)
self.air.questManager.toonMadeFriend(inviter, invitee)
if invite.sendSpecialResponse:
# If this flag is set, the "invite" was generated via the
# codeword system, instead of through the normal path. In
# this case, we need to send the acknowledgement back to
# the client.
if result:
# Success! Send a result code of 1.
result = 1
else:
# Failure, some friends list problem. Result code of 2.
result = 2
self.down_submitSecretResponse(invite.inviterId, result,
invite.inviteeId)
# Also send a notification to the other avatar, if he's on.
avatar = DistributedAvatarAI.DistributedAvatarAI(self.air)
avatar.doId = invite.inviteeId
avatar.d_friendsNotify(invite.inviterId, 2)
self.clearInvite(invite)