2024-05-06 07:15:06 +00:00
|
|
|
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, hidden, 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 all_locations:
|
|
|
|
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 = []
|
|
|
|
|
|
|
|
if options["randomize_enemies"] == 0:
|
|
|
|
return stream.getvalue()
|
|
|
|
|
|
|
|
if options["randomize_bosses"] == 2:
|
|
|
|
for pos in bosses:
|
|
|
|
stream.seek(pos + 1)
|
|
|
|
groups += [stream.read(0x1F)]
|
|
|
|
|
|
|
|
for pos in enemies:
|
|
|
|
stream.seek(pos + 8)
|
|
|
|
for _ in range(6):
|
|
|
|
enemy = int.from_bytes(stream.read(1))
|
|
|
|
if enemy > 0:
|
|
|
|
stream.seek(1, 1)
|
|
|
|
flag = int.from_bytes(stream.read(1))
|
|
|
|
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))
|
|
|
|
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))
|
|
|
|
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
|
|
|
|
|
|
|
|
random.shuffle(groups)
|
|
|
|
arr = enemies
|
|
|
|
if options["randomize_bosses"] == 2:
|
|
|
|
arr += bosses
|
|
|
|
|
|
|
|
for pos in arr:
|
|
|
|
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]))
|
|
|
|
|
2024-06-09 14:54:07 +00:00
|
|
|
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
|
2024-05-06 07:15:06 +00:00
|
|
|
|
|
|
|
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 (
|
|
|
|
(world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name)
|
|
|
|
or (world.options.castle_skip and "Bowser" in location_name)
|
|
|
|
or (world.options.disable_surf and "Surf Minigame" in location_name)
|
|
|
|
or (world.options.harhalls_pants and "Harhall's" in location_name)
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or (
|
|
|
|
world.options.chuckle_beans == 1 and location_table[location_name] in hidden
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
if not world.options.coins and "Coin" in location_name:
|
|
|
|
continue
|
|
|
|
location = world.multiworld.get_location(location_name, world.player)
|
|
|
|
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, 0xD11000 + (index * 0x40), dstring.encode("UTF8"))
|