diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 472b9900..5e4996fe 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -53,6 +53,7 @@ def main(): ''') parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true') parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true') + parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true') parser.add_argument('--disablemusic', help='Disables game music.', action='store_true') parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'], @@ -155,7 +156,7 @@ def adjust(args): apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music, args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world, - deathlink=args.deathlink) + deathlink=args.deathlink, allowcollect=args.allowcollect) path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc') rom.write_to_file(path) @@ -210,6 +211,7 @@ def adjustGUI(): guiargs.music = bool(rom_vars.MusicVar.get()) guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get()) guiargs.deathlink = bool(rom_vars.DeathLinkVar.get()) + guiargs.allowcollect = bool(rom_vars.AllowCollectVar.get()) guiargs.rom = romVar2.get() guiargs.baserom = romVar.get() guiargs.sprite = rom_vars.sprite @@ -246,6 +248,7 @@ def adjustGUI(): guiargs.music = bool(rom_vars.MusicVar.get()) guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get()) guiargs.deathlink = bool(rom_vars.DeathLinkVar.get()) + guiargs.allowcollect = bool(rom_vars.AllowCollectVar.get()) guiargs.baserom = romVar.get() if isinstance(rom_vars.sprite, Sprite): guiargs.sprite = rom_vars.sprite.name @@ -508,6 +511,7 @@ def get_rom_options_frame(parent=None): adjuster_settings.music = True adjuster_settings.reduceflashing = True adjuster_settings.deathlink = False + adjuster_settings.allowcollect = False adjuster_settings.sprite = None adjuster_settings.quickswap = True adjuster_settings.menuspeed = 'normal' @@ -542,6 +546,10 @@ def get_rom_options_frame(parent=None): DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar) DeathLinkCheckbutton.grid(row=7, column=0, sticky=W) + vars.AllowCollectVar = IntVar(value=adjuster_settings.allowcollect) + AllowCollectCheckbutton = Checkbutton(romOptionsFrame, text="Allow Collect", variable=vars.AllowCollectVar) + AllowCollectCheckbutton.grid(row=8, column=0, sticky=W) + spriteDialogFrame = Frame(romOptionsFrame) spriteDialogFrame.grid(row=0, column=1) baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:') @@ -703,7 +711,7 @@ def get_rom_options_frame(parent=None): vars.auto_apply = StringVar(value=adjuster_settings.auto_apply) autoApplyFrame = Frame(romOptionsFrame) - autoApplyFrame.grid(row=8, column=0, columnspan=2, sticky=W) + autoApplyFrame.grid(row=9, column=0, columnspan=2, sticky=W) filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .apbp files") filler.pack(side=TOP, expand=True, fill=X) askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask') diff --git a/SNIClient.py b/SNIClient.py index 00143c03..e358cfba 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -124,6 +124,7 @@ class Context(CommonContext): self.snes_connector_lock = threading.Lock() self.death_state = DeathState.alive # for death link flop behaviour self.killing_player_task = None + self.allow_collect = False self.awaiting_rom = False self.rom = None @@ -872,7 +873,7 @@ async def track_locations(ctx: Context, roomid, roomdata): location = Shops.SHOP_ID_START + cnt if int(b) and location not in ctx.locations_checked: new_check(location) - if location in ctx.checked_locations and location not in ctx.locations_checked \ + if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \ and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot: if not int(b): shop_data[cnt] += 1 @@ -900,9 +901,9 @@ async def track_locations(ctx: Context, roomid, roomdata): uw_unchecked[location_id] = (roomid, mask) uw_begin = min(uw_begin, roomid) uw_end = max(uw_end, roomid + 1) - if location_id in ctx.checked_locations and location_id not in ctx.locations_checked and \ - location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot and \ - location_id not in boss_locations: + if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ + and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ + and ctx.locations_info[location_id].player != ctx.slot: uw_begin = min(uw_begin, roomid) uw_end = max(uw_end, roomid + 1) uw_checked[location_id] = (roomid, mask) @@ -933,7 +934,7 @@ async def track_locations(ctx: Context, roomid, roomdata): ow_unchecked[location_id] = screenid ow_begin = min(ow_begin, screenid) ow_end = max(ow_end, screenid + 1) - if location_id in ctx.checked_locations and location_id in ctx.locations_info \ + if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \ and ctx.locations_info[location_id].player != ctx.slot: ow_checked[location_id] = screenid @@ -957,7 +958,7 @@ async def track_locations(ctx: Context, roomid, roomdata): for location_id, mask in location_table_npc_id.items(): if npc_value & mask != 0 and location_id not in ctx.locations_checked: new_check(location_id) - if location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ + if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: npc_value |= mask npc_value_changed = True @@ -974,7 +975,7 @@ async def track_locations(ctx: Context, roomid, roomdata): assert (0x3c6 <= offset <= 0x3c9) if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked: new_check(location_id) - if location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ + if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: misc_data_changed = True misc_data[offset - 0x3c6] |= mask @@ -1030,6 +1031,7 @@ async def game_watcher(ctx: Context): death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else SM_DEATH_LINK_ACTIVE_ADDR, 1) if death_link: + ctx.allow_collect = bool(death_link[0] & 0b100) ctx.death_link_allow_survive = bool(death_link[0] & 0b10) await ctx.update_death_link(bool(death_link[0] & 0b1)) if not ctx.prev_rom or ctx.prev_rom != ctx.rom: @@ -1327,7 +1329,7 @@ def get_alttp_settings(romfile: str): whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", - "reduceflashing", "deathlink"} + "reduceflashing", "deathlink", "allowcollect"} printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist} if hasattr(lastSettings, "sprite_pool"): sprite_pool = {} diff --git a/Utils.py b/Utils.py index 6f79d688..b81e9cca 100644 --- a/Utils.py +++ b/Utils.py @@ -25,7 +25,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.1" +__version__ = "0.3.2" version_tuple = tuplize_version(__version__) from yaml import load, dump, SafeLoader diff --git a/playerSettings.yaml b/playerSettings.yaml index 30295f9c..716b9fa5 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -444,6 +444,11 @@ A Link to the Past: death_link: false: 50 true: 0 + + allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players. + # Off by default, because it currently crashes on real hardware. + false: 50 + true: 0 linked_options: - name: crosskeys diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 7e7b1c77..9c5db86c 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -299,6 +299,12 @@ class BeemizerTrapChance(BeemizerRange): display_name = "Beemizer Trap Chance" +class AllowCollect(Toggle): + """Allows for !collect / co-op to auto-open chests containing items for other players. + Off by default, because it currently crashes on real hardware.""" + display_name = "Allow Collection of checks for other players" + + alttp_options: typing.Dict[str, type(Option)] = { "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, @@ -334,6 +340,6 @@ alttp_options: typing.Dict[str, type(Option)] = { "glitch_boots": DefaultOnToggle, "beemizer_total_chance": BeemizerTotalChance, "beemizer_trap_chance": BeemizerTrapChance, - "death_link": DeathLink - + "death_link": DeathLink, + "allow_collect": AllowCollect } diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 9a602ae0..a2ee8481 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1765,7 +1765,7 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options, world=None, player=1, allow_random_on_event=False, reduceflashing=False, - triforcehud: str = None, deathlink: bool = False): + triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False): local_random = random if not world else world.slot_seeds[player] disable_music: bool = not music # enable instant item menu @@ -1899,7 +1899,9 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri elif palettes_options['dungeon'] == 'random': randomize_uw_palettes(rom, local_random) - rom.write_byte(0x18008D, int(deathlink)) + rom.write_byte(0x18008D, (0b00000001 if deathlink else 0) | + # 0b00000010 is already used for death_link_allow_survive in super metroid. + (0b00000100 if allowcollect else 0)) apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event, world.sprite_pool[player] if world else []) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 65894085..301dc4b3 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -295,7 +295,8 @@ class ALTTPWorld(World): palettes_options, world, player, True, reduceflashing=world.reduceflashing[player] or world.is_race, triforcehud=world.triforcehud[player].current_key, - deathlink=world.death_link[player]) + deathlink=world.death_link[player], + allowcollect=world.allow_collect[player]) outfilepname = f'_P{player}' outfilepname += f"_{world.get_file_safe_player_name(player).replace(' ', '_')}" \ @@ -324,7 +325,7 @@ class ALTTPWorld(World): multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]] def get_required_client_version(self) -> tuple: - return max((0, 3, 1), super(ALTTPWorld, self).get_required_client_version()) + return max((0, 3, 2), super(ALTTPWorld, self).get_required_client_version()) def create_item(self, name: str) -> Item: return ALttPItem(name, self.player, **as_dict_item_table[name])