 2639796255
			
		
	
	2639796255
	
	
	
		
			
			* Item groups + small changes * Add alternate goal * New Locations and Logic Updates + Basepatch * Update basepatch.bsdiff * Update Basepatch * Update basepatch.bsdiff * Update bowsers castle logic with emblem hunt * Update Archipelago Unittests.run.xml * Update Archipelago Unittests.run.xml * Fix for overlapping ROM addresses * Update Rom.py * Update __init__.py * Update basepatch.bsdiff * Update Rom.py * Update client with new helper function * Update basepatch.bsdiff * Update worlds/mlss/__init__.py Co-authored-by: qwint <qwint.42@gmail.com> * Update worlds/mlss/__init__.py Co-authored-by: qwint <qwint.42@gmail.com> * Review Refactor * Review Refactor --------- Co-authored-by: qwint <qwint.42@gmail.com>
		
			
				
	
	
		
			435 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			435 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import io
 | |
| import json
 | |
| import random
 | |
| 
 | |
| from . import Data
 | |
| from typing import TYPE_CHECKING, Optional
 | |
| from BaseClasses import Item, Location
 | |
| from settings import get_settings
 | |
| from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension
 | |
| from .Items import item_table
 | |
| from .Locations import shop, badge, pants, location_table, all_locations
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from . import MLSSWorld
 | |
| 
 | |
| colors = [
 | |
|     Data.redHat,
 | |
|     Data.greenHat,
 | |
|     Data.blueHat,
 | |
|     Data.azureHat,
 | |
|     Data.yellowHat,
 | |
|     Data.orangeHat,
 | |
|     Data.purpleHat,
 | |
|     Data.pinkHat,
 | |
|     Data.blackHat,
 | |
|     Data.whiteHat,
 | |
|     Data.silhouetteHat,
 | |
|     Data.chaosHat,
 | |
|     Data.truechaosHat
 | |
| ]
 | |
| 
 | |
| cpants = [
 | |
|     Data.vanilla,
 | |
|     Data.redPants,
 | |
|     Data.greenPants,
 | |
|     Data.bluePants,
 | |
|     Data.azurePants,
 | |
|     Data.yellowPants,
 | |
|     Data.orangePants,
 | |
|     Data.purplePants,
 | |
|     Data.pinkPants,
 | |
|     Data.blackPants,
 | |
|     Data.whitePants,
 | |
|     Data.chaosPants
 | |
| ]
 | |
| 
 | |
| 
 | |
| def get_base_rom_as_bytes() -> bytes:
 | |
|     with open(get_settings().mlss_options.rom_file, "rb") as infile:
 | |
|         base_rom_bytes = bytes(infile.read())
 | |
|     return base_rom_bytes
 | |
| 
 | |
| 
 | |
| class MLSSPatchExtension(APPatchExtension):
 | |
|     game = "Mario & Luigi Superstar Saga"
 | |
| 
 | |
|     @staticmethod
 | |
|     def randomize_music(caller: APProcedurePatch, rom: bytes):
 | |
|         options = json.loads(caller.get_file("options.json").decode("UTF-8"))
 | |
|         if options["music_options"] != 1:
 | |
|             return rom
 | |
|         stream = io.BytesIO(rom)
 | |
|         random.seed(options["seed"] + options["player"])
 | |
| 
 | |
|         songs = []
 | |
|         stream.seek(0x21CB74)
 | |
|         for _ in range(50):
 | |
|             if stream.tell() == 0x21CBD8:
 | |
|                 stream.seek(4, 1)
 | |
|                 continue
 | |
|             temp = stream.read(4)
 | |
|             songs.append(temp)
 | |
| 
 | |
|         random.shuffle(songs)
 | |
|         stream.seek(0x21CB74)
 | |
|         for _ in range(50):
 | |
|             if stream.tell() == 0x21CBD8:
 | |
|                 stream.seek(4, 1)
 | |
|                 continue
 | |
|             stream.write(songs.pop())
 | |
| 
 | |
|         return stream.getvalue()
 | |
| 
 | |
|     @staticmethod
 | |
|     def hidden_visible(caller: APProcedurePatch, rom: bytes):
 | |
|         options = json.loads(caller.get_file("options.json").decode("UTF-8"))
 | |
|         if options["block_visibility"] == 0:
 | |
|             return rom
 | |
|         stream = io.BytesIO(rom)
 | |
| 
 | |
|         for location in [location for location in all_locations if location.itemType == 0]:
 | |
|             stream.seek(location.id - 6)
 | |
|             b = stream.read(1)
 | |
|             if b[0] == 0x10 and options["block_visibility"] == 1:
 | |
|                 stream.seek(location.id - 6)
 | |
|                 stream.write(bytes([0x0]))
 | |
|             if b[0] == 0x0 and options["block_visibility"] == 2:
 | |
|                 stream.seek(location.id - 6)
 | |
|                 stream.write(bytes([0x10]))
 | |
| 
 | |
|         return stream.getvalue()
 | |
| 
 | |
|     @staticmethod
 | |
|     def randomize_sounds(caller: APProcedurePatch, rom: bytes):
 | |
|         options = json.loads(caller.get_file("options.json").decode("UTF-8"))
 | |
|         if options["randomize_sounds"] != 1:
 | |
|             return rom
 | |
|         stream = io.BytesIO(rom)
 | |
|         random.seed(options["seed"] + options["player"])
 | |
|         fresh_pointers = Data.sounds
 | |
|         pointers = Data.sounds
 | |
| 
 | |
|         random.shuffle(pointers)
 | |
|         stream.seek(0x21CC44, 0)
 | |
|         for i in range(354):
 | |
|             current_position = stream.tell()
 | |
|             value = int.from_bytes(stream.read(3), "little")
 | |
|             if value in fresh_pointers:
 | |
|                 stream.seek(current_position)
 | |
|                 stream.write(pointers.pop().to_bytes(3, "little"))
 | |
|             stream.seek(1, 1)
 | |
| 
 | |
|         return stream.getvalue()
 | |
| 
 | |
|     @staticmethod
 | |
|     def enemy_randomize(caller: APProcedurePatch, rom: bytes):
 | |
|         options = json.loads(caller.get_file("options.json").decode("UTF-8"))
 | |
|         if options["randomize_bosses"] == 0 and options["randomize_enemies"] == 0:
 | |
|             return rom
 | |
| 
 | |
|         enemies = [pos for pos in Data.enemies if pos not in Data.bowsers] if options["castle_skip"] else Data.enemies
 | |
|         bosses = [pos for pos in Data.bosses if pos not in Data.bowsers] if options["castle_skip"] else Data.bosses
 | |
|         stream = io.BytesIO(rom)
 | |
|         random.seed(options["seed"] + options["player"])
 | |
| 
 | |
|         if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2 and options["randomize_enemies"] == 0):
 | |
|             raw = []
 | |
|             for pos in bosses:
 | |
|                 stream.seek(pos + 1)
 | |
|                 raw += [stream.read(0x1F)]
 | |
|             random.shuffle(raw)
 | |
|             for pos in bosses:
 | |
|                 stream.seek(pos + 1)
 | |
|                 stream.write(raw.pop())
 | |
| 
 | |
|         if options["randomize_enemies"] == 1:
 | |
|             raw = []
 | |
|             for pos in enemies:
 | |
|                 stream.seek(pos + 1)
 | |
|                 raw += [stream.read(0x1F)]
 | |
|             if options["randomize_bosses"] == 2:
 | |
|                 for pos in bosses:
 | |
|                     stream.seek(pos + 1)
 | |
|                     raw += [stream.read(0x1F)]
 | |
|             random.shuffle(raw)
 | |
|             for pos in enemies:
 | |
|                 stream.seek(pos + 1)
 | |
|                 stream.write(raw.pop())
 | |
|             if options["randomize_bosses"] == 2:
 | |
|                 for pos in bosses:
 | |
|                     stream.seek(pos + 1)
 | |
|                     stream.write(raw.pop())
 | |
|             return stream.getvalue()
 | |
| 
 | |
|         enemies_raw = []
 | |
|         groups = []
 | |
|         boss_groups = []
 | |
| 
 | |
|         if options["randomize_enemies"] == 0:
 | |
|             return stream.getvalue()
 | |
| 
 | |
|         if options["randomize_bosses"] == 2:
 | |
|             for pos in bosses:
 | |
|                 stream.seek(pos + 1)
 | |
|                 boss_groups += [stream.read(0x1F)]
 | |
| 
 | |
|         for pos in enemies:
 | |
|             stream.seek(pos + 8)
 | |
|             for _ in range(6):
 | |
|                 enemy = int.from_bytes(stream.read(1), "little")
 | |
|                 if enemy > 0:
 | |
|                     stream.seek(1, 1)
 | |
|                     flag = int.from_bytes(stream.read(1), "little")
 | |
|                     if flag == 0x7:
 | |
|                         break
 | |
|                     if flag in [0x0, 0x2, 0x4]:
 | |
|                         if enemy not in Data.pestnut and enemy not in Data.flying:
 | |
|                             enemies_raw += [enemy]
 | |
|                     stream.seek(1, 1)
 | |
|                 else:
 | |
|                     stream.seek(3, 1)
 | |
| 
 | |
|         random.shuffle(enemies_raw)
 | |
|         chomp = False
 | |
|         for pos in enemies:
 | |
|             stream.seek(pos + 8)
 | |
| 
 | |
|             for _ in range(6):
 | |
|                 enemy = int.from_bytes(stream.read(1), "little")
 | |
|                 if enemy > 0 and enemy not in Data.flying and enemy not in Data.pestnut:
 | |
|                     if enemy == 0x52:
 | |
|                         chomp = True
 | |
|                     stream.seek(1, 1)
 | |
|                     flag = int.from_bytes(stream.read(1), "little")
 | |
|                     if flag not in [0x0, 0x2, 0x4]:
 | |
|                         stream.seek(1, 1)
 | |
|                         continue
 | |
|                     stream.seek(-3, 1)
 | |
|                     stream.write(bytes([enemies_raw.pop()]))
 | |
|                     stream.seek(1, 1)
 | |
|                     stream.write(bytes([0x6]))
 | |
|                     stream.seek(1, 1)
 | |
|                 else:
 | |
|                     stream.seek(3, 1)
 | |
| 
 | |
|             stream.seek(pos + 1)
 | |
|             raw = stream.read(0x1F)
 | |
|             if chomp:
 | |
|                 raw = raw[0:3] + bytes([0x67, 0xAB, 0x28, 0x08]) + raw[7:]
 | |
|             else:
 | |
|                 raw = raw[0:3] + bytes([0xEE, 0x2C, 0x28, 0x08]) + raw[7:]
 | |
|             groups += [raw]
 | |
|             chomp = False
 | |
| 
 | |
|         arr = enemies
 | |
|         if options["randomize_bosses"] == 2:
 | |
|             arr += bosses
 | |
|             groups += boss_groups
 | |
| 
 | |
|         random.shuffle(groups)
 | |
| 
 | |
|         for pos in arr:
 | |
|             if arr[-1] in boss_groups:
 | |
|                 stream.seek(pos)
 | |
|                 temp = stream.read(1)
 | |
|                 stream.seek(pos)
 | |
|                 stream.write(bytes([temp[0] | 0x80]))
 | |
|             stream.seek(pos + 1)
 | |
|             stream.write(groups.pop())
 | |
| 
 | |
|         return stream.getvalue()
 | |
| 
 | |
| 
 | |
| class MLSSProcedurePatch(APProcedurePatch, APTokenMixin):
 | |
|     game = "Mario & Luigi Superstar Saga"
 | |
|     hash = "4b1a5897d89d9e74ec7f630eefdfd435"
 | |
|     patch_file_ending = ".apmlss"
 | |
|     result_file_ending = ".gba"
 | |
| 
 | |
|     procedure = [
 | |
|         ("apply_bsdiff4", ["base_patch.bsdiff4"]),
 | |
|         ("apply_tokens", ["token_data.bin"]),
 | |
|         ("enemy_randomize", []),
 | |
|         ("hidden_visible", []),
 | |
|         ("randomize_sounds", []),
 | |
|         ("randomize_music", []),
 | |
|     ]
 | |
| 
 | |
|     @classmethod
 | |
|     def get_source_data(cls) -> bytes:
 | |
|         return get_base_rom_as_bytes()
 | |
| 
 | |
| 
 | |
| def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
 | |
|     options_dict = {
 | |
|         "randomize_enemies": world.options.randomize_enemies.value,
 | |
|         "randomize_bosses": world.options.randomize_bosses.value,
 | |
|         "castle_skip": world.options.castle_skip.value,
 | |
|         "randomize_sounds": world.options.randomize_sounds.value,
 | |
|         "music_options": world.options.music_options.value,
 | |
|         "block_visibility": world.options.block_visibility.value,
 | |
|         "seed": world.multiworld.seed,
 | |
|         "player": world.player,
 | |
|     }
 | |
|     patch.write_file("options.json", json.dumps(options_dict).encode("UTF-8"))
 | |
| 
 | |
|     # Bake player name into ROM
 | |
|     patch.write_token(APTokenTypes.WRITE, 0xDF0000, world.multiworld.player_name[world.player].encode("UTF-8"))
 | |
| 
 | |
|     # Bake seed name into ROM
 | |
|     patch.write_token(APTokenTypes.WRITE, 0xDF00A0, world.multiworld.seed_name.encode("UTF-8"))
 | |
| 
 | |
|     # Intro Skip
 | |
|     patch.write_token(
 | |
|         APTokenTypes.WRITE,
 | |
|         0x244D08,
 | |
|         bytes([0x88, 0x0, 0x19, 0x91, 0x1, 0x20, 0x58, 0x1, 0xF, 0xA0, 0x3, 0x15, 0x27, 0x8]),
 | |
|     )
 | |
| 
 | |
|     # Patch S.S Chuckola Loading Zones
 | |
|     patch.write_token(APTokenTypes.WRITE, 0x25FD4E, bytes([0x48, 0x30, 0x80, 0x60, 0x50, 0x2, 0xF]))
 | |
| 
 | |
|     patch.write_token(APTokenTypes.WRITE, 0x25FD83, bytes([0x48, 0x30, 0x80, 0x60, 0xC0, 0x2, 0xF]))
 | |
| 
 | |
|     patch.write_token(APTokenTypes.WRITE, 0x25FDB8, bytes([0x48, 0x30, 0x05, 0x80, 0xE4, 0x0, 0xF]))
 | |
| 
 | |
|     patch.write_token(APTokenTypes.WRITE, 0x25FDED, bytes([0x48, 0x30, 0x06, 0x80, 0xE4, 0x0, 0xF]))
 | |
| 
 | |
|     patch.write_token(APTokenTypes.WRITE, 0x25FE22, bytes([0x48, 0x30, 0x07, 0x80, 0xE4, 0x0, 0xF]))
 | |
| 
 | |
|     patch.write_token(APTokenTypes.WRITE, 0x25FE57, bytes([0x48, 0x30, 0x08, 0x80, 0xE4, 0x0, 0xF]))
 | |
| 
 | |
|     if world.options.extra_pipes:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0xD00001, bytes([0x1]))
 | |
| 
 | |
|     if world.options.castle_skip:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0x3AEAB0, bytes([0xC1, 0x67, 0x0, 0x6, 0x1C, 0x08, 0x3]))
 | |
|         patch.write_token(APTokenTypes.WRITE, 0x3AEC18, bytes([0x89, 0x65, 0x0, 0xE, 0xA, 0x08, 0x1]))
 | |
| 
 | |
|     if world.options.skip_minecart:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0x3AC728, bytes([0x89, 0x13, 0x0, 0x10, 0xF, 0x08, 0x1]))
 | |
|         patch.write_token(APTokenTypes.WRITE, 0x3AC56C, bytes([0x49, 0x16, 0x0, 0x8, 0x8, 0x08, 0x1]))
 | |
| 
 | |
|     if world.options.scale_stats:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1]))
 | |
| 
 | |
|     patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
 | |
| 
 | |
|     if world.options.goal == 1:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0xD00008, bytes([world.options.goal.value]))
 | |
|         patch.write_token(APTokenTypes.WRITE, 0xD00009, bytes([world.options.emblems_required.value]))
 | |
| 
 | |
|     if world.options.tattle_hp:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))
 | |
| 
 | |
|     if world.options.music_options == 2:
 | |
|         patch.write_token(APTokenTypes.WRITE, 0x19B118, bytes([0x0, 0x25]))
 | |
| 
 | |
|     if world.options.randomize_backgrounds:
 | |
|         all_enemies = Data.enemies + Data.bosses
 | |
|         for address in all_enemies:
 | |
|             patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)]))
 | |
| 
 | |
|     for location_name in location_table.keys():
 | |
|         if location_name in world.disabled_locations:
 | |
|             continue
 | |
|         location = world.get_location(location_name)
 | |
|         item = location.item
 | |
|         address = [address for address in all_locations if address.name == location.name]
 | |
|         item_inject(world, patch, location.address, address[0].itemType, item)
 | |
|         if "Shop" in location_name and "Coffee" not in location_name and item.player != world.player:
 | |
|             desc_inject(world, patch, location, item)
 | |
| 
 | |
|     swap_colors(world, patch, world.options.mario_pants.value, 0, True)
 | |
|     swap_colors(world, patch, world.options.luigi_pants.value, 1, True)
 | |
|     swap_colors(world, patch, world.options.mario_color.value, 0)
 | |
|     swap_colors(world, patch, world.options.luigi_color.value, 1)
 | |
| 
 | |
|     patch.write_file("token_data.bin", patch.get_token_binary())
 | |
| 
 | |
| 
 | |
| def swap_colors(world: "MLSSWorld", patch: MLSSProcedurePatch, color: int, bro: int,
 | |
|                 pants_option: Optional[bool] = False):
 | |
|     if not pants_option and color == bro:
 | |
|         return
 | |
|     chaos = False
 | |
|     if not pants_option and color == 11 or color == 12:
 | |
|         chaos = True
 | |
|     if pants_option and color == 11:
 | |
|         chaos = True
 | |
|     for c in [c for c in (cpants[color] if pants_option else colors[color])
 | |
|               if (c[3] == bro if not chaos else c[1] == bro)]:
 | |
|         if chaos:
 | |
|             patch.write_token(APTokenTypes.WRITE, c[0],
 | |
|                               bytes([world.random.randint(0, 255), world.random.randint(0, 127)]))
 | |
|         else:
 | |
|             patch.write_token(APTokenTypes.WRITE, c[0], bytes([c[1], c[2]]))
 | |
| 
 | |
| 
 | |
| def item_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: int, item_type: int, item: Item):
 | |
|     if item.player == world.player:
 | |
|         code = item_table[item.name].itemID
 | |
|     else:
 | |
|         code = 0x3F
 | |
|     if item_type == 0:
 | |
|         patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
 | |
|     elif item_type == 1:
 | |
|         if code == 0x1D or code == 0x1E:
 | |
|             code += 0xE
 | |
|         if 0x20 <= code <= 0x26:
 | |
|             code -= 0x4
 | |
|         insert = int(code)
 | |
|         insert2 = insert % 0x10
 | |
|         insert2 *= 0x10
 | |
|         insert //= 0x10
 | |
|         insert += 0x20
 | |
|         patch.write_token(APTokenTypes.WRITE, location, bytes([insert, insert2]))
 | |
|     elif item_type == 2:
 | |
|         if code == 0x1D or code == 0x1E:
 | |
|             code += 0xE
 | |
|         if 0x20 <= code <= 0x26:
 | |
|             code -= 0x4
 | |
|         patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
 | |
|     elif item_type == 3:
 | |
|         if code == 0x1D or code == 0x1E:
 | |
|             code += 0xE
 | |
|         if code < 0x1D:
 | |
|             code -= 0xA
 | |
|         if 0x20 <= code <= 0x26:
 | |
|             code -= 0xE
 | |
|         patch.write_token(APTokenTypes.WRITE, location, bytes([code]))
 | |
|     else:
 | |
|         patch.write_token(APTokenTypes.WRITE, location, bytes([0x18]))
 | |
| 
 | |
| 
 | |
| def desc_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: Location, item: Item):
 | |
|     index = -1
 | |
|     for key, value in shop.items():
 | |
|         if location.address in value:
 | |
|             if key == 0x3C05F0:
 | |
|                 index = value.index(location.address)
 | |
|             else:
 | |
|                 index = value.index(location.address) + 14
 | |
| 
 | |
|     for key, value in badge.items():
 | |
|         if index != -1:
 | |
|             break
 | |
|         if location.address in value:
 | |
|             if key == 0x3C0618:
 | |
|                 index = value.index(location.address) + 24
 | |
|             else:
 | |
|                 index = value.index(location.address) + 41
 | |
| 
 | |
|     for key, value in pants.items():
 | |
|         if index != -1:
 | |
|             break
 | |
|         if location.address in value:
 | |
|             if key == 0x3C0618:
 | |
|                 index = value.index(location.address) + 48
 | |
|             else:
 | |
|                 index = value.index(location.address) + 66
 | |
| 
 | |
|     dstring = f"{world.multiworld.player_name[item.player]}: {item.name}"
 | |
|     patch.write_token(APTokenTypes.WRITE, 0xD12000 + (index * 0x40), dstring.encode("UTF8"))
 |