bass, how low can you go

master
Lucent 2023-10-12 00:16:25 +02:00
parent 28f2adc3d1
commit 8674605497
2 changed files with 512 additions and 18 deletions

View File

@ -31,7 +31,58 @@ class Game:
self.minorItemLocations.clear() self.minorItemLocations.clear()
self.itemLocations.clear() self.itemLocations.clear()
self.patcher.clear() self.patcher.clear()
# WARNING: Decompyle incomplete
try:
with open(vanillaGame, 'rb') as sourceRom:
sourceRom.seek(3967888, 0)
sourceArea = int.from_bytes(sourceRom.read(1), 'little')
sourceDoor = int.from_bytes(sourceRom.read(1), 'little')
targetOffset = sourceRom.tell()
targetArea = int.from_bytes(sourceRom.read(1), 'little')
if sourceArea != 255:
self.areaConnections.update({
'S{}-{:02X}'.format(sourceArea, sourceDoor): targetArea })
self.areaConnectionOffsets.update({
'S{}-{:02X}'.format(sourceArea, sourceDoor): targetOffset })
sourceArea = int.from_bytes(sourceRom.read(1), 'little')
sourceDoor = int.from_bytes(sourceRom.read(1), 'little')
targetOffset = sourceRom.tell()
targetArea = int.from_bytes(sourceRom.read(1), 'little')
continue
for currentArea in range(7):
sourceRom.seek(7977108 + currentArea * 4, 0)
data = sourceRom.read(4)
unpacked = struct.unpack('<L', data)
doorData = unpacked[0] - 134217728
sourceRom.seek(doorData, 0)
doorNumber = 0
connectionType = int.from_bytes(sourceRom.read(1), 'little')
roomNumber = int.from_bytes(sourceRom.read(1), 'little')
sourceRom.seek(4, 1)
connectedDoor = int.from_bytes(sourceRom.read(1), 'little')
sourceRom.seek(5, 1)
if connectionType == 0 and connectedDoor == 0:
continue
sourceNode = 'S{}-{:02X}'.format(currentArea, doorNumber)
connectedArea = self.areaConnections.get(sourceNode, currentArea)
targetNode = 'S{}-{:02X}'.format(connectedArea, connectedDoor)
self.doorConnections.update({
sourceNode: targetNode })
roomNumber = format(roomNumber, '02X')
roomString = 'Room-S{}-{}'.format(currentArea, roomNumber)
doorString = 'S{}-{:02X}'.format(currentArea, doorNumber)
if roomString in self.rooms:
self.rooms[roomString].append(doorString)
else:
self.rooms[roomString] = [
doorString]
doorNumber += 1
finally:
pass
except FileNotFoundError:
sys.exit('Error:', vanillaGame, 'could not be opened.')
return None
def set_setting(self, setting, value): def set_setting(self, setting, value):

View File

@ -4685,40 +4685,414 @@ def patch_game():
os.unlink(os.path.join('.', 'seeds', '{}.gba'.format(FileName))) os.unlink(os.path.join('.', 'seeds', '{}.gba'.format(FileName)))
print('Error: Base patch file has been modified. Please go to https://metroidconstruction.com/ and re-download MFOR.') print('Error: Base patch file has been modified. Please go to https://metroidconstruction.com/ and re-download MFOR.')
sys.exit(1) sys.exit(1)
# WARNING: Decompyle incomplete
with open(os.path.join('.', 'seeds', '{}.gba'.format(FileName)), 'rb+') as patchedGame:
if Debug:
sym = dict()
sym.clear()
with open(os.path.join('.', 'data', 'asm', 'temp.sym'), 'r') as symFile:
for line in symFile:
x = line.split()
if len(x) > 1 and re.search('^[a-zA-Z]', x[1]):
sym.update({
x[1]: x[0][2:] })
finally:
pass
os.remove(os.path.join('.', 'data', 'asm', 'temp.sym'))
roomEventOffset = int(sym.get('t_bossanddownloadevents'), 16)
itemEventOffset = int(sym.get('t_obtainitemevents'), 16)
securityOffset = int(sym.get('b_unlocklowerlevels'), 16)
print('roomEventOffset: 0x{:06X}'.format(roomEventOffset))
print('itemEventOffset: 0x{:06X}'.format(itemEventOffset))
print('securityOffset: 0x{:06X}'.format(securityOffset))
roomEventOffset = 8326320
itemEventOffset = 5726112
securityOffset = 479192
for area in RoomNodes:
areaIndex = list(RoomNodes.keys()).index(area)
for node in RoomNodes[area]:
name = node.get('Name')
nodeType = node.get('Type')
if nodeType != None:
itemName = ''
if 'Tank' in nodeType:
itemName = PlacedItems[UsedLocations.index(name)]
bg1 = int(node.get('BG1'), 16)
clipdata = int(node.get('Clipdata'), 16)
tileset = int(node.get('Tileset'), 16)
if SeedSettings['HideItems']:
if tileset == 33:
blockValue = 7
elif tileset == 34:
blockValue = 7
elif tileset == 40:
blockValue = 7
elif tileset == 42:
blockValue = 7
else:
blockValue = 3
else:
blockValue = ItemList.index(itemName)
if blockValue < 2:
blockValue = blockValue ^ 1
elif blockValue > 2:
if tileset == 33:
blockValue = 7
elif tileset == 34:
blockValue = 7
elif tileset == 40:
blockValue = 7
elif tileset == 42:
blockValue = 7
else:
blockValue = 3
blockValue += 70
if tileset == 9:
blockValue += 1
elif tileset == 11:
blockValue += 1
elif tileset == 25:
blockValue += 1
elif tileset >= 30 and tileset <= 34:
blockValue += 1
elif tileset == 40:
blockValue += 1
elif tileset == 42:
blockValue += 1
elif tileset == 48:
blockValue += 1
elif tileset == 49:
blockValue += 1
elif tileset == 52:
blockValue += 1
elif tileset == 56:
blockValue += 1
elif tileset == 61:
blockValue += 1
elif tileset == 62:
blockValue += 1
elif tileset == 64:
blockValue += 1
elif tileset == 67:
blockValue += 1
elif tileset == 72:
blockValue += 1
clipValue = ItemList.index(itemName)
if clipValue < 2:
clipValue = clipValue ^ 99
if 'Hidden' in nodeType:
clipValue += 2
elif 'Underwater' in nodeType:
clipValue += 4
elif clipValue == 2:
clipValue = 104
if 'Hidden' in nodeType:
clipValue += 1
elif 'Underwater' in nodeType:
clipValue += 2
elif clipValue >= 3:
clipValue = (clipValue - 3) * 3 + 120
if 'Hidden' in nodeType:
clipValue += 1
elif 'Underwater' in nodeType:
clipValue += 2
if 'Hidden' not in nodeType:
patchedGame.seek(bg1)
patchedGame.write(blockValue.to_bytes(1, 'little'))
patchedGame.seek(clipdata)
patchedGame.write(clipValue.to_bytes(1, 'little'))
elif 'Boss' in nodeType or 'Data' in nodeType:
itemName = PlacedItems[UsedLocations.index(name)]
itemValue = ItemList.index(itemName)
slot = BossDataList.index(name)
roomEvent = roomEventOffset
if roomEvent != None:
roomEvent = roomEvent + 3 + slot * 4
if itemValue < 3:
itemValue += 1
else:
itemValue -= 3
itemEvent = itemEventOffset
if itemEvent != None:
itemEvent = itemEvent + 1 + itemValue
patchedGame.seek(itemEvent)
itemValue = int.from_bytes(patchedGame.read(1), 'little')
patchedGame.seek(roomEvent)
patchedGame.write(itemValue.to_bytes(1, 'little'))
if name == 'Data S5':
roomEvent = roomEventOffset
roomEvent = roomEvent + 3 + (slot + 1) * 4
patchedGame.seek(roomEvent)
patchedGame.write(itemValue.to_bytes(1, 'little'))
if itemName in MajorItems:
offset = CreditsOffsets.get(itemName)
if 'Boss' in nodeType:
location = name
elif 'S0' in name:
location = 'Main Deck : '
else:
location = 'Sector {} : '.format(name[6:7])
if 'Tank' in nodeType:
location = location + name[8:]
else:
location = location + nodeType + ' Room'
spaces = ceiling(30 - len(location), 2)
location = ' ' * spaces + location
patchedGame.seek(offset)
patchedGame.write(location.encode('ascii'))
for x in range(len(location), 35):
patchedGame.write(0.to_bytes(1, 'little'))
if SeedSettings['HideItems']:
patchedGame.seek(3926048)
patchedGame.write(76.to_bytes(2, 'little'))
patchedGame.write(77.to_bytes(2, 'little'))
patchedGame.write(78.to_bytes(2, 'little'))
patchedGame.write(79.to_bytes(2, 'little'))
patchedGame.write(76.to_bytes(2, 'little'))
patchedGame.write(77.to_bytes(2, 'little'))
patchedGame.write(78.to_bytes(2, 'little'))
patchedGame.write(79.to_bytes(2, 'little'))
patchedGame.write(76.to_bytes(2, 'little'))
patchedGame.write(77.to_bytes(2, 'little'))
patchedGame.write(78.to_bytes(2, 'little'))
patchedGame.write(79.to_bytes(2, 'little'))
if SeedSettings['SplitSecurity'] == True:
security = securityOffset
patchedGame.seek(security)
patchedGame.write(0.to_bytes(2, 'little'))
if SeedSettings['MissilesWithoutMainData']:
patchedGame.seek(24828)
patchedGame.write(15.to_bytes(1, 'little'))
patchedGame.seek(395742)
patchedGame.write(15.to_bytes(1, 'little'))
patchedGame.seek(465582)
patchedGame.write(15.to_bytes(1, 'little'))
if SeedSettings['PowerBombsWithoutBombs']:
patchedGame.seek(24756)
patchedGame.write(32.to_bytes(1, 'little'))
patchedGame.seek(465672)
patchedGame.write(32.to_bytes(1, 'little'))
if SeedSettings['SectorShuffle'] == True:
for currentArea in range(7):
patchedGame.seek(7977108 + currentArea * 4, 0)
data = patchedGame.read(4)
unpacked = struct.unpack('<L', data)
doorData = unpacked[0] - 134217728
patchedGame.seek(doorData, 0)
doorNumber = 0
connectionType = int.from_bytes(patchedGame.read(1), 'little')
patchedGame.seek(5, 1)
offset = patchedGame.tell()
connectedDoor = int.from_bytes(patchedGame.read(1), 'little')
patchedGame.seek(5, 1)
resume = patchedGame.tell()
if connectionType == 0 and connectedDoor == 0:
continue
sourceDoor = 'S{}-{:02X}'.format(currentArea, doorNumber)
targetDoorStr = World.doorConnections.get(sourceDoor)
connectedArea = World.areaConnections.get(sourceDoor, currentArea)
connectedDoorStr = 'S{}-{:02X}'.format(connectedArea, connectedDoor)
if connectedDoorStr != targetDoorStr:
targetDoor = int(targetDoorStr[-2:], 16)
patchedGame.seek(offset)
patchedGame.write(targetDoor.to_bytes(1, 'little'))
patchedGame.seek(resume)
doorNumber += 1
for door, targetArea in World.areaConnections.items():
offset = World.areaConnectionOffsets.get(door)
patchedGame.seek(offset)
areaByte = int.from_bytes(patchedGame.read(1), 'little')
if areaByte != targetArea:
patchedGame.seek(offset)
patchedGame.write(targetArea.to_bytes(1, 'little'))
continue
for target in World.patcher:
value = World.patcher.get(target)
patchedGame.seek(target)
patchedGame.write(value.to_bytes(1, 'little'))
finally:
pass
if Patch:
# FIXME: replace with internal patch creator
os.system('.\\flips\\flips.exe --create --bps "{}" ".\\seeds\\{}.gba"'.format(BaseGame, FileName))
print('')
ItemNames = [
'Missile Data',
'Morph Ball',
'Charge Beam',
'Bombs',
'Hi-Jump Boots',
'Speed Booster',
'Super Missile Data',
'Varia Suit',
'Ice Missile Data',
'Wide Beam',
'Power Bomb Data',
'Space Jump',
'Plasma Beam',
'Gravity Suit',
'Diffusion Data',
'Wave Beam',
'Screw Attack',
'Ice Beam']
if SeedSettings['RaceSeed'] == False:
spoilerLog = dict()
spoilerLog.update({
'MFOR Version': version })
spoilerLog.update({
'Seed': SeedValue })
settingsDict = dict()
settingsDict.update({
'Difficulty': SeedSettings['Difficulty'] })
if SeedSettings['MajorMinor'] == False:
settingsDict.update({
'Item pool': 'Major items anywhere' })
else:
settingsDict.update({
'Item pool': 'Limited major item locations' })
settingsDict.update({
'Missile upgrades enable Missiles': SeedSettings['MissilesWithoutMainData'] })
settingsDict.update({
'Power Bombs without normal Bombs': SeedSettings['PowerBombsWithoutBombs'] })
settingsDict.update({
'Allow logical damage runs': SeedSettings['DamageRuns'] })
settingsDict.update({
'Separated security levels': SeedSettings['SplitSecurity'] })
settingsDict.update({
'Sector shuffle': SeedSettings['SectorShuffle'] })
if SeedSettings['SectorShuffle'] == True:
sectorLayout = str()
for x in areaLayout:
sectorLayout = sectorLayout.strip() + ' {}'.format(x)
settingsDict.update({
'Sector layout:': sectorLayout })
settingsDict.update({
'Hide item graphics': SeedSettings['HideItems'] })
settingsDict.update({
'E-Tanks': PlacedETanks })
settingsDict.update({
'Missile Tanks': PlacedMissiles })
settingsDict.update({
'Power Bomb Tanks': PlacedPowerBombs })
spoilerLog.update({
'Settings': settingsDict })
itemProgression = dict()
for x in range(len(PlacedItems)):
if PlacedItems[x] in MajorItems:
namedItem = ItemNames[MajorItems.index(PlacedItems[x])]
PlacedItems[x] = namedItem
itemProgression.update({
UsedLocations[x]: PlacedItems[x] })
continue
spoilerLog.update({
'Item order': itemProgression })
itemDict = dict()
World.itemLocations.sort()
for x in range(0, len(World.itemLocations)):
itemDict.update({
World.itemLocations[x]: 0 })
for x in range(0, len(PlacedItems)):
itemDict.update({
UsedLocations[x]: PlacedItems[x] })
spoilerLog.update({
'Items': itemDict })
with open(os.path.join('.', 'spoilers', '{}.json'.format(FileName)), 'w') as spoiler:
json.dump(spoilerLog, spoiler, indent=4)
finally:
pass
return None
def initialize(): def initialize():
global HashList, HashList global HashList, HashList, AreaOpen, Energy, WallJumpSetting, InfiniteBombJump, BackwardsLabs, MaxETanks, MaxMissiles, MaxPowerBombs, PlacedETanks, PlacedMissiles, PlacedPowerBombs, MissileDamage, MissileCount, MajorItems, MinorItems, BossDataList, ItemList, PlacedItems, BossLocations, AccessibleLocations, UsedLocations
HashList = list() HashList = list()
# WARNING: Decompyle incomplete with open(os.path.join('.', 'data', 'SeedHash.json')) as jsonFile:
HashList = json.load(jsonFile)
finally:
pass
HashList.sort()
AreaOpen = list()
for area in range(0, 7):
AreaOpen.append(None)
Energy = 0
WallJumpSetting = False
InfiniteBombJump = False
BackwardsLabs = False
MaxETanks = 20
MaxMissiles = 48
MaxPowerBombs = 32
PlacedETanks = PlacedMissiles = PlacedPowerBombs = 0
MissileDamage = 0
MissileCount = 10
MajorItems = [
'MainMissiles',
'MorphBall',
'ChargeBeam',
'Bombs',
'HiJumpBoots',
'SpeedBooster',
'SuperMissileItem',
'VariaSuit',
'IceMissileItem',
'WideBeam',
'MainPowerBombs',
'SpaceJump',
'PlasmaBeam',
'GravitySuit',
'DiffusionItem',
'WaveBeam',
'ScrewAttack',
'IceBeam']
MinorItems = [
'Energy Tank',
'Missile Tank',
'Power Bomb Tank']
BossDataList = [
'Data S0',
'Arachnus',
'Charge Core-X',
'Data S2',
'Zazabi',
'Serris',
'Data S3',
'Mega Core-X',
'Data S5',
'Data S5',
'Wide Core-X',
'Data S6',
'Yakuza',
'Nettori',
'Nightmare',
'Data S4',
'Box-2',
'Ridley',
'Omega Metroid']
ItemList = MinorItems + MajorItems
PlacedItems = list()
BossLocations = list()
AccessibleLocations = list()
UsedLocations = list()
return None
def start_randomizer(rom, settings): def start_randomizer(rom, settings):
global BaseGame, Debug, SeedValue, SeedValue, Patch, Patch, SeedSettings, SeedValue, Difficulty, DamageRuns, World, AreaItemLocations, RoomNodes global BaseGame, Debug, SeedValue, SeedValue, Patch, Patch, SeedSettings, SeedValue, Difficulty, DamageRuns, World, AreaItemLocations, RoomNodes, StartLocation
print('DEBUG: entered start_randomizer')
BaseGame = rom BaseGame = rom
print(BaseGame)
if BaseGame == None or BaseGame == '': if BaseGame == None or BaseGame == '':
print('Error: no base game provided.') sys.exit('Error: no base game provided.')
sys.exit(1)
checksum = fileHash(BaseGame) checksum = fileHash(BaseGame)
if checksum != 1819625372: if checksum != 1819625372:
print('Only Metroid Fusion (U) is supported. Check the CRC32 value: it should be 6C75479C') sys.exit('Only Metroid Fusion (U) is supported. Check the CRC32 value: it should be 6C75479C')
sys.exit(1)
Debug = settings['Debug'] Debug = settings['Debug']
settings.pop('Debug') settings.pop('Debug')
if Debug == False: if Debug == False:
checksum = fileHash(os.path.join('.', 'data', 'MFOR.bps')) checksum = fileHash(os.path.join('.', 'data', 'MFOR.bps'))
if checksum != 558161692: if checksum != 558161692:
print('Error: Base patch file has been modified. Please go to https://metroidconstruction.com/ and re-download MFOR.') sys.exit('Error: Base patch file has been modified. Please go to https://metroidconstruction.com/ and re-download MFOR.')
sys.exit(1)
totalRandoTime = time.time() totalRandoTime = time.time()
if settings['Seed']: if settings['Seed']:
SeedValue = str(settings['Seed']).strip(' \n') SeedValue = str(settings['Seed']).strip(' \n')
else: else:
SeedValue = str(random.randrange(sys.maxsize)) SeedValue = str(random.randrange(sys.maxsize))
print(SeedValue)
settings.pop('Seed') settings.pop('Seed')
repeat = 1 repeat = 1
if settings['Num'] and settings['Num'] > repeat: if settings['Num'] and settings['Num'] > repeat:
@ -4729,7 +5103,76 @@ def start_randomizer(rom, settings):
else: else:
Patch = False Patch = False
settings.pop('Patch') settings.pop('Patch')
patch_game() for loop in range(repeat):
initialize()
# WARNING: Decompyle incomplete SeedSettings = settings.copy()
if Debug:
print('Debug generation enabled')
if loop > 0:
SeedValue = str(random.randrange(sys.maxsize))
Difficulty = SeedSettings['Difficulty']
DamageRuns = SeedSettings['DamageRuns']
random.seed(SeedValue + str(SeedSettings))
World = Graph.Game(BaseGame)
World.RemoveNodeFromRoom('Room-S2-07', 'S2-10')
World.RemoveNodeFromRoom('Room-S2-0D', 'S2-1E')
World.RemoveNodeFromRoom('Room-S3-07', 'S3-10')
World.RemoveNodeFromRoom('Room-S3-12', 'S3-2B')
World.RemoveNodeFromRoom('Room-S5-15', 'S5-2A')
World.AddNodeToRoom('Room-S2-1F', 'S2-10')
World.AddNodeToRoom('Room-S2-2E', 'S2-1E')
World.AddNodeToRoom('Room-S3-16', 'S3-10')
World.AddNodeToRoom('Room-S3-17', 'S3-2B')
World.AddNodeToRoom('Room-S5-16', 'S5-2A')
World.UpdateDoorConnection('S0-4A', 'S0-61')
World.UpdateDoorConnection('S0-96', 'S0-22')
World.UpdateDoorConnection('S0-98', 'S0-20')
World.UpdateDoorConnection('S2-13', 'S2-45')
World.UpdateDoorConnection('S2-19', 'S2-2F')
World.UpdateDoorConnection('S2-1B', 'S2-68')
World.UpdateDoorConnection('S2-23', 'S2-69')
World.UpdateDoorConnection('S2-5F', 'S2-6A')
World.UpdateDoorConnection('S2-6E', 'S2-70')
World.UpdateDoorConnection('S3-2D', 'S3-2E')
World.UpdateDoorConnection('S5-28', 'S5-6C')
AreaItemLocations = list()
for area in range(7):
AreaItemLocations.append(list())
with open(os.path.join('.', 'data', 'NodeData.json')) as jsonFile:
RoomNodes = json.load(jsonFile)
finally:
pass
for area in RoomNodes:
areaIndex = list(RoomNodes.keys()).index(area)
for node in RoomNodes[area]:
name = node.get('Name')
room = node.get('Room')
item = node.get('Item')
nodeType = node.get('Type')
World.AddNodeToRoom('Room-S{}-{:02X}'.format(areaIndex, int(room, 16)), name)
if item == 'Missile' or item == 'Power Bomb':
World.add_to_minors(name)
elif nodeType == 'Boss':
World.add_to_majors(name)
BossLocations.append(name)
elif nodeType == 'Data' or item == 'E-Tank':
World.add_to_majors(name)
if name == 'Data S3':
BossLocations.append(name)
if not 'Item' in name and nodeType == 'Boss':
if nodeType == 'Data':
AreaItemLocations[areaIndex].append(name)
continue
continue
World.ConnectAllNodes()
StartLocation = 'S0-00'
startTime = time.time()
randomize_game(World)
seedTime = time.time() - startTime
print(str(FileName))
print('Randomized in:', seedTime)
continue
totalRandoTime = time.time() - totalRandoTime
print('All seeds took:', totalRandoTime)
return FileName