oldschool-toontown/toontown/estate/EstateManagerAI.py
2024-06-25 21:00:27 -05:00

649 lines
No EOL
23 KiB
Python

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)