Compare commits

...

12 commits

Author SHA1 Message Date
2a73e487a5 impromptu cli bugfix 2024-09-04 02:17:32 +02:00
1523860c9a Merge from 2024.02.10+chozo1 into master
Reviewed-on: #1
2024-09-03 20:30:48 +00:00
a36c9a95f0 update readme 2024-09-03 12:20:14 +02:00
fe6fa75972 fix boss item placement + update flips submodule 2024-09-03 11:49:03 +02:00
672cf3ca23 upload last edits 2024-09-03 01:47:49 +02:00
01dee434bd wip: fix boss/dataroom placement act2 2024-09-03 00:23:28 +02:00
367fdd75ec wip: fix boss/dataroom placement? 2024-09-02 23:33:35 +02:00
0c17440bde fix icon 2024-09-02 22:51:09 +02:00
dc597e2924 fix Windows build + stuff 2024-09-02 22:25:36 +02:00
49daaf1da8 Freestyler, rock the microphone
Straight from the top of my dome
Freestyler, rock the microphone
Carry on with the freestyler

I got to throw on and go on
You know I got to flow on, selectors on ya radio play us
'Cause we're friendly for ozone, but that's not all so hold on
Tight as I rock the mic right, oh, excuse me, pardon
As I synchronize with the analysed upcoming vibes
The session, let there be a lesson, question
You carry protection or will your heart go on
Like Celine Dion, Karma Chameleon
2024-09-02 21:48:35 +02:00
0c9d1a472f wip: initial disassembly of new gui and cli commands 2024-08-31 01:23:57 +02:00
b2bcbb3027 update README 2024-08-29 00:21:03 +02:00
9 changed files with 1371 additions and 453 deletions

673
Fusion_Palette_Shuffle.py Normal file
View file

@ -0,0 +1,673 @@
# Source Generated with Decompyle++
# File: Fusion_Palette_Shuffle.pyc (Python 3.8)
import struct
import random
import sys
from pathlib import Path
from MFOR import file_hash
tileset_fixes = {
4640526: 575,
4640528: 735,
4640530: 863,
4640558: 575,
4640560: 735,
4640562: 863,
4640592: 575,
4640594: 735,
4640626: 575
}
static_tilesets = [
[4219100, 448],
[4223388, 448],
[4224028, 448],
[4225244, 448],
[4226268, 448],
[4226716, 448],
[4632980, 448],
[4640084, 448],
[4640788, 448],
[4641236, 448],
[4641684, 448],
[4642132, 448],
[4643124, 448],
[4643572, 448],
[4644020, 448],
[4644468, 448],
[4645172, 192],
[4645620, 448],
[4646068, 448],
[4646644, 448],
[4648820, 448],
[4649268, 448],
[4818056, 448],
[4818504, 448],
[4818952, 448],
[4819656, 448],
[4820104, 128],
[5031360, 448],
[5031808, 448],
[5032256, 448],
[5032704, 448],
[5033152, 448],
[5033600, 448],
[5034048, 448],
[5186040, 448],
[5186488, 448],
[5186936, 448],
[5187512, 448],
[5188184, 448],
[5310568, 448],
[5311016, 448],
[5311464, 352],
[5460880, 288],
[5461456, 448],
[5461904, 448],
[5462800, 448],
[5558492, 448],
[5559164, 448],
[5559612, 448]
]
tileset_single_pass = {
4641812: 32,
4647188: 32,
4819000: 8,
4819016: 8,
4819048: 8,
4819704: 8,
4819720: 8,
4819752: 8,
5186568: 5,
5186584: 7,
5186604: 6,
5186640: 4,
5186704: 4,
5186732: 6,
5186860: 6,
5311144: 32,
5460752: 32,
5460784: 32,
5460816: 32,
5460848: 32,
5462576: 32,
5462864: 32,
5462960: 16,
5463088: 16
}
animated_tilesets = [
[4223836, 192],
[4224476, 192],
[4633428, 320],
[4636308, 256],
[4640532, 256],
[4645364, 64],
[5187960, 224],
[5188632, 256],
[4819400, 256],
[4820232, 64],
[5558940, 224],
[5461168, 288],
[4644916, 256],
[5187384, 128],
[5185272, 160],
[5185880, 160],
[4646516, 128],
[4635092, 320],
[4642580, 256],
[4642836, 288],
[5311816, 64]
]
paired_tilesets = [
[
[3930000, 192], [4633748, 448]
],
[
[3280920, 96], [3561816, 32], [4218524, 448], [4218972, 128]
],
[
[3109872, 96], [4222236, 448], [4222684, 128]
],
[
[3297776, 96], [4222812, 448], [4223260, 128]
],
[
[3267440, 96], [4224668, 448], [4225116, 128]
],
[
[3084920, 96], [4225692, 448], [4226140, 128]
],
[
[3456412, 64], [4632340, 448], [4632788, 192], [4647540, 448], [4647988, 128], [4648116, 448], [4648564, 256]
],
[
[4647092, 448], [3451140, 32]
],
[
[4634644, 448], [4635412, 448]
],
[
[4219548, 448], [4635860, 448], [4636308, 256]
],
[
[4219996, 448], [4636564, 448], [4637012, 256]
],
[
[4220444, 448], [4637268, 448], [4637716, 256]
],
[
[4220892, 448], [4637972, 448], [4638420, 256]
],
[
[4221340, 448], [4638676, 448], [4639124, 256]
],
[
[4221788, 448], [4639380, 448], [4639828, 256]
],
[
[5184824, 448], [5185432, 448]
],
[
[3447972, 64], [5462352, 448]
]
]
sprite_fixes = {
3468542: 8266,
3468544: 7178,
3499380: 142,
3499406: 523,
3558744: 3264,
3558758: 14
}
sprites = [
[2835304, 128],
[3042180, 32],
[3058284, 96],
[3066148, 64],
[3070696, 32],
[3076896, 96],
[3103328, 64],
[3113676, 32],
[3117204, 32],
[3124184, 96],
[3130896, 64],
[3150104, 256],
[3169028, 160],
[3181768, 160],
[3192168, 160],
[3202568, 160],
[3212968, 160],
[3223368, 160],
[3231720, 128],
[3241064, 64],
[3245764, 64],
[3252504, 64],
[3254656, 32],
[3257752, 32],
[3260912, 32],
[3273680, 64],
[3288044, 64],
[3305456, 96],
[3310352, 32],
[3321092, 160],
[3332584, 160],
[3342984, 160],
[3353384, 160],
[3368040, 64],
[3373196, 64],
[3377084, 32],
[3384092, 64],
[3390996, 32],
[3391028, 32],
[3398460, 96],
[3403768, 32],
[3406232, 32],
[3411720, 64],
[3436688, 256],
[3443020, 32],
[3462012, 64],
[3468532, 64],
[3473740, 64],
[3479228, 64],
[3485468, 96],
[3493060, 96],
[3499376, 64],
[3503052, 32],
[3505876, 32],
[3508068, 32],
[3512272, 32],
[3519012, 96],
[3529712, 96],
[3534488, 64],
[3537920, 32],
[3540236, 32],
[3544716, 64],
[3547808, 32],
[3552044, 64],
[3554544, 32],
[3558732, 64],
[3578836, 256],
[3581056, 32],
[3581088, 32],
[3581120, 96],
[3611928, 256],
[3641296, 32],
[3643992, 32],
[3646200, 32],
[3652548, 64],
[3657176, 32],
[3659632, 32],
[3662052, 32],
[3664548, 32],
[3667884, 32],
[3672824, 64],
[3684520, 128],
[3690908, 32],
[3693436, 32],
[3695788, 32],
[3700504, 32],
[3755348, 224],
[3772744, 160],
[3798160, 32],
[3822584, 256],
[3829592, 64],
[3837700, 32],
[3887568, 256],
[3917260, 256]
]
paired_sprites = [
[
[5865648, 64], [5865776, 32], [6368840, 32], [6369096, 64], [6369224, 32]
],
[
[3098012, 192], [3794264, 224], [6515960, 96], [6516536, 32]
],
[
[3634936, 256], [3639120, 128]
],
[
[3803008, 64], [3858444, 256], [3868728, 64]
],
[
[2835432, 32], [3046752, 64], [3051992, 64], [3723188, 256]
],
[
[3363784, 160], [3835340, 32]
],
[
[4634196, 448], [6516792, 512]
]
]
beam_fixes = {
5813384: 8031,
5813388: 8472,
5813480: 24480
}
beams = [
[5813348, 32],
[5813380, 32],
[5813412, 32],
[5813444, 32],
[5813476, 32]
]
paired_beams = [
[
[5813508, 32], [4079900, 10], [4079996, 32]
]
]
suit_fixes = {
2678144: 32585,
2678150: 16643,
2678154: 799,
2678164: 501,
2678180: 6730,
2678182: 328,
2678186: 23070,
2678202: 10406,
2678208: 28311,
2678228: 648,
2678234: 10406,
2678246: 201
}
suit_misc = [
[2678268, 32],
[2678332, 32],
[2678364, 32],
[2678396, 32],
[2678428, 32],
[2678460, 32],
[2679036, 32],
[2679068, 32],
[2679868, 32],
[2679900, 32],
[2679932, 32],
[2679964, 32],
[2679996, 32],
[2680028, 32],
[2680060, 32],
[2680092, 32],
[2680124, 32],
[2680156, 32]
]
suits = [
[
[2678140, 14], [2678300, 14], [2678492, 14], [2679100, 14], [2679132, 14], [2679164, 14], [2679196, 14], [2679228, 14], [2679260, 14], [2679292, 14], [2679324, 14], [2679356, 14], [2679388, 14], [2679420, 14], [2679452, 14], [2679484, 14], [2679516, 14], [2679548, 14], [2679580, 14], [2679612, 14], [2679644, 14], [2679676, 14], [2679708, 14], [2679740, 14], [2679772, 14], [2679804, 14], [2679836, 14], [2680188, 14], [2680220, 14], [2680252, 14], [2680284, 14], [2680316, 14], [2681596, 14], [2681628, 14], [2681660, 14], [2681692, 14], [2682108, 14], [2682140, 14], [2682172, 14], [2682204, 14], [2682620, 14], [2682652, 14], [2682684, 14], [2682716, 14], [2683132, 14], [2683164, 14], [2683196, 14], [2683228, 14], [2683644, 14], [2683676, 14], [2683708, 14], [2683740, 14], [2684156, 14], [2684188, 14], [2684220, 14], [2684252, 14], [2684668, 14], [2684700, 14], [2684732, 14], [2684764, 14]
],
[
[2678172, 14], [2680348, 14], [2680380, 14], [2680412, 14], [2680444, 14], [2680476, 14], [2681724, 14], [2681756, 14], [2681788, 14], [2681820, 14], [2682236, 14], [2682268, 14], [2682300, 14], [2682332, 14], [2682748, 14], [2682780, 14], [2682812, 14], [2682844, 14], [2683260, 14], [2683292, 14], [2683324, 14], [2683356, 14], [2683772, 14], [2683804, 14], [2683836, 14], [2683868, 14], [2684284, 14], [2684316, 14], [2684348, 14], [2684380, 14], [2684796, 14], [2684828, 14], [2684860, 14], [2684892, 14]
],
[
[2678204, 14], [2680508, 14], [2680540, 14], [2680572, 14], [2680604, 14], [2680636, 14], [2681852, 14], [2681884, 14], [2681916, 14], [2681948, 14], [2682364, 14], [2682396, 14], [2682428, 14], [2682460, 14], [2682876, 14], [2682908, 14], [2682940, 14], [2682972, 14], [2683388, 14], [2683420, 14], [2683452, 14], [2683484, 14], [2683900, 14], [2683932, 14], [2683964, 14], [2683996, 14], [2684412, 14], [2684444, 14], [2684476, 14], [2684508, 14], [2684924, 14], [2684956, 14], [2684988, 14], [2685020, 14]
],
[
[2678236, 14], [2680988, 14], [2681020, 14], [2681052, 14], [2681084, 14], [2681116, 14], [2681980, 14], [2682012, 14], [2682044, 14], [2682076, 14], [2682492, 14], [2682524, 14], [2682556, 14], [2682588, 14], [2683004, 14], [2683036, 14], [2683068, 14], [2683100, 14], [2683516, 14], [2683548, 14], [2683580, 14], [2683612, 14], [2684028, 14], [2684060, 14], [2684092, 14], [2684124, 14], [2684540, 14], [2684572, 14], [2684604, 14], [2684636, 14], [2685052, 14], [2685084, 14], [2685116, 14], [2685148, 14]
]
]
skip_colors = {
4218620: 16,
4218716: 16,
4640148: 8,
4647764: 16,
4649428: 8,
5031520: 16,
5186680: 12,
5187704: 11,
5310686: 5,
5310696: 8,
5310760: 16,
5311112: 11,
5311176: 16,
5311528: 16,
5461634: 1
}
base_color_list = [
['blue', 'blue', 'green'],
['blue', 'blue', 'red'],
['blue', 'blue', 'None'],
['blue', 'green', 'blue'],
['blue', 'green', 'green'],
['blue', 'green', 'None'],
['blue', 'red', 'blue'],
['blue', 'red', 'green'],
['blue', 'red', 'red'],
['blue', 'red', 'None'],
['blue', 'None', 'blue'],
['blue', 'None', 'green'],
['blue', 'None', 'red'],
['green', 'blue', 'blue'],
['green', 'blue', 'green'],
['green', 'blue', 'red'],
['green', 'blue', 'None'],
['green', 'green', 'blue'],
['green', 'green', 'red'],
['green', 'green', 'None'],
['green', 'red', 'blue'],
['green', 'red', 'green'],
['green', 'red', 'red'],
['green', 'red', 'None'],
['green', 'None', 'blue'],
['green', 'None', 'green'],
['green', 'None', 'red'],
['red', 'blue', 'blue'],
['red', 'blue', 'green'],
['red', 'blue', 'red'],
['red', 'blue', 'None'],
['red', 'green', 'blue'],
['red', 'green', 'green'],
['red', 'green', 'red'],
['red', 'green', 'None'],
['red', 'red', 'blue'],
['red', 'red', 'green'],
['red', 'red', 'None'],
['red', 'None', 'blue'],
['red', 'None', 'green'],
['red', 'None', 'red'],
['None', 'blue', 'blue'],
['None', 'blue', 'green'],
['None', 'blue', 'red'],
['None', 'green', 'blue'],
['None', 'green', 'green'],
['None', 'green', 'red'],
['None', 'red', 'blue'],
['None', 'red', 'green'],
['None', 'red', 'red'],
['green', 'green', 'green']
]
def modify_color(color, arrangement, tileset=False):
if color == 0 or color == 32767:
return color
blue = (color & 31744) >> 10
green = (color & 992) >> 5
red = color & 31
if arrangement[0] == "blue":
high = blue << 10
elif arrangement[0] == "green":
high = green << 10
elif arrangement[0] == "red":
high = red << 10
else:
high = 0
if arrangement[1] == "blue":
mid = blue << 5
elif arrangement[1] == "green":
mid = green << 5
elif arrangement[1] == "red":
mid = red << 5
else:
mid = 0
if arrangement[2] == "blue":
low = blue
elif arrangement[2] == "green":
low = green
elif arrangement[2] == "red":
low = red
else:
low = 0
return high | mid | low
def get_color_slot():
color_list = base_color_list.copy()
old_color = None
while True:
if len(color_list) < 1:
color_list = base_color_list.copy()
new_color = color_list.pop(random.randrange(len(color_list)))
if new_color == old_color:
print("Repeat color")
new_color = color_list.pop(random.randrange(len(color_list)))
old_color = new_color
yield new_color
def randomize_palettes(data, seed=None, shuffle_tilesets=True, shuffle_sprites=True, shuffle_suits=True, shuffle_beams=True):
if seed is None:
random.seed()
else:
random.seed(seed)
if shuffle_tilesets:
for (offset, length) in static_tilesets:
skip = 0
color_slot = next(get_color_slot())
fixed_color_count = 0
fixed_color_slot = next(get_color_slot())
for x in range(0, length, 2):
current_offset = offset + x
if x % 32 == 0:
color_slot = next(get_color_slot())
if current_offset in skip_colors.keys():
skip = skip_colors.get(current_offset)
if skip > 0:
skip = skip - 1
else:
if current_offset in tileset_fixes.keys():
half_word = tileset_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
if current_offset in tileset_single_pass.keys():
fixed_color_count = tileset_single_pass.get(current_offset)
if fixed_color_count > 0:
half_word = modify_color(half_word, fixed_color_slot)
fixed_color_count = fixed_color_count - 1
else:
half_word = modify_color(half_word, color_slot, True)
struct.pack_into("<H", data, current_offset, half_word)
for (offset, length) in animated_tilesets:
skip = 0
color_slot = next(get_color_slot())
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in skip_colors.keys():
skip = skip_colors.get(current_offset)
if skip > 0:
skip = skip - 1
else:
if current_offset in tileset_fixes.keys():
half_word = tileset_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
for group in paired_tilesets:
color_slot = next(get_color_slot())
for (offset, length) in group:
skip = 0
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in skip_colors.keys():
skip = skip_colors.get(current_offset)
if skip > 0:
skip = skip - 1
else:
if current_offset in tileset_fixes.keys():
half_word = tileset_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot, True)
struct.pack_into("<H", data, current_offset, half_word)
if shuffle_sprites:
for (offset, length) in sprites:
skip = 0
color_slot = next(get_color_slot())
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in skip_colors.keys():
skip = skip_colors.get(current_offset)
if skip > 0:
skip = skip - 1
else:
if current_offset in sprite_fixes.keys():
half_word = sprite_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
for group in paired_sprites:
skip = 0
color_slot = next(get_color_slot())
for (offset, length) in group:
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in skip_colors.keys():
skip = skip_colors.get(current_offset)
if skip > 0:
skip = skip - 1
else:
if current_offset in sprite_fixes.keys():
half_word = sprite_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
if shuffle_suits:
for (offset, length) in suit_misc:
color_slot = next(get_color_slot())
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in suit_fixes.keys():
half_word = suit_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
for group in suits:
color_slot = next(get_color_slot())
for (offset, length) in group:
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in suit_fixes.keys():
half_word = suit_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
if shuffle_beams:
for (offset, length) in beams:
color_slot = next(get_color_slot())
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in beam_fixes.keys():
half_word = beam_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
for group in paired_beams:
color_slot = next(get_color_slot())
for (offset, length) in group:
for x in range(0, length, 2):
current_offset = offset + x
if current_offset in beam_fixes.keys():
half_word = beam_fixes.get(current_offset)
else:
half_word = struct.unpack_from("<H", data, current_offset)[0]
half_word = modify_color(half_word, color_slot)
struct.pack_into("<H", data, current_offset, half_word)
if __name__ == "__main__":
if len(sys.argv) > 1:
filename = sys.argv[1]
if len(sys.argv) > 2:
seed_arg = sys.argv[2]
else:
seed_arg = None
try:
checksum = file_hash(filename)
if checksum != 1819625372:
sys.exit("Only Metroid Fusion (U) is supported. Check the CRC32 value: it should be 6C75479C")
game = bytearray(Path(filename).read_bytes())
randomize_palettes(game, seed_arg)
filename = filename.replace(".gba", " Colorized.gba")
Path(filename).write_bytes(game)
except FileNotFoundError:
print(f"Could not open file {filename}")
except PermissionError:
print(f"Could not write file {filename}, it may be write protected.")
else:
exit("Usage: 'Metroid Fusion(U).gba' <optional seed>")

413
GUI.py
View file

@ -10,299 +10,276 @@
# Source Generated with Decompyle++
# File: GUI.pyc (Python 3.8)
import PySimpleGUI as ui
import zlib
import time
import PySimpleGUI as sg
import threading
import os
ui.theme('SystemDefault')
from pathlib import Path
from MFOR import file_hash
sg.theme('SystemDefault')
tt_race = ' Generating a race seed will not generate a spoiler log. '
tt_createpatch = ' Automatically creates a .bps patch after generating a randomized game, so you can easily share with others. '
tt_difficulty = ' Higher difficulty means harder tricks. Difficulty 0 means no tricks are expected outside of vanilla strategies. '
tt_itempool = ' Limit where important items can be placed. If checked, major upgrades will only be in Data rooms, \n at bosses, or at vanilla E-tank locations. If unchecked, any item can be anywhere within logic. '
tt_missileswithupgrades = ' If checked, collecting any major Missile item will enable Missiles. Logic will account for this setting. '
tt_pbswithoutbombs = ' If checked, Power Bombs can be used without having regular Bombs. Logic will account for this setting. '
tt_damageruns = ' Allow logic to require Samus to run through damaging environments to acquire important items. \n Examples include heated rooms, electrified water, and lava. '
tt_splitsecurity = ' If checked, each door color must be unlocked independently. \n If unchecked, unlocking higher security levels will unlock all security levels below it. \n E.G.: unlocking Level 2 (Green) doors will also unlock Level 1 (Blue) doors. '
tt_SectorShuffle = ' Randomly shuffle the arrangement of the Sectors. '
tt_HideItems = ' Make all items appear as ? tanks. '
tt_patch = ' Create a .bps patch after generating a game, so you can easily share with others. '
tt_difficulty = ' Higher difficulty means harder tricks can be required.\n Difficulty 0 means no tricks are expected outside of vanilla strategies. '
tt_major_minor = ' Limit where important items can be placed. If checked, major items will only be in Data rooms, \n at bosses, or at vanilla E-tank locations. If unchecked, any item can be anywhere within logic. '
tt_missile_data = ' If checked, collecting any Missile data will enable Missiles. Logic will account for this setting. '
tt_pb_data = ' If checked, Power Bombs can be used without having regular Bombs. Logic will account for this setting. '
tt_hazard_runs = ' Allow logic to require Samus to run through hazardous environments to acquire important items. \n Examples include heated rooms, electrified water, and lava. '
tt_split_security = ' If checked, each security level must be unlocked independently. \n If unchecked, unlocking higher security levels will unlock all lesser security levels. \n E.G.: unlocking Level 2 (Green) doors will also unlock Level 1 (Blue) doors. '
tt_shuffle_sectors = ' Randomly shuffle the arrangement of the Sectors. '
tt_shuffle_tubes = ' Randomly shuffle the tube connections between the Sectors. '
tt_hide_items = ' Make all items appear as ? tanks. '
generating = False
def fileHash(fileName):
with open(fileName, 'rb') as fh:
hash = 0
while True:
s = fh.read(65536)
if not s:
break
hash = zlib.crc32(s, hash)
return hash & 0xFFFFFFFF
def rando_thread(window = None, values = None):
global fileName, basegame, failedgen, randoerror, generating, finishGenerating
global failed_gen, rando_error, generating, file_name, failed_gen, generating, finish_generating
values.update({
'Difficulty': int(values.get('Difficulty')) })
if values['Num']:
'-DIFFICULTY-': int(values.get('-DIFFICULTY-')) })
if values['-NUM-']:
values.update({
'Num': int(values.get('Num')) })
'-NUM-': int(values.get('-NUM-')) })
else:
values.update({
'Num': 1 })
if values['Num'] < 1:
'-NUM-': 1 })
if values['-NUM-'] < 1:
values.update({
'Num': 1 })
'-NUM-': 1 })
values.pop(0)
failedgen = False
randoerror = False
# FIXME: whatever this is
# values = (lambda .0 = None: pass# WARNING: Decompyle incomplete
#)(sorted(values.keys()))
failed_gen = False
rando_error = False
generating = True
from Randomizer import start_randomizer
if values['Debug']:
fileName = start_randomizer(basegame, values)
file_name = start_randomizer(base_game, values)
else:
# FIXME: let it explode
#try:
fileName = start_randomizer(basegame, values)
# finally:
# pass
#except SystemExit:
# failedgen = True
#except:
# print('An error occurred, no game was generated.')
# randoerror = True
try:
file_name = start_randomizer(base_game, values)
except SystemExit:
failed_gen = True
generating = False
finishGenerating = True
finish_generating = True
def main_window(debug):
global fileName, basegame, generating, finishGenerating
itemPool = [
global file_name, finish_generating, base_game, finish_generating
item_pool = [
[
ui.Text('Charge Beam'),
ui.Radio('Shuffled', 'radioChargeBeam', default=True),
ui.Radio('Starting', 'radioChargeBeam'),
ui.Radio('Disabled', 'radioChargeBeam')],
sg.Text('Charge Beam'),
sg.Radio('Shuffled', 'radioChargeBeam', default=True),
sg.Radio('Starting', 'radioChargeBeam'),
sg.Radio('Disabled', 'radioChargeBeam')],
[
ui.Text('Wide Beam'),
ui.Radio('Shuffled', 'radioWideBeam', default=True),
ui.Radio('Starting', 'radioWideBeam'),
ui.Radio('Disabled', 'radioWideBeam')],
sg.Text('Wide Beam'),
sg.Radio('Shuffled', 'radioWideBeam', default=True),
sg.Radio('Starting', 'radioWideBeam'),
sg.Radio('Disabled', 'radioWideBeam')],
[
ui.Text('Plasma Beam'),
ui.Radio('Shuffled', 'radioPlasmaBeam', default=True),
ui.Radio('Starting', 'radioPlasmaBeam'),
ui.Radio('Disabled', 'radioPlasmaBeam')],
sg.Text('Plasma Beam'),
sg.Radio('Shuffled', 'radioPlasmaBeam', default=True),
sg.Radio('Starting', 'radioPlasmaBeam'),
sg.Radio('Disabled', 'radioPlasmaBeam')],
[
ui.Text('Wave Beam'),
ui.Radio('Shuffled', 'radioWaveBeam', default=True),
ui.Radio('Starting', 'radioWaveBeam'),
ui.Radio('Disabled', 'radioWaveBeam')],
sg.Text('Wave Beam'),
sg.Radio('Shuffled', 'radioWaveBeam', default=True),
sg.Radio('Starting', 'radioWaveBeam'),
sg.Radio('Disabled', 'radioWaveBeam')],
[
ui.Text('Ice Beam'),
ui.Radio('Shuffled', 'radioIceBeam', default=True),
ui.Radio('Starting', 'radioIceBeam'),
ui.Radio('Disabled', 'radioIceBeam')],
sg.Text('Ice Beam'),
sg.Radio('Shuffled', 'radioIceBeam', default=True),
sg.Radio('Starting', 'radioIceBeam'),
sg.Radio('Disabled', 'radioIceBeam')],
[
ui.Text('Missile Data'),
ui.Radio('Shuffled', 'radioMissileData', default=True),
ui.Radio('Starting', 'radioMissileData'),
ui.Radio('Disabled', 'radioMissileData')],
sg.Text('Missile Data'),
sg.Radio('Shuffled', 'radioMissileData', default=True),
sg.Radio('Starting', 'radioMissileData'),
sg.Radio('Disabled', 'radioMissileData')],
[
ui.Text('Super Missile Data'),
ui.Radio('Shuffled', 'radioSuperMissile', default=True),
ui.Radio('Starting', 'radioSuperMissile'),
ui.Radio('Disabled', 'radioSuperMissile')],
sg.Text('Super Missile Data'),
sg.Radio('Shuffled', 'radioSuperMissile', default=True),
sg.Radio('Starting', 'radioSuperMissile'),
sg.Radio('Disabled', 'radioSuperMissile')],
[
ui.Text('Ice Missile Data'),
ui.Radio('Shuffled', 'radioIceMissile', default=True),
ui.Radio('Starting', 'radioIceMissile'),
ui.Radio('Disabled', 'radioIceMissile')],
sg.Text('Ice Missile Data'),
sg.Radio('Shuffled', 'radioIceMissile', default=True),
sg.Radio('Starting', 'radioIceMissile'),
sg.Radio('Disabled', 'radioIceMissile')],
[
ui.Text('Diffusion Data'),
ui.Radio('Shuffled', 'radioDiffusion', default=True),
ui.Radio('Starting', 'radioDiffusion'),
ui.Radio('Disabled', 'radioDiffusion')],
sg.Text('Diffusion Data'),
sg.Radio('Shuffled', 'radioDiffusion', default=True),
sg.Radio('Starting', 'radioDiffusion'),
sg.Radio('Disabled', 'radioDiffusion')],
[
ui.Text('Bombs'),
ui.Radio('Shuffled', 'radioBombs', default=True),
ui.Radio('Starting', 'radioBombs'),
ui.Radio('Disabled', 'radioBombs')],
sg.Text('Bombs'),
sg.Radio('Shuffled', 'radioBombs', default=True),
sg.Radio('Starting', 'radioBombs'),
sg.Radio('Disabled', 'radioBombs')],
[
ui.Text('Power Bomb Data'),
ui.Radio('Shuffled', 'radioPowerBombData', default=True),
ui.Radio('Starting', 'radioPowerBombData'),
ui.Radio('Disabled', 'radioPowerBombData')],
sg.Text('Power Bomb Data'),
sg.Radio('Shuffled', 'radioPowerBombData', default=True),
sg.Radio('Starting', 'radioPowerBombData'),
sg.Radio('Disabled', 'radioPowerBombData')],
[
ui.Text('Hi-Jump Boots'),
ui.Radio('Shuffled', 'radioHiJump', default=True),
ui.Radio('Starting', 'radioHiJump'),
ui.Radio('Disabled', 'radioHiJump')],
sg.Text('Hi-Jump Boots'),
sg.Radio('Shuffled', 'radioHiJump', default=True),
sg.Radio('Starting', 'radioHiJump'),
sg.Radio('Disabled', 'radioHiJump')],
[
ui.Text('Speed Booster'),
ui.Radio('Shuffled', 'radioSpeedBooster', default=True),
ui.Radio('Starting', 'radioSpeedBooster'),
ui.Radio('Disabled', 'radioSpeedBooster')],
sg.Text('Speed Booster'),
sg.Radio('Shuffled', 'radioSpeedBooster', default=True),
sg.Radio('Starting', 'radioSpeedBooster'),
sg.Radio('Disabled', 'radioSpeedBooster')],
[
ui.Text('Space Jump'),
ui.Radio('Shuffled', 'radioSpaceJump', default=True),
ui.Radio('Starting', 'radioSpaceJump'),
ui.Radio('Disabled', 'radioSpaceJump')],
sg.Text('Space Jump'),
sg.Radio('Shuffled', 'radioSpaceJump', default=True),
sg.Radio('Starting', 'radioSpaceJump'),
sg.Radio('Disabled', 'radioSpaceJump')],
[
ui.Text('Screw Attack'),
ui.Radio('Shuffled', 'radioScrewAttack', default=True),
ui.Radio('Starting', 'radioScrewAttack'),
ui.Radio('Disabled', 'radioScrewAttack')],
sg.Text('Screw Attack'),
sg.Radio('Shuffled', 'radioScrewAttack', default=True),
sg.Radio('Starting', 'radioScrewAttack'),
sg.Radio('Disabled', 'radioScrewAttack')],
[
ui.Text('Varia Suit'),
ui.Radio('Shuffled', 'radioVariaSuit', default=True),
ui.Radio('Starting', 'radioVariaSuit'),
ui.Radio('Disabled', 'radioVariaSuit')],
sg.Text('Varia Suit'),
sg.Radio('Shuffled', 'radioVariaSuit', default=True),
sg.Radio('Starting', 'radioVariaSuit'),
sg.Radio('Disabled', 'radioVariaSuit')],
[
ui.Text('Gravity Suit'),
ui.Radio('Shuffled', 'radioGravitySuit', default=True),
ui.Radio('Starting', 'radioGravitySuit'),
ui.Radio('Disabled', 'radioGravitySuit')],
sg.Text('Gravity Suit'),
sg.Radio('Shuffled', 'radioGravitySuit', default=True),
sg.Radio('Starting', 'radioGravitySuit'),
sg.Radio('Disabled', 'radioGravitySuit')],
[
ui.Text('Morph Ball'),
ui.Radio('Shuffled', 'radioMorphBall', default=True),
ui.Radio('Starting', 'radioMorphBall'),
ui.Radio('Disabled', 'radioMorphBall')],
sg.Text('Morph Ball'),
sg.Radio('Shuffled', 'radioMorphBall', default=True),
sg.Radio('Starting', 'radioMorphBall'),
sg.Radio('Disabled', 'radioMorphBall')],
[
ui.Text('Blue Doors (Level 1)'),
ui.Radio('Standard', 'radioBlueDoors', default=True),
ui.Radio('Starting', 'radioBlueDoors'),
ui.Radio('Shuffled', 'radioBlueDoors')],
sg.Text('Blue Doors (Level 1)'),
sg.Radio('Standard', 'radioBlueDoors', default=True),
sg.Radio('Starting', 'radioBlueDoors'),
sg.Radio('Shuffled', 'radioBlueDoors')],
[
ui.Text('Green Doors (Level 2)'),
ui.Radio('Standard', 'radioGreenDoors', default=True),
ui.Radio('Starting', 'radioGreenDoors'),
ui.Radio('Shuffled', 'radioGreenDoors')],
sg.Text('Green Doors (Level 2)'),
sg.Radio('Standard', 'radioGreenDoors', default=True),
sg.Radio('Starting', 'radioGreenDoors'),
sg.Radio('Shuffled', 'radioGreenDoors')],
[
ui.Text('Yellow Doors (Level 3)'),
ui.Radio('Standard', 'radioYellowDoors', default=True),
ui.Radio('Starting', 'radioYellowDoors'),
ui.Radio('Shuffled', 'radioYellowDoors')],
sg.Text('Yellow Doors (Level 3)'),
sg.Radio('Standard', 'radioYellowDoors', default=True),
sg.Radio('Starting', 'radioYellowDoors'),
sg.Radio('Shuffled', 'radioYellowDoors')],
[
ui.Text('Red Doors (Level 4)'),
ui.Radio('Standard', 'radioRedDoors', default=True),
ui.Radio('Starting', 'radioRedDoors'),
ui.Radio('Shuffled', 'radioRedDoors')]]
sg.Text('Red Doors (Level 4)'),
sg.Radio('Standard', 'radioRedDoors', default=True),
sg.Radio('Starting', 'radioRedDoors'),
sg.Radio('Shuffled', 'radioRedDoors')]]
settings = [
[
ui.Text('Difficulty'),
ui.Slider(range=(0, 5), orientation='h', tooltip=tt_difficulty, key='Difficulty')],
sg.Text('Difficulty'),
sg.Slider(range=(0, 5), orientation='h', tooltip=tt_difficulty, key='-DIFFICULTY-')],
[
ui.Checkbox(text='Major/minor item split', key='MajorMinor', tooltip=tt_itempool)],
sg.Checkbox(text='Generate race seed', key='-RACE-', tooltip=tt_race)],
[
ui.Checkbox(text='Missile upgrades enable Missiles', key='MissilesWithoutMainData', tooltip=tt_missileswithupgrades)],
sg.Checkbox(text='Hide item graphics', key='-HIDEITEMS-', tooltip=tt_hide_items)],
[
ui.Checkbox(text='Enable Power Bombs without Bombs', key='PowerBombsWithoutBombs', tooltip=tt_pbswithoutbombs)],
sg.Checkbox(text='Major/minor item split', key='-MAJORMINOR-', tooltip=tt_major_minor)],
[
ui.Checkbox(text='Damage runs', key='DamageRuns', tooltip=tt_damageruns)],
sg.Checkbox(text='Missile upgrades enable Missiles', key='-MISSILEDATA-', tooltip=tt_missile_data)],
[
ui.Checkbox(text='Split security levels', key='SplitSecurity', tooltip=tt_splitsecurity)],
sg.Checkbox(text='Enable Power Bombs without Bombs', key='-PBDATA-', tooltip=tt_pb_data)],
[
ui.Checkbox(text='Sector Shuffle', key='SectorShuffle', tooltip=tt_SectorShuffle)],
sg.Checkbox(text='Hazard runs', key='-HAZARDRUNS-', tooltip=tt_hazard_runs)],
[
ui.Checkbox(text='Hide item graphics', key='HideItems', tooltip=tt_HideItems)],
sg.Checkbox(text='Split security levels', key='-SPLITSECURITY-', tooltip=tt_split_security)],
[
ui.Checkbox(text='Race seed', key='RaceSeed', tooltip=tt_race)]]
tabLayout = [
sg.Text(text='Sector Shuffle:')],
[
ui.TabGroup([
sg.Checkbox(text='Shuffle Sectors', key='-SHUFFLESECTORS-', tooltip=tt_shuffle_sectors),
sg.Checkbox(text='Shuffle Tubes', key='-SHUFFLETUBES-', tooltip=tt_shuffle_tubes)]]
cosmetic = [
[
sg.Text('Palette Shuffle')],
[
sg.Checkbox(text='Tilesets', key='-PALTILESETS-')],
[
sg.Checkbox(text='Sprites', key='-PALSPRITES-')],
[
sg.Checkbox(text='Suits', key='-PALSUITS-')],
[
sg.Checkbox(text='Beams', key='-PALBEAMS-')]]
tab_layout = [
[
sg.TabGroup([
[
ui.Tab('Logic', settings)]], tab_location='topleft')]]
sg.Tab('Logic', settings)],
[
sg.Tab('Cosmetic', cosmetic)]], tab_location='topleft', expand_x=True)]]
layout = [
[
ui.Text('Seed value'),
ui.Input(key='Seed')],
sg.Text('Seed value'),
sg.Input(key='-SEED-')],
[
tabLayout],
tab_layout],
[
ui.Text('Number of Seeds to generate:'),
ui.Input(key='Num', size=5, justification='center', enable_events=True),
ui.Push(),
ui.Checkbox(text='Create patch', key='Patch', tooltip=tt_createpatch),
ui.Button(button_text='Generate', bind_return_key=True)]]
window = ui.Window('Open Metroid Fusion Open Randomizer', layout)
fileName = str()
finishGenerating = False
oldNum = str()
sg.Text('Number of Seeds to generate:'),
sg.Input(key='-NUM-', size=5, justification='center', enable_events=True),
sg.Push(),
sg.Checkbox(text='Create patch', key='-PATCH-', tooltip=tt_patch),
sg.Button(button_text='Generate', bind_return_key=True)]]
window = sg.Window('Open Metroid Fusion Open Randomizer', layout, icon=str(Path.cwd() / 'data' / 'MFOR.ico'))
file_name = str()
finish_generating = False
old_num = str()
disable_exceptions = (0, 'Logic', 'Cosmetic')
while True:
(event, values) = window.read()
if event == ui.WINDOW_CLOSED:
if event == sg.WINDOW_CLOSED:
break
if event == 'Num':
if len(values['Num']) > 3:
if event == '-NUM-':
if len(values['-NUM-']) > 3:
window['Num'].update(oldNum)
for x in range(4):
if x < len(values['Num']):
if values['Num'][x] not in '0123456789':
if x < len(values['-NUM-']):
if values['-NUM-'][x] not in '0123456789':
window['Num'].update(oldNum)
break
else:
oldNum = values['Num']
oldNum = values['-NUM-']
if event == 'Generate':
basegame = ui.popup_get_file('Choose base game', no_titlebar=True, file_types=[('GBA File', '*.gba')], history=True, history_setting_filename=os.path.join('.', 'settings.json'))
if basegame == '':
ui.popup('Please select a Metroid Fusion (U) rom.', title='No source rom selected')
if basegame != None and basegame != '':
checksum = fileHash(basegame)
base_game = sg.popup_get_file('Choose base game', default_path=str(Path.cwd()), no_titlebar=True, file_types=[('GBA File', '*.gba')], history=True, history_setting_filename=str(Path.cwd() / 'settings.json'))
if base_game == '':
sg.popup('Please select a Metroid Fusion (U) rom.', title='No source rom selected')
if base_game != None and base_game != '':
checksum = file_hash(base_game)
if checksum != 1819625372:
ui.popup('Only Metroid Fusion (U) is supported.\nCheck the CRC32 value: it should be 6C75479C', title='Bad Checksum')
sg.popup('Only Metroid Fusion (U) is supported.\nCheck the CRC32 value: it should be 6C75479C', title='Bad Checksum')
else:
values.update({
'Debug': debug })
threading.Thread(target=rando_thread, args=[window, values], daemon=True).start()
window['Difficulty'].update(disabled=True)
window['MajorMinor'].update(disabled=True)
window['MissilesWithoutMainData'].update(disabled=True)
window['PowerBombsWithoutBombs'].update(disabled=True)
window['DamageRuns'].update(disabled=True)
window['SplitSecurity'].update(disabled=True)
window['SectorShuffle'].update(disabled=True)
window['HideItems'].update(disabled=True)
window['Seed'].update(disabled=True)
window['RaceSeed'].update(disabled=True)
window['Num'].update(disabled=True)
window['Patch'].update(disabled=True)
window['Generate'].update(disabled=True)
while generating:
ui.popup_animated(ui.DEFAULT_BASE64_LOADING_GIF, 'Generating game, please wait...', time_between_frames=20)
sg.popup_animated(sg.DEFAULT_BASE64_LOADING_GIF, 'Generating game, please wait...', time_between_frames=20)
window.Refresh()
if finishGenerating:
ui.popup_animated(None)
if failedgen:
ui.popup('Could not generate a game with the current settings. Try changing your settings.', title='Metroid Fusion Open Randomizer')
elif randoerror:
ui.popup('An error occurred, no game was generated.', title='Error')
elif ui.Input.get(window['Num']) != '':
if int(ui.Input.get(window['Num'])) > 1:
ui.popup('Multiple games have been added to the seeds folder.', title='Success!')
if finish_generating:
sg.popup_animated(None)
if failed_gen:
sg.popup('Could not generate a game with the current settings. Try changing your settings.', title='Metroid Fusion Open Randomizer')
elif rando_error:
sg.popup('An error occurred, no game was generated.', title='Error')
elif sg.Input.get(window['-NUM-']) != '':
if int(sg.Input.get(window['-NUM-'])) > 1:
sg.popup('Multiple games have been added to the seeds folder.', title='Success!')
else:
ui.popup('{}\nhas been added to the seeds folder.'.format(fileName), title='Success!')
sg.popup(f'''{file_name}\nhas been added to the seeds folder.''', title='Success!')
else:
ui.popup('{}\nhas been added to the seeds folder.'.format(fileName), title='Success!')
window['Difficulty'].update(disabled=False)
window['MajorMinor'].update(disabled=False)
window['MissilesWithoutMainData'].update(disabled=False)
window['PowerBombsWithoutBombs'].update(disabled=False)
window['DamageRuns'].update(disabled=False)
window['SplitSecurity'].update(disabled=False)
window['SectorShuffle'].update(disabled=False)
window['HideItems'].update(disabled=False)
window['Seed'].update(disabled=False)
window['RaceSeed'].update(disabled=False)
window['Num'].update(disabled=False)
window['Patch'].update(disabled=False)
window['Generate'].update(disabled=False)
finishGenerating = False
window.close()
sg.popup(f'''{file_name}\nhas been added to the seeds folder.''', title='Success!')
finish_generating = False
window.close()

242
MFOR.py
View file

@ -7,79 +7,179 @@
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Source Generated with Decompyle++
# File: MFOR.pyc (Python 3.8)
import argparse
import os
parser = argparse.ArgumentParser()
parser.add_argument('-seed', help='Seed value, optional')
parser.add_argument('-race', default=False, action='store_true', help='Generate race rom (no spoiler log)')
parser.add_argument('-patch', default=False, action='store_true', help='Generate BPS patch with seed (for easy sharing)')
parser.add_argument('-diff', help='Trick difficulty level (0-5, default 0)')
parser.add_argument('-limit', default=False, action='store_true', help='Major/minor item pool')
parser.add_argument('-missiles', default=False, action='store_true', help='Missile upgrades enable the missile launcher (default off)')
parser.add_argument('-pbs', default=False, action='store_true', help='Enable Power Bombs without normal Bombs (default off)')
parser.add_argument('-damage-runs', default=False, action='store_true', help='Allow logical damage runs (default off)')
parser.add_argument('-split-security', default=False, action='store_true', help='Separated Security levels (default off)')
parser.add_argument('-sector-shuffle', default=False, action='store_true', help='Randomly shuffle the arrangement of the Sectors')
parser.add_argument('-rom', help='Base rom (Metroid Fusion (U).gba)')
parser.add_argument('-nogui', default=False, action='store_true', help='Termnial based seed generation (this)')
parser.add_argument('-num', help='Number of seeds to generate (default 1)')
parser.add_argument('-hideitems', default=False, action='store_true', help='All items use ? tank graphics')
parser.add_argument('-debug', default=False, action='store_true', help='Debugging, for my own use')
args = parser.parse_args()
import random
import sys
import zlib
from datetime import datetime
from pathlib import Path
if not os.path.exists(os.path.join('.', 'data')):
os.mkdir('data')
if not os.path.exists(os.path.join('.', 'seeds')):
os.mkdir('seeds')
if not os.path.exists(os.path.join('.', 'spoilers')):
os.mkdir('spoilers')
if not args.debug:
args.debug = False
if args.nogui:
settings = dict()
settings.update({
'Seed': None })
if args.seed:
settings.update({
'Seed': args.seed })
settings.update({
'Debug': args.debug })
settings.update({
'Patch': args.patch })
if not args.diff:
args.diff = 0
if int(args.diff) <= 0:
args.diff = 0
elif int(args.diff) >= 5:
args.diff = 5
settings.update({
'Difficulty': args.diff })
settings.update({
'MajorMinor': args.limit })
settings.update({
'MissilesWithoutMainData': args.missiles })
settings.update({
'PowerBombsWithoutBombs': args.pbs })
settings.update({
'DamageRuns': args.damage_runs })
settings.update({
'SplitSecurity': args.split_security })
settings.update({
'SectorShuffle': args.sector_shuffle })
settings.update({
'HideItems': args.hideitems })
settings.update({
'RaceSeed': args.race })
settings.update({
'Num': None })
if args.num:
settings.update({
'Num': int(args.num) })
from Randomizer import start_randomizer
start_randomizer(args.rom, settings)
else:
import GUI as gui
gui.main_window(args.debug)
version = '2024.02.10+chozo1'
def main():
parser = argparse.ArgumentParser(description=f'''Metroid Fusion Open Randomizer v{version}''')
parser.add_argument('-file', help='Path to unmodified game (Metroid Fusion (U).gba)')
parser.add_argument('-seed', default=None, help='Optional seed value')
parser.add_argument('-difficulty', choices=['0','1','2','3','4','5','r'], default='0', help='Trick difficulty level 0-5 (default 0)')
parser.add_argument('-race', choices=['t','f','r'], default='f', help='Generate race seed (no spoiler log)')
parser.add_argument('-hidden', choices=['t','f','r'], default='f', help='All items use ? tank graphics')
parser.add_argument('-limit', choices=['t','f','r'], default='f', help='Major/minor item pool')
parser.add_argument('-missiles', choices=['t','f','r'], default='f', help='Missile upgrades enable the Missile launcher (default f)')
parser.add_argument('-bombs', choices=['t','f','r'], default='f', help='Enable Power Bombs without normal Bombs (default f)')
parser.add_argument('-hazards', choices=['t','f','r'], default='f', help='Logic allows hazard runs (default f)')
parser.add_argument('-security', choices=['t','f','r'], default='f', help='Split security: if split, security stations only unlock their specific security level. By default, higher level security stations will unlock all lesser security levels.')
parser.add_argument('-sectors', choices=['t','f','r'], default='f', help='Choose whether to shuffle the arrangement of the Sectors')
parser.add_argument('-tubes', choices=['t','f','r'], default='f', help='Choose whether to shuffle the tube connections between the Sectors')
parser.add_argument('-palettes', help='Enable palette shuffle: [t]ilesets, [b]eams, [s]uits, s[p]rites')
parser.add_argument('-num', default=1, help='Number of seeds to generate')
parser.add_argument('-patch', default=False, action='store_true', help='Generate BPS patch for easy sharing')
args = parser.parse_args()
if not hasattr(args, "debug"):
args.debug = False
if args.file:
from Randomizer import start_randomizer
settings = dict()
logic = list()
settings.update({"-SEED-": (args.seed)})
if args.difficulty == "r":
args.difficulty = random.randrange(6)
logic.append(f"Difficulty: {args.difficulty}")
else:
args.difficulty = int(args.difficulty)
if args.race == "r":
args.race = random.choice([True, False])
logic.append(f"Race: {args.race}")
elif args.race == "t":
args.race = True
else:
args.race = False
if args.hidden == "r":
args.hidden = random.choice([True, False])
logic.append(f"Hide items: {args.hidden}")
elif args.hidden == "t":
args.hidden = True
else:
args.hidden = False
if args.limit == "r":
args.limit = random.choice([True, False])
logic.append(f"Major/minor: {args.limit}")
elif args.limit == "t":
args.limit = True
else:
args.limit = False
if args.missiles == "r":
args.missiles = random.choice([True, False])
logic.append(f'Missile launcher: {"any" if args.missiles else "main"}')
elif args.missiles == "t":
args.missiles = True
else:
args.missiles = False
if args.bombs == "r":
args.bombs = random.choice([True, False])
logic.append(f'Power Bombs require: {"data" if args.bombs else "bombs"}')
elif args.bombs == "t":
args.bombs = True
else:
args.bombs = False
if args.hazards == "r":
args.hazards = random.choice([True, False])
logic.append(f"Hazard runs: {args.hazards}")
elif args.hazards == "t":
args.hazards = True
else:
args.hazards = False
if args.security == "r":
args.security = random.choice([True, False])
logic.append(f"Split security: {args.security}")
elif args.security == "t":
args.security = True
else:
args.security = False
if args.sectors == "r":
args.sectors = random.choice([True, False])
logic.append(f"Shuffle Sectors: {args.sectors}")
elif args.sectors == "t":
args.sectors = True
else:
args.sectors = False
if args.tubes == "r":
args.tubes = random.choice([True, False])
logic.append(f"Shuffle tubes: {args.tubes}")
elif args.tubes == "t":
args.tubes = True
else:
args.tubes = False
settings.update({"-DIFFICULTY-": (args.difficulty)})
settings.update({"-RACE-": (args.race)})
settings.update({"-HIDEITEMS-": (args.hidden)})
settings.update({"-MAJORMINOR-": (args.limit)})
settings.update({"-MISSILEDATA-": (args.missiles)})
settings.update({"-PBDATA-": (args.bombs)})
settings.update({"-HAZARDRUNS-": (args.hazards)})
settings.update({"-SPLITSECURITY-": (args.security)})
settings.update({"-SHUFFLESECTORS-": (args.sectors)})
settings.update({"-SHUFFLETUBES-": (args.tubes)})
if args.num:
args.num = int(args.num)
if args.num < 1:
args.num = 1
settings.update({"-NUM-": (args.num)})
else:
settings.update({"-NUM-": 1})
settings.update({"-PATCH-": (args.patch)})
settings.update({"Debug": (args.debug)})
settings.update({"-PALTILESETS-": False})
settings.update({"-PALSPRITES-": False})
settings.update({"-PALSUITS-": False})
settings.update({"-PALBEAMS-": False})
if args.palettes:
if "t" in args.palettes:
settings.update({"-PALTILESETS-": True})
if "b" in args.palettes:
settings.update({"-PALBEAMS-": True})
if "s" in args.palettes:
settings.update({"-PALSUITS-": True})
if "p" in args.palettes:
settings.update({"-PALSPRITES-": True})
settings = {key: settings[key] for key in sorted(settings.keys())}
start_randomizer(args.file, settings)
if len(logic) > 0:
print("\nRandom settings:")
for item in logic:
print(item)
else:
import GUI
GUI.main_window(args.debug)
def _init():
checksum = file_hash(Path.cwd() / 'data' / 'MFOR.bps')
if checksum != 558161692:
sys.exit('Error: Core files could not be read. Please go to https://metroidconstruction.com/ and re-download MFOR.')
Path(Path.cwd() / 'seeds').mkdir(exist_ok=True)
Path(Path.cwd() / 'spoilers').mkdir(exist_ok=True)
def file_hash(file):
checksum = 0
try:
with open(file, 'rb') as source:
while True:
s = source.read(65536)
if not s:
break
checksum = zlib.crc32(s, checksum)
except FileNotFoundError:
sys.exit('Error:', file, 'could not be opened.')
return checksum & 0xFFFFFFFF
if __name__ == '__main__':
_init()
main()

View file

@ -25,11 +25,26 @@ Caveat emptor: the project is already diverging from the original decompilation
- [x] Reconstructing missing parts of code
- [x] Patching
- [x] Generating BPS patches (python-bps-continued used to apply base patch as it's platform independent, but FLIPS to generate BPS files)
- [ ] Potentially merging/reimplementing the new features from v2024.02.10
- [x] Potentially merging/reimplementing the new features from v2024.02.10
- [ ] Merging the logic edits from v2024.02.10
- [ ] Solver refactoring
- [ ] CI/CD for Windows frozen builds
- [x] CI/CD for Windows frozen builds
## Install
## Quick start (Windows)
- fetch the latest [OpenMFOR release](https://git.inabaudonge.reisen/OpenMFOR/OpenMFOR/releases/latest)
- fetch the latest [FLIPS for Windows release](https://github.com/Alcaro/Flips/releases/latest)
- extract OpenMFOR
- put `flips.exe` extracted from the second archive into the `flips` folder in OpenMFOR
- double click `OpenMFOR.exe`
- ????
- PROFIT
Prebuilt release builds might be available on the [releases](https://git.inabaudonge.reisen/OpenMFOR/OpenMFOR/releases) tab.
If you feel more inclined testing the tip of the development branches or playing with the logic, part of the following guide can be used on Windows too to run the randomizer from sources rather than PyInstaller packed executables.
## Install (any OS)
On *NIX OSes, the only strict prerequisites are Python 3.x and pip. For the GUI, sourcing `python3-tk` from your package manager is highly recommended.
@ -61,7 +76,6 @@ The randomizer currently works **only** with the USA (and, by proxy, the Austral
## Help
* [OpenMFOR Discord guild](https://discord.gg/QV3p8MWKab) since we've been told to refrain to bring this up on the wider community server over ongoing diplomacy issues.
* soon™ we might have an IRC channel or a Matrix group bridged to the Discord guild.
## Special Thanks
@ -70,6 +84,8 @@ The randomizer currently works **only** with the USA (and, by proxy, the Austral
- [Decompyle++](https://github.com/zrax/pycdc) has been used for the initial disassembly. [R. Bernstein](https://github.com/rocky) because uncompyle6, decompile3, xdis and python-control-flow kickstarted this project.
- [tcprescott's python-bps-continued](https://github.com/tcprescott/python-bps-continued) since this makes at least rolling the seeds completely platform independent.
- [Alcaro's Floating IPS](https://github.com/Alcaro/Flips)
- Fedor Batogonov's [docker-pyinstaller](https://gitlab.com/batonogov/docker-pyinstaller) used by the CI/CD to make Windows builds on AppVeyor
- The testers in the various Discord guilds that are spotting the massive scams before I do! (SkullAdult, Mr. Fox, baria, Fpiz_, jakoliath)
[Kazuto88](https://github.com/Kazuto88) being the original randomizer developer. The following lines are his special thanks to the people that contributed to making this a reality:

View file

@ -20,11 +20,14 @@ import json
import os
import zlib
import math
from pathlib import Path
import Fusion_Graph as Graph
import Fusion_Palette_Shuffle
from Fusion_Items import *
from MFOR import file_hash
from bps.apply import apply_to_files
version = '0.11.6+chozo1'
version = '2024.02.10+chozo1'
# FIXME: Debug should not be set here
# Debug = False
@ -32,20 +35,6 @@ version = '0.11.6+chozo1'
def ceiling(num, denom):
return -(num // -denom)
def fileHash(file):
checksum = 0
try:
with open(file, 'rb') as source:
while True:
s = source.read(65536)
if not s:
break
checksum = zlib.crc32(s, checksum)
except FileNotFoundError:
sys.exit('Error:', file, 'could not be opened.')
return checksum & 0xFFFFFFFF
def enable_item(graph, item):
globals()[item] = True
update_items(graph)
@ -72,7 +61,7 @@ def update_items(graph):
global SuperMissiles
global UseBombs
global WallJump
if SeedSettings['MissilesWithoutMainData'] == True:
if SeedSettings['-MISSILEDATA-'] == True:
Missiles = MainMissiles or SuperMissileItem or IceMissileItem or DiffusionItem
else:
Missiles = MainMissiles
@ -83,7 +72,7 @@ def update_items(graph):
HiJump = HiJumpBoots or SpaceJump
SpringBall = HiJumpBoots
WallJump = SpaceJump or WallJumpSetting
if SeedSettings['PowerBombsWithoutBombs'] == True:
if SeedSettings['-PBDATA-'] == True:
PowerBombs = MorphBall and MainPowerBombs
else:
PowerBombs = MorphBall and Bombs and MainPowerBombs
@ -1769,7 +1758,7 @@ def find_available_areas(graph):
path = graph.get_path('Security-Level-2', StartLocation, depth=250)
if path == None:
disable_item(graph, 'GreenDoors')
elif SeedSettings['SplitSecurity'] == False:
elif SeedSettings['-SPLITSECURITY-'] == False:
enable_item(graph, 'BlueDoors')
if YellowDoors == False:
path = graph.get_path(StartLocation, 'Security-Level-3', depth=250)
@ -1778,7 +1767,7 @@ def find_available_areas(graph):
path = graph.get_path('Security-Level-3', StartLocation, depth=250)
if path == None:
disable_item(graph, 'YellowDoors')
elif SeedSettings['SplitSecurity'] == False:
elif SeedSettings['-SPLITSECURITY-'] == False:
enable_item(graph, 'GreenDoors')
enable_item(graph, 'BlueDoors')
if RedDoors == False:
@ -1788,7 +1777,7 @@ def find_available_areas(graph):
path = graph.get_path('Security-Level-4', StartLocation, depth=250)
if path == None:
disable_item(graph, 'RedDoors')
elif SeedSettings['SplitSecurity'] == False:
elif SeedSettings['-SPLITSECURITY-'] == False:
enable_item(graph, 'YellowDoors')
enable_item(graph, 'GreenDoors')
enable_item(graph, 'BlueDoors')
@ -1811,6 +1800,8 @@ def randomize_game(graph):
global PlacedMissiles
global PlacedPowerBombs
global areaLayout
global tubeLayout
update_requirements(graph)
PlacedETanks = PlacedMissiles = PlacedPowerBombs = 0
gameBeatable = False
@ -1834,68 +1825,76 @@ def randomize_game(graph):
FirstItem = None
AddETank = False
attemptedLayouts = [[5,3,1,2,4,6]]
WeightedMajors = { 'MainMissiles': 16,
'MorphBall': 16,
'ChargeBeam': 8,
'Bombs': 14,
'HiJumpBoots': 14,
'SpeedBooster': 8,
'SuperMissileItem': 8,
'VariaSuit': 12,
'IceMissileItem': 6,
'WideBeam': 4,
'MainPowerBombs': 6,
'SpaceJump': 4,
'PlasmaBeam': 2,
'GravitySuit': 4,
'DiffusionItem': 6,
'WaveBeam': 4,
'ScrewAttack': 2,
'IceBeam': 2}
WeightedMajors = {
'MainMissiles': 6,
'MorphBall': 6,
'ChargeBeam': 3,
'Bombs': 5,
'HiJumpBoots': 5,
'SpeedBooster': 4,
'SuperMissileItem': 4,
'VariaSuit': 3,
'IceMissileItem': 3,
'WideBeam': 4,
'MainPowerBombs': 2,
'SpaceJump': 2,
'PlasmaBeam': 2,
'GravitySuit': 1,
'DiffusionItem': 3,
'WaveBeam': 1,
'ScrewAttack': 1,
'IceBeam': 1 }
BossHealths = {
'Arachnus': 0,
'Yakuza': 1000,
'Ridley': 4500,
'Charge Core-X': 0,
'Zazabi': 100,
'Nettori': 2000,
'Data S3': 300,
'Wide Core-X': 0,
'Serris': 50,
'Nightmare': 1800,
'Mega Core-X': 0,
'Box-2': 500}
'Arachnus': 0,
'Charge Core-X': 0,
'Zazabi': 100,
'Serris': 50,
'Data S3': 300,
'Mega Core-X': 0,
'Wide Core-X': 0,
'Yakuza': 1000,
'Nettori': 2000,
'Nightmare': 1800,
'Ridley': 4500,
'BOX-2': 500 }
print('Generating, please wait...')
while gameBeatable == False:
if init == False:
if SeedSettings['SectorShuffle'] == True:
if SeedSettings['-SHUFFLESECTORS-'] == True or SeedSettings['-SHUFFLETUBES-'] == True:
areaDoors = {1:['S1-6B', 'S1-68'],
2:['S2-7F', 'S2-82'],
3:['S3-59', 'S3-56'],
4:['S4-6C', 'S4-6A'],
5:['S5-02', 'S5-53'],
6:['S6-51', 'S6-54']}
areaLayout = random.sample(range(1, 7), 6)
if SeedSettings['MajorMinor'] == True:
while areaLayout[2] != 1 and areaLayout[2] != 2 and areaLayout[3] != 1 and areaLayout[3] != 2:
attemptedLayouts.append(areaLayout)
if SeedSettings['-SHUFFLESECTORS-'] == True:
areaLayout = random.sample(range(1, 7), 6)
if SeedSettings['-MAJORMINOR-'] == True:
while areaLayout[2] != 1 and areaLayout[2] != 2 and areaLayout[3] != 1 and areaLayout[3] != 2:
attemptedLayouts.append(areaLayout)
while areaLayout in attemptedLayouts:
areaLayout = random.sample(range(1, 7), 6)
else:
while areaLayout in attemptedLayouts:
areaLayout = random.sample(range(1, 7), 6)
else:
while areaLayout in attemptedLayouts:
areaLayout = random.sample(range(1, 7), 6)
attemptedLayouts.append(areaLayout)
attemptedLayouts.append(areaLayout)
tubeLayout = areaLayout
if SeedSettings['-SHUFFLETUBES-'] == True:
tubeLayout = random.sample(range(1, 7), 6)
for area in areaLayout:
elevator = areaLayout.index(area)
if elevator == 0:
leftArea = areaLayout[5]
if tubeLayout.index(area) == 0:
leftArea = tubeLayout[5]
else:
leftArea = areaLayout[areaLayout.index(area) - 1]
if elevator == 5:
rightArea = areaLayout[0]
leftArea = tubeLayout[tubeLayout.index(area) - 1]
if tubeLayout.index(area) == 5:
rightArea = tubeLayout[0]
else:
rightArea = areaLayout[areaLayout.index(area) + 1]
rightArea = tubeLayout[tubeLayout.index(area) + 1]
if area == 1:
topTile = 19
bottomTile = 35
@ -2145,7 +2144,7 @@ def randomize_game(graph):
graph.ConnectAllNodes()
PlacedETanks = PlacedMissiles = PlacedPowerBombs = 0
if SeedSettings['DamageRuns']:
if SeedSettings['-HAZARDRUNS-']:
Energy = PlacedETanks * 100 + 99
MissileCount = 10
locationWeights.clear()
@ -2165,7 +2164,7 @@ def randomize_game(graph):
oldLocations.clear()
oldWeights.clear()
MajorLocations.extend(graph.majorItemLocations)
if SeedSettings['MajorMinor'] == False:
if SeedSettings['-MAJORMINOR-'] == False:
MajorLocations.extend(graph.minorItemLocations)
MinorLocations.extend(graph.minorItemLocations)
AllItemLocations.extend(graph.majorItemLocations)
@ -2188,22 +2187,22 @@ def randomize_game(graph):
weightValue.append(1)
MajorList = list(WeightedMajors.keys())
if SeedSettings['MissilesWithoutMainData']:
WeightedMajors.update({'MainMissiles': 8})
WeightedMajors.update({'SuperMissileItem': 8})
WeightedMajors.update({'IceMissileItem': 8})
WeightedMajors.update({'DiffusionItem': 8})
if SeedSettings['PowerBombsWithoutBombs']:
WeightedMajors.update({'Bombs': 10})
WeightedMajors.update({'MainPowerBombs': 10})
if SeedSettings['DamageRuns']:
WeightedMajors.update({'VariaSuit': 4})
if SeedSettings['-MISSILEDATA-']:
WeightedMajors.update({'MainMissiles': 4})
WeightedMajors.update({'SuperMissileItem': 4})
WeightedMajors.update({'IceMissileItem': 4})
WeightedMajors.update({'DiffusionItem': 4})
if SeedSettings['-PBDATA-']:
WeightedMajors.update({'Bombs': 3})
WeightedMajors.update({'MainPowerBombs': 3})
if SeedSettings['-HAZARDRUNS-']:
WeightedMajors.update({'VariaSuit': 3})
MajorWeights = list(WeightedMajors.values())
MajorWeights.pop(MajorList.index('Bombs'))
MajorList.remove('Bombs')
MajorWeights.pop(MajorList.index('MainPowerBombs'))
MajorList.remove('MainPowerBombs')
if SeedSettings['MissilesWithoutMainData'] == False:
if SeedSettings['-MISSILEDATA-'] == False:
MajorWeights.pop(MajorList.index('IceMissileItem'))
MajorList.remove('IceMissileItem')
MajorWeights.pop(MajorList.index('SuperMissileItem'))
@ -2273,7 +2272,7 @@ def randomize_game(graph):
MajorList.remove(FirstItem)
enable_item(graph, FirstItem)
path = None
if SeedSettings['DamageRuns']:
if SeedSettings['-HAZARDRUNS-']:
Energy = Energy / 2
while path == None:
location = random.choice(AccessibleLocations)
@ -2288,7 +2287,7 @@ def randomize_game(graph):
weightValue[area] += 1
if FirstItem == 'MainMissiles':
if SeedSettings['MissilesWithoutMainData'] == False:
if SeedSettings['-MISSILEDATA-'] == False:
MajorList.append('IceMissileItem')
MajorWeights.append(WeightedMajors.get('IceMissileItem'))
MajorList.append('SuperMissileItem')
@ -2298,17 +2297,17 @@ def randomize_game(graph):
elif FirstItem == 'MorphBall':
MajorList.append('Bombs')
MajorWeights.append(WeightedMajors.get('Bombs'))
if SeedSettings['PowerBombsWithoutBombs'] == True:
if SeedSettings['-PBDATA-'] == True:
MajorList.append('MainPowerBombs')
MajorWeights.append(WeightedMajors.get('MainPowerBombs'))
elif FirstItem == 'Bombs':
if SeedSettings['PowerBombsWithoutBombs'] == False:
if SeedSettings['-PBDATA-'] == False:
MajorList.append('MainPowerBombs')
MajorWeights.append(WeightedMajors.get('MainPowerBombs'))
init = True
if SeedSettings['DamageRuns']:
if SeedSettings['-HAZARDRUNS-']:
Energy = PlacedETanks * 100 + 99
find_available_areas(graph)
@ -2344,7 +2343,7 @@ def randomize_game(graph):
for area in range(0, 7):
if len(locationsPerArea[area]) > 0:
if SeedSettings['MajorMinor']:
if SeedSettings['-MAJORMINOR-']:
areaWeights[area] = weightValue[area] / len(locationsPerArea[area])
else:
areaWeights[area] = weightValue[area] / len(locationsPerArea[area]) + area
@ -2354,7 +2353,7 @@ def randomize_game(graph):
if location in AreaItemLocations[area]:
if location in newLocations:
locationWeights.append(areaWeights[area] * 1.8)
elif location in BossLocations and SeedSettings['MajorMinor'] == False:
elif location in BossLocations and SeedSettings['-MAJORMINOR-'] == False:
locationWeights.append(areaWeights[area] * 1.5)
else:
locationWeights.append(areaWeights[area])
@ -2367,7 +2366,7 @@ def randomize_game(graph):
MajorList.remove(item)
enable_item(graph, item)
if SeedSettings['DamageRuns']:
if SeedSettings['-HAZARDRUNS-']:
Energy = Energy / 2
while path == None and len(AccessibleLocations) > 0:
@ -2385,11 +2384,11 @@ def randomize_game(graph):
if itemLocation in BossLocations:
if path != None:
disable_item(graph, item)
if ChargeBeam == False and SeedSettings['Difficulty'] > 0:
if ChargeBeam == False and SeedSettings['-DIFFICULTY-'] > 0:
healthCheck = BossHealths.get(itemLocation)
if SeedSettings['Difficulty'] > 4:
if SeedSettings['-DIFFICULTY-'] > 4:
healthCheck = healthCheck * 1.02
elif SeedSettings['Difficulty'] > 2:
elif SeedSettings['-DIFFICULTY-'] > 2:
healthCheck = healthCheck * 1.05
else:
healthCheck = healthCheck * 1.1
@ -2398,7 +2397,7 @@ def randomize_game(graph):
if MissileDamage > 0:
tankCheck = math.ceil(healthCheck / MissileDamage / 5)
PossibleMissileTanks.clear()
if SeedSettings['MajorMinor']:
if SeedSettings['-MAJORMINOR-']:
for missileLocation in MinorLocations:
if missileLocation not in UsedLocations:
missilePath = None
@ -2435,7 +2434,7 @@ def randomize_game(graph):
for x in range(0, int(tankCheck)):
missileLocation = random.choice(PossibleMissileTanks)
PossibleMissileTanks.remove(missileLocation)
if SeedSettings['MajorMinor'] == False:
if SeedSettings['-MAJORMINOR-'] == False:
locationWeights.pop(AccessibleLocations.index(missileLocation))
AccessibleLocations.remove(missileLocation)
UsedLocations.append(missileLocation)
@ -2487,7 +2486,7 @@ def randomize_game(graph):
weightValue[area] += 1
if item == 'MainMissiles':
if SeedSettings['MissilesWithoutMainData'] == False:
if SeedSettings['-MISSILEDATA-'] == False:
MajorList.append('IceMissileItem')
MajorWeights.append(WeightedMajors.get('IceMissileItem'))
MajorList.append('SuperMissileItem')
@ -2497,15 +2496,15 @@ def randomize_game(graph):
elif item == 'MorphBall':
MajorList.append('Bombs')
MajorWeights.append(WeightedMajors.get('Bombs'))
if SeedSettings['PowerBombsWithoutBombs'] == True:
if SeedSettings['-PBDATA-'] == True:
MajorList.append('MainPowerBombs')
MajorWeights.append(WeightedMajors.get('MainPowerBombs'))
elif item == 'Bombs':
if SeedSettings['PowerBombsWithoutBombs'] == False:
if SeedSettings['-PBDATA-'] == False:
MajorList.append('MainPowerBombs')
MajorWeights.append(WeightedMajors.get('MainPowerBombs'))
if SeedSettings['MajorMinor'] == False:
if SeedSettings['-MAJORMINOR-'] == False:
if len(AccessibleLocations) > 3:
if PlacedETanks < MaxETanks:
itemLocation = random.choice(AccessibleLocations)
@ -2539,7 +2538,7 @@ def randomize_game(graph):
UsedLocations.append(location)
PlacedItems.append(item)
if SeedSettings['MajorMinor']:
if SeedSettings['-MAJORMINOR-']:
for location in MajorLocations:
if PlacedETanks < MaxETanks:
if location not in UsedLocations:
@ -2600,6 +2599,107 @@ def patch_game():
'BlueDoors': 7657669,
'YellowDoors': 7657777,
'RedDoors': 7657885 }
CreditsNames = {
'Item S0-05-08': 'Main Elevator',
'Item S0-05-16': 'Restricted Lab',
'Item S0-08-0B': 'Main Deck: Spitter Hallway',
'Item S0-09-04': 'Habitation Deck',
'Item S0-0C-09': 'Quarantine Bay Morph Tunnel',
'Item S0-0E-07': 'Main Deck: Power Bomb Storage',
'Item S0-0E-0B': 'Main Deck: Maintenance Tunnel',
'Item S0-13-07': 'Vent Shaft Hidden Item',
'Item S0-14-07': 'Main Deck: Vent Shaft',
'Item S0-15-10': 'Reactor Silo Hallway',
'Item S0-16-12': 'Lower Reactor Silo',
'Item S0-18-06': 'Arachnus Room',
'Item S0-19-06': 'Arachnus Alcove',
'Item S1-03-0A': 'Ridley Area E-Tank',
'Item S1-05-03': 'Sector 1: Across Lava Lake',
'Item S1-06-03': 'Sector 1: Lava Lake Center',
'Item S1-07-00': 'Sector 1: Connector to S2',
'Item S1-07-04': 'Sector 1: Lava Dive',
'Item S1-08-0B': 'Ridley Area Golden Pirate',
'Item S1-09-04': 'Sector 1: Speed Ceiling',
'Item S1-0A-04': 'Charge Core-X Missile Alcove',
'Item S1-0C-07': 'Sector 1: Gravity Well',
'Item S1-0D-02': 'Sector 1: Entry Hallway',
'Item S1-0D-08': 'Sector 1: Crab Pond',
'Item S1-11-02': 'Sector 1: Wall Jump Climb',
'Item S2-00-05': 'Sector 2: Data Room Escape',
'Item S2-02-0E': 'Sector 2: Blue Zoro Room',
'Item S2-03-0C': 'Sector 2: Wonderwall',
'Item S2-04-03': 'Sector 2: Kago Hallway',
'Item S2-04-04': 'Sector 2: Data Room Access',
'Item S2-04-0B': 'Sector 2: Oasis',
'Item S2-05-00': 'Crumble City Upper Item',
'Item S2-05-01': 'Crumble City Lower Item',
'Item S2-05-08': 'Ripper Tower Upper Item',
'Item S2-05-0A': 'Ripper Tower Lower Item',
'Item S2-08-08': 'Sector 2: Puyo Tank',
'Item S2-09-03': 'Sector 2: Blue Door Owtches',
'Item S2-09-05': 'Sector 2: Hub Morph Tunnel',
'Item S2-0C-04': 'Sector 2: Nettori Owtches',
'Item S2-0C-0B': 'Sector 2: Zazabi Access',
'Item S2-10-0C': 'Zazabi Speedway Upper Item',
'Item S2-10-0E': 'Zazabi Speedway Lower Item',
'Item S3-00-05': 'Sector 3: Connector to S5',
'Item S3-01-02': 'Sector 3: Shinespark Puzzle',
'Item S3-03-04': 'Sector 3: Lava Hall Access',
'Item S3-06-06': 'Sector 3: Security Access',
'Item S3-07-0B': 'Sector 3: Magma Dive',
'Item S3-0A-01': 'Sector 3: Hidden PB Alcove',
'Item S3-0B-02': 'Sector 3: Sidehopper Hallway',
'Item S3-0B-04': 'Sector 3: Bob Tank',
'Item S3-0B-06': 'Sector 3: Power Bomb Fune',
'Item S3-0E-0A': 'Sector 3: Lava Pool Access',
'Item S3-0F-00': 'Sector 3: BOX Attic',
'Item S3-11-04': 'Sector 3: BOX Basement',
'Item S3-11-0A': 'Sector 3: Lower Nova Stairway',
'Item S3-12-09': 'Sector 3: Upper Nova Stairway',
'Item S3-14-03': 'Sector 3: Upper Trash Chute',
'Item S3-14-09': 'Sector 3: Lower Trash Chute',
'Item S4-00-06': 'Sector 4: Pump Control',
'Item S4-05-03': 'Sector 4: Owtch Room',
'Item S4-06-0E': 'Sector 4: Crab Battle',
'Item S4-07-08': 'Lower S4: Screw Block Alcove',
'Item S4-07-0A': 'Lower S4: Security Access',
'Item S4-09-02': 'Sector 4: Collapsed Ceiling',
'Item S4-09-06': 'S4: Speed Booster Aquarium',
'Item S4-0A-0C': 'Sector 4: Pirate Fish Tank',
'Item S4-0B-08': 'Sector 4: Snail Hallway',
'Item S4-0C-06': 'Sector 4: Electric PB Tank',
'Item S4-0D-01': 'Serris Escape, Upper Item',
'Item S4-0E-02': 'Serris Escape, Lower Item',
'Item S4-0F-06': 'Sector 4: Underwater Gate',
'Item S4-12-07': 'Sector 4: Powamp Path',
'Item S4-13-07': 'Sector 4: Coral Speed Boost',
'Item S5-03-04': 'Sector 5: Connector to S6',
'Item S5-04-01': 'Sector 5: Speed Wall',
'Item S5-05-01': 'Sector 5: Choot Climb',
'Item S5-05-04': 'Sector 5: Gerubus Room',
'Item S5-06-05': 'Sector 5: Ripper Hallway',
'Item S5-07-0B': 'Sector 5: Security Access',
'Item S5-08-07': 'Sector 5: Ripper Climb',
'Item S5-0B-01': "Sector 5: Crow's Nest",
'Item S5-0C-07': 'Sector 5: E-Tank Mimic',
'Item S5-0E-08': 'Sector 5: Ripper Spawn Puzzle',
'Item S5-0F-07': 'Sector 5: Minifridge',
'Item S5-11-05': 'Sector 5: Crab Room',
'Item S5-12-04': 'Sector 5: Nightmare Recharge',
'Item S5-14-07': 'Sector 5: Sector 4 Access',
'Item S5-16-04': 'Sector 5: Nightmare Drop',
'Item S6-01-06': 'Sector 6: Shinespark Lower',
'Item S6-03-04': 'Sector 6: Shinespark Upper',
'Item S6-05-03': 'Sector 6: Entrance Tunnel',
'Item S6-05-0B': 'Sector 6: Data Access Alcove',
'Item S6-06-08': 'Sector 6: Power Bomb Wall',
'Item S6-08-03': 'Sector 6: Bomb Chain Alcove',
'Item S6-09-05': 'Sector 6: Owtch Puzzle',
'Item S6-0A-06': 'Sector 6: Wave Gate',
'Item S6-0B-09': 'Sector 6: Mega Core-X Attic',
'Item S6-0C-08': 'Sector 6: Blue X Climb',
'Item S6-0E-03': 'Sector 6: Missile Mimic',
'Item S6-0E-04': 'Sector 6: Pillar Highway' }
print('Patching game, please wait')
HashValue = random.sample(HashList, 4)
FileName = 'OpenMFOR -'
@ -2626,11 +2726,12 @@ def patch_game():
print('Error: failed to patch game with base patch!')
sys.exit(1)
checksum = fileHash(os.path.join('.', 'seeds', '{}.gba'.format(FileName)))
if checksum != 2455114263:
os.remove(os.path.join('.', 'seeds', '{}.gba'.format(FileName)))
print('Error: Base patch file has been modified. Please go to the repository you picked this from and re-download OpenMFOR.')
sys.exit(1)
# FIXME: no gatekeeping my scams
# checksum = file_hash(os.path.join('.', 'seeds', '{}.gba'.format(FileName)))
# if checksum != 2455114263:
# os.remove(os.path.join('.', 'seeds', '{}.gba'.format(FileName)))
# print('Error: Base patch file has been modified. Please go to the repository you picked this from and re-download OpenMFOR.')
# sys.exit(1)
with open(os.path.join('.', 'seeds', '{}.gba'.format(FileName)), 'rb+') as patchedGame:
if Debug:
@ -2645,15 +2746,24 @@ def patch_game():
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))
saxAnyOffset = int(sym.get('@t_saxany'), 16)
spawnBox = int(sym.get('@spawnbox'), 16)
spawnMegaCoreX = int(sym.get('@spawnmegacorex'), 16)
print('Debug values')
print(f'''roomEventOffset={roomEventOffset:06X}''')
print(f'''itemEventOffset={itemEventOffset:06X}''')
print(f'''securityOffset={securityOffset:06X}''')
print(f'''saxAnyOffset={saxAnyOffset:06X}''')
print(f'''spawnBox={spawnBox:06X}''')
print(f'''spawnMegaCoreX={spawnMegaCoreX:06X}''')
else:
roomEventOffset = 8326320
roomEventOffset = 8326304
itemEventOffset = 5726112
securityOffset = 479192
saxAnyOffset = 395796
spawnBox = 8326096
spawnMegaCoreX = 8326260
for area in RoomNodes:
areaIndex = list(RoomNodes.keys()).index(area)
for node in RoomNodes[area]:
name = node.get('Name')
nodeType = node.get('Type')
@ -2664,7 +2774,7 @@ def patch_game():
bg1 = int(node.get('BG1'), 16)
clipdata = int(node.get('Clipdata'), 16)
tileset = int(node.get('Tileset'), 16)
if SeedSettings['HideItems']:
if SeedSettings['-HIDEITEMS-']:
if tileset == 33:
blockValue = 7
elif tileset == 34:
@ -2677,6 +2787,7 @@ def patch_game():
blockValue = 3
else:
blockValue = ItemList.index(itemName)
if blockValue < 2:
blockValue = blockValue ^ 1
elif blockValue > 2:
@ -2690,6 +2801,7 @@ def patch_game():
blockValue = 7
else:
blockValue = 3
blockValue += 70
if tileset == 9:
blockValue += 1
@ -2721,6 +2833,7 @@ def patch_game():
blockValue += 1
elif tileset == 72:
blockValue += 1
clipValue = ItemList.index(itemName)
if clipValue < 2:
# Energy Tank or Missile Tank
@ -2741,18 +2854,21 @@ def patch_game():
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:
if '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
roomEvent = roomEvent + 3 + slot * 4
if itemValue < 3:
itemValue += 1
else:
@ -2774,22 +2890,28 @@ def patch_game():
offset = CreditsOffsets.get(itemName)
if 'Boss' in nodeType:
location = name
elif name in CreditsNames:
location = CreditsNames.get(name)
elif 'S0' in name:
location = 'Main Deck : '
else:
if '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'
location = 'Sector {} : '.format(name[6:7])
spaces = ceiling(30 - len(location), 2)
location = ' '*spaces + location
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(5726109)
patchedGame.write(PlacedETanks.to_bytes(1, 'little'))
patchedGame.seek(5726110)
patchedGame.write(PlacedMissiles.to_bytes(1, 'little'))
patchedGame.seek(5726111)
patchedGame.write(PlacedPowerBombs.to_bytes(1, 'little'))
if SeedSettings['-HIDEITEMS-']:
patchedGame.seek(3926048)
patchedGame.write((76).to_bytes(2, 'little'))
patchedGame.write((77).to_bytes(2, 'little'))
@ -2803,23 +2925,34 @@ def patch_game():
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)
if SeedSettings['-SPLITSECURITY-'] == True:
patchedGame.seek(securityOffset)
patchedGame.write((0).to_bytes(2, 'little'))
if SeedSettings['MissilesWithoutMainData']:
if SeedSettings['-MISSILEDATA-']:
patchedGame.seek(24828)
patchedGame.write((15).to_bytes(1, 'little'))
patchedGame.seek(395742)
patchedGame.seek(saxAnyOffset + 1)
patchedGame.write((15).to_bytes(1, 'little'))
patchedGame.seek(465582)
patchedGame.write((15).to_bytes(1, 'little'))
if SeedSettings['PowerBombsWithoutBombs']:
patchedGame.seek(spawnBox + 13)
patchedGame.write((2).to_bytes(1, 'little'))
patchedGame.seek(spawnBox + 21)
patchedGame.write((4).to_bytes(1, 'little'))
patchedGame.seek(spawnBox + 29)
patchedGame.write((8).to_bytes(1, 'little'))
patchedGame.seek(spawnMegaCoreX + 13)
patchedGame.write((2).to_bytes(1, 'little'))
patchedGame.seek(spawnMegaCoreX + 21)
patchedGame.write((4).to_bytes(1, 'little'))
patchedGame.seek(spawnMegaCoreX + 29)
patchedGame.write((8).to_bytes(1, 'little'))
if SeedSettings['-PBDATA-']:
patchedGame.seek(24756)
patchedGame.write((32).to_bytes(1, 'little'))
patchedGame.seek(465672)
patchedGame.write((32).to_bytes(1, 'little'))
if SeedSettings['SectorShuffle'] == True:
if SeedSettings['-SHUFFLESECTORS-'] == True or SeedSettings['-SHUFFLETUBES-'] == True:
for currentArea in range(7):
patchedGame.seek(7977108 + currentArea * 4, 0)
data = patchedGame.read(4)
@ -2857,6 +2990,10 @@ def patch_game():
value = World.patcher.get(target)
patchedGame.seek(target)
patchedGame.write(value.to_bytes(1, 'little'))
game = bytearray((Path.cwd() / 'seeds' / f'''{FileName}.gba''').read_bytes())
Fusion_Palette_Shuffle.randomize_palettes(game, SeedValue + str(SeedSettings), palette_tilesets, palette_sprites, palette_suits, palette_beams)
(Path.cwd() / 'seeds' / f'''{FileName}.gba''').write_bytes(game)
if Patch:
if os.path.exists(os.path.join('.', 'flips', 'flips.exe')):
@ -2887,27 +3024,33 @@ def patch_game():
'Wave Beam',
'Screw Attack',
'Ice Beam']
if SeedSettings['RaceSeed'] == False:
if SeedSettings['-RACE-'] == False:
spoilerLog = dict()
spoilerLog.update({ 'OpenMFOR Version': version })
spoilerLog.update({ 'Seed': SeedValue })
settingsDict = dict()
settingsDict.update({ 'Difficulty': SeedSettings['Difficulty'] })
if SeedSettings['MajorMinor'] == False:
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:
settingsDict.update({ 'Missile upgrades enable Missiles': SeedSettings['-MISSILEDATA-'] })
settingsDict.update({ 'Power Bombs without normal Bombs': SeedSettings['-PBDATA-'] })
settingsDict.update({ 'Allow logical damage runs': SeedSettings['-HAZARDRUNS-'] })
settingsDict.update({ 'Separated security levels': SeedSettings['-SPLITSECURITY-'] })
settingsDict.update({ 'Sector shuffle': SeedSettings['-SHUFFLESECTORS-'] })
if SeedSettings['-SHUFFLESECTORS-'] == 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({ 'Tube shuffle': SeedSettings['-SHUFFLETUBES-'] })
if SeedSettings['-SHUFFLETUBES-'] == True:
tubeStr = str()
for x in tubeLayout:
tubeStr = tubeStr.strip() + f''' {x}'''
settingsDict.update({ 'Tube layout:': tubeStr })
settingsDict.update({ 'Hide item graphics': SeedSettings['-HIDEITEMS-'] })
settingsDict.update({ 'E-Tanks': PlacedETanks })
settingsDict.update({ 'Missile Tanks': PlacedMissiles })
settingsDict.update({ 'Power Bomb Tanks': PlacedPowerBombs })
@ -3008,7 +3151,7 @@ def initialize():
'Nettori',
'Nightmare',
'Data S4',
'Box-2',
'BOX-2',
'Ridley',
'Omega Metroid']
ItemList = MinorItems + MajorItems
@ -3029,37 +3172,45 @@ def start_randomizer(rom, settings):
global SeedValue
global StartLocation
global World
global palette_tilesets
global palette_sprites
global palette_beams
global palette_suits
BaseGame = rom
if BaseGame == None or BaseGame == '':
print('Error: no base game provided.')
sys.exit(1)
checksum = fileHash(BaseGame)
checksum = file_hash(BaseGame)
if checksum != 1819625372:
print('Only Metroid Fusion (U) is supported. Check the CRC32 value: it should be 6C75479C')
sys.exit(1)
Debug = settings['Debug']
settings.pop('Debug')
Debug = settings.pop('Debug')
palette_tilesets = settings.pop('-PALTILESETS-')
palette_sprites = settings.pop('-PALSPRITES-')
palette_beams = settings.pop('-PALBEAMS-')
palette_suits = settings.pop('-PALSUITS-')
if Debug == False:
checksum = fileHash(os.path.join('.', 'data', 'MFOR.bps'))
checksum = file_hash(os.path.join('.', 'data', 'MFOR.bps'))
if checksum != 558161692:
print('Error: Base patch file has been modified. Please go to the repository you picked this from and re-download OpenMFOR.')
sys.exit(1)
totalRandoTime = time.time()
if settings['Seed']:
SeedValue = str(settings['Seed']).strip(' \n')
totalRandoTime = time.perf_counter()
if settings['-SEED-']:
SeedValue = str(settings['-SEED-']).strip(' \n')
else:
SeedValue = str(random.randrange(sys.maxsize))
settings.pop('Seed')
settings.pop('-SEED-')
repeat = 1
if settings['Num']:
if settings['Num'] > repeat:
repeat = settings['Num']
settings.pop('Num')
if settings['Patch']:
if settings['-NUM-']:
if settings['-NUM-'] > repeat:
repeat = settings['-NUM-']
settings.pop('-NUM-')
if settings['-PATCH-']:
Patch = True
else:
Patch = False
settings.pop('Patch')
settings.pop('-PATCH-')
for loop in range(repeat):
initialize()
@ -3069,8 +3220,8 @@ def start_randomizer(rom, settings):
if loop > 0:
SeedValue = str(random.randrange(sys.maxsize))
Difficulty = SeedSettings['Difficulty']
DamageRuns = SeedSettings['DamageRuns']
Difficulty = SeedSettings['-DIFFICULTY-']
DamageRuns = SeedSettings['-HAZARDRUNS-']
random.seed(SeedValue + str(SeedSettings))
World = Graph.Game(BaseGame)
World.RemoveNodeFromRoom('Room-S2-07', 'S2-10')
@ -3123,13 +3274,14 @@ def start_randomizer(rom, settings):
World.ConnectAllNodes()
StartLocation = 'S0-00'
startTime = time.time()
startTime = time.perf_counter()
randomize_game(World)
seedTime = time.time() - startTime
seedTime = time.perf_counter() - startTime
print(str(FileName))
print('Randomized in:', seedTime)
print(f'''Randomized in: {round(seedTime, 3)} seconds''')
totalRandoTime = time.time() - totalRandoTime
print('All seeds took:', totalRandoTime)
totalRandoTime = round(time.perf_counter() - totalRandoTime, 3)
if repeat > 1:
print(f'''All seeds took: {totalRandoTime} seconds''')
return FileName

Binary file not shown.

BIN
data/MFOR.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

View file

@ -1017,7 +1017,7 @@
"Type": "Boss"
},
{
"Name": "Box-2",
"Name": "BOX-2",
"Room": "0x10",
"Type": "Boss"
}

2
flips

@ -1 +1 @@
Subproject commit fdd5c6e34285beef5b9be759c9b91390df486c66
Subproject commit e12ef189900b2c720c6dcd55036a8bb43925ea53