489 lines
19 KiB
Python
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)
|