LttP: move some LttP specific things more towards locations where they belong.

This commit is contained in:
Fabian Dill 2021-08-10 08:00:53 +02:00
parent 4bfeb77a3a
commit 299036ecca
8 changed files with 54 additions and 75 deletions

View File

@ -852,9 +852,8 @@ class Region(object):
def can_fill(self, item: Item): def can_fill(self, item: Item):
inside_dungeon_item = item.locked_dungeon_item inside_dungeon_item = item.locked_dungeon_item
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)' if inside_dungeon_item:
if sewer_hack or inside_dungeon_item: return self.dungeon.is_dungeon_item(item) and item.player == self.player
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
return True return True

View File

@ -130,7 +130,6 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
if world.mode[player] == 'standard' and world.keyshuffle[player] is True: if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
standard_keyshuffle_players.add(player) standard_keyshuffle_players.add(player)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if standard_keyshuffle_players: if standard_keyshuffle_players:
progitempool.sort( progitempool.sort(

View File

@ -19,7 +19,7 @@ from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox,
from urllib.parse import urlparse from urllib.parse import urlparse
from urllib.request import urlopen from urllib.request import urlopen
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, open_file from Utils import output_path, local_path, open_file
@ -222,7 +222,6 @@ def adjustGUI():
else: else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}") messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
from Utils import persistent_store from Utils import persistent_store
from worlds.alttp.Rom import Sprite
if isinstance(guiargs.sprite, Sprite): if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs) persistent_store("adjuster", "last_settings_3", guiargs)
@ -412,9 +411,8 @@ def get_rom_frame(parent=None):
def RomSelect(): def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")]) rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
import Patch
try: try:
Patch.get_base_rom_bytes(rom) # throws error on checksum fail get_base_rom_bytes(rom) # throws error on checksum fail
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
messagebox.showerror(title="Error while reading ROM", message=str(e)) messagebox.showerror(title="Error while reading ROM", message=str(e))

View File

@ -2,7 +2,6 @@ import bsdiff4
import yaml import yaml
import os import os
import lzma import lzma
import hashlib
import threading import threading
import concurrent.futures import concurrent.futures
import zipfile import zipfile
@ -10,37 +9,13 @@ import sys
from typing import Tuple, Optional from typing import Tuple, Optional
import Utils import Utils
from worlds.alttp.Rom import JAP10HASH
current_patch_version = 2 current_patch_version = 2
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["lttp_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name
def get_base_rom_bytes(file_name: str = "") -> bytes:
from worlds.alttp.Rom import read_rom
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import JAP10HASH
patch = yaml.dump({"meta": metadata, patch = yaml.dump({"meta": metadata,
"patch": patch, "patch": patch,
"game": "A Link to the Past", "game": "A Link to the Past",
@ -52,6 +27,7 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes: def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import get_base_rom_bytes
if metadata is None: if metadata is None:
metadata = {} metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom) patch = bsdiff4.diff(get_base_rom_bytes(), rom)
@ -71,6 +47,7 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
from worlds.alttp.Rom import get_base_rom_bytes
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
if not ignore_version and data["compatible_version"] > current_patch_version: if not ignore_version and data["compatible_version"] > current_patch_version:
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
@ -184,3 +161,11 @@ if __name__ == "__main__":
traceback.print_exc() traceback.print_exc()
input("Press enter to close.") input("Press enter to close.")
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer

View File

@ -286,9 +286,9 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
if adjuster_settings: if adjuster_settings:
import pprint import pprint
import Patch from worlds.alttp.Rom import get_base_rom_path
adjuster_settings.rom = romfile adjuster_settings.rom = romfile
adjuster_settings.baserom = Patch.get_base_rom_path() adjuster_settings.baserom = get_base_rom_path()
adjuster_settings.world = None adjuster_settings.world = None
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite"} "uw_palettes", "sprite"}

View File

@ -492,6 +492,7 @@ def set_up_take_anys(world, player):
world.initialize_regions() world.initialize_regions()
def create_dynamic_shop_locations(world, player): def create_dynamic_shop_locations(world, player):
for shop in world.shops: for shop in world.shops:
if shop.region.player == player: if shop.region.player == player:
@ -511,35 +512,7 @@ def create_dynamic_shop_locations(world, player):
loc.locked = True loc.locked = True
def fill_prizes(world, attempts=15):
all_state = world.get_all_state(keys=True)
for player in world.get_game_players("A Link to the Past"):
crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)
crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player),
world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player),
world.get_location('Misery Mire - Prize', player)]
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
for attempt in range(attempts):
try:
prizepool = unplaced_prizes.copy()
prize_locs = empty_crystal_locations.copy()
world.random.shuffle(prize_locs)
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
except FillError as e:
logging.getLogger('').exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
attempts - attempt)
for location in empty_crystal_locations:
location.item = None
continue
break
else:
raise FillError('Unable to place dungeon prizes')
def get_pool_core(world, player: int): def get_pool_core(world, player: int):
progressive = world.progressive[player]
shuffle = world.shuffle[player] shuffle = world.shuffle[player]
difficulty = world.difficulty[player] difficulty = world.difficulty[player]
timer = world.timer[player] timer = world.timer[player]

View File

@ -1,4 +1,5 @@
import typing import typing
import random
from Options import Choice, Range, Option, Toggle, DefaultOnToggle from Options import Choice, Range, Option, Toggle, DefaultOnToggle
@ -94,6 +95,7 @@ class Palette(Choice):
option_negative = 6 option_negative = 6
option_dizzy = 7 option_dizzy = 7
option_sick = 8 option_sick = 8
alias_random = 1
class OWPalette(Palette): class OWPalette(Palette):
@ -136,6 +138,11 @@ class HeartColor(Choice):
option_green = 2 option_green = 2
option_yellow = 3 option_yellow = 3
@classmethod
def from_text(cls, text: str) -> Choice:
# remove when this becomes a base Choice feature
if text == "random":
return cls(random.randint(0, 3))
class QuickSwap(DefaultOnToggle): class QuickSwap(DefaultOnToggle):
displayname = "L/R Quickswapping" displayname = "L/R Quickswapping"

View File

@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
import Utils
from Patch import read_rom
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d' RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d'
@ -31,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
DeathMountain_texts, \ DeathMountain_texts, \
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
from worlds.alttp.Items import ItemFactory, item_table from worlds.alttp.Items import ItemFactory, item_table
from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.EntranceShuffle import door_addresses
import Patch import Patch
@ -168,14 +171,6 @@ class LocalRom(object):
self.write_int32(startaddress + (i * 4), value) self.write_int32(startaddress + (i * 4), value)
def read_rom(stream) -> bytearray:
"Reads rom into bytearray and strips off any smc header"
buffer = bytearray(stream.read())
if len(buffer) % 0x400 == 0x200:
buffer = buffer[0x200:]
return buffer
check_lock = threading.Lock() check_lock = threading.Lock()
@ -546,7 +541,6 @@ class Sprite():
self.valid = False self.valid = False
def get_vanilla_sprite_data(self): def get_vanilla_sprite_data(self):
from Patch import get_base_rom_path
file_name = get_base_rom_path() file_name = get_base_rom_path()
base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000] Sprite.sprite = base_rom_bytes[0x80000:0x87000]
@ -2934,3 +2928,27 @@ hash_alphabet = [
"Lamp", "Hammer", "Shovel", "Flute", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", "Lamp", "Hammer", "Shovel", "Flute", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key" "Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
] ]
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["lttp_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name