import random import copy from logic.smbool import SMBool from rom.rom_patches import RomPatches import utils.log, logging LOG = utils.log.get('DoorsManager') colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice'] # 1/15 chance to have the door set to grey colorsListGrey = colorsList * 2 + ['grey'] class Facing: Left = 0 Right = 1 Top = 2 Bottom = 3 # door facing left - right - top - bottom plmRed = [0xc88a, 0xc890, 0xc896, 0xc89c] plmGreen = [0xc872, 0xc878, 0xc87e, 0xc884] plmYellow = [0xc85a, 0xc860, 0xc866, 0xc86c] plmGrey = [0xc842, 0xc848, 0xc84e, 0xc854] plmWave = [0xf763, 0xf769, 0xf70f, 0xf715] plmSpazer = [0xf733, 0xf739, 0xf73f, 0xf745] plmPlasma = [0xf74b, 0xf751, 0xf757, 0xf75d] plmIce = [0xf71b, 0xf721, 0xf727, 0xf72d] colors2plm = { 'red': plmRed, 'green': plmGreen, 'yellow': plmYellow, 'grey': plmGrey, 'wave': plmWave, 'spazer': plmSpazer, 'plasma': plmPlasma, 'ice': plmIce } class Door(object): __slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors') def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None): self.name = name self.address = address self.vanillaColor = vanillaColor self.setColor(vanillaColor) self.forced = False self.facing = facing self.hidden = False self.canGrey = canGrey self.id = id # list of forbidden colors self.forbiddenColors = forbiddenColors def forceBlue(self): # custom start location, area, patches can force doors to blue self.setColor('blue') self.forced = True def setColor(self, color): self.color = color def getColor(self): if self.hidden: return 'grey' else: return self.color def isRandom(self): return self.color != self.vanillaColor and not self.isBlue() def isBlue(self): return self.color == 'blue' def canRandomize(self): return not self.forced and self.id is None def filterColorList(self, colorsList): if self.forbiddenColors is None: return colorsList else: return [color for color in colorsList if color not in self.forbiddenColors] def randomize(self, allowGreyDoors): if self.canRandomize(): if self.canGrey and allowGreyDoors: self.setColor(random.choice(self.filterColorList(colorsListGrey))) else: self.setColor(random.choice(self.filterColorList(colorsList))) def traverse(self, smbm): if self.hidden or self.color == 'grey': return SMBool(False) elif self.color == 'red': return smbm.canOpenRedDoors() elif self.color == 'green': return smbm.canOpenGreenDoors() elif self.color == 'yellow': return smbm.canOpenYellowDoors() elif self.color == 'wave': return smbm.haveItem('Wave') elif self.color == 'spazer': return smbm.haveItem('Spazer') elif self.color == 'plasma': return smbm.haveItem('Plasma') elif self.color == 'ice': return smbm.haveItem('Ice') else: return SMBool(True) def __repr__(self): return "Door({}, {})".format(self.name, self.color) def isRefillSave(self): return self.address is None def writeColor(self, rom): if self.isBlue() or self.isRefillSave(): return rom.writeWord(colors2plm[self.color][self.facing], self.address) # also set plm args high byte to never opened, even during escape if self.color == 'grey': rom.writeByte(0x90, self.address+5) def readColor(self, rom): if self.forced or self.isRefillSave(): return plm = rom.readWord(self.address) if plm in plmRed: self.setColor('red') elif plm in plmGreen: self.setColor('green') elif plm in plmYellow: self.setColor('yellow') elif plm in plmGrey: self.setColor('grey') elif plm in plmWave: self.setColor('wave') elif plm in plmSpazer: self.setColor('spazer') elif plm in plmPlasma: self.setColor('plasma') elif plm in plmIce: self.setColor('ice') else: raise Exception("Unknown color {} for {}".format(hex(plm), self.name)) # for tracker def canHide(self): return self.color != 'blue' def hide(self): if self.canHide(): self.hidden = True def reveal(self): self.hidden = False def switch(self): if self.hidden: self.reveal() else: self.hide() # to send/receive state to tracker/plando def serialize(self): return (self.color, self.facing, self.hidden) def unserialize(self, data): self.setColor(data[0]) self.facing = data[1] self.hidden = data[2] class DoorsManager(): doorsDict = {} doors = { # crateria 'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True), 'LandingSiteTopRight': Door('LandingSiteTopRight', 0x07801e, 'yellow', Facing.Left), 'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True), 'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True), 'FlywayRight': Door('FlywayRight', 0x78420, 'red', Facing.Left), 'GreenPiratesShaftBottomRight': Door('GreenPiratesShaftBottomRight', 0x78470, 'red', Facing.Left, canGrey=True), 'RedBrinstarElevatorTop': Door('RedBrinstarElevatorTop', 0x78256, 'yellow', Facing.Bottom), 'ClimbRight': Door('ClimbRight', 0x78304, 'yellow', Facing.Left), # blue brinstar 'ConstructionZoneRight': Door('ConstructionZoneRight', 0x78784, 'red', Facing.Left), # green brinstar 'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True), 'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True), 'MainShaftRight': Door('MainShaftRight', 0x784be, 'red', Facing.Left), 'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True), 'EarlySupersRight': Door('EarlySupersRight', 0x78512, 'red', Facing.Left), 'EtecoonEnergyTankLeft': Door('EtecoonEnergyTankLeft', 0x787c8, 'green', Facing.Right), # pink brinstar 'BigPinkTopRight': Door('BigPinkTopRight', 0x78626, 'red', Facing.Left), 'BigPinkRight': Door('BigPinkRight', 0x7861a, 'yellow', Facing.Left), 'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True), 'BigPinkBottomLeft': Door('BigPinkBottomLeft', 0x7862c, 'red', Facing.Right), # red brinstar 'RedTowerLeft': Door('RedTowerLeft', 0x78866, 'yellow', Facing.Right), 'RedBrinstarFirefleaLeft': Door('RedBrinstarFirefleaLeft', 0x7886e, 'red', Facing.Right), 'RedTowerElevatorTopLeft': Door('RedTowerElevatorTopLeft', 0x788aa, 'green', Facing.Right), 'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right), 'RedTowerElevatorBottomLeft': Door('RedTowerElevatorBottomLeft', 0x788b6, 'green', Facing.Right), 'BelowSpazerTopRight': Door('BelowSpazerTopRight', 0x78966, 'green', Facing.Left), # Wrecked ship 'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True), 'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True), 'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top), 'ElectricDeathRoomTopLeft': Door('ElectricDeathRoomTopLeft', 0x7c32f, 'red', Facing.Right), # Upper Norfair 'BusinessCenterTopLeft': Door('BusinessCenterTopLeft', 0x78b00, 'green', Facing.Right), 'BusinessCenterBottomLeft': Door('BusinessCenterBottomLeft', 0x78b0c, 'red', Facing.Right), 'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True), 'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left), 'BubbleMountainTopRight': Door('BubbleMountainTopRight', 0x78c60, 'green', Facing.Left), 'BubbleMountainTopLeft': Door('BubbleMountainTopLeft', 0x78c5a, 'green', Facing.Right), 'SpeedBoosterHallRight': Door('SpeedBoosterHallRight', 0x78c7a, 'red', Facing.Left), 'SingleChamberRight': Door('SingleChamberRight', 0x78ca8, 'red', Facing.Left), 'DoubleChamberRight': Door('DoubleChamberRight', 0x78cc2, 'red', Facing.Left), 'KronicBoostBottomLeft': Door('KronicBoostBottomLeft', 0x78d4e, 'yellow', Facing.Right, canGrey=True), 'CrocomireSpeedwayBottom': Door('CrocomireSpeedwayBottom', 0x78b96, 'green', Facing.Top, canGrey=True), # Crocomire 'PostCrocomireUpperLeft': Door('PostCrocomireUpperLeft', 0x78bf4, 'red', Facing.Right), 'PostCrocomireShaftRight': Door('PostCrocomireShaftRight', 0x78c0c, 'red', Facing.Left), # Lower Norfair 'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top), 'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow']), # Maridia 'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left), 'FishTankRight': Door('FishTankRight', 0x7c475, 'red', Facing.Left), 'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left), 'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left), 'PlasmaSparkBottom': Door('PlasmaSparkBottom', 0x7c577, 'green', Facing.Top), 'OasisTop': Door('OasisTop', 0x7c5d3, 'green', Facing.Bottom), # refill/save 'GreenBrinstarSaveStation': Door('GreenBrinstarSaveStation', None, 'red', Facing.Right, id=0x1f), 'MaridiaBottomSaveStation': Door('MaridiaBottomSaveStation', None, 'red', Facing.Left, id=0x8c), 'MaridiaAqueductSaveStation': Door('MaridiaAqueductSaveStation', None, 'red', Facing.Right, id=0x96), 'ForgottenHighwaySaveStation': Door('ForgottenHighwaySaveStation', None, 'red', Facing.Left, id=0x92), 'DraygonSaveRefillStation': Door('DraygonSaveRefillStation', None, 'red', Facing.Left, id=0x98), 'KraidRefillStation': Door('KraidRefillStation', None, 'green', Facing.Left, id=0x44), 'RedBrinstarEnergyRefill': Door('RedBrinstarEnergyRefill', None, 'green', Facing.Right, id=0x38), 'GreenBrinstarMissileRefill': Door('GreenBrinstarMissileRefill', None, 'red', Facing.Right, id=0x23) } # call from logic def traverse(self, smbm, doorName): return DoorsManager.doorsDict[smbm.player][doorName].traverse(smbm) @staticmethod def setDoorsColor(player=0): if player not in DoorsManager.doorsDict.keys(): DoorsManager.doorsDict[player] = copy.deepcopy(DoorsManager.doors) currentDoors = DoorsManager.doorsDict[player] # depending on loaded patches, force some doors to blue, excluding them from randomization if RomPatches.has(player, RomPatches.BlueBrinstarBlueDoor): currentDoors['ConstructionZoneRight'].forceBlue() if RomPatches.has(player, RomPatches.BrinReserveBlueDoors): currentDoors['MainShaftRight'].forceBlue() currentDoors['EarlySupersRight'].forceBlue() if RomPatches.has(player, RomPatches.EtecoonSupersBlueDoor): currentDoors['EtecoonEnergyTankLeft'].forceBlue() #if RomPatches.has(player, RomPatches.SpongeBathBlueDoor): # currentDoors[''].forceBlue() if RomPatches.has(player, RomPatches.HiJumpAreaBlueDoor): currentDoors['BusinessCenterBottomLeft'].forceBlue() if RomPatches.has(player, RomPatches.SpeedAreaBlueDoors): currentDoors['BubbleMountainTopRight'].forceBlue() currentDoors['SpeedBoosterHallRight'].forceBlue() if RomPatches.has(player, RomPatches.MamaTurtleBlueDoor): currentDoors['FishTankRight'].forceBlue() if RomPatches.has(player, RomPatches.HellwayBlueDoor): currentDoors['RedTowerElevatorLeft'].forceBlue() if RomPatches.has(player, RomPatches.RedTowerBlueDoors): currentDoors['RedBrinstarElevatorTop'].forceBlue() if RomPatches.has(player, RomPatches.AreaRandoBlueDoors): currentDoors['GreenHillZoneTopRight'].forceBlue() currentDoors['NoobBridgeRight'].forceBlue() currentDoors['LeCoudeBottom'].forceBlue() currentDoors['KronicBoostBottomLeft'].forceBlue() else: # no area rando, prevent some doors to be in the grey doors pool currentDoors['GreenPiratesShaftBottomRight'].canGrey = False currentDoors['CrocomireSpeedwayBottom'].canGrey = False currentDoors['KronicBoostBottomLeft'].canGrey = False if RomPatches.has(player, RomPatches.AreaRandoMoreBlueDoors): currentDoors['KihunterBottom'].forceBlue() currentDoors['GreenPiratesShaftBottomRight'].forceBlue() if RomPatches.has(player, RomPatches.CrocBlueDoors): currentDoors['CrocomireSpeedwayBottom'].forceBlue() if RomPatches.has(player, RomPatches.CrabShaftBlueDoor): currentDoors['CrabShaftRight'].forceBlue() @staticmethod def randomize(allowGreyDoors, player): for door in DoorsManager.doorsDict[player].values(): door.randomize(allowGreyDoors) # set both ends of toilet to the same color to avoid soft locking in area rando toiletTop = DoorsManager.doorsDict[player]['PlasmaSparkBottom'] toiletBottom = DoorsManager.doorsDict[player]['OasisTop'] if toiletTop.color != toiletBottom.color: toiletBottom.setColor(toiletTop.color) DoorsManager.debugDoorsColor() # call from rom loader @staticmethod def loadDoorsColor(rom): # force to blue some doors depending on patches DoorsManager.setDoorsColor() # for each door store it's color for door in DoorsManager.doors.values(): door.readColor(rom) DoorsManager.debugDoorsColor() # tell that we have randomized doors isRandom = DoorsManager.isRandom() if isRandom: DoorsManager.setRefillSaveToBlue() return isRandom @staticmethod def isRandom(player): return any(door.isRandom() for door in DoorsManager.doorsDict[player].values()) @staticmethod def setRefillSaveToBlue(player): for door in DoorsManager.doorsDict[player].values(): if door.id is not None: door.forceBlue() @staticmethod def debugDoorsColor(): if LOG.getEffectiveLevel() == logging.DEBUG: for door in DoorsManager.doors.values(): LOG.debug("{:>32}: {:>6}".format(door.name, door.color)) # call from rom patcher @staticmethod def writeDoorsColor(rom, doors, player): for door in DoorsManager.doorsDict[player].values(): door.writeColor(rom) # also set save/refill doors to blue if door.id is not None: doors.append(door.id) # call from web @staticmethod def getAddressesToRead(): return [door.address for door in DoorsManager.doors.values() if door.address is not None] + [door.address+1 for door in DoorsManager.doors.values() if door.address is not None] # for isolver state @staticmethod def serialize(): return {door.name: door.serialize() for door in DoorsManager.doors.values()} @staticmethod def unserialize(state): for name, data in state.items(): DoorsManager.doors[name].unserialize(data) @staticmethod def allDoorsRevealed(): for door in DoorsManager.doors.values(): if door.hidden: return False return True # when using the tracker, first set all colored doors to grey until the user clicks on it @staticmethod def initTracker(): for door in DoorsManager.doors.values(): door.hide() # when the user clicks on a door in the tracker @staticmethod def switchVisibility(name): DoorsManager.doors[name].switch() # when the user clicks on a door in the race tracker or the plando @staticmethod def setColor(name, color): # in race mode the doors are hidden DoorsManager.doors[name].reveal() DoorsManager.doors[name].setColor(color) # in autotracker we need the current doors state @staticmethod def getDoorsState(): hiddenDoors = set([door.name for door in DoorsManager.doors.values() if door.hidden]) revealedDoor = set([door.name for door in DoorsManager.doors.values() if (not door.hidden) and door.canHide()]) return (hiddenDoors, revealedDoor)