add widescreen patch, fix chat/friends.

This commit is contained in:
Sam Sneed 2024-06-25 21:00:27 -05:00
parent a5ecbb8b1e
commit 4fbb08316c
23 changed files with 5463 additions and 183 deletions

View file

@ -1,5 +1,5 @@
# Open Toontown
This repository contains the code for Open Toontown, based on the latest version of Disney's Toontown Online (sv1.0.47.38).
# Oldschool Toontown
This repository contains the code for Open Toontown, plus some added features from (at the time of adding) bleeding-edge commits. It is based on the latest version of Disney's Toontown Online (sv1.0.47.38).
# Setup
After cloning the repository, you will need to clone the [resources](https://github.com/open-toontown/resources) repository inside the directory where you cloned the source repo.
@ -47,3 +47,9 @@ How you commit changes is your choice, but please include what you did and a bas
* `minigames: Fix crash when entering the trolley`
* `racing: Fix possible race condition when two racers tied`
* `golf: Refix wonky physics once and for all (hopefully)`
# Known issues...
* Estates don't work.
* Fishing doesn't work.
* Parties don't work.

View file

@ -0,0 +1,5 @@
from otp.avatar.DistributedAvatarUD import DistributedAvatarUD
from direct.directnotify.DirectNotifyGlobal import directNotify
class DistributedPlayerUD(DistributedAvatarUD):
notify = directNotify.newCategory('DistributedPlayerUD')

View file

@ -61,7 +61,6 @@ class LocalAvatar(DistributedAvatar.DistributedAvatar, DistributedSmoothNode.Dis
self.soundRun = None
self.soundWalk = None
self.sleepFlag = 0
self.noSleep = 0
self.isDisguised = 0
self.movingFlag = 0
self.swimmingFlag = 0
@ -985,7 +984,7 @@ class LocalAvatar(DistributedAvatar.DistributedAvatar, DistributedSmoothNode.Dis
return
def gotoSleep(self):
if not self.sleepFlag and not self.noSleep:
if not self.sleepFlag:
self.b_setAnimState('Sleep', self.animMultiplier)
self.sleepFlag = 1

31
otp/chat/ChatHandler.py Normal file
View file

@ -0,0 +1,31 @@
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.distributed.DistributedObjectGlobal import DistributedObjectGlobal
class ChatHandler(DistributedObjectGlobal):
"""
The purpose of this class is to handle chat messages from the client to the
uberdog to filter out unwanted words. Then send it through the server.
"""
notify = directNotify.newCategory('ChatRouter')
def sendChatMessage(self, message):
"""
send a chat message to the uberdog
Args:
message (string): the message to send that was typed in by the user
"""
self.sendUpdate('chatMessage', [message])
def sendWhisperMessage(self, message, receiverAvId):
"""
send a whisper message to the uberdog
Args:
message (string): the message to send that was typed in by the user
receiverAvId (int): the avatar id of the person to send the message to
"""
self.sendUpdate('whisperMessage', [message, receiverAvId])

76
otp/chat/ChatHandlerUD.py Normal file
View file

@ -0,0 +1,76 @@
from direct.directnotify.DirectNotifyGlobal import directNotify
from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD
from toontown.chat.TTWhiteList import TTWhiteList
whiteList = TTWhiteList()
class ChatHandlerUD(DistributedObjectGlobalUD):
"""
The purpose of this class is to handle chat messages from the client to the
uberdog to filter out unwanted words. Then send it through the server.
"""
notify = directNotify.newCategory('ChatRouterUD')
def filterWhitelist(self, message):
"""
this function filters out words that are not in the whitelist
Args:
message (string): the original message to filter
Returns:
mods (string): the filtered message
"""
words = message.split(' ')
offset = 0
mods = []
for word in words:
if not whiteList.isWord(word):
mods.append((offset, offset + len(word) - 1))
offset += len(word) + 1
return mods
def chatMessage(self, message):
"""
send a chat message through the server
Args:
message (string): the message to send that was typed in by the user
"""
avId = self.air.getAvatarIdFromSender()
if not avId:
return
channel = avId
mods = self.filterWhitelist(message)
do = self.air.dclassesByName['DistributedPlayerUD']
args = [avId, 0, '', message, mods, 0]
datagram = do.aiFormatUpdate('setTalk', avId, channel, self.air.ourChannel, args)
self.air.send(datagram)
def whisperMessage(self, message, receiverAvId):
"""
send a whisper message through the server
Args:
message (string): the message to send that was typed in by the user
receiverAvId (int): the avatar id of the person to send the message to
"""
avId = self.air.getAvatarIdFromSender()
if not avId:
return
mods = self.filterWhitelist(message)
do = self.air.dclassesByName['DistributedPlayerUD']
args = [avId, 0, '', message, mods, 0]
datagram = do.aiFormatUpdate('setTalkWhisper', receiverAvId, receiverAvId, self.air.ourChannel, args)
self.air.send(datagram)

View file

@ -0,0 +1,752 @@
import sys
from direct.showbase import DirectObject
from otp.otpbase import OTPLocalizer
from direct.directnotify import DirectNotifyGlobal
from otp.otpbase import OTPGlobals
from otp.speedchat import SCDecoders
from panda3d.core import *
from otp.chat.TalkMessage import TalkMessage
from otp.chat.TalkHandle import TalkHandle
import time
from otp.chat.TalkGlobals import *
from otp.chat.ChatGlobals import *
from panda3d.otp import CFSpeech, CFTimeout, CFThought
ThoughtPrefix = '.'
class TalkAssistant(DirectObject.DirectObject):
ExecNamespace = None
notify = DirectNotifyGlobal.directNotify.newCategory('TalkAssistant')
execChat = ConfigVariableBool('exec-chat', 0).value
def __init__(self):
self.logWhispers = 1
self.whiteList = None
self.clearHistory()
self.zeroTimeDay = time.time()
self.zeroTimeGame = globalClock.getRealTime()
self.floodThreshold = 10.0
self.useWhiteListFilter = ConfigVariableBool('white-list-filter-openchat', 0).value
self.lastWhisperDoId = None
self.lastWhisperPlayerId = None
self.lastWhisper = None
self.SCDecoder = SCDecoders
return
def clearHistory(self):
self.historyComplete = []
self.historyOpen = []
self.historyUpdates = []
self.historyGuild = []
self.historyByDoId = {}
self.historyByDISLId = {}
self.floodDataByDoId = {}
self.spamDictByDoId = {}
self.labelGuild = OTPLocalizer.TalkGuild
self.handleDict = {}
self.messageCount = 0
self.shownWhiteListWarning = 0
def delete(self):
self.ignoreAll()
self.clearHistory()
def start(self):
pass
def stop(self):
pass
def countMessage(self):
self.messageCount += 1
return self.messageCount - 1
def getOpenText(self, numLines, startPoint = 0):
return self.historyOpen[startPoint:startPoint + numLines]
def getSizeOpenText(self):
return len(self.historyOpen)
def getCompleteText(self, numLines, startPoint = 0):
return self.historyComplete[startPoint:startPoint + numLines]
def getCompleteTextFromRecent(self, numLines, startPoint = 0):
start = len(self.historyComplete) - startPoint
if start < 0:
start = 0
backStart = max(start - numLines, 0)
text = self.historyComplete[backStart:start]
text.reverse()
return text
def getAllCompleteText(self):
return self.historyComplete
def getAllHistory(self):
return self.historyComplete
def getSizeCompleteText(self):
return len(self.historyComplete)
def getHandle(self, doId):
return self.handleDict.get(doId)
def doWhiteListWarning(self):
pass
def addToHistoryDoId(self, message, doId, scrubbed = 0):
if message.getTalkType() == TALK_WHISPER and doId != localAvatar.doId:
self.lastWhisperDoId = doId
self.lastWhisper = self.lastWhisperDoId
if doId not in self.historyByDoId:
self.historyByDoId[doId] = []
self.historyByDoId[doId].append(message)
if not self.shownWhiteListWarning and scrubbed and doId == localAvatar.doId:
self.doWhiteListWarning()
self.shownWhiteListWarning = 1
if doId not in self.floodDataByDoId:
self.floodDataByDoId[doId] = [0.0, self.stampTime(), message]
else:
oldTime = self.floodDataByDoId[doId][1]
newTime = self.stampTime()
timeDiff = newTime - oldTime
oldRating = self.floodDataByDoId[doId][0]
contentMult = 1.0
if len(message.getBody()) < 6:
contentMult += 0.2 * float(6 - len(message.getBody()))
if self.floodDataByDoId[doId][2].getBody() == message.getBody():
contentMult += 1.0
floodRating = max(0, 3.0 * contentMult + oldRating - timeDiff)
self.floodDataByDoId[doId] = [floodRating, self.stampTime(), message]
if floodRating > self.floodThreshold:
if oldRating < self.floodThreshold:
self.floodDataByDoId[doId] = [floodRating + 3.0, self.stampTime(), message]
return 1
else:
self.floodDataByDoId[doId] = [oldRating - timeDiff, self.stampTime(), message]
return 2
return 0
def addToHistoryDISLId(self, message, dISLId, scrubbed = 0):
if message.getTalkType() == TALK_ACCOUNT and dISLId != base.cr.accountDetailRecord.playerAccountId:
self.lastWhisperPlayerId = dISLId
self.lastWhisper = self.lastWhisperPlayerId
if dISLId not in self.historyByDISLId:
self.historyByDISLId[dISLId] = []
self.historyByDISLId[dISLId].append(message)
def addHandle(self, doId, message):
if doId == localAvatar.doId:
return
handle = self.handleDict.get(doId)
if not handle:
handle = TalkHandle(doId, message)
self.handleDict[doId] = handle
else:
handle.addMessageInfo(message)
def stampTime(self):
return globalClock.getRealTime() - self.zeroTimeGame
def findName(self, id, isPlayer = 0):
if isPlayer:
return self.findPlayerName(id)
else:
return self.findAvatarName(id)
def findAvatarName(self, id):
info = base.cr.identifyAvatar(id)
if info:
return info.getName()
else:
return ''
def findPlayerName(self, id):
info = base.cr.playerFriendsManager.getFriendInfo(id)
if info:
return info.playerName
else:
return ''
def whiteListFilterMessage(self, text):
if not self.useWhiteListFilter:
return text
elif not base.whiteList:
return 'no list'
words = text.split(' ')
newwords = []
for word in words:
if word == '' or base.whiteList.isWord(word):
newwords.append(word)
else:
newwords.append(base.whiteList.defaultWord)
newText = ' '.join(newwords)
return newText
def colorMessageByWhiteListFilter(self, text):
if not base.whiteList:
return text
words = text.split(' ')
newwords = []
for word in words:
if word == '' or base.whiteList.isWord(word):
newwords.append(word)
else:
newwords.append('\x01WLRed\x01' + word + '\x02')
newText = ' '.join(newwords)
return newText
def executeSlashCommand(self, text):
pass
def executeGMCommand(self, text):
pass
def isThought(self, message):
if not message:
return 0
elif len(message) == 0:
return 0
elif message.find(ThoughtPrefix, 0, len(ThoughtPrefix)) >= 0:
return 1
else:
return 0
def removeThoughtPrefix(self, message):
if self.isThought(message):
return message[len(ThoughtPrefix):]
else:
return message
def fillWithTestText(self):
hold = self.floodThreshold
self.floodThreshold = 1000.0
self.receiveOpenTalk(1001, 'Bob the Ghost', None, None, 'Hello from the machine')
self.receiveOpenTalk(1001, 'Bob the Ghost', None, None, 'More text for ya!')
self.receiveOpenTalk(1001, 'Bob the Ghost', None, None, 'Hope this makes life easier')
self.receiveOpenTalk(1002, 'Doug the Spirit', None, None, 'Now we need some longer text that will spill over onto two lines')
self.receiveOpenTalk(1002, 'Doug the Spirit', None, None, 'Maybe I will tell you')
self.receiveOpenTalk(1001, 'Bob the Ghost', None, None, 'If you are seeing this text it is because you are cool')
self.receiveOpenTalk(1002, 'Doug the Spirit', None, None, "That's right, there is no need to call tech support")
self.receiveOpenTalk(localAvatar.doId, localAvatar.getName, None, None, "Okay I won't call tech support, because I am cool")
self.receiveGMTalk(1003, 'God of Text', None, None, 'Good because I have seen it already')
self.floodThreshold = hold
return
def printHistoryComplete(self):
print('HISTORY COMPLETE')
for message in self.historyComplete:
print('%s %s %s\n%s\n' % (message.getTimeStamp(),
message.getSenderAvatarName(),
message.getSenderAccountName(),
message.getBody()))
def importExecNamespace(self):
pass
def execMessage(self, message):
print('execMessage %s' % message)
if not TalkAssistant.ExecNamespace:
TalkAssistant.ExecNamespace = {}
exec('from panda3d.core import *', globals(), self.ExecNamespace)
self.importExecNamespace()
try:
if not __debug__ or __execWarnings__:
print('EXECWARNING TalkAssistant eval: %s' % message)
printStack()
return str(eval(message, globals(), TalkAssistant.ExecNamespace))
except SyntaxError:
try:
if not __debug__ or __execWarnings__:
print('EXECWARNING TalkAssistant exec: %s' % message)
printStack()
exec(message, globals(), TalkAssistant.ExecNamespace)
return 'ok'
except:
exception = sys.exc_info()[0]
extraInfo = sys.exc_info()[1]
if extraInfo:
return str(extraInfo)
else:
return str(exception)
except:
exception = sys.exc_info()[0]
extraInfo = sys.exc_info()[1]
if extraInfo:
return str(extraInfo)
else:
return str(exception)
def checkOpenTypedChat(self):
if base.localAvatar.commonChatFlags & OTPGlobals.CommonChat:
return True
return False
def checkAnyTypedChat(self):
if base.localAvatar.commonChatFlags & OTPGlobals.CommonChat:
return True
if base.localAvatar.canChat():
return True
return False
def checkOpenSpeedChat(self):
return True
def checkWhisperTypedChatAvatar(self, avatarId):
remoteAvatar = base.cr.doId2do.get(avatarId)
if remoteAvatar:
if remoteAvatar.isUnderstandable():
return True
if base.localAvatar.commonChatFlags & OTPGlobals.SuperChat:
return True
remoteAvatarOrHandleOrInfo = base.cr.identifyAvatar(avatarId)
if remoteAvatarOrHandleOrInfo and hasattr(remoteAvatarOrHandleOrInfo, 'isUnderstandable'):
if remoteAvatarOrHandleOrInfo.isUnderstandable():
return True
info = base.cr.playerFriendsManager.findPlayerInfoFromAvId(avatarId)
if info:
if info.understandableYesNo:
return True
info = base.cr.avatarFriendsManager.getFriendInfo(avatarId)
if info:
if info.understandableYesNo:
return True
if base.cr.getFriendFlags(avatarId) & OTPGlobals.FriendChat:
return True
return False
def checkWhisperSpeedChatAvatar(self, avatarId):
return True
def checkWhisperTypedChatPlayer(self, playerId):
info = base.cr.playerFriendsManager.getFriendInfo(playerId)
if info:
if info.understandableYesNo:
return True
return False
def checkWhisperSpeedChatPlayer(self, playerId):
if base.cr.playerFriendsManager.isPlayerFriend(playerId):
return True
return False
def checkOpenSpeedChat(self):
return True
def checkWhisperSpeedChatAvatar(self, avatarId):
return True
def checkWhisperSpeedChatPlayer(self, playerId):
if base.cr.playerFriendsManager.isPlayerFriend(playerId):
return True
return False
def checkGuildTypedChat(self):
if localAvatar.guildId:
return True
return False
def checkGuildSpeedChat(self):
if localAvatar.guildId:
return True
return False
def receiveOpenTalk(self, senderAvId, avatarName, accountId, accountName, message, scrubbed = 0):
error = None
if not avatarName and senderAvId:
localAvatar.sendUpdate('logSuspiciousEvent', ['receiveOpenTalk: invalid avatar name (%s)' % senderAvId])
avatarName = self.findAvatarName(senderAvId)
if not accountName and accountId:
accountName = self.findPlayerName(accountId)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, senderAvId, avatarName, accountId, accountName, None, None, None, None, TALK_OPEN, None)
if senderAvId != localAvatar.doId:
self.addHandle(senderAvId, newMessage)
reject = 0
if senderAvId:
reject = self.addToHistoryDoId(newMessage, senderAvId, scrubbed)
if accountId:
self.addToHistoryDISLId(newMessage, accountId)
if reject == 1:
newMessage.setBody(OTPLocalizer.AntiSpamInChat)
if reject != 2:
isSpam = self.spamDictByDoId.get(senderAvId) and reject
if not isSpam:
self.historyComplete.append(newMessage)
self.historyOpen.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
if newMessage.getBody() == OTPLocalizer.AntiSpamInChat:
self.spamDictByDoId[senderAvId] = 1
else:
self.spamDictByDoId[senderAvId] = 0
return error
def receiveWhisperTalk(self, avatarId, avatarName, accountId, accountName, toId, toName, message, scrubbed = 0):
error = None
print('receiveWhisperTalk %s %s %s %s %s' % (avatarId,
avatarName,
accountId,
accountName,
message))
if not avatarName and avatarId:
avatarName = self.findAvatarName(avatarId)
if not accountName and accountId:
accountName = self.findPlayerName(accountId)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, avatarId, avatarName, accountId, accountName, toId, toName, None, None, TALK_WHISPER, None)
if avatarId == localAvatar.doId:
self.addHandle(toId, newMessage)
else:
self.addHandle(avatarId, newMessage)
self.historyComplete.append(newMessage)
if avatarId:
self.addToHistoryDoId(newMessage, avatarId, scrubbed)
if accountId:
self.addToHistoryDISLId(newMessage, accountId)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveAccountTalk(self, avatarId, avatarName, accountId, accountName, toId, toName, message, scrubbed = 0):
if not accountName and base.cr.playerFriendsManager.playerId2Info.get(accountId):
accountName = base.cr.playerFriendsManager.playerId2Info.get(accountId).playerName
error = None
if not avatarName and avatarId:
avatarName = self.findAvatarName(avatarId)
if not accountName and accountId:
accountName = self.findPlayerName(accountId)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, avatarId, avatarName, accountId, accountName, None, None, toId, toName, TALK_ACCOUNT, None)
self.historyComplete.append(newMessage)
if avatarId:
self.addToHistoryDoId(newMessage, avatarId, scrubbed)
if accountId:
self.addToHistoryDISLId(newMessage, accountId, scrubbed)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveGuildTalk(self, senderAvId, fromAC, avatarName, message, scrubbed = 0):
error = None
if not self.isThought(message):
accountName = self.findName(fromAC, 1)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, senderAvId, avatarName, fromAC, accountName, None, None, None, None, TALK_GUILD, None)
reject = self.addToHistoryDoId(newMessage, senderAvId)
if reject == 1:
newMessage.setBody(OTPLocalizer.AntiSpamInChat)
if reject != 2:
isSpam = self.spamDictByDoId.get(senderAvId) and reject
if not isSpam:
self.historyComplete.append(newMessage)
self.historyGuild.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
if newMessage.getBody() == OTPLocalizer.AntiSpamInChat:
self.spamDictByDoId[senderAvId] = 1
else:
self.spamDictByDoId[senderAvId] = 0
return error
def receiveGMTalk(self, avatarId, avatarName, accountId, accountName, message, scrubbed = 0):
error = None
if not avatarName and avatarId:
avatarName = self.findAvatarName(avatarId)
if not accountName and accountId:
accountName = self.findPlayerName(accountId)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, avatarId, avatarName, accountId, accountName, None, None, None, None, TALK_GM, None)
self.historyComplete.append(newMessage)
self.historyOpen.append(newMessage)
if avatarId:
self.addToHistoryDoId(newMessage, avatarId)
if accountId:
self.addToHistoryDISLId(newMessage, accountId)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveThought(self, avatarId, avatarName, accountId, accountName, message, scrubbed = 0):
error = None
if not avatarName and avatarId:
avatarName = self.findAvatarName(avatarId)
if not accountName and accountId:
accountName = self.findPlayerName(accountId)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, avatarId, avatarName, accountId, accountName, None, None, None, None, AVATAR_THOUGHT, None)
if avatarId != localAvatar.doId:
self.addHandle(avatarId, newMessage)
reject = 0
if avatarId:
reject = self.addToHistoryDoId(newMessage, avatarId, scrubbed)
if accountId:
self.addToHistoryDISLId(newMessage, accountId)
if reject == 1:
newMessage.setBody(OTPLocalizer.AntiSpamInChat)
if reject != 2:
self.historyComplete.append(newMessage)
self.historyOpen.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveGameMessage(self, message):
error = None
if not self.isThought(message):
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, None, None, None, None, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, INFO_GAME, None)
self.historyComplete.append(newMessage)
self.historyUpdates.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveSystemMessage(self, message):
error = None
if not self.isThought(message):
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, None, None, None, None, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, INFO_SYSTEM, None)
self.historyComplete.append(newMessage)
self.historyUpdates.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveDeveloperMessage(self, message):
error = None
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, None, None, None, None, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, INFO_DEV, None)
self.historyComplete.append(newMessage)
self.historyUpdates.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveGuildMessage(self, message, senderAvId, senderName):
error = None
if not self.isThought(message):
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, senderAvId, senderName, None, None, None, None, None, None, TALK_GUILD, None)
self.historyComplete.append(newMessage)
self.historyGuild.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveGuildUpdateMessage(self, message, senderId, senderName, receiverId, receiverName, extraInfo = None):
error = None
if not self.isThought(message):
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, senderId, senderName, None, None, receiverId, receiverName, None, None, INFO_GUILD, extraInfo)
self.historyComplete.append(newMessage)
self.historyGuild.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveFriendUpdate(self, friendId, friendName, isOnline):
if isOnline:
onlineMessage = OTPLocalizer.FriendOnline
else:
onlineMessage = OTPLocalizer.FriendOffline
newMessage = TalkMessage(self.countMessage(), self.stampTime(), onlineMessage, friendId, friendName, None, None, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, UPDATE_FRIEND, None)
self.addHandle(friendId, newMessage)
self.historyComplete.append(newMessage)
self.historyUpdates.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return
def receiveFriendAccountUpdate(self, friendId, friendName, isOnline):
if isOnline:
onlineMessage = OTPLocalizer.FriendOnline
else:
onlineMessage = OTPLocalizer.FriendOffline
newMessage = TalkMessage(self.countMessage(), self.stampTime(), onlineMessage, None, None, friendId, friendName, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, UPDATE_FRIEND, None)
self.historyComplete.append(newMessage)
self.historyUpdates.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return
def receiveGuildUpdate(self, memberId, memberName, isOnline):
if base.cr.identifyFriend(memberId) is None:
if isOnline:
onlineMessage = OTPLocalizer.GuildMemberOnline
else:
onlineMessage = OTPLocalizer.GuildMemberOffline
newMessage = TalkMessage(self.countMessage(), self.stampTime(), onlineMessage, memberId, memberName, None, None, None, None, None, None, UPDATE_GUILD, None)
self.addHandle(memberId, newMessage)
self.historyComplete.append(newMessage)
self.historyUpdates.append(newMessage)
self.historyGuild.append(newMessage)
messenger.send('NewOpenMessage', [newMessage])
return
def receiveOpenSpeedChat(self, type, messageIndex, senderAvId, name = None):
error = None
if not name and senderAvId:
name = self.findName(senderAvId, 0)
if type == SPEEDCHAT_NORMAL:
message = self.SCDecoder.decodeSCStaticTextMsg(messageIndex)
elif type == SPEEDCHAT_EMOTE:
message = self.SCDecoder.decodeSCEmoteWhisperMsg(messageIndex, name)
elif type == SPEEDCHAT_CUSTOM:
message = self.SCDecoder.decodeSCCustomMsg(messageIndex)
if message in (None, ''):
return
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, senderAvId, name, None, None, None, None, None, None, TALK_OPEN, None)
self.historyComplete.append(newMessage)
self.historyOpen.append(newMessage)
self.addToHistoryDoId(newMessage, senderAvId)
messenger.send('NewOpenMessage', [newMessage])
return error
def receiveAvatarWhisperSpeedChat(self, type, messageIndex, senderAvId, name = None):
error = None
if not name and senderAvId:
name = self.findName(senderAvId, 0)
if type == SPEEDCHAT_NORMAL:
message = self.SCDecoder.decodeSCStaticTextMsg(messageIndex)
elif type == SPEEDCHAT_EMOTE:
message = self.SCDecoder.decodeSCEmoteWhisperMsg(messageIndex, name)
elif type == SPEEDCHAT_CUSTOM:
message = self.SCDecoder.decodeSCCustomMsg(messageIndex)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, senderAvId, name, None, None, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, TALK_WHISPER, None)
self.historyComplete.append(newMessage)
self.historyOpen.append(newMessage)
self.addToHistoryDoId(newMessage, senderAvId)
messenger.send('NewOpenMessage', [newMessage])
return error
def receivePlayerWhisperSpeedChat(self, type, messageIndex, senderAvId, name = None):
error = None
if not name and senderAvId:
name = self.findName(senderAvId, 1)
if type == SPEEDCHAT_NORMAL:
message = self.SCDecoder.decodeSCStaticTextMsg(messageIndex)
elif type == SPEEDCHAT_EMOTE:
message = self.SCDecoder.decodeSCEmoteWhisperMsg(messageIndex, name)
elif type == SPEEDCHAT_CUSTOM:
message = self.SCDecoder.decodeSCCustomMsg(messageIndex)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, None, None, senderAvId, name, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, TALK_WHISPER, None)
self.historyComplete.append(newMessage)
self.historyOpen.append(newMessage)
self.addToHistoryDISLId(newMessage, senderAvId)
messenger.send('NewOpenMessage', [newMessage])
return error
def sendOpenTalk(self, message):
error = None
if base.cr.magicWordManager and base.cr.wantMagicWords and len(message) > 0 and message[0] == base.cr.magicWordManager.chatPrefix:
messenger.send('magicWord', [message])
self.receiveDeveloperMessage(message)
else:
chatFlags = CFSpeech | CFTimeout
if self.isThought(message):
chatFlags = CFThought
base.localAvatar.sendUpdate('setTalk', [0,
0,
'',
message,
[],
0])
messenger.send('chatUpdate', [message, chatFlags])
return error
def sendWhisperTalk(self, message, receiverAvId):
error = None
receiver = base.cr.doId2do.get(receiverAvId)
if receiver:
receiver.sendUpdate('setTalkWhisper', [0,
0,
'',
message,
[],
0])
else:
receiver = base.cr.identifyAvatar(receiverAvId)
if receiver:
base.localAvatar.sendUpdate('setTalkWhisper', [0,
0,
'',
message,
[],
0], sendToId=receiverAvId)
return error
def sendAccountTalk(self, message, receiverAccount):
error = None
base.cr.playerFriendsManager.sendUpdate('setTalkAccount', [receiverAccount,
0,
'',
message,
[],
0])
return error
def sendGuildTalk(self, message):
error = None
if self.checkGuildTypedChat():
base.cr.guildManager.sendTalk(message)
else:
print('Guild chat error')
error = ERROR_NO_GUILD_CHAT
return error
def sendOpenSpeedChat(self, type, messageIndex):
error = None
if type == SPEEDCHAT_NORMAL:
messenger.send(SCChatEvent)
messenger.send('chatUpdateSC', [messageIndex])
base.localAvatar.b_setSC(messageIndex)
elif type == SPEEDCHAT_EMOTE:
messenger.send('chatUpdateSCEmote', [messageIndex])
messenger.send(SCEmoteChatEvent)
base.localAvatar.b_setSCEmote(messageIndex)
elif type == SPEEDCHAT_CUSTOM:
messenger.send('chatUpdateSCCustom', [messageIndex])
messenger.send(SCCustomChatEvent)
base.localAvatar.b_setSCCustom(messageIndex)
return error
def sendAvatarWhisperSpeedChat(self, type, messageIndex, receiverId):
error = None
if type == SPEEDCHAT_NORMAL:
base.localAvatar.whisperSCTo(messageIndex, receiverId, 0)
message = self.SCDecoder.decodeSCStaticTextMsg(messageIndex)
elif type == SPEEDCHAT_EMOTE:
base.localAvatar.whisperSCEmoteTo(messageIndex, receiverId, 0)
message = self.SCDecoder.decodeSCEmoteWhisperMsg(messageIndex, localAvatar.getName())
elif type == SPEEDCHAT_CUSTOM:
base.localAvatar.whisperSCCustomTo(messageIndex, receiverId, 0)
message = self.SCDecoder.decodeSCCustomMsg(messageIndex)
if self.logWhispers:
avatarName = None
accountId = None
avatar = base.cr.identifyAvatar(receiverId)
if avatar:
avatarName = avatar.getName()
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, receiverId, avatarName, None, None, TALK_WHISPER, None)
self.historyComplete.append(newMessage)
self.addToHistoryDoId(newMessage, localAvatar.doId)
self.addToHistoryDISLId(newMessage, base.cr.accountDetailRecord.playerAccountId)
messenger.send('NewOpenMessage', [newMessage])
return error
def sendPlayerWhisperSpeedChat(self, type, messageIndex, receiverId):
error = None
if type == SPEEDCHAT_NORMAL:
base.cr.speedchatRelay.sendSpeedchat(receiverId, messageIndex)
message = self.SCDecoder.decodeSCStaticTextMsg(messageIndex)
elif type == SPEEDCHAT_EMOTE:
base.cr.speedchatRelay.sendSpeedchatEmote(receiverId, messageIndex)
message = self.SCDecoder.decodeSCEmoteWhisperMsg(messageIndex, localAvatar.getName())
return
elif type == SPEEDCHAT_CUSTOM:
base.cr.speedchatRelay.sendSpeedchatCustom(receiverId, messageIndex)
message = self.SCDecoder.decodeSCCustomMsg(messageIndex)
if self.logWhispers:
receiverName = self.findName(receiverId, 1)
newMessage = TalkMessage(self.countMessage(), self.stampTime(), message, localAvatar.doId, localAvatar.getName(), localAvatar.DISLid, localAvatar.DISLname, None, None, receiverId, receiverName, TALK_ACCOUNT, None)
self.historyComplete.append(newMessage)
self.addToHistoryDoId(newMessage, localAvatar.doId)
self.addToHistoryDISLId(newMessage, base.cr.accountDetailRecord.playerAccountId)
messenger.send('NewOpenMessage', [newMessage])
return error
def sendGuildSpeedChat(self, type, msgIndex):
error = None
if self.checkGuildSpeedChat():
base.cr.guildManager.sendSC(msgIndex)
else:
print('Guild Speedchat error')
error = ERROR_NO_GUILD_CHAT
return error
def getWhisperReplyId(self):
if self.lastWhisper:
toPlayer = 0
if self.lastWhisper == self.lastWhisperPlayerId:
toPlayer = 1
return (self.lastWhisper, toPlayer)
return (0, 0)

View file

@ -630,6 +630,7 @@ class TalkAssistant(DirectObject.DirectObject):
message,
[],
0])
base.cr.chatHandler.sendChatMessage(message)
messenger.send('chatUpdate', [message, chatFlags])
return error
@ -652,6 +653,7 @@ class TalkAssistant(DirectObject.DirectObject):
message,
[],
0], sendToId=receiverAvId)
base.cr.chatHandler.sendWhisperMessage(message, receiverAvId)
return error
def sendAccountTalk(self, message, receiverAccount):

View file

@ -418,6 +418,7 @@ class OTPClientRepository(ClientRepositoryBase):
self.centralLogger = self.generateGlobalObject(OtpDoGlobals.OTP_DO_ID_CENTRAL_LOGGER, 'CentralLogger')
if __astron__:
self.astronLoginManager = self.generateGlobalObject(OtpDoGlobals.OTP_DO_ID_ASTRON_LOGIN_MANAGER, 'AstronLoginManager')
self.chatHandler = self.generateGlobalObject(OtpDoGlobals.OTP_DO_ID_CHAT_ROUTER, 'ChatHandler')
def startLeakDetector(self):
if hasattr(self, 'leakDetector'):

View file

@ -87,4 +87,6 @@ OTP_ZONE_ID_MANAGEMENT = 2
OTP_ZONE_ID_DISTRICTS = 3
OTP_ZONE_ID_DISTRICTS_STATS = 4
OTP_ZONE_ID_ELEMENTS = 5
OTP_DO_ID_CHAT_HANDLER = 4681
OTP_DO_ID_CHAT_ROUTER = OTP_DO_ID_CHAT_HANDLER
OTP_NET_MESSENGER_CHANNEL = (OTP_DO_ID_UBER_DOG << 32) + OTP_ZONE_ID_MANAGEMENT

View file

@ -0,0 +1,175 @@
import MySQLdb
import MySQLdb.constants.CR
#import MySQLdb
import datetime
from direct.directnotify.DirectNotifyGlobal import directNotify
from otp.distributed import OtpDoGlobals
from otp.uberdog.DBInterface import DBInterface
SERVER_GONE_ERROR = MySQLdb.constants.CR.SERVER_GONE_ERROR
SERVER_LOST = MySQLdb.constants.CR.SERVER_LOST
class AvatarFriendsDB(DBInterface):
"""
DB wrapper class for avatar friends! All SQL code for avatar friends should be in here.
"""
notify = directNotify.newCategory('AvatarFriendsDB')
def __init__(self,host,port,user,password,dbname):
self.sqlAvailable = True
if not self.sqlAvailable:
return
self.host = host
self.port = port
self.user = user
self.password = password
self.dbname = self.processDBName(dbname)
try:
self.db = MySQLdb.connect(host=host,
port=port,
user=user,
password=password)
except MySQLdb.OperationalError as e:
if __debug__:
self.notify.warning("Failed to connect to MySQL at %s:%d. Avatar friends DB is disabled."%(host,port))
self.sqlAvailable = 0
uber.sqlAvailable = 0
return
if __debug__:
self.notify.info("Connected to avatar friends MySQL db at %s:%d."%(host,port))
#temp hack for initial dev, create DB structure if it doesn't exist already
cursor = self.db.cursor()
try:
cursor.execute("CREATE DATABASE `%s`"%self.dbname)
if __debug__:
self.notify.info("Database '%s' did not exist, created a new one!"%self.dbname)
except MySQLdb.ProgrammingError as e:
pass
cursor.execute("USE `%s`"%self.dbname)
if __debug__:
self.notify.debug("Using database '%s'"%self.dbname)
try:
cursor.execute("""
CREATE TABLE `avatarfriends` (
`friendId1` int(32) UNSIGNED NOT NULL,
`friendId2` int(32) UNSIGNED NOT NULL,
`openChatYesNo` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`friendId1`,`friendId2`),
KEY `idxFriend1` (`friendId1`),
KEY `idxFriend2` (`friendId2`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
""")
if __debug__:
self.notify.info("Table avatarfriends did not exist, created a new one!")
except MySQLdb.OperationalError as e:
pass
def reconnect(self):
if __debug__:
self.notify.debug("MySQL server was missing, attempting to reconnect.")
try: self.db.close()
except: pass
self.db = MySQLdb.connect(host=self.host,
port=self.port,
user=self.user,
password=self.password)
cursor = self.db.cursor()
cursor.execute("USE `%s`"%self.dbname)
if __debug__:
self.notify.debug("Reconnected to MySQL server at %s:%d."%(self.host,self.port))
def disconnect(self):
if not self.sqlAvailable:
return
self.db.close()
self.db = None
def getFriends(self,avatarId):
if not self.sqlAvailable:
return []
cursor = MySQLdb.cursors.DictCursor(self.db)
try:
cursor.execute("SELECT * FROM avatarfriends WHERE friendId1=%s OR friendId2=%s",(avatarId,avatarId))
except MySQLdb.OperationalError as e:
if e[0] == SERVER_GONE_ERROR or e[0] == SERVER_LOST:
self.reconnect()
cursor = MySQLdb.cursors.DictCursor(self.db)
cursor.execute("SELECT * FROM avatarfriends WHERE friendId1=%s OR friendId2=%s",(avatarId,avatarId))
else:
raise e
friends = cursor.fetchall()
cleanfriends = {}
for f in friends:
if f['friendId1'] == avatarId:
cleanfriends[f['friendId2']] = f['openChatYesNo']
else:
cleanfriends[f['friendId1']] = f['openChatYesNo']
return cleanfriends
def addFriendship(self,avatarId1,avatarId2,openChat=0):
if not self.sqlAvailable:
return
cursor = MySQLdb.cursors.DictCursor(self.db)
try:
if avatarId1 < avatarId2:
cursor.execute("INSERT INTO avatarfriends (friendId1,friendId2,openChatYesNo) VALUES (%s,%s,%s)",(avatarId1,avatarId2,openChat))
else:
cursor.execute("INSERT INTO avatarfriends (friendId1,friendId2,openChatYesNo) VALUES (%s,%s,%s)",(avatarId2,avatarId1,openChat))
except MySQLdb.OperationalError as e:
if e[0] == SERVER_GONE_ERROR or e[0] == SERVER_LOST:
self.reconnect()
cursor = MySQLdb.cursors.DictCursor(self.db)
if avatarId1 < avatarId2:
cursor.execute("INSERT INTO avatarfriends (friendId1,friendId2,openChatYesNo) VALUES (%s,%s,%s)",(avatarId1,avatarId2,openChat))
else:
cursor.execute("INSERT INTO avatarfriends (friendId1,friendId2,openChatYesNo) VALUES (%s,%s,%s)",(avatarId2,avatarId1,openChat))
else:
raise e
self.db.commit()
def removeFriendship(self,avatarId1,avatarId2):
if not self.sqlAvailable:
return
cursor = MySQLdb.cursors.DictCursor(self.db)
try:
if avatarId1 < avatarId2:
cursor.execute("DELETE FROM avatarfriends where friendId1=%s AND friendId2=%s",(avatarId1,avatarId2))
else:
cursor.execute("DELETE FROM avatarfriends where friendId1=%s AND friendId2=%s",(avatarId2,avatarId1))
except MySQLdb.OperationalError as e:
if e[0] == SERVER_GONE_ERROR or e[0] == SERVER_LOST: # 'Lost connection to MySQL server during query'
self.reconnect()
cursor = MySQLdb.cursors.DictCursor(self.db)
if avatarId1 < avatarId2:
cursor.execute("DELETE FROM avatarfriends where friendId1=%s AND friendId2=%s",(avatarId1,avatarId2))
else:
cursor.execute("DELETE FROM avatarfriends where friendId1=%s AND friendId2=%s",(avatarId2,avatarId1))
else:
raise e
self.db.commit()
#for debugging only
def dumpFriendsTable(self):
assert self.db,"Tried to call dumpFriendsTable when DB was closed."
cursor = MySQLdb.cursors.DictCursor(self.db)
cursor.execute("SELECT * FROM avatarfriends")
return cursor.fetchallDict()
#for debugging only
def clearFriendsTable(self):
assert self.db,"Tried to call clearFriendsTable when DB was closed."
cursor = MySQLdb.cursors.DictCursor(self.db)
cursor.execute("TRUNCATE TABLE avatarfriends")
self.db.commit()

View file

@ -0,0 +1,9 @@
CREATE TABLE `avatarfriends` (
`friendId1` int(32) NOT NULL,
`friendId2` int(32) NOT NULL,
`openChatYesNo` tinyint(1) NOT NULL default '0',
PRIMARY KEY (`friendId1`,`friendId2`),
KEY `idxFriend1` (`friendId1`),
KEY `idxFriend2` (`friendId2`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

View file

@ -1,5 +1,387 @@
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectUD import DistributedObjectUD
from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD
from otp.otpbase import OTPGlobals
from otp.ai import AIMsgTypes
from otp.uberdog.RejectCode import RejectCode
class AvatarFriendsManagerUD(DistributedObjectUD):
notify = DirectNotifyGlobal.directNotify.newCategory('AvatarFriendsManagerUD')
from direct.directnotify.DirectNotifyGlobal import directNotify
from otp.friends.AvatarFriendInfo import AvatarFriendInfo
class AvatarFriendsManagerUD(DistributedObjectGlobalUD):
"""
The Avatar Friends Manager is a global object.
This object handles client requests on avatar-level (as opposed to player-level) friends.
See Also:
"otp/src/friends/AvatarFriendsManager.py"
"otp/src/friends/PlayerFriendsManager.py"
"pirates/src/friends/PiratesFriendsList.py"
"otp/src/configfiles/otp.dc"
"pirates/src/configfiles/pirates.dc"
"""
notify = directNotify.newCategory('AvatarFriendsManagerUD')
def __init__(self, air):
assert self.notify.debugCall()
DistributedObjectGlobalUD.__init__(self, air)
self.DBuser = config.GetString("mysql-user", "ud_rw")
self.DBpassword = config.GetString("mysql-password", "r3adwr1te")
self.DBhost = config.GetString("avatarfriends-db-host","localhost")
self.DBport = config.GetInt("avatarfriends-db-port",3306)
self.DBname = config.GetString("avatarfriends-db-name","avatar_friends")
self.friends = None
self.friendsList = None
self.wantMysql = config.GetBool('want-mysql-db', 0)
if self.wantMysql:
from otp.friends.AvatarFriendsDB import AvatarFriendsDB
self.db = AvatarFriendsDB(host=self.DBhost,
port=self.DBport,
user=self.DBuser,
password=self.DBpassword,
dbname=self.DBname)
else:
# lets use the astron db or openotp db
if __astron__:
self.db = self.air.dbInterface
else:
# TODO
self.db = None
self.avatarId2FriendsList = {}
self.avatarId2Invitations = {}
self.avatarId2Unvitations = {} #an unvitation is a rejected (but not retracted) invitation
#self.avatarId2Name = {}
self.avatarId2Info = {}
self.asyncRequests = {}
self.isAvatarOnline = {}
def announceGenerate(self):
assert self.notify.debugCall()
#self.accept("avatarOnline", self.avatarOnline, [])
self.accept("avatarOnlinePlusAccountInfo", self.avatarOnlinePlusAccountInfo, [])
self.accept("avatarOffline", self.avatarOffline, [])
DistributedObjectGlobalUD.announceGenerate(self)
self.sendUpdateToChannel(
AIMsgTypes.CHANNEL_CLIENT_BROADCAST, "online", [])
self.sendUpdateToChannel(
AIMsgTypes.OTP_CHANNEL_AI_AND_UD_BROADCAST, "online", [])
def delete(self):
assert self.notify.debugCall()
self.ignoreAll()
for i in list(self.asyncRequests.values()):
i.delete()
DistributedObjectGlobalUD.delete(self)
#----------------------------------
def avatarOnlinePlusAccountInfo(self,avatarId,accountId,playerName,
playerNameApproved,openChatEnabled,
createFriendsWithChat,chatCodeCreation):
assert self.notify.debugCall()
assert avatarId
self.notify.debug("avatarOnlinePlusAccountInfo")
if avatarId in self.isAvatarOnline:
assert self.notify.debug(
"\n\nWe got a duplicate avatar online notice %s"%(avatarId,))
if avatarId and avatarId not in self.isAvatarOnline:
self.isAvatarOnline[avatarId]=True
self.avatarId2Info[avatarId] = AvatarFriendInfo(avatarName=str(avatarId),
playerName = playerName,
playerId = accountId,
onlineYesNo=1,
openChatEnabledYesNo=openChatEnabled,)
# Get my friends list from the SQL DB
if self.wantMysql:
self.friends = self.db.getFriends(avatarId)
else:
self.db.queryObject(self.air.dbId, avatarId, self.__gotFriendsList)
self.avatarId2FriendsList[avatarId]=self.friends
if not hasattr(self.friends, "keys"): #check for error
self.notify.warning("self.db.getFriends(avatarId) has no keys %s" % (self.friends))
return
# Callback function for asynchronous avatar name fetch
def setName(avatarId, avatarId2info, friends, context, name):
if avatarId in avatarId2info:
avatarId2info[avatarId].avatarName = name[0]
for friendId in friends:
if friendId in self.isAvatarOnline:
if (friendId in self.avatarId2FriendsList) and (avatarId in self.avatarId2FriendsList[friendId]):
self.sendUpdateToAvatarId(friendId,"updateAvatarFriend",
[avatarId,self.getFriendView(friendId,avatarId)])
self.sendExtraUpdates(friendId,avatarId)
# Get my friends' info to me
for friend in list(self.friends.keys()):
friendId = friend
if friendId not in self.isAvatarOnline:
if friendId not in self.avatarId2Info:
self.avatarId2Info[friendId] = AvatarFriendInfo()
#fetch this friend's name from the gameDB since we don't have it yet
context=self.air.allocateContext()
dclassName="DistributedAvatarUD"
self.air.contextToClassName[context]=dclassName
self.acceptOnce(
"doFieldResponse-%s"%context,setName,[friendId,self.avatarId2Info,[avatarId,]])
self.air.queryObjectField(dclassName,"setName",friendId,context)
else:
#print "AFMUD warning: info entry found for offline friend"
self.sendUpdateToAvatarId(avatarId,"updateAvatarFriend",[friendId,self.getFriendView(avatarId,friendId)])
self.sendExtraUpdates(avatarId,friendId)
else:
assert friendId in self.avatarId2Info
self.sendUpdateToAvatarId(avatarId,"updateAvatarFriend",[friendId,self.getFriendView(avatarId,friendId)])
self.sendExtraUpdates(avatarId,friendId)
# Get my info to my friends
context=self.air.allocateContext()
dclassName="DistributedAvatarUD"
self.air.contextToClassName[context]=dclassName
self.acceptOnce(
"doFieldResponse-%s"%(context,),
setName, [avatarId, self.avatarId2Info, list(self.friends.keys())])
self.air.queryObjectField(
dclassName, "setName", avatarId, context)
def __gotFriendsList(self, dclass, fields):
self.friends = fields.get('setFriendsList', [])
self.friendsList = self.friends
def getFriendView(self, viewerId, friendId):
info = self.avatarId2Info[friendId]
assert viewerId in self.avatarId2FriendsList, "avatarId2FriendsList has no key %d" % viewerId
assert friendId in self.avatarId2FriendsList[viewerId], "avatarId2FriendsList[%d] has no key %d" % (viewerId, friendId)
info.openChatFriendshipYesNo = self.avatarId2FriendsList[viewerId][friendId]
if info.openChatFriendshipYesNo or \
(info.openChatEnabledYesNo and \
self.avatarId2Info[viewerId].openChatEnabledYesNo):
info.understandableYesNo = 1
else:
info.understandableYesNo = 0
return info
def sendExtraUpdates(self,destId,aboutId):
pass
@report(types = ['args'], dConfigParam = 'orphanedavatar')
def avatarOffline(self, avatarId):
"""
Is called from handleAvatarUsage when the avatar leaves the game.
Also is called from DistributedAvatarManagerUD when it detects
an orphaned avatar in the world.
"""
assert self.notify.debugCall()
self.isAvatarOnline.pop(avatarId,None)
if avatarId in self.avatarId2Info:
self.avatarId2Info[avatarId].onlineYesNo = 0
if avatarId:
friendsList = self.avatarId2FriendsList.get(avatarId, None)
if friendsList is not None and avatarId in self.avatarId2Info:
for friend in friendsList:
self.sendUpdateToAvatarId(
friend, "updateAvatarFriend", [avatarId,self.avatarId2Info[avatarId]])
invitations = self.avatarId2Invitations.pop(avatarId, [])
for invitee in invitations:
self.sendUpdateToAvatarId(
invitee, "retractInvite", [avatarId])
self.avatarId2FriendsList.pop(avatarId,None)
self.avatarId2Info.pop(avatarId,None)
#----------------------------------------------------------------------
# Functions called by the client
def requestInvite(self, otherAvatarId):
avatarId = self.air.getAvatarIdFromSender()
assert self.notify.debugCall("avatarId:%s"%(avatarId,))
invitations = self.avatarId2Invitations.setdefault(avatarId, [])
othersInvitations = self.avatarId2Invitations.setdefault(
otherAvatarId, [])
friendsList = self.avatarId2FriendsList.get(avatarId)
otherFriendsList = self.avatarId2FriendsList.get(otherAvatarId)
def reject(reason):
self.sendUpdateToAvatarId(
avatarId, "rejectInvite", [otherAvatarId, reason])
#clear unvitations
unvitations = self.avatarId2Unvitations.setdefault(avatarId, [])
if otherAvatarId in unvitations:
unvitations.remove(otherAvatarId)
if friendsList is None:
reject(RejectCode.FRIENDS_LIST_NOT_HANDY)
elif otherFriendsList is None:
reject(RejectCode.INVITEE_NOT_ONLINE)
elif avatarId in self.avatarId2Unvitations.setdefault(otherAvatarId, []): #check for unvitation
reject(RejectCode.INVITATION_DECLINED)
elif otherAvatarId in invitations:
reject(RejectCode.ALREADY_INVITED)
elif avatarId == otherAvatarId:
reject(RejectCode.ALREADY_FRIENDS_WITH_SELF)
elif otherAvatarId in friendsList:
reject(RejectCode.ALREADY_YOUR_FRIEND)
elif avatarId in otherFriendsList:
reject(RejectCode.ALREADY_YOUR_FRIEND)
self.notify.error(
"Friends lists out of sync %s %s"%(avatarId, otherAvatarId))
#should be adding player friends list?
elif (len(friendsList)
+ 0
> OTPGlobals.MaxFriends):
reject(RejectCode.FRIENDS_LIST_FULL)
#should be adding player friends list?
elif (len(otherFriendsList)
+ 0
> OTPGlobals.MaxFriends):
reject(RejectCode.OTHER_FRIENDS_LIST_FULL)
elif avatarId in othersInvitations:
othersInvitations.remove(avatarId)
assert otherAvatarId not in invitations
self.avatarId2FriendsList[avatarId][otherAvatarId] = 0
if otherAvatarId in self.avatarId2FriendsList:
self.avatarId2FriendsList[otherAvatarId][avatarId] = 0
#update the friends database
try:
if self.wantMysql:
self.db.addFriendship(avatarId,otherAvatarId)
else:
if __astron__:
# update setFriendsList field in the distributedtoon dclass to include the new friend
friendsList = self.avatarId2FriendsList[avatarId]
friendsList.append(otherAvatarId)
self.db.updateObject(self.air.dbId, avatarId, self.air.dclassesByName['DistributedToonUD'], {'setFriendsList': [friendsList]} )
else:
# TODO openotp: update setFriendsList field in the distributedtoon dclass to include the new friend
pass
except:
pass #HACK for testing
self.air.writeServerEvent('friendAccept', avatarId, '%s' % otherAvatarId)
#tell them they're friends and give presence info, includes online status
self.sendUpdateToAvatarId(otherAvatarId,"updateAvatarFriend",[avatarId,self.getFriendView(otherAvatarId,avatarId)])
self.sendUpdateToAvatarId(avatarId,"updateAvatarFriend",[otherAvatarId,self.getFriendView(avatarId,otherAvatarId)])
self.sendExtraUpdates(avatarId,otherAvatarId)
self.sendExtraUpdates(otherAvatarId,avatarId)
else:
invitations.append(otherAvatarId)
# Tell the other guy we're inviting him!
self.air.writeServerEvent('friendInvite', avatarId, '%s' % otherAvatarId)
self.sendUpdateToAvatarId(avatarId, "friendConsidering", [otherAvatarId])
self.sendUpdateToAvatarId(otherAvatarId, "invitationFrom", [avatarId,self.avatarId2Info[avatarId].avatarName])
def requestRemove(self, otherAvatarId):
"""
Call this function if you want to retract an invitation you've
made, or to decline an invitation from otherAvatarId, or to
remove an existing friend from your friends list.
otherAvatarId may be online or offline.
"""
avatarId = self.air.getAvatarIdFromSender()
self.air.writeServerEvent('friendRemove', avatarId, '%s' % otherAvatarId)
self.friendsList = self.avatarId2FriendsList.get(avatarId,None)
if self.friendsList is None:
if self.wantMysql:
self.friendsList = self.db.getFriends(avatarId)
self.avatarId2FriendsList[avatarId] = self.friendsList
else:
if __astron__:
self.db.queryObject(self.air.dbId, avatarId, self.__gotFriendsList)
self.avatarId2FriendsList[avatarId] = self.friendsList
else:
# TODO openotp: queryObject
pass
assert self.notify.debugCall("avatarId:%s"%(avatarId,))
def reject(reason):
self.sendUpdateToAvatarId(
avatarId, "rejectRemove", [otherAvatarId, reason])
invitations = self.avatarId2Invitations.setdefault(avatarId, [])
if otherAvatarId in invitations:
# The other avatar was only invited and had not yet accepted
self.sendUpdateToAvatarId(otherAvatarId, "retractInvite", [avatarId])
invitations.remove(otherAvatarId)
assert otherAvatarId not in invitations
assert otherAvatarId not in friendsList
return
else: # create an unvitation
unvitations = self.avatarId2Unvitations.setdefault(avatarId, [])
if otherAvatarId in unvitations:
pass
else:
unvitations.append(otherAvatarId)
othersInvitations = self.avatarId2Invitations.setdefault(otherAvatarId, [])
if avatarId in othersInvitations:
# I was only invited and had not yet accepted
self.sendUpdateToAvatarId(
otherAvatarId, "rejectInvite",
[avatarId, RejectCode.INVITATION_DECLINED])
othersInvitations.remove(avatarId)
assert avatarId not in othersInvitations
assert otherAvatarId not in friendsList
return
if otherAvatarId not in friendsList:
reject(RejectCode.ALREADY_NOT_YOUR_FRIEND)
else:
if avatarId in self.avatarId2FriendsList:
self.avatarId2FriendsList[avatarId].pop(otherAvatarId,None)
if otherAvatarId in self.avatarId2FriendsList:
self.avatarId2FriendsList[otherAvatarId].pop(avatarId,None)
if self.wantMysql:
self.db.removeFriendship(avatarId,otherAvatarId)
else:
if __astron__:
# update setFriendsList field in the distributedtoon dclass to remove the friend
friendsList = self.avatarId2FriendsList[avatarId]
friendsList.remove(otherAvatarId)
self.db.updateObject(self.air.dbId, avatarId, self.air.dclassesByName['DistributedToonUD'], {'setFriendsList': [friendsList]} )
else:
# TODO openotp: update setFriendsList field in the distributedtoon dclass to remove the friend
pass
self.sendUpdateToAvatarId(avatarId,"removeAvatarFriend",[otherAvatarId])
self.sendUpdateToAvatarId(otherAvatarId,"removeAvatarFriend",[avatarId])
def updateAvatarName(self, avatarId, avatarName):
if avatarId in self.avatarId2Info:
self.avatarId2Info[avatarId].avatarName = avatarName
friends = self.avatarId2FriendsList.get(avatarId,[])
for friendId in friends:
if friendId in self.isAvatarOnline:
self.sendUpdateToAvatarId(friendId,"updateAvatarFriend",
[avatarId,self.getFriendView(friendId,avatarId)])
self.sendExtraUpdates(friendId,avatarId)

View file

@ -1,5 +1,752 @@
from otp.ai.AIBaseGlobal import *
from pandac.PandaModules import *
from direct.distributed import DistributedObjectAI
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectAI import DistributedObjectAI
from direct.distributed.PyDatagram import *
from otp.avatar import DistributedAvatarAI
from otp.otpbase import OTPGlobals
import datetime
import json
import random
import os
# all of this is commented out because the friend manager was moved
# to the OTP server
# yeah which we don't have access too lul so lets uncomment it out
class FriendManagerAI(DistributedObjectAI):
notify = DirectNotifyGlobal.directNotify.newCategory('FriendManagerAI')
# we need something for the AI DC parser to load
class FriendManagerAI(DistributedObjectAI.DistributedObjectAI):
# These structures record the invitations currently being handled.
nextContext = 0
invites = {}
inviters = {}
invitees = {}
# This is the length of time, in seconds, to sit on a secret guess
# before processing it. This serves to make it difficult to guess
# passwords at random.
SecretDelay = 1.0
# This is the length of time that should elapse before we start to
# forget who has declined friendships from whom.
DeclineFriendshipTimeout = 600.0
notify = DirectNotifyGlobal.directNotify.newCategory("FriendManagerAI")
friendDataFolder = simbase.config.GetString('server-data-folder', 'dependencies/backups/')
# 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):
DistributedObjectAI.DistributedObjectAI.__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
self.filename = self.getFilename()
self.trueFriendCodes = self.loadTrueFriendCodes()
self.district = str(self.air.districtId)
taskMgr.add(self.trueFriendCodesTask, 'truefriend-codes-clear-task')
def generate(self):
DistributedObjectAI.DistributedObjectAI.generate(self)
# The FriendManagerAI always listens for these events, which
# will be sent in response to secret requests to the database,
# via the AIR.
self.accept("makeFriendsReply", self.makeFriendsReply)
self.accept("requestSecretReply", self.__requestSecretReply)
self.accept("submitSecretReply", self.__submitSecretReply)
def delete(self):
self.ignore("makeFriendsReply")
self.ignore("requestSecretReply")
self.ignore("submitSecretReply")
# Clean up all outstanding secret request tasks.
taskMgr.removeTasksMatching("secret-*")
DistributedObjectAI.DistributedObjectAI.delete(self)
# 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 ' + `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 ' + 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()
if not avId:
return
try:
invite = FriendManagerAI.invites[context]
except:
self.air.writeServerEvent('suspicious', avId, 'FriendManagerAI.inviteeFriendResponse unknown context')
FriendManagerAI.notify.warning('Message for unknown context ' + context)
return
if yesNoMaybe == 1:
invitee = invite.inviteeId
if invitee in self.air.doId2do:
invitee = self.air.doId2do[invitee]
else:
del FriendManagerAI.invites[context]
return
inviter = invite.inviterId
if not (invitee and inviter):
#probably logged off before a response
return
if inviter in self.air.doId2do:
inviter = self.air.doId2do[inviter]
else:
del FriendManagerAI.invites[context]
return
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(invitee.getDoId()), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(inviter.getDoId())
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(inviter.getDoId()), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(invitee.getDoId())
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
invitee.extendFriendsList(inviter.getDoId(), 0)
inviter.extendFriendsList(invitee.getDoId(), 0)
invitee.d_setFriendsList(invitee.getFriendsList())
inviter.d_setFriendsList(inviter.getFriendsList())
del FriendManagerAI.invites[context]
# else:
# self.noFriends(invite, yesNoMaybe)
def inviteeAcknowledgeCancel(self, context):
"""inviteeAcknowledgeCancel(self, int context)
Sent by the invitee to the AI, in response to an
inviteeCancelFriendQuery message. This simply acknowledges
receipt of the message and tells the AI that it is safe to
clean up the context.
"""
self.notify.debug("AI: inviteeAcknowledgeCancel(%d)" % (context))
avId = self.air.getAvatarIdFromSender()
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.inviteeAcknowledgeCancel unknown context')
#FriendManagerAI.notify.warning('Message for unknown context ' + `context`)
return
self.clearInvite(invite)
# 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))
# 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))
def getFilename(self):
if not hasattr(self, 'district'):
self.district = str(self.air.districtId)
return '{0}{1}{2}{3}.json'.format(self.friendDataFolder, 'trueFriendCodes/', 'trueFriendCodes_', self.district)
def loadTrueFriendCodes(self):
try:
trueFriendCodesFile = open(self.filename, 'r')
trueFriendsCodesData = json.load(trueFriendCodesFile)
return trueFriendsCodesData
except:
return {}
# Messages involving secrets
def deleteSecret(self, secret):
#delete secrets in true friend codes
if secret in self.trueFriendCodes:
del self.trueFriendCodes[secret]
self.updateTrueFriendCodesFile()
def updateTrueFriendCodesFile(self):
#update the file for true friend codes
if not os.path.exists(os.path.dirname(self.filename)):
os.makedirs(os.path.dirname(self.filename))
trueFriendCodesFile = open(self.filename, 'w')
trueFriendCodesFile.seek(0)
json.dump(self.trueFriendCodes, trueFriendCodesFile)
trueFriendCodesFile.close()
def trueFriendCodesTask(self, task):
for trueFriendCode in list(self.trueFriendCodes.keys()):
trueFriendCodeInfo = self.trueFriendCodes[trueFriendCode] #get info about the code
trueFriendCodeDay = trueFriendCodeInfo[1] #get the day of the code
today = datetime.datetime.now().day
if trueFriendCodeDay + 2 == today:
#true friend code is 2 days old remove it
self.deleteSecret(trueFriendCode)
return task.again
def requestSecret(self):
"""requestSecret(self)
Sent by the client to the AI to request a new "secret" for the
user.
"""
avId = self.air.getAvatarIdFromSender()
av = self.air.doId2do.get(avId)
if not av:
self.notify.warning('no av from avid: {0}'.format(avId))
return
#if friendslist is less than the friends limit
if len(av.getFriendsList()) < OTPGlobals.MaxFriends:
#get current day
day = datetime.datetime.now().day
#cretae a true friend code
trueFriendCode = self.createTrueFriendCode()
#set the information up
self.trueFriendCodes[trueFriendCode] = (avId, day)
#update the file to include the true friend code
self.updateTrueFriendCodesFile()
self.down_requestSecretResponse(avId, 1, trueFriendCode)
self.air.writeServerEvent('true-friend-code-requested', avId=avId, trueFriendCode=trueFriendCode)
else:
self.down_requestSecretResponse(avId, 2, "")
#self.air.requestSecret(avId)
def down_requestSecretResponse(self, recipient, result, secret):
"""requestSecret(self, int8 result, string secret)
Sent by the AI to the client in response to requestSecret().
result is one of:
0 - Too many secrets outstanding. Try again later.
1 - Success. The new secret is supplied.
2 - Too many friends
"""
self.sendUpdateToAvatarId(recipient, 'requestSecretResponse', [result, secret])
def createTrueFriendCode(self):
chars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
def randomChar():
return random.choice(chars)
trueFriendCode = '{0}{1}{2} {3}{4}{5}'.format(randomChar(), randomChar(), randomChar(), randomChar(), randomChar(), randomChar())
return trueFriendCode
def submitSecret(self, secret):
"""submitSecret(self, string secret)
Sent by the client to the AI to submit a "secret" typed in by
the user.
"""
avId = self.air.getAvatarIdFromSender()
av = self.air.doId2do.get(avId)
if not av:
self.notify.warning('submitSecret: no av ')
return
secretInfo = self.trueFriendCodes.get(secret)
if not secretInfo:
self.down_submitSecretResponse(avId, 0, 0)
return
friendId = secretInfo[0]
friend = self.air.doId2do.get(friendId)
if av:
if friend:
if avId == friendId:
self.down_submitSecretResponse(avId, 3, 0)
self.deleteSecret(secret)
elif len(friend.getFriendsList()) < OTPGlobals.MaxFriends or len(
av.getFriendsList()) < OTPGlobals.MaxFriends:
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(friendId), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(avId)
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
dg = PyDatagram()
dg.addServerHeader(self.GetPuppetConnectionChannel(avId), self.air.ourChannel,
CLIENTAGENT_DECLARE_OBJECT)
dg.addUint32(friendId)
dg.addUint16(self.air.dclassesByName['DistributedToonAI'].getNumber())
self.air.send(dg)
friend.extendFriendsList(avId, 1)
av.extendFriendsList(friendId, 1)
friend.d_setFriendsList(friend.getFriendsList())
av.d_setFriendsList(av.getFriendsList())
av.addTrueFriend(friendId)
self.down_submitSecretResponse(avId, 1, friendId)
self.deleteSecret(secret)
else:
#friends list is full
self.down_submitSecretResponse(avId, 2, friendId)
else:
#offline friend
def handleAvatar(dclass, fields):
if dclass != self.air.dclassesByName['DistributedToonAI']:
return
newFriendsList = []
oldFriendsList = fields['setFriendsList'][0]
if len(oldFriendsList) >= OTPGlobals.MaxFriends:
self.d_submitSecretResponse(avId, 2, friendId)
return
for i in oldFriendsList:
newFriendsList.append(i)
newFriendsList.append((avId, 1))
self.air.dbInterface.updateObject(self.air.dbId, friendId,
self.air.dclassesByName['DistributedToonAI'],
{'setFriendsList': [newFriendsList]})
av.extendFriendsList(friendId, 1)
av.d_setFriendsList(av.getFriendsList())
self.d_submitSecretResponse(avId, 1, friendId)
self.deleteSecret(secret)
self.air.dbInterface.queryObject(self.air.dbId, friendId, handleAvatar)
self.air.writeServerEvent('truefriend-code-submitted', avId=avId, friendId=friendId, trueFriendCode=secret)
# We have to sit on this request for a few seconds before
# processing it. This delay is solely to discourage password
# guessing.
# taskName = "secret-" + str(avId)
# taskMgr.remove(taskName)
#if FriendManagerAI.SecretDelay:
# taskMgr.doMethodLater(FriendManagerAI.SecretDelay,
# self.continueSubmission,
#taskName,
#extraArgs = (avId, secret))
# else:
# No delay
# self.continueSubmission(avId, secret)
def continueSubmission(self, avId, secret):
"""continueSubmission(self, avId, secret)
Finishes the work of submitSecret, a short time later.
"""
self.air.submitSecret(avId, secret)
def down_submitSecretResponse(self, recipient, result, avId):
"""submitSecret(self, int8 result, int32 avId)
Sent by the AI to the client in response to submitSecret().
result is one of:
0 - Failure. The secret is unknown or has timed out.
1 - Success. You are now friends with the indicated avId.
2 - Failure. One of the avatars has too many friends already.
3 - Failure. You just used up your own secret.
"""
self.sendUpdateToAvatarId(recipient, 'submitSecretResponse', [result, avId])
# 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 gotInviter(self, handle, invite):
# if not invite.inviter:
# invite.inviter = handle
# if invite.invitee:
# self.beginInvite(invite)
# def gotInvitee(self, handle, invite):
# if not invite.invitee:
# invite.invitee = handle
# if invite.inviter:
# 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.air.makeFriends(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 makeSpecialFriends(self, requesterId, avId):
# The "requester" has typed in a codeword that successfully
# matches a friend in the world. Attempt to make the
# friendship (with chat permission), and send back a code
# indicating success or failure.
# Get a special Invite structure just for this purpose.
context = FriendManagerAI.nextContext
FriendManagerAI.nextContext += 1
invite = FriendManagerAI.Invite(context, requesterId, avId)
FriendManagerAI.invites[context] = invite
invite.sendSpecialResponse = 1
self.air.makeFriends(invite.inviteeId, invite.inviterId, 1,
invite.context)
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 ' + 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)
def __requestSecretReply(self, result, secret, requesterId):
self.notify.debug("request secret result = %d, secret = '%s', requesterId = %d" % (result, secret, requesterId))
self.down_requestSecretResponse(requesterId, result, secret)
def __submitSecretReply(self, result, secret, requesterId, avId):
self.notify.debug("submit secret result = %d, secret = '%s', requesterId = %d, avId = %d" % (result, secret, requesterId, avId))
if result == 1:
# We successfully matched the secret, so we should do a
# few more sanity checks and then try to make friends.
if avId == requesterId:
# This is the requester's own secret!
result = 3
else:
# Ok, make us special friends.
self.makeSpecialFriends(requesterId, avId)
# In this case, the response gets sent after the
# friends transaction completes.
return
self.down_submitSecretResponse(requesterId, result, avId)
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

View file

@ -760,7 +760,7 @@ class LoadAvatarOperation(AvatarOperation):
def __handleSetAvatar(self):
# Get the client channel.
channel = self.loginManager.GetAccountConnectionChannel(self.sender)
# We will first assign a POST_REMOVE that will unload the
# avatar in the event of them disconnecting while we are working.
cleanupDatagram = PyDatagram()
@ -808,7 +808,7 @@ class UnloadAvatarOperation(GameOperation):
def __handleUnloadAvatar(self):
channel = self.loginManager.GetAccountConnectionChannel(self.sender)
self.loginManager.air.avatarFriendsManager.avatarOffline(self.avId)
datagram = PyDatagram()
datagram.addServerHeader(channel, self.loginManager.air.ourChannel, CLIENTAGENT_CLEAR_POST_REMOVES)
self.loginManager.air.send(datagram)

View file

@ -5,6 +5,7 @@ from panda3d.toontown import *
from otp.ai.AIZoneData import AIZoneDataStore
from otp.ai.TimeManagerAI import TimeManagerAI
from otp.distributed.OtpDoGlobals import *
from otp.friends.FriendManagerAI import FriendManagerAI
from toontown.ai.HolidayManagerAI import HolidayManagerAI
from toontown.ai.NewsManagerAI import NewsManagerAI
from toontown.ai.WelcomeValleyManagerAI import WelcomeValleyManagerAI
@ -232,6 +233,9 @@ class ToontownAIRepository(ToontownInternalRepository):
self.partyManager = DistributedPartyManagerAI(self)
self.partyManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT)
self.friendManager = FriendManagerAI(self)
self.friendManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT)
def generateHood(self, hoodConstructor, zoneId):
# Bossbot HQ doesn't use DNA, so we skip over that.
if zoneId != ToontownGlobals.BossbotHQ:

View file

@ -1,5 +1,649 @@
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectAI import DistributedObjectAI
import functools
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectAI import DistributedObjectAI
from direct.fsm.FSM import FSM
from toontown.estate import HouseGlobals
from toontown.estate.DistributedHouseAI import DistributedHouseAI
from toontown.toon import ToonDNA
class LoadHouseOperation(FSM):
def __init__(self, mgr, estate, index, avatar, callback):
FSM.__init__(self, 'LoadHouseOperation')
self.mgr = mgr
self.estate = estate
self.index = index
self.avatar = avatar
self.callback = callback
self.done = False
self.houseId = None
self.house = None
self.gender = None
def start(self):
# We have a few different cases here:
if self.avatar is None:
# Case #1: There isn't an avatar in that estate slot. Make a blank house.
# Because this state completes so fast, we'll use taskMgr to delay
# it until the next iteration. This solves reentrancy problems.
taskMgr.doMethodLater(0.0, self.demand, 'makeBlankHouse-%s' % id(self), extraArgs=['MakeBlankHouse'])
return
style = ToonDNA.ToonDNA()
style.makeFromNetString(self.avatar.get('setDNAString')[0])
self.houseId = self.avatar.get('setHouseId', [0])[0]
self.gender = style.gender
if self.houseId == 0:
# Case #2: There is an avatar, but no setHouseId. Make a new house:
self.demand('CreateHouse')
else:
# Case #3: Avatar with a setHouseId. Load it:
self.demand('LoadHouse')
def enterMakeBlankHouse(self):
self.house = DistributedHouseAI(self.mgr.air)
self.house.setHousePos(self.index)
self.house.setColor(self.index)
self.house.generateWithRequired(self.estate.zoneId)
self.estate.houses[self.index] = self.house
self.demand('Off')
def enterCreateHouse(self):
self.mgr.air.dbInterface.createObject(self.mgr.air.dbId, self.mgr.air.dclassesByName['DistributedHouseAI'],
{'setName': [self.avatar['setName'][0]],
'setAvatarId': [self.avatar['avId']]}, self.__handleHouseCreated)
def __handleHouseCreated(self, houseId):
if self.state != 'CreateHouse':
# This operation was likely aborted.
return
# Update the avatar's houseId:
av = self.mgr.air.doId2do.get(self.avatar['avId'])
if av:
av.b_setHouseId(houseId)
else:
self.mgr.air.dbInterface.updateObject(self.mgr.air.dbId, self.avatar['avId'],
self.mgr.air.dclassesByName['DistributedToonAI'],
{'setHouseId': [houseId]})
self.houseId = houseId
self.demand('LoadHouse')
def enterLoadHouse(self):
# Activate the house:
self.mgr.air.sendActivate(self.houseId, self.mgr.air.districtId, self.estate.zoneId,
self.mgr.air.dclassesByName['DistributedHouseAI'],
{'setHousePos': [self.index],
'setColor': [self.index],
'setName': [self.avatar['setName'][0]],
'setAvatarId': [self.avatar['avId']]})
# Wait for the house to generate:
self.acceptOnce('generate-%d' % self.houseId, self.__handleHouseGenerated)
def __handleHouseGenerated(self, house):
# The house will need to be able to reference
# the estate for setting up gardens, so:
house.estate = self.estate
# Initialize our interior:
house.interior.gender = self.gender
house.interior.start()
self.house = house
self.estate.houses[self.index] = self.house
if config.GetBool('want-gardening', False):
# Initialize our garden:
self.house.createGardenManager()
self.demand('Off')
def exitLoadHouse(self):
self.ignore('generate-%d' % self.houseId)
def enterOff(self):
self.done = True
self.callback(self.house)
class LoadEstateOperation(FSM):
def __init__(self, mgr, callback):
FSM.__init__(self, 'LoadEstateOperation')
self.mgr = mgr
self.callback = callback
self.estate = None
self.accId = None
self.zoneId = None
self.avIds = None
self.avatars = None
self.houseOperations = None
self.petOperations = None
def start(self, accId, zoneId):
self.accId = accId
self.zoneId = zoneId
self.demand('QueryAccount')
def enterQueryAccount(self):
self.mgr.air.dbInterface.queryObject(self.mgr.air.dbId, self.accId, self.__handleQueryAccount)
def __handleQueryAccount(self, dclass, fields):
if self.state != 'QueryAccount':
# This operation was likely aborted.
return
if dclass != self.mgr.air.dclassesByName['AccountAI']:
self.mgr.notify.warning('Account %d has non-account dclass %d!' % (self.accId, dclass))
self.demand('Failure')
return
self.accFields = fields
self.estateId = fields.get('ESTATE_ID', 0)
self.demand('QueryAvatars')
def enterQueryAvatars(self):
self.avIds = self.accFields.get('ACCOUNT_AV_SET', [0] * 6)
self.avatars = {}
for index, avId in enumerate(self.avIds):
if avId == 0:
self.avatars[index] = None
continue
self.mgr.air.dbInterface.queryObject(self.mgr.air.dbId, avId,
functools.partial(self.__handleQueryAvatar, index=index))
def __handleQueryAvatar(self, dclass, fields, index):
if self.state != 'QueryAvatars':
# This operation was likely aborted.
return
if dclass != self.mgr.air.dclassesByName['DistributedToonAI']:
self.mgr.notify.warning(
'Account %d has avatar %d with non-Toon dclass %d!' % (self.accId, self.avIds[index], dclass))
self.demand('Failure')
return
fields['avId'] = self.avIds[index]
self.avatars[index] = fields
if len(self.avatars) == 6:
self.__gotAllAvatars()
def __gotAllAvatars(self):
# We have all of our avatars, so now we can handle the estate.
if self.estateId:
# We already have an estate, so let's load that:
self.demand('LoadEstate')
else:
# We don't yet have an estate, so let's make one:
self.demand('CreateEstate')
def enterCreateEstate(self):
# Create a blank estate object:
self.mgr.air.dbInterface.createObject(self.mgr.air.dbId, self.mgr.air.dclassesByName['DistributedEstateAI'], {},
self.__handleEstateCreated)
def __handleEstateCreated(self, estateId):
if self.state != 'CreateEstate':
# This operation was likely aborted.
return
self.estateId = estateId
# Store the new estate object on our account:
self.mgr.air.dbInterface.updateObject(self.mgr.air.dbId, self.accId, self.mgr.air.dclassesByName['AccountAI'],
{'ESTATE_ID': estateId})
self.demand('LoadEstate')
def enterLoadEstate(self):
# Set the estate fields:
fields = {'setSlot%dToonId' % i: (avId,) for i, avId in enumerate(self.avIds)}
# Activate the estate:
self.mgr.air.sendActivate(self.estateId, self.mgr.air.districtId, self.zoneId,
self.mgr.air.dclassesByName['DistributedEstateAI'], fields)
# Wait for the estate to generate:
self.acceptOnce('generate-%d' % self.estateId, self.__handleEstateGenerated)
def __handleEstateGenerated(self, estate):
# Get the estate:
self.estate = estate
# For keeping track of pets in this estate:
self.estate.pets = []
# Map the owner to the estate:
ownerId = self.mgr.getOwnerFromZone(self.estate.zoneId)
owner = self.mgr.air.doId2do.get(ownerId)
if owner:
self.mgr.toon2estate[owner] = self.estate
# Set the estate's ID list:
self.estate.b_setIdList(self.avIds)
# Load houses:
self.demand('LoadHouses')
def exitLoadEstate(self):
self.ignore('generate-%d' % self.estateId)
def enterLoadHouses(self):
self.houseOperations = []
for houseIndex in xrange(6):
houseOperation = LoadHouseOperation(self.mgr, self.estate, houseIndex, self.avatars[houseIndex],
self.__handleHouseLoaded)
self.houseOperations.append(houseOperation)
houseOperation.start()
def __handleHouseLoaded(self, house):
if self.state != 'LoadHouses':
# We aren't loading houses, so we probably got cancelled. Therefore,
# the only sensible thing to do is simply destroy the house.
house.requestDelete()
return
# A house operation just finished! Let's see if all of them are done:
if all(houseOperation.done for houseOperation in self.houseOperations):
# Load our pets:
self.demand('LoadPets')
def enterLoadPets(self):
self.petOperations = []
for houseIndex in xrange(6):
av = self.avatars[houseIndex]
if av and av['setPetId'][0] != 0:
petOperation = LoadPetOperation(self.mgr, self.estate, av, self.__handlePetLoaded)
self.petOperations.append(petOperation)
petOperation.start()
if not self.petOperations:
taskMgr.doMethodLater(0, lambda: self.demand('Finished'), 'no-pets', extraArgs=[])
def __handlePetLoaded(self, pet):
if self.state != 'LoadPets':
pet.requestDelete()
return
# A pet operation just finished! Let's see if all of them are done:
if all(petOperation.done for petOperation in self.petOperations):
self.demand('Finished')
def enterFinished(self):
self.petOperations = []
self.callback(True)
def enterFailure(self):
self.cancel()
self.callback(False)
def cancel(self):
if self.estate:
self.estate.destroy()
self.estate = None
self.demand('Off')
class LoadPetOperation(FSM):
def __init__(self, mgr, estate, toon, callback):
FSM.__init__(self, 'LoadPetFSM')
self.mgr = mgr
self.estate = estate
self.toon = toon
self.callback = callback
self.done = False
self.petId = 0
def start(self):
if type(self.toon) == dict:
self.petId = self.toon['setPetId'][0]
else:
self.petId = self.toon.getPetId()
if self.petId not in self.mgr.air.doId2do:
self.mgr.air.sendActivate(self.petId, self.mgr.air.districtId, self.estate.zoneId)
self.acceptOnce('generate-%d' % self.petId, self.__generated)
else:
self.__generated(self.mgr.air.doId2do[self.petId])
def __generated(self, pet):
self.pet = pet
self.estate.pets.append(pet)
self.demand('Off')
def enterOff(self):
self.ignore('generate-%d' % self.petId)
self.done = True
self.callback(self.pet)
class EstateManagerAI(DistributedObjectAI):
notify = DirectNotifyGlobal.directNotify.newCategory('EstateManagerAI')
def __init__(self, air):
DistributedObjectAI.__init__(self, air)
self.toon2estate = {}
self.estate = {}
self.estate2toons = {}
self.estate2timeout = {}
self.zone2toons = {}
self.zone2owner = {}
self.petOperations = []
def getEstateZone(self, avId, name):
# Thank you name, very cool!
senderId = self.air.getAvatarIdFromSender()
accId = self.air.getAccountIdFromSender()
senderAv = self.air.doId2do.get(senderId)
if not senderAv:
self.air.writeServerEvent('suspicious', senderId, 'Sent getEstateZone() but not on district!')
return
# If an avId has been provided, then the sender wants to visit a friend.
# In this case, we do not need to load the estate, we only need to check
# to see if it already exists.
if avId and avId != senderId:
av = self.air.doId2do.get(avId)
if av and av.dclass == self.air.dclassesByName['DistributedToonAI']:
estate = self.toon2estate.get(av)
if estate:
# Found an estate!
avId = estate.owner.doId
zoneId = estate.zoneId
self._mapToEstate(senderAv, estate)
# In case the sender is teleporting from their estate
# to another estate, we want to unload their estate.
self._unloadEstate(senderAv)
if senderAv and senderAv.getPetId() != 0:
pet = self.air.doId2do.get(senderAv.getPetId())
if pet:
self.acceptOnce(self.air.getAvatarExitEvent(senderAv.getPetId()), self.__handleLoadPet,
extraArgs=[estate, senderAv])
pet.requestDelete()
else:
self.__handleLoadPet(estate, senderAv)
# Now we want to send the sender to the estate.
if hasattr(senderAv, 'enterEstate'):
senderAv.enterEstate(avId, zoneId)
self.sendUpdateToAvatarId(senderId, 'setEstateZone', [avId, zoneId])
# We weren't able to find the given avId at an estate, that's pretty sad.
self.sendUpdateToAvatarId(senderId, 'setEstateZone', [0, 0])
return
# Otherwise, the sender wants to go to their own estate.
estate = getattr(senderAv, 'estate', None)
if estate:
# The sender already has an estate loaded, so let's send them there.
self._mapToEstate(senderAv, senderAv.estate)
if senderAv and senderAv.getPetId() != 0:
pet = self.air.doId2do.get(senderAv.getPetId())
if pet:
self.acceptOnce(self.air.getAvatarExitEvent(senderAv.getPetId()), self.__handleLoadPet,
extraArgs=[estate, senderAv])
pet.requestDelete()
else:
self.__handleLoadPet(estate, senderAv)
if hasattr(senderAv, 'enterEstate'):
senderAv.enterEstate(senderId, estate.zoneId)
self.sendUpdateToAvatarId(senderId, 'setEstateZone', [senderId, estate.zoneId])
# If a timeout is active, cancel it:
if estate in self.estate2timeout:
self.estate2timeout[estate].remove()
del self.estate2timeout[estate]
return
if getattr(senderAv, 'loadEstateOperation', None):
# We already have a loading operation underway; ignore this second
# request since the first operation will setEstateZone() when it
# finishes anyway.
return
zoneId = self.air.allocateZone()
self.zone2owner[zoneId] = avId
def estateLoaded(success):
if success:
senderAv.estate = senderAv.loadEstateOperation.estate
senderAv.estate.owner = senderAv
self._mapToEstate(senderAv, senderAv.estate)
if hasattr(senderAv, 'enterEstate'):
senderAv.enterEstate(senderId, zoneId)
self.sendUpdateToAvatarId(senderId, 'setEstateZone', [senderId, zoneId])
else:
# Estate loading failed. Sad!
self.sendUpdateToAvatarId(senderId, 'setEstateZone', [0, 0])
# Might as well free up the zoneId as well.
self.air.deallocateZone(zoneId)
del self.zone2owner[zoneId]
senderAv.loadEstateOperation = None
self.acceptOnce(self.air.getAvatarExitEvent(senderAv.doId), self.__handleUnexpectedExit, extraArgs=[senderAv])
if senderAv and senderAv.getPetId() != 0:
pet = self.air.doId2do.get(senderAv.getPetId())
if pet:
self.acceptOnce(self.air.getAvatarExitEvent(senderAv.getPetId()), self.__handleLoadEstate,
extraArgs=[senderAv, estateLoaded, accId, zoneId])
pet.requestDelete()
return
self.__handleLoadEstate(senderAv, estateLoaded, accId, zoneId)
def __handleUnexpectedExit(self, senderAv):
self._unmapFromEstate(senderAv)
self._unloadEstate(senderAv)
def exitEstate(self):
senderId = self.air.getAvatarIdFromSender()
senderAv = self.air.doId2do.get(senderId)
if not senderAv:
self.air.writeServerEvent('suspicious', senderId, 'Sent exitEstate() but not on district!')
return
self._unmapFromEstate(senderAv)
self._unloadEstate(senderAv)
def removeFriend(self, ownerId, avId):
if not (ownerId or avId):
return
owner = self.air.doId2do.get(ownerId)
if not owner:
return
friend = self.air.doId2do.get(avId)
if not friend:
return
estate = self.estate.get(ownerId)
if not estate:
return
if ownerId not in estate.getIdList():
return
toons = self.estate2toons.get(estate, [])
if owner not in toons and friend not in toons:
return
friendInList = False
for friendPair in owner.getFriendsList():
if type(friendPair) == tuple:
friendId = friendPair[0]
else:
friendId = friendPair
if friendId == avId:
friendInList = True
break
if not friendInList:
self.sendUpdateToAvatarId(friend.doId, 'sendAvToPlayground', [friend.doId, 1])
def _unloadEstate(self, av):
if getattr(av, 'estate', None):
estate = av.estate
if estate not in self.estate2timeout:
self.estate2timeout[estate] = taskMgr.doMethodLater(HouseGlobals.BOOT_GRACE_PERIOD, self._cleanupEstate,
estate.uniqueName('unload-estate'),
extraArgs=[estate])
# Send warning:
self._sendToonsToPlayground(av.estate, 0)
if getattr(av, 'loadEstateOperation', None):
self.air.deallocateZone(av.loadEstateOperation.zoneId)
av.loadEstateOperation.cancel()
av.loadEstateOperation = None
if av and hasattr(av, 'exitEstate') and hasattr(av, 'isInEstate') and av.isInEstate():
av.exitEstate()
if av and av.getPetId() != 0:
self.ignore(self.air.getAvatarExitEvent(av.getPetId()))
pet = self.air.doId2do.get(av.getPetId())
if pet:
pet.requestDelete()
self.ignore(self.air.getAvatarExitEvent(av.doId))
def _mapToEstate(self, av, estate):
self._unmapFromEstate(av)
self.estate[av.doId] = estate
self.estate2toons.setdefault(estate, []).append(av)
if av not in self.toon2estate:
self.toon2estate[av] = estate
self.zone2toons.setdefault(estate.zoneId, []).append(av.doId)
def _unmapFromEstate(self, av):
estate = self.toon2estate.get(av)
if not estate:
return
try:
del self.estate[av.doId]
except KeyError:
pass
del self.toon2estate[av]
try:
self.estate2toons[estate].remove(av)
except (KeyError, ValueError):
pass
try:
self.zone2toons[estate.zoneId].remove(av.doId)
except (KeyError, ValueError):
pass
def _cleanupEstate(self, estate):
# Boot all avatars from estate:
self._sendToonsToPlayground(estate, 1)
# Clean up avatar <-> estate mappings:
for av in self.estate2toons.get(estate, []):
try:
del self.estate[av.doId]
del self.toon2estate[av]
except KeyError:
pass
try:
del self.estate2toons[estate]
except KeyError:
pass
try:
del self.zone2toons[estate.zoneId]
except KeyError:
pass
# Clean up timeout, if it exists:
if estate in self.estate2timeout:
del self.estate2timeout[estate]
# Destroy estate and unmap from owner:
estate.destroy()
estate.owner.estate = None
# Destroy pets:
for pet in estate.pets:
pet.requestDelete()
estate.pets = []
# Free estate's zone:
self.air.deallocateZone(estate.zoneId)
del self.zone2owner[estate.zoneId]
def _sendToonsToPlayground(self, estate, reason):
for toon in self.estate2toons.get(estate, []):
self.sendUpdateToAvatarId(toon.doId, 'sendAvToPlayground', [toon.doId, reason])
def getEstateZones(self, ownerId):
toon = self.air.doId2do.get(ownerId)
if not toon:
return []
estate = self.toon2estate.get(toon)
if not estate:
return []
return [estate.zoneId]
def getEstateHouseZones(self, ownerId):
houseZones = []
toon = self.air.doId2do.get(ownerId)
if not toon:
return houseZones
estate = self.toon2estate.get(toon)
if not estate:
return houseZones
houses = estate.houses
for house in houses:
houseZones.append(house.interiorZone)
return houseZones
def _lookupEstate(self, toon):
return self.toon2estate.get(toon)
def getOwnerFromZone(self, zoneId):
return self.zone2owner.get(zoneId, 0)
def __handleLoadPet(self, estate, av):
petOperation = LoadPetOperation(self, estate, av, self.__handlePetLoaded)
self.petOperations.append(petOperation)
petOperation.start()
def __handlePetLoaded(self, _):
# A pet operation just finished! Let's see if all of them are done:
if all(petOperation.done for petOperation in self.petOperations):
self.petOperations = []
def __handleLoadEstate(self, av, callback, accId, zoneId):
self._unmapFromEstate(av)
av.loadEstateOperation = LoadEstateOperation(self, callback)
av.loadEstateOperation.start(accId, zoneId)

View file

@ -176,6 +176,7 @@ class MagicWord(DirectObject):
def handleWord(self, invoker, avId, toon, *args):
raise NotImplementedError
class SetHP(MagicWord):
aliases = ["hp", "setlaff", "laff"]
desc = "Sets the target's current laff."
@ -199,6 +200,7 @@ class SetHP(MagicWord):
toon.b_setHp(hp)
return "{}'s laff has been set to {}.".format(toon.getName(), hp)
class SetMaxHP(MagicWord):
aliases = ["maxhp", "setmaxlaff", "maxlaff"]
desc = "Sets the target's max laff."
@ -217,6 +219,7 @@ class SetMaxHP(MagicWord):
toon.toonUp(maxhp)
return "{}'s max laff has been set to {}.".format(toon.getName(), maxhp)
class ToggleOobe(MagicWord):
aliases = ["oobe"]
desc = "Toggles the out of body experience mode, which lets you move the camera freely."
@ -229,6 +232,7 @@ class ToggleOobe(MagicWord):
base.oobe()
return "Oobe mode has been toggled."
class ToggleRun(MagicWord):
aliases = ["run"]
desc = "Toggles run mode, which gives you a faster running speed."
@ -304,6 +308,8 @@ class Inventory(MagicWord):
toon.d_setInventory(toon.inventory.makeNetString())
return ("Zeroing inventory for " + toon.getName() + ".")
class SetPinkSlips(MagicWord):
# this command gives the target toon the specified amount of pink slips
# default is 255
@ -324,52 +330,7 @@ class AbortMinigame(MagicWord):
def handleWord(self, invoker, avId, toon, *args):
messenger.send("minigameAbort")
return "Requested minigame abort."
class SkipMiniGolfHole(MagicWord):
aliases = ["skipgolfhole", "skipgolf", "skiphole"]
desc = "Skips the current golf hole."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = []
def handleWord(self, invoker, avId, toon, *args):
from toontown.golf.DistributedGolfCourseAI import DistributedGolfCourseAI
course = None
for do in simbase.air.doId2do.values(): # For all doids, check whether it's a golf course, then check if our target is part of it.
if isinstance(do, DistributedGolfCourseAI):
if invoker.doId in do.avIdList:
course = do
break
if not course:
return "You aren't in a golf course!"
if course.isPlayingLastHole(): # If the Toon is on the final hole, calling holeOver() will softlock, so instead we move onto the reward screen.
course.demand('WaitReward')
else:
course.holeOver()
return "Skipped the current hole."
class AbortGolfCourse(MagicWord):
aliases = ["abortminigolf", "abortgolf", "abortcourse", "leavegolf", "leavecourse"]
desc = "Aborts the current golf course."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = []
def handleWord(self, invoker, avId, toon, *args):
from toontown.golf.DistributedGolfCourseAI import DistributedGolfCourseAI
course = None
for do in simbase.air.doId2do.values(): # For all doids, check whether it's a golf course, then check if our target is part of it.
if isinstance(do, DistributedGolfCourseAI):
if invoker.doId in do.avIdList:
course = do
break
if not course:
return "You aren't in a golf course!"
course.setCourseAbort()
return "Aborted golf course."
return "Requested minigame abort."
class Minigame(MagicWord):
aliases = ["mg"]
@ -555,7 +516,7 @@ class BossBattle(MagicWord):
if not start:
respText += " in Frolic state"
return respText + ", teleporting...", toon.doId, ["cogHQLoader", "cogHQBossBattle", "movie" if start else "teleportIn", boss.getHoodId(), boss.zoneId, 0]
return respText + ", teleporting...", ["cogHQLoader", "cogHQBossBattle", "movie" if start else "teleportIn", boss.getHoodId(), boss.zoneId, 0]
elif command == "list":
# List all the ongoing boss battles.
@ -597,7 +558,7 @@ class BossBattle(MagicWord):
return "Index out of range!"
boss = AllBossCogs[index]
return "Teleporting to boss battle...", toon.doId, ["cogHQLoader", "cogHQBossBattle", "", boss.getHoodId(), boss.zoneId, 0]
return "Teleporting to boss battle...", ["cogHQLoader", "cogHQBossBattle", "", boss.getHoodId(), boss.zoneId, 0]
# The following commands needs the invoker to be in a boss battle.
@ -636,129 +597,13 @@ class BossBattle(MagicWord):
# The create command is already described when the invoker is not in a battle. These are the commands
# they can use INSIDE the battle.
return f"Unknown command: \"{command}\". Valid commands: \"start\", \"stop\", \"skip\", \"final\", \"kill\"."
return respText + f"Unknown command: \"{command}\". Valid commands: \"start\", \"stop\", \"skip\", \"final\", \"kill\"."
def __destroyBoss(self, boss):
bossZone = boss.zoneId
boss.requestDelete()
self.air.deallocateZone(bossZone)
class GlobalTeleport(MagicWord):
aliases = ["globaltp", "tpaccess"]
desc = "Enables teleport access to all zones."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
def handleWord(self, invoker, avId, toon, *args):
from toontown.toonbase import ToontownGlobals
toon.b_setHoodsVisited(ToontownGlobals.HoodsForTeleportAll)
toon.b_setTeleportAccess(ToontownGlobals.HoodsForTeleportAll)
return f"Enabled teleport access to all zones for {toon.getName()}."
class Teleport(MagicWord):
aliases = ["tp", "goto"]
desc = "Teleport to a specified zone."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = [("zoneName", str, False, '')]
def handleWord(self, invoker, avId, toon, *args):
from toontown.hood import ZoneUtil
from toontown.toonbase import ToontownGlobals
zoneName = args[0]
# Can add stuff like streets to this too if you wanted, but if you do you'll want it to be a valid zone on that street. eg: 2100 is invalid, but any value 2101 to 2156 is fine.
# so if you wanted to add a silly street key, theroetically you could do something like this: 'sillystreet': ToontownGlobals.SillyStreet +1,
zoneName2Id = {'ttc': ToontownGlobals.ToontownCentral,
'dd': ToontownGlobals.DonaldsDock,
'dg': ToontownGlobals.DaisyGardens,
'mml': ToontownGlobals.MinniesMelodyland,
'tb': ToontownGlobals.TheBrrrgh,
'ddl': ToontownGlobals.DonaldsDreamland,
'gs': ToontownGlobals.GoofySpeedway,
'oz': ToontownGlobals.OutdoorZone,
'aa': ToontownGlobals.OutdoorZone,
'gz': ToontownGlobals.GolfZone,
'sbhq': ToontownGlobals.SellbotHQ,
'factory': ToontownGlobals.SellbotFactoryExt,
'cbhq': ToontownGlobals.CashbotHQ,
'lbhq': ToontownGlobals.LawbotHQ,
'bbhq': ToontownGlobals.BossbotHQ}
try:
zone = zoneName2Id[zoneName]
except KeyError:
return "Unknown zone name!"
return f"Requested to teleport {toon.getName()} to zone {zone}.", toon.doId, [ZoneUtil.getBranchLoaderName(zone), ZoneUtil.getToonWhereName(zone), "", ZoneUtil.getHoodId(zone), zone, 0]
class ToggleSleep(MagicWord):
aliases = ["sleep", "nosleep", "neversleep", "togglesleeping", "insomnia"]
desc = "Toggles sleeping for the target."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
def handleWord(self, invoker, avId, toon, *args):
toon.d_toggleSleep()
return f"Toggled sleeping for {toon.getName()}."
class ToggleImmortal(MagicWord):
aliases = ["immortal", "invincible", "invulnerable"]
desc = "Toggle immortal mode. This makes the Toon immune to damage."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
def handleWord(self, invoker, avId, toon, *args):
toon.setImmortalMode(not toon.immortalMode)
return f"Toggled immortal mode for {toon.getName()}"
class ToggleGhost(MagicWord):
aliases = ["ghost", "invisible", "spy"]
desc = "Toggle ghost mode."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
def handleWord(self, invoker, avId, toon, *args):
# 1 is for the attic, 2 enables you to see yourself other ghost toons. 0 is off.
toon.b_setGhostMode(2 if not toon.ghostMode else 0) # As it's primarily for moderation purposes, we set it to 2 here, or 0 if it's already on.
return f"Toggled ghost mode for {toon.getName()}"
class SetGM(MagicWord):
aliases = ["icon", "seticon", "gm", "gmicon", "setgmicon"]
desc = "Sets the GM icon on the target."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = [("iconRequest", int, False, 0),]
def handleWord(self, invoker, avId, toon, *args):
from toontown.toonbase import TTLocalizer
iconRequest = args[0]
if iconRequest > len(TTLocalizer.GM_NAMES) or iconRequest < 0:
return "Invalid GM icon ID!"
toon.b_setGM(0) # Reset it first, otherwise the Toon keeps the old icon, but the name still changes.
toon.b_setGM(iconRequest)
return f"GM icon set to {iconRequest} for {toon.getName()}"
class SetMaxCarry(MagicWord):
aliases = ["gagpouch", "pouch", "gagcapacity"]
desc = "Set a Toon's gag pouch size."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
arguments = [("pouchSize", int, True)]
def handleWord(self, invoker, avId, toon, *args):
pouchSize = args[0]
if pouchSize > 255 or pouchSize < 0:
return "Specified pouch size must be between 1 and 255."
toon.b_setMaxCarry(pouchSize)
return f"Set gag pouch size to {pouchSize} for {toon.getName()}"
class ToggleInstantKill(MagicWord):
aliases = ["instantkill", "instakill"]
desc = "Toggle the ability to instantly kill a Cog with any gag."
execLocation = MagicWordConfig.EXEC_LOC_SERVER
def handleWord(self, invoker, avId, toon, *args):
toon.setInstantKillMode(not toon.instantKillMode)
return f"Toggled instant-kill mode for {toon.getName()}"
class Fireworks(MagicWord):
aliases = ["firework"]
desc = "Starts a firework show."
@ -830,4 +675,4 @@ class Fireworks(MagicWord):
# A bit hacky, but better than the old system
for item in list(globals().values()):
if isinstance(item, type) and issubclass(item, MagicWord):
i = item()
i = item()

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ class ToonBase(OTPBase.OTPBase):
self.wantDynamicShadows = 0
self.exitErrorCode = 0
camera.setPosHpr(0, 0, 0, 0, 0, 0)
self.camLens.setFov(ToontownGlobals.DefaultCameraFov)
self.camLens.setMinFov(ToontownGlobals.DefaultCameraFov / (4. / 3.))
self.camLens.setNearFar(ToontownGlobals.DefaultCameraNear, ToontownGlobals.DefaultCameraFar)
self.musicManager.setVolume(0.65)
self.setBackgroundColor(ToontownGlobals.DefaultBackgroundColor)

View file

@ -63,9 +63,10 @@ ConfigVariableDouble('decompressor-step-time').setValue(0.01)
ConfigVariableDouble('extractor-step-time').setValue(0.01)
backgroundNodePath = aspect2d.attachNewNode(backgroundNode, 0)
backgroundNodePath.setPos(0.0, 0.0, 0.0)
backgroundNodePath.setScale(render2d, VBase3(1))
backgroundNodePath.setScale(aspect2d, VBase3(1.33, 1, 1))
backgroundNodePath.find('**/fg').setBin('fixed', 20)
backgroundNodePath.find('**/bg').setBin('fixed', 10)
backgroundNodePath.find('**/bg').setScale(aspect2d, VBase3(base.getAspectRatio(), 1, 1))
base.graphicsEngine.renderFrame()
DirectGuiGlobals.setDefaultRolloverSound(base.loader.loadSfx('phase_3/audio/sfx/GUI_rollover.ogg'))
DirectGuiGlobals.setDefaultClickSound(base.loader.loadSfx('phase_3/audio/sfx/GUI_create_toon_fwd.ogg'))
@ -89,7 +90,7 @@ else:
from direct.gui.DirectGui import OnscreenText
serverVersion = ConfigVariableString('server-version', 'no_version_set').value
print('ToontownStart: serverVersion: ', serverVersion)
version = OnscreenText(serverVersion, pos=(-1.3, -0.975), scale=0.06, fg=Vec4(0, 0, 1, 0.6), align=TextNode.ALeft)
version = OnscreenText(serverVersion, parent=base.a2dBottomLeft, pos=(0.033, 0.025), scale=0.06, fg=Vec4(0, 0, 1, 0.6), align=TextNode.ALeft)
loader.beginBulkLoad('init', TTLocalizer.LoaderLabel, 138, 0, TTLocalizer.TIP_NONE)
from toontown.distributed.ToontownClientRepository import ToontownClientRepository
cr = ToontownClientRepository(serverVersion, launcher)

View file

@ -41,6 +41,7 @@ class ToontownLoadingScreen:
self.waitBar.reparentTo(self.gui)
self.title.reparentTo(self.gui)
self.gui.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
self.gui.find('**/bg').setScale(aspect2dp, VBase3(base.getAspectRatio(), 1, 1))
else:
self.waitBar.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)
self.title.reparentTo(aspect2dp, DGG.NO_FADE_SORT_INDEX)

View file

@ -45,3 +45,5 @@ class ToontownUDRepository(ToontownInternalRepository):
if __astron__:
# Create our Astron login manager...
self.astronLoginManager = self.generateGlobalObject(OTP_DO_ID_ASTRON_LOGIN_MANAGER, 'AstronLoginManager')
self.chatHandler = self.generateGlobalObject(OTP_DO_ID_CHAT_ROUTER, 'ChatHandler')
self.avatarFriendsManager = self.generateGlobalObject(OTP_DO_ID_AVATAR_FRIENDS_MANAGER, 'AvatarFriendsManager')

View file

@ -5,7 +5,9 @@ cd..
rem Read the contents of PPYTHON_PATH into %PPYTHON_PATH%:
set /P PPYTHON_PATH=<PPYTHON_PATH
set LOGIN_TOKEN=dev
set /P LOGIN_TOKEN="Login password: "
:GAME
%PPYTHON_PATH% -m toontown.launcher.QuickStartLauncher
pause
GOTO GAME