historical/toontown-classic.git/toontown/estate/EstateManagerAI.py

645 lines
23 KiB
Python
Raw Normal View History

2024-01-16 17:20:27 +00:00
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 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)