mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 04:01:32 -06:00

* First Pass removal of game-specific code * SMW, DKC3, and SM hooked into AutoClient * All SNES autoclients functional * Fix ALttP Deathlink * Don't default to being ALttP, and properly error check ctx.game * Adjust variable naming * In response to: > we should probably document usage somewhere. I'm open to suggestions of where this should be documented. I think the most valuable documentation for APIs is docstrings and full typing. about websockets change in imports - from websockets documentation: > For convenience, many public APIs can be imported from the websockets package. However, this feature is incompatible with static code analysis. It breaks autocompletion in an IDE or type checking with mypy. If you’re using such tools, use the real import paths. * todo note for python 3.11 typing.NotRequired * missed staging in previous commit * added missing death Game States for DeathLink Co-authored-by: beauxq <beauxq@users.noreply.github.com> Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
694 lines
35 KiB
Python
694 lines
35 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import asyncio
|
|
import shutil
|
|
import time
|
|
|
|
import Utils
|
|
|
|
from NetUtils import ClientStatus, color
|
|
from worlds.AutoSNIClient import SNIClient
|
|
|
|
from worlds.alttp import Shops, Regions
|
|
from .Rom import ROM_PLAYER_LIMIT
|
|
|
|
snes_logger = logging.getLogger("SNES")
|
|
|
|
GAME_ALTTP = "A Link to the Past"
|
|
|
|
# FXPAK Pro protocol memory mapping used by SNI
|
|
ROM_START = 0x000000
|
|
WRAM_START = 0xF50000
|
|
WRAM_SIZE = 0x20000
|
|
SRAM_START = 0xE00000
|
|
|
|
ROMNAME_START = SRAM_START + 0x2000
|
|
ROMNAME_SIZE = 0x15
|
|
|
|
INGAME_MODES = {0x07, 0x09, 0x0b}
|
|
ENDGAME_MODES = {0x19, 0x1a}
|
|
DEATH_MODES = {0x12}
|
|
|
|
SAVEDATA_START = WRAM_START + 0xF000
|
|
SAVEDATA_SIZE = 0x500
|
|
|
|
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
|
|
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
|
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
|
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
|
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
|
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
|
|
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
|
|
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
|
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
|
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
|
SHOP_LEN = (len(Shops.shop_table) * 3) + 5
|
|
|
|
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
|
|
|
|
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
|
|
|
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
|
"Blind's Hideout - Left": (0x11d, 0x20),
|
|
"Blind's Hideout - Right": (0x11d, 0x40),
|
|
"Blind's Hideout - Far Left": (0x11d, 0x80),
|
|
"Blind's Hideout - Far Right": (0x11d, 0x100),
|
|
'Secret Passage': (0x55, 0x10),
|
|
'Waterfall Fairy - Left': (0x114, 0x10),
|
|
'Waterfall Fairy - Right': (0x114, 0x20),
|
|
"King's Tomb": (0x113, 0x10),
|
|
'Floodgate Chest': (0x10b, 0x10),
|
|
"Link's House": (0x104, 0x10),
|
|
'Kakariko Tavern': (0x103, 0x10),
|
|
'Chicken House': (0x108, 0x10),
|
|
"Aginah's Cave": (0x10a, 0x10),
|
|
"Sahasrahla's Hut - Left": (0x105, 0x10),
|
|
"Sahasrahla's Hut - Middle": (0x105, 0x20),
|
|
"Sahasrahla's Hut - Right": (0x105, 0x40),
|
|
'Kakariko Well - Top': (0x2f, 0x10),
|
|
'Kakariko Well - Left': (0x2f, 0x20),
|
|
'Kakariko Well - Middle': (0x2f, 0x40),
|
|
'Kakariko Well - Right': (0x2f, 0x80),
|
|
'Kakariko Well - Bottom': (0x2f, 0x100),
|
|
'Lost Woods Hideout': (0xe1, 0x200),
|
|
'Lumberjack Tree': (0xe2, 0x200),
|
|
'Cave 45': (0x11b, 0x400),
|
|
'Graveyard Cave': (0x11b, 0x200),
|
|
'Checkerboard Cave': (0x126, 0x200),
|
|
'Mini Moldorm Cave - Far Left': (0x123, 0x10),
|
|
'Mini Moldorm Cave - Left': (0x123, 0x20),
|
|
'Mini Moldorm Cave - Right': (0x123, 0x40),
|
|
'Mini Moldorm Cave - Far Right': (0x123, 0x80),
|
|
'Mini Moldorm Cave - Generous Guy': (0x123, 0x400),
|
|
'Ice Rod Cave': (0x120, 0x10),
|
|
'Bonk Rock Cave': (0x124, 0x10),
|
|
'Desert Palace - Big Chest': (0x73, 0x10),
|
|
'Desert Palace - Torch': (0x73, 0x400),
|
|
'Desert Palace - Map Chest': (0x74, 0x10),
|
|
'Desert Palace - Compass Chest': (0x85, 0x10),
|
|
'Desert Palace - Big Key Chest': (0x75, 0x10),
|
|
'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400),
|
|
'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400),
|
|
'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400),
|
|
'Desert Palace - Boss': (0x33, 0x800),
|
|
'Eastern Palace - Compass Chest': (0xa8, 0x10),
|
|
'Eastern Palace - Big Chest': (0xa9, 0x10),
|
|
'Eastern Palace - Dark Square Pot Key': (0xba, 0x400),
|
|
'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400),
|
|
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
|
|
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
|
|
'Eastern Palace - Map Chest': (0xaa, 0x10),
|
|
'Eastern Palace - Boss': (0xc8, 0x800),
|
|
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
|
|
'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400),
|
|
'Hyrule Castle - Map Chest': (0x72, 0x10),
|
|
'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400),
|
|
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
|
|
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
|
|
'Sewers - Dark Cross': (0x32, 0x10),
|
|
'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
|
|
'Sewers - Secret Room - Left': (0x11, 0x10),
|
|
'Sewers - Secret Room - Middle': (0x11, 0x20),
|
|
'Sewers - Secret Room - Right': (0x11, 0x40),
|
|
'Sanctuary': (0x12, 0x10),
|
|
'Castle Tower - Room 03': (0xe0, 0x10),
|
|
'Castle Tower - Dark Maze': (0xd0, 0x10),
|
|
'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400),
|
|
'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400),
|
|
'Spectacle Rock Cave': (0xea, 0x400),
|
|
'Paradox Cave Lower - Far Left': (0xef, 0x10),
|
|
'Paradox Cave Lower - Left': (0xef, 0x20),
|
|
'Paradox Cave Lower - Right': (0xef, 0x40),
|
|
'Paradox Cave Lower - Far Right': (0xef, 0x80),
|
|
'Paradox Cave Lower - Middle': (0xef, 0x100),
|
|
'Paradox Cave Upper - Left': (0xff, 0x10),
|
|
'Paradox Cave Upper - Right': (0xff, 0x20),
|
|
'Spiral Cave': (0xfe, 0x10),
|
|
'Tower of Hera - Basement Cage': (0x87, 0x400),
|
|
'Tower of Hera - Map Chest': (0x77, 0x10),
|
|
'Tower of Hera - Big Key Chest': (0x87, 0x10),
|
|
'Tower of Hera - Compass Chest': (0x27, 0x20),
|
|
'Tower of Hera - Big Chest': (0x27, 0x10),
|
|
'Tower of Hera - Boss': (0x7, 0x800),
|
|
'Hype Cave - Top': (0x11e, 0x10),
|
|
'Hype Cave - Middle Right': (0x11e, 0x20),
|
|
'Hype Cave - Middle Left': (0x11e, 0x40),
|
|
'Hype Cave - Bottom': (0x11e, 0x80),
|
|
'Hype Cave - Generous Guy': (0x11e, 0x400),
|
|
'Peg Cave': (0x127, 0x400),
|
|
'Pyramid Fairy - Left': (0x116, 0x10),
|
|
'Pyramid Fairy - Right': (0x116, 0x20),
|
|
'Brewery': (0x106, 0x10),
|
|
'C-Shaped House': (0x11c, 0x10),
|
|
'Chest Game': (0x106, 0x400),
|
|
'Mire Shed - Left': (0x10d, 0x10),
|
|
'Mire Shed - Right': (0x10d, 0x20),
|
|
'Superbunny Cave - Top': (0xf8, 0x10),
|
|
'Superbunny Cave - Bottom': (0xf8, 0x20),
|
|
'Spike Cave': (0x117, 0x10),
|
|
'Hookshot Cave - Top Right': (0x3c, 0x10),
|
|
'Hookshot Cave - Top Left': (0x3c, 0x20),
|
|
'Hookshot Cave - Bottom Right': (0x3c, 0x80),
|
|
'Hookshot Cave - Bottom Left': (0x3c, 0x40),
|
|
'Mimic Cave': (0x10c, 0x10),
|
|
'Swamp Palace - Entrance': (0x28, 0x10),
|
|
'Swamp Palace - Map Chest': (0x37, 0x10),
|
|
'Swamp Palace - Pot Row Pot Key': (0x38, 0x400),
|
|
'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400),
|
|
'Swamp Palace - Hookshot Pot Key': (0x36, 0x400),
|
|
'Swamp Palace - Big Chest': (0x36, 0x10),
|
|
'Swamp Palace - Compass Chest': (0x46, 0x10),
|
|
'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400),
|
|
'Swamp Palace - Big Key Chest': (0x35, 0x10),
|
|
'Swamp Palace - West Chest': (0x34, 0x10),
|
|
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
|
|
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
|
|
'Swamp Palace - Waterfall Room': (0x66, 0x10),
|
|
'Swamp Palace - Waterway Pot Key': (0x16, 0x400),
|
|
'Swamp Palace - Boss': (0x6, 0x800),
|
|
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
|
|
"Thieves' Town - Map Chest": (0xdb, 0x10),
|
|
"Thieves' Town - Compass Chest": (0xdc, 0x10),
|
|
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
|
|
"Thieves' Town - Hallway Pot Key": (0xbc, 0x400),
|
|
"Thieves' Town - Spike Switch Pot Key": (0xab, 0x400),
|
|
"Thieves' Town - Attic": (0x65, 0x10),
|
|
"Thieves' Town - Big Chest": (0x44, 0x10),
|
|
"Thieves' Town - Blind's Cell": (0x45, 0x10),
|
|
"Thieves' Town - Boss": (0xac, 0x800),
|
|
'Skull Woods - Compass Chest': (0x67, 0x10),
|
|
'Skull Woods - Map Chest': (0x58, 0x20),
|
|
'Skull Woods - Big Chest': (0x58, 0x10),
|
|
'Skull Woods - Pot Prison': (0x57, 0x20),
|
|
'Skull Woods - Pinball Room': (0x68, 0x10),
|
|
'Skull Woods - Big Key Chest': (0x57, 0x10),
|
|
'Skull Woods - West Lobby Pot Key': (0x56, 0x400),
|
|
'Skull Woods - Bridge Room': (0x59, 0x10),
|
|
'Skull Woods - Spike Corner Key Drop': (0x39, 0x400),
|
|
'Skull Woods - Boss': (0x29, 0x800),
|
|
'Ice Palace - Jelly Key Drop': (0x0e, 0x400),
|
|
'Ice Palace - Compass Chest': (0x2e, 0x10),
|
|
'Ice Palace - Conveyor Key Drop': (0x3e, 0x400),
|
|
'Ice Palace - Freezor Chest': (0x7e, 0x10),
|
|
'Ice Palace - Big Chest': (0x9e, 0x10),
|
|
'Ice Palace - Iced T Room': (0xae, 0x10),
|
|
'Ice Palace - Many Pots Pot Key': (0x9f, 0x400),
|
|
'Ice Palace - Spike Room': (0x5f, 0x10),
|
|
'Ice Palace - Big Key Chest': (0x1f, 0x10),
|
|
'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400),
|
|
'Ice Palace - Map Chest': (0x3f, 0x10),
|
|
'Ice Palace - Boss': (0xde, 0x800),
|
|
'Misery Mire - Big Chest': (0xc3, 0x10),
|
|
'Misery Mire - Map Chest': (0xc3, 0x20),
|
|
'Misery Mire - Main Lobby': (0xc2, 0x10),
|
|
'Misery Mire - Bridge Chest': (0xa2, 0x10),
|
|
'Misery Mire - Spikes Pot Key': (0xb3, 0x400),
|
|
'Misery Mire - Spike Chest': (0xb3, 0x10),
|
|
'Misery Mire - Fishbone Pot Key': (0xa1, 0x400),
|
|
'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400),
|
|
'Misery Mire - Compass Chest': (0xc1, 0x10),
|
|
'Misery Mire - Big Key Chest': (0xd1, 0x10),
|
|
'Misery Mire - Boss': (0x90, 0x800),
|
|
'Turtle Rock - Compass Chest': (0xd6, 0x10),
|
|
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
|
|
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
|
|
'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400),
|
|
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
|
|
'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400),
|
|
'Turtle Rock - Big Key Chest': (0x14, 0x10),
|
|
'Turtle Rock - Big Chest': (0x24, 0x10),
|
|
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
|
|
'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80),
|
|
'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40),
|
|
'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20),
|
|
'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10),
|
|
'Turtle Rock - Boss': (0xa4, 0x800),
|
|
'Palace of Darkness - Shooter Room': (0x9, 0x10),
|
|
'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20),
|
|
'Palace of Darkness - Stalfos Basement': (0xa, 0x10),
|
|
'Palace of Darkness - Big Key Chest': (0x3a, 0x10),
|
|
'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10),
|
|
'Palace of Darkness - Map Chest': (0x2b, 0x10),
|
|
'Palace of Darkness - Compass Chest': (0x1a, 0x20),
|
|
'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10),
|
|
'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20),
|
|
'Palace of Darkness - Dark Maze - Top': (0x19, 0x10),
|
|
'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20),
|
|
'Palace of Darkness - Big Chest': (0x1a, 0x10),
|
|
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
|
|
'Palace of Darkness - Boss': (0x5a, 0x800),
|
|
'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400),
|
|
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
|
|
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
|
|
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
|
|
'Ganons Tower - Tile Room': (0x8d, 0x10),
|
|
'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10),
|
|
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
|
|
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
|
|
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
|
|
'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400),
|
|
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
|
|
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
|
|
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
|
|
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
|
|
'Ganons Tower - Map Chest': (0x8b, 0x10),
|
|
'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400),
|
|
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
|
|
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
|
|
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
|
|
'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40),
|
|
'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80),
|
|
"Ganons Tower - Bob's Chest": (0x8c, 0x80),
|
|
'Ganons Tower - Big Chest': (0x8c, 0x10),
|
|
'Ganons Tower - Big Key Room - Left': (0x1c, 0x20),
|
|
'Ganons Tower - Big Key Room - Right': (0x1c, 0x40),
|
|
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
|
|
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
|
|
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
|
|
'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400),
|
|
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
|
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
|
|
|
boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss',
|
|
'Desert Palace - Boss',
|
|
'Tower of Hera - Boss',
|
|
'Palace of Darkness - Boss',
|
|
'Swamp Palace - Boss',
|
|
'Skull Woods - Boss',
|
|
"Thieves' Town - Boss",
|
|
'Ice Palace - Boss',
|
|
'Misery Mire - Boss',
|
|
'Turtle Rock - Boss',
|
|
'Sahasrahla'}}
|
|
|
|
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
|
|
|
|
location_table_npc = {'Mushroom': 0x1000,
|
|
'King Zora': 0x2,
|
|
'Sahasrahla': 0x10,
|
|
'Blacksmith': 0x400,
|
|
'Magic Bat': 0x8000,
|
|
'Sick Kid': 0x4,
|
|
'Library': 0x80,
|
|
'Potion Shop': 0x2000,
|
|
'Old Man': 0x1,
|
|
'Ether Tablet': 0x100,
|
|
'Catfish': 0x20,
|
|
'Stumpy': 0x8,
|
|
'Bombos Tablet': 0x200}
|
|
|
|
location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()}
|
|
|
|
location_table_ow = {'Flute Spot': 0x2a,
|
|
'Sunken Treasure': 0x3b,
|
|
"Zora's Ledge": 0x81,
|
|
'Lake Hylia Island': 0x35,
|
|
'Maze Race': 0x28,
|
|
'Desert Ledge': 0x30,
|
|
'Master Sword Pedestal': 0x80,
|
|
'Spectacle Rock': 0x3,
|
|
'Pyramid': 0x5b,
|
|
'Digging Game': 0x68,
|
|
'Bumper Cave Ledge': 0x4a,
|
|
'Floating Island': 0x5}
|
|
|
|
location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()}
|
|
|
|
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
|
'Purple Chest': (0x3c9, 0x10),
|
|
"Link's Uncle": (0x3c6, 0x1),
|
|
'Hobo': (0x3c9, 0x1)}
|
|
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
|
|
|
|
|
async def track_locations(ctx, roomid, roomdata):
|
|
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
|
new_locations = []
|
|
|
|
def new_check(location_id):
|
|
new_locations.append(location_id)
|
|
ctx.locations_checked.add(location_id)
|
|
location = ctx.location_names[location_id]
|
|
snes_logger.info(
|
|
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
|
|
|
try:
|
|
shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN)
|
|
shop_data_changed = False
|
|
shop_data = list(shop_data)
|
|
for cnt, b in enumerate(shop_data):
|
|
location = Shops.SHOP_ID_START + cnt
|
|
if int(b) and location not in ctx.locations_checked:
|
|
new_check(location)
|
|
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
|
|
shop_data_changed = True
|
|
if shop_data_changed:
|
|
snes_buffered_write(ctx, SHOP_ADDR, bytes(shop_data))
|
|
except Exception as e:
|
|
snes_logger.info(f"Exception: {e}")
|
|
|
|
for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items():
|
|
try:
|
|
if location_id not in ctx.locations_checked and loc_roomid == roomid and \
|
|
(roomdata << 4) & loc_mask != 0:
|
|
new_check(location_id)
|
|
except Exception as e:
|
|
snes_logger.exception(f"Exception: {e}")
|
|
|
|
uw_begin = 0x129
|
|
ow_end = uw_end = 0
|
|
uw_unchecked = {}
|
|
uw_checked = {}
|
|
for location, (roomid, mask) in location_table_uw.items():
|
|
location_id = Regions.lookup_name_to_id[location]
|
|
if location_id not in ctx.locations_checked:
|
|
uw_unchecked[location_id] = (roomid, mask)
|
|
uw_begin = min(uw_begin, roomid)
|
|
uw_end = max(uw_end, roomid + 1)
|
|
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)
|
|
|
|
if uw_begin < uw_end:
|
|
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
|
|
if uw_data is not None:
|
|
for location_id, (roomid, mask) in uw_unchecked.items():
|
|
offset = (roomid - uw_begin) * 2
|
|
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
|
if roomdata & mask != 0:
|
|
new_check(location_id)
|
|
if uw_checked:
|
|
uw_data = list(uw_data)
|
|
for location_id, (roomid, mask) in uw_checked.items():
|
|
offset = (roomid - uw_begin) * 2
|
|
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
|
roomdata |= mask
|
|
uw_data[offset] = roomdata & 0xFF
|
|
uw_data[offset + 1] = roomdata >> 8
|
|
snes_buffered_write(ctx, SAVEDATA_START + (uw_begin * 2), bytes(uw_data))
|
|
|
|
ow_begin = 0x82
|
|
ow_unchecked = {}
|
|
ow_checked = {}
|
|
for location_id, screenid in location_table_ow_id.items():
|
|
if location_id not in ctx.locations_checked:
|
|
ow_unchecked[location_id] = screenid
|
|
ow_begin = min(ow_begin, screenid)
|
|
ow_end = max(ow_end, screenid + 1)
|
|
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
|
|
|
|
if ow_begin < ow_end:
|
|
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
|
|
if ow_data is not None:
|
|
for location_id, screenid in ow_unchecked.items():
|
|
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
|
new_check(location_id)
|
|
if ow_checked:
|
|
ow_data = list(ow_data)
|
|
for location_id, screenid in ow_checked.items():
|
|
ow_data[screenid - ow_begin] |= 0x40
|
|
snes_buffered_write(ctx, SAVEDATA_START + 0x280 + ow_begin, bytes(ow_data))
|
|
|
|
if not ctx.locations_checked.issuperset(location_table_npc_id):
|
|
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
|
if npc_data is not None:
|
|
npc_value_changed = False
|
|
npc_value = npc_data[0] | (npc_data[1] << 8)
|
|
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 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:
|
|
npc_value |= mask
|
|
npc_value_changed = True
|
|
if npc_value_changed:
|
|
npc_data = bytes([npc_value & 0xFF, npc_value >> 8])
|
|
snes_buffered_write(ctx, SAVEDATA_START + 0x410, npc_data)
|
|
|
|
if not ctx.locations_checked.issuperset(location_table_misc_id):
|
|
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
|
if misc_data is not None:
|
|
misc_data = list(misc_data)
|
|
misc_data_changed = False
|
|
for location_id, (offset, mask) in location_table_misc_id.items():
|
|
assert (0x3c6 <= offset <= 0x3c9)
|
|
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
|
|
new_check(location_id)
|
|
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
|
|
if misc_data_changed:
|
|
snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data))
|
|
|
|
|
|
if new_locations:
|
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
|
await snes_flush_writes(ctx)
|
|
|
|
|
|
def get_alttp_settings(romfile: str):
|
|
lastSettings = Utils.get_adjuster_settings(GAME_ALTTP)
|
|
adjustedromfile = ''
|
|
if lastSettings:
|
|
choice = 'no'
|
|
if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply:
|
|
|
|
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
|
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
|
|
"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 = {}
|
|
for sprite in lastSettings.sprite_pool:
|
|
if sprite in sprite_pool:
|
|
sprite_pool[sprite] += 1
|
|
else:
|
|
sprite_pool[sprite] = 1
|
|
if sprite_pool:
|
|
printed_options["sprite_pool"] = sprite_pool
|
|
import pprint
|
|
|
|
from CommonClient import gui_enabled
|
|
if gui_enabled:
|
|
|
|
try:
|
|
from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button
|
|
applyPromptWindow = Tk()
|
|
except Exception as e:
|
|
logging.error('Could not load tkinter, which is likely not installed.')
|
|
return '', False
|
|
|
|
applyPromptWindow.resizable(False, False)
|
|
applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick())
|
|
logo = PhotoImage(file=Utils.local_path('data', 'icon.png'))
|
|
applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo)
|
|
applyPromptWindow.wm_title("Last adjuster settings LttP")
|
|
|
|
label = LabelFrame(applyPromptWindow,
|
|
text='Last used adjuster settings were found. Would you like to apply these?')
|
|
label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5)
|
|
label.grid_columnconfigure(0, weight=1)
|
|
label.grid_columnconfigure(1, weight=1)
|
|
label.grid_columnconfigure(2, weight=1)
|
|
label.grid_columnconfigure(3, weight=1)
|
|
|
|
def onButtonClick(answer: str = 'no'):
|
|
setattr(onButtonClick, 'choice', answer)
|
|
applyPromptWindow.destroy()
|
|
|
|
framedOptions = Frame(label)
|
|
framedOptions.grid(column=0, columnspan=4, row=0)
|
|
framedOptions.grid_columnconfigure(0, weight=1)
|
|
framedOptions.grid_columnconfigure(1, weight=1)
|
|
framedOptions.grid_columnconfigure(2, weight=1)
|
|
curRow = 0
|
|
curCol = 0
|
|
for name, value in printed_options.items():
|
|
Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5)
|
|
if (curCol == 2):
|
|
curRow += 1
|
|
curCol = 0
|
|
else:
|
|
curCol += 1
|
|
|
|
yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10)
|
|
yesButton.grid(column=0, row=1)
|
|
noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10)
|
|
noButton.grid(column=1, row=1)
|
|
alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10)
|
|
alwaysButton.grid(column=2, row=1)
|
|
neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10)
|
|
neverButton.grid(column=3, row=1)
|
|
|
|
Utils.tkinter_center_window(applyPromptWindow)
|
|
applyPromptWindow.mainloop()
|
|
choice = getattr(onButtonClick, 'choice')
|
|
else:
|
|
choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
|
f"{pprint.pformat(printed_options)}\n"
|
|
f"Enter yes, no, always or never: ")
|
|
if choice and choice.startswith("y"):
|
|
choice = 'yes'
|
|
elif choice and "never" in choice:
|
|
choice = 'no'
|
|
lastSettings.auto_apply = 'never'
|
|
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
|
|
elif choice and "always" in choice:
|
|
choice = 'yes'
|
|
lastSettings.auto_apply = 'always'
|
|
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
|
|
else:
|
|
choice = 'no'
|
|
elif 'never' in lastSettings.auto_apply:
|
|
choice = 'no'
|
|
elif 'always' in lastSettings.auto_apply:
|
|
choice = 'yes'
|
|
|
|
if 'yes' in choice:
|
|
from worlds.alttp.Rom import get_base_rom_path
|
|
lastSettings.rom = romfile
|
|
lastSettings.baserom = get_base_rom_path()
|
|
lastSettings.world = None
|
|
|
|
if hasattr(lastSettings, "sprite_pool"):
|
|
from LttPAdjuster import AdjusterWorld
|
|
lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool"))
|
|
|
|
adjusted = True
|
|
import LttPAdjuster
|
|
_, adjustedromfile = LttPAdjuster.adjust(lastSettings)
|
|
|
|
if hasattr(lastSettings, "world"):
|
|
delattr(lastSettings, "world")
|
|
else:
|
|
adjusted = False
|
|
if adjusted:
|
|
try:
|
|
shutil.move(adjustedromfile, romfile)
|
|
adjustedromfile = romfile
|
|
except Exception as e:
|
|
logging.exception(e)
|
|
else:
|
|
adjusted = False
|
|
return adjustedromfile, adjusted
|
|
|
|
|
|
class ALTTPSNIClient(SNIClient):
|
|
game = "A Link to the Past"
|
|
|
|
async def deathlink_kill_player(self, ctx):
|
|
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
|
|
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
|
|
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
|
await asyncio.sleep(0.25)
|
|
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
|
if not invincible or not last_health or not health:
|
|
ctx.death_state = DeathState.dead
|
|
ctx.last_death_link = time.time()
|
|
return
|
|
if not invincible[0] and last_health[0] == health[0]:
|
|
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
|
|
snes_buffered_write(ctx, WRAM_START + 0x0373,
|
|
bytes([8])) # deal 1 full heart of damage at next opportunity
|
|
|
|
await snes_flush_writes(ctx)
|
|
await asyncio.sleep(1)
|
|
|
|
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
|
if not gamemode or gamemode[0] in DEATH_MODES:
|
|
ctx.death_state = DeathState.dead
|
|
|
|
|
|
async def validate_rom(self, ctx):
|
|
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
|
|
|
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
|
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP":
|
|
return False
|
|
|
|
ctx.game = self.game
|
|
ctx.items_handling = 0b001 # full local
|
|
|
|
ctx.rom = rom_name
|
|
|
|
death_link = await snes_read(ctx, 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))
|
|
|
|
return True
|
|
|
|
|
|
async def game_watcher(self, ctx):
|
|
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
|
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
|
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
|
currently_dead = gamemode[0] in DEATH_MODES
|
|
await ctx.handle_deathlink_state(currently_dead)
|
|
|
|
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
|
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
|
if gamemode is None or gameend is None or game_timer is None or \
|
|
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
|
return
|
|
|
|
if gameend[0]:
|
|
if not ctx.finished_game:
|
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
|
ctx.finished_game = True
|
|
|
|
if gamemode in ENDGAME_MODES: # triforce room and credits
|
|
return
|
|
|
|
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
|
if data is None:
|
|
return
|
|
|
|
recv_index = data[0] | (data[1] << 8)
|
|
recv_item = data[2]
|
|
roomid = data[4] | (data[5] << 8)
|
|
roomdata = data[6]
|
|
scout_location = data[7]
|
|
|
|
if recv_index < len(ctx.items_received) and recv_item == 0:
|
|
item = ctx.items_received[recv_index]
|
|
recv_index += 1
|
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
|
color(ctx.item_names[item.item], 'red', 'bold'),
|
|
color(ctx.player_names[item.player], 'yellow'),
|
|
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
|
|
|
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
|
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
|
snes_buffered_write(ctx, RECV_ITEM_ADDR,
|
|
bytes([item.item]))
|
|
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
|
|
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
|
|
if scout_location > 0 and scout_location in ctx.locations_info:
|
|
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
|
|
bytes([scout_location]))
|
|
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
|
|
bytes([ctx.locations_info[scout_location].item]))
|
|
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
|
|
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
|
|
|
|
await snes_flush_writes(ctx)
|
|
|
|
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
|
ctx.locations_scouted.add(scout_location)
|
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
|
await track_locations(ctx, roomid, roomdata)
|