diff --git a/Adjuster.py b/Adjuster.py index d8118cc7..df923148 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -6,7 +6,7 @@ import textwrap import sys from AdjusterMain import adjust -from Rom import get_sprite_from_name +from worlds.alttp.Rom import get_sprite_from_name class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): diff --git a/AdjusterMain.py b/AdjusterMain.py index 167ab96a..b9497695 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -3,7 +3,7 @@ import time import logging from Utils import output_path -from Rom import LocalRom, apply_rom_settings +from worlds.alttp.Rom import LocalRom, apply_rom_settings def adjust(args): diff --git a/BaseClasses.py b/BaseClasses.py index dbed1e52..1c79392f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -5,16 +5,20 @@ from enum import Enum, unique import logging import json from collections import OrderedDict, Counter, deque -from typing import Union, Optional, List, Set, Dict +from typing import Union, Optional, List, Dict import secrets import random -from EntranceShuffle import door_addresses, indirect_connections +import worlds.alttp +from worlds.alttp.EntranceShuffle import door_addresses, indirect_connections from Utils import int16_as_bytes -from Items import item_name_groups +from worlds.alttp.Items import item_name_groups -class World(object): +class World(): + pass + +class MultiWorld(): debug_types = False player_names: list _region_cache: dict @@ -26,15 +30,7 @@ class World(object): def __init__(self, players: int, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): - if self.debug_types: - import inspect - methods = inspect.getmembers(self, predicate=inspect.ismethod) - for name, method in methods: - if name.startswith("_debug_"): - setattr(self, name[7:], method) - logging.debug(f"Set {self}.{name[7:]} to {method}") - self.get_location = self._debug_get_location self.random = random.Random() # world-local random state is saved in case of future use a # persistently running program with multiple worlds rolling concurrently self.players = players @@ -131,6 +127,10 @@ class World(object): set_player_attr('dark_room_logic', "lamp") set_player_attr('restrict_dungeon_item_on_boss', False) + self.worlds = [] + #for i in range(players): + # self.worlds.append(worlds.alttp.ALTTPWorld({}, i)) + def secure(self): self.random = secrets.SystemRandom() @@ -170,18 +170,6 @@ class World(object): self._recache() return self._region_cache[player][regionname] - def _debug_get_region(self, regionname: str, player: int) -> Region: - if type(regionname) != str: - raise TypeError(f"expected str, got {type(regionname)} instead") - try: - return self._region_cache[player][regionname] - except KeyError: - for region in self.regions: - if region.name == regionname and region.player == player: - assert not region.world # this should only happen before initialization - self._region_cache[player][regionname] = region - return region - raise KeyError('No such region %s for player %d' % (regionname, player)) def get_entrance(self, entrance: str, player: int) -> Entrance: try: @@ -190,19 +178,6 @@ class World(object): self._recache() return self._entrance_cache[entrance, player] - def _debug_get_entrance(self, entrance: str, player: int) -> Entrance: - if type(entrance) != str: - raise TypeError(f"expected str, got {type(entrance)} instead") - try: - return self._entrance_cache[(entrance, player)] - except KeyError: - for region in self.regions: - for exit in region.exits: - if exit.name == entrance and exit.player == player: - self._entrance_cache[(entrance, player)] = exit - return exit - - raise KeyError('No such entrance %s for player %d' % (entrance, player)) def get_location(self, location: str, player: int) -> Location: try: @@ -211,19 +186,6 @@ class World(object): self._recache() return self._location_cache[location, player] - def _debug_get_location(self, location: str, player: int) -> Location: - if type(location) != str: - raise TypeError(f"expected str, got {type(location)} instead") - try: - return self._location_cache[(location, player)] - except KeyError: - for region in self.regions: - for r_location in region.locations: - if r_location.name == location and r_location.player == player: - self._location_cache[(location, player)] = r_location - return r_location - - raise KeyError('No such location %s for player %d' % (location, player)) def get_dungeon(self, dungeonname: str, player: int) -> Dungeon: for dungeon in self.dungeons: @@ -231,13 +193,6 @@ class World(object): return dungeon raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) - def _debug_get_dungeon(self, dungeonname: str, player: int) -> Dungeon: - if type(dungeonname) != str: - raise TypeError(f"expected str, got {type(dungeonname)} instead") - for dungeon in self.dungeons: - if dungeon.name == dungeonname and dungeon.player == player: - return dungeon - raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) def get_all_state(self, keys=False) -> CollectionState: ret = CollectionState(self) @@ -291,7 +246,7 @@ class World(object): if keys: for p in range(1, self.players + 1): - from Items import ItemFactory + from worlds.alttp.Items import ItemFactory for item in ItemFactory( ['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)', @@ -432,7 +387,7 @@ class World(object): class CollectionState(object): - def __init__(self, parent: World): + def __init__(self, parent: MultiWorld): self.prog_items = Counter() self.world = parent self.reachable_regions = {player: set() for player in range(1, parent.players + 1)} @@ -1164,7 +1119,7 @@ class UpgradeShop(Shop): class Spoiler(object): - world: World + world: MultiWorld def __init__(self, world): self.world = world diff --git a/Gui.py b/Gui.py index a488aae7..d65fef16 100755 --- a/Gui.py +++ b/Gui.py @@ -15,10 +15,10 @@ import ModuleUpdate ModuleUpdate.update() from AdjusterMain import adjust -from EntranceRandomizer import parse_arguments +from worlds.alttp.EntranceRandomizer import parse_arguments from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress -from Main import main, get_seed, __version__ as MWVersion -from Rom import Sprite +from worlds.alttp.Main import main, get_seed, __version__ as MWVersion +from worlds.alttp.Rom import Sprite from Utils import is_bundled, local_path, output_path, open_file diff --git a/MultiClient.py b/MultiClient.py index 2efcf4c9..5b58ad60 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -33,7 +33,7 @@ from prompt_toolkit.patch_stdout import patch_stdout from NetUtils import Endpoint import WebUI -import Regions +from worlds.alttp import Regions import Utils diff --git a/MultiServer.py b/MultiServer.py index 69923d78..540bae20 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -24,8 +24,7 @@ import prompt_toolkit from prompt_toolkit.patch_stdout import patch_stdout from fuzzywuzzy import process as fuzzy_process -import Items -import Regions +from worlds.alttp import Items, Regions import Utils from Utils import get_item_name_from_id, get_location_name_from_address, \ ReceivedItem, _version_tuple, restricted_loads diff --git a/Mystery.py b/Mystery.py index 1b123c8f..3ee8976d 100644 --- a/Mystery.py +++ b/Mystery.py @@ -11,11 +11,11 @@ import ModuleUpdate ModuleUpdate.update() from Utils import parse_yaml -from Rom import get_sprite_from_name -from EntranceRandomizer import parse_arguments -from Main import main as ERmain -from Main import get_seed, seeddigits -from Items import item_name_groups, item_table +from worlds.alttp.Rom import get_sprite_from_name +from worlds.alttp.EntranceRandomizer import parse_arguments +from worlds.alttp.Main import main as ERmain +from worlds.alttp.Main import get_seed, seeddigits +from worlds.alttp.Items import item_name_groups, item_table def mystery_argparse(): diff --git a/Patch.py b/Patch.py index d8b1f500..26a9d261 100644 --- a/Patch.py +++ b/Patch.py @@ -10,8 +10,9 @@ import sys from typing import Tuple, Optional import Utils -from Rom import JAP10HASH +from worlds.alttp.Rom import JAP10HASH +current_patch_version = 1 def get_base_rom_path(file_name: str = "") -> str: options = Utils.get_options() @@ -23,7 +24,7 @@ def get_base_rom_path(file_name: str = "") -> str: def get_base_rom_bytes(file_name: str = "") -> bytes: - from Rom import read_rom + 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) @@ -42,6 +43,8 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: patch = yaml.dump({"meta": metadata, "patch": patch, "game": "alttp", + "compatible_version": 1, # minimum version of patch system expected for patching to be successful + "version": current_patch_version, "base_checksum": JAP10HASH}) return patch.encode(encoding="utf-8-sig") @@ -63,6 +66,8 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str def create_rom_bytes(patch_file: str) -> Tuple[dict, str, bytearray]: data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) + if data["compatible_version"] > current_patch_version: + raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"]) rom_hash = patched_data[int(0x7FC0):int(0x7FD5)] data["meta"]["hash"] = "".join(chr(x) for x in rom_hash) diff --git a/Utils.py b/Utils.py index 48739e22..01055770 100644 --- a/Utils.py +++ b/Utils.py @@ -181,12 +181,12 @@ def get_options() -> dict: def get_item_name_from_id(code): - import Items + from worlds.alttp import Items return Items.lookup_id_to_name.get(code, f'Unknown item (ID:{code})') def get_location_name_from_address(address): - import Regions + from worlds.alttp import Regions return Regions.lookup_id_to_name.get(address, f'Unknown location (ID:{address})') diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index dd0b72cc..19178822 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -6,9 +6,9 @@ import json from flask import request, flash, redirect, url_for, session, render_template -from EntranceRandomizer import parse_arguments -from Main import main as ERmain -from Main import get_seed, seeddigits +from worlds.alttp.EntranceRandomizer import parse_arguments +from worlds.alttp.Main import main as ERmain +from worlds.alttp.Main import get_seed, seeddigits import pickle from .models import * diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d7277910..3b179ed7 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -3,11 +3,9 @@ import collections from flask import render_template from werkzeug.exceptions import abort import datetime -import logging from uuid import UUID -import Items -import Regions +from worlds.alttp import Items, Regions from WebHostLib import app, cache, Room from Utils import Hint diff --git a/dumpSprites.py b/dumpSprites.py index c9289ea9..9c91d9ce 100644 --- a/dumpSprites.py +++ b/dumpSprites.py @@ -2,7 +2,7 @@ import argparse import json from os import listdir from os.path import isfile, join -from Rom import Sprite +from worlds.alttp.Rom import Sprite from Gui import get_image_for_sprite parser = argparse.ArgumentParser(description='Dump sprite data and .png files to a directory.') diff --git a/test/TestBase.py b/test/TestBase.py index df5c981f..b8f3df5b 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -1,7 +1,7 @@ import unittest from BaseClasses import CollectionState -from Items import ItemFactory +from worlds.alttp.Items import ItemFactory class TestBase(unittest.TestCase): diff --git a/test/dungeons/TestDungeon.py b/test/dungeons/TestDungeon.py index b4a4bb06..8f52fc62 100644 --- a/test/dungeons/TestDungeon.py +++ b/test/dungeons/TestDungeon.py @@ -1,18 +1,18 @@ import unittest -from BaseClasses import World, CollectionState -from Dungeons import create_dungeons, get_dungeon_item_pool -from EntranceShuffle import mandatory_connections, connect_simple -from ItemPool import difficulties, generate_itempool -from Items import ItemFactory -from Regions import create_regions, create_shops -from Rules import set_rules +from BaseClasses import MultiWorld, CollectionState +from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple +from worlds.alttp.ItemPool import difficulties, generate_itempool +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import create_regions, create_shops +from worlds.alttp.Rules import set_rules class TestDungeon(unittest.TestCase): def setUp(self): - self.world = World(1, {1:'vanilla'}, {1:'noglitches'}, {1:'open'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, + True, {1:False}, False, None, {1:False}) self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.world.difficulty_requirements[1] = difficulties['normal'] diff --git a/test/inverted/TestInverted.py b/test/inverted/TestInverted.py index 93a52c57..0e403515 100644 --- a/test/inverted/TestInverted.py +++ b/test/inverted/TestInverted.py @@ -1,18 +1,18 @@ -from BaseClasses import World -from Dungeons import create_dungeons, get_dungeon_item_pool -from EntranceShuffle import link_inverted_entrances -from InvertedRegions import create_inverted_regions -from ItemPool import generate_itempool, difficulties -from Items import ItemFactory -from Regions import mark_light_world_regions, create_shops -from Rules import set_rules +from BaseClasses import MultiWorld +from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.EntranceShuffle import link_inverted_entrances +from worlds.alttp.InvertedRegions import create_inverted_regions +from worlds.alttp.ItemPool import generate_itempool, difficulties +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import mark_light_world_regions, create_shops +from worlds.alttp.Rules import set_rules from test.TestBase import TestBase class TestInverted(TestBase): def setUp(self): - self.world = World(1, {1:'vanilla'}, {1:'noglitches'}, {1:'inverted'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, + True, {1:False}, False, None, {1:False}) self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/inverted/TestInvertedBombRules.py b/test/inverted/TestInvertedBombRules.py index 109e26c6..716b8e9d 100644 --- a/test/inverted/TestInvertedBombRules.py +++ b/test/inverted/TestInvertedBombRules.py @@ -1,19 +1,19 @@ import unittest -from BaseClasses import World -from Dungeons import create_dungeons -from EntranceShuffle import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \ +from BaseClasses import MultiWorld +from worlds.alttp.Dungeons import create_dungeons +from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances, Inverted_LW_Dungeon_Entrances, Inverted_LW_Single_Cave_Doors, Inverted_Old_Man_Entrances, Inverted_DW_Entrances, Inverted_DW_Dungeon_Entrances, Inverted_DW_Single_Cave_Doors, \ Inverted_LW_Entrances_Must_Exit, Inverted_LW_Dungeon_Entrances_Must_Exit, Inverted_Bomb_Shop_Multi_Cave_Doors, Inverted_Bomb_Shop_Single_Cave_Doors, Blacksmith_Single_Cave_Doors, Inverted_Blacksmith_Multi_Cave_Doors -from InvertedRegions import create_inverted_regions -from ItemPool import difficulties -from Rules import set_inverted_big_bomb_rules +from worlds.alttp.InvertedRegions import create_inverted_regions +from worlds.alttp.ItemPool import difficulties +from worlds.alttp.Rules import set_inverted_big_bomb_rules class TestInvertedBombRules(unittest.TestCase): def setUp(self): - self.world = World(1, {1:'vanilla'}, {1:'noglitches'}, {1:'inverted'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, + True, {1:False}, False, None, {1:False}) self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 7cf44c84..303435ae 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -1,18 +1,18 @@ -from BaseClasses import World -from Dungeons import create_dungeons, get_dungeon_item_pool -from EntranceShuffle import link_inverted_entrances -from InvertedRegions import create_inverted_regions -from ItemPool import generate_itempool, difficulties -from Items import ItemFactory -from Regions import mark_light_world_regions, create_shops -from Rules import set_rules +from BaseClasses import MultiWorld +from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.EntranceShuffle import link_inverted_entrances +from worlds.alttp.InvertedRegions import create_inverted_regions +from worlds.alttp.ItemPool import generate_itempool, difficulties +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import mark_light_world_regions, create_shops +from worlds.alttp.Rules import set_rules from test.TestBase import TestBase class TestInvertedOWG(TestBase): def setUp(self): - self.world = World(1, {1:'vanilla'}, {1:'owglitches'}, {1:'inverted'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'owglitches'}, {1: 'inverted'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, + True, {1:False}, False, None, {1:False}) self.world.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/items/TestDifficulty.py b/test/items/TestDifficulty.py index ee884f63..8fee56f3 100644 --- a/test/items/TestDifficulty.py +++ b/test/items/TestDifficulty.py @@ -1,4 +1,4 @@ -from ItemPool import difficulties +from worlds.alttp.ItemPool import difficulties from test.TestBase import TestBase base_items = 41 diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index bd22db13..8aa67392 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -1,18 +1,18 @@ -from BaseClasses import World -from Dungeons import create_dungeons, get_dungeon_item_pool -from EntranceShuffle import link_entrances -from InvertedRegions import mark_dark_world_regions -from ItemPool import difficulties, generate_itempool -from Items import ItemFactory -from Regions import create_regions, create_shops -from Rules import set_rules +from BaseClasses import MultiWorld +from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.EntranceShuffle import link_entrances +from worlds.alttp.InvertedRegions import mark_dark_world_regions +from worlds.alttp.ItemPool import difficulties, generate_itempool +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import create_regions, create_shops +from worlds.alttp.Rules import set_rules from test.TestBase import TestBase class TestVanillaOWG(TestBase): def setUp(self): - self.world = World(1, {1:'vanilla'}, {1:'owglitches'}, {1:'open'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'owglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, + True, {1:False}, False, None, {1:False}) self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/test/vanilla/TestVanilla.py b/test/vanilla/TestVanilla.py index e21b8408..42c8be40 100644 --- a/test/vanilla/TestVanilla.py +++ b/test/vanilla/TestVanilla.py @@ -1,18 +1,18 @@ -from BaseClasses import World -from Dungeons import create_dungeons, get_dungeon_item_pool -from EntranceShuffle import link_entrances -from InvertedRegions import mark_dark_world_regions -from ItemPool import difficulties, generate_itempool -from Items import ItemFactory -from Regions import create_regions, create_shops -from Rules import set_rules +from BaseClasses import MultiWorld +from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.EntranceShuffle import link_entrances +from worlds.alttp.InvertedRegions import mark_dark_world_regions +from worlds.alttp.ItemPool import difficulties, generate_itempool +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import create_regions, create_shops +from worlds.alttp.Rules import set_rules from test.TestBase import TestBase class TestVanilla(TestBase): def setUp(self): - self.world = World(1, {1:'vanilla'}, {1:'noglitches'}, {1:'open'}, {1:'random'}, {1:'normal'}, {1:'normal'}, {1:False}, {1:'on'}, {1:'ganon'}, 'balanced', {1:'items'}, - True, {1:False}, False, None, {1:False}) + self.world = MultiWorld(1, {1: 'vanilla'}, {1: 'noglitches'}, {1: 'open'}, {1: 'random'}, {1: 'normal'}, {1: 'normal'}, {1:False}, {1: 'on'}, {1: 'ganon'}, 'balanced', {1: 'items'}, + True, {1:False}, False, None, {1:False}) self.world.difficulty_requirements[1] = difficulties['normal'] create_regions(self.world, 1) create_dungeons(self.world, 1) diff --git a/worlds/__init__.py b/worlds/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Bosses.py b/worlds/alttp/Bosses.py similarity index 100% rename from Bosses.py rename to worlds/alttp/Bosses.py diff --git a/Dungeons.py b/worlds/alttp/Dungeons.py similarity index 98% rename from Dungeons.py rename to worlds/alttp/Dungeons.py index 9e4111e8..758ce0f7 100644 --- a/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -1,8 +1,8 @@ from BaseClasses import Dungeon -from Bosses import BossFactory +from worlds.alttp.Bosses import BossFactory from Fill import fill_restrictive -from Items import ItemFactory -from Regions import lookup_boss_drops +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import lookup_boss_drops def create_dungeons(world, player): diff --git a/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py old mode 100755 new mode 100644 similarity index 94% rename from EntranceRandomizer.py rename to worlds/alttp/EntranceRandomizer.py index 2d8b0cf1..98d6a2ab --- a/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -7,8 +7,8 @@ import textwrap import shlex import sys -from Main import main, get_seed -from Rom import get_sprite_from_name +from worlds.alttp.Main import main, get_seed +from worlds.alttp.Rom import get_sprite_from_name from Utils import is_bundled, close_console @@ -375,45 +375,4 @@ def parse_arguments(argv, no_defaults=False): else: getattr(ret, name)[player] = value - return ret - -def start(): - args = parse_arguments(None) - - if is_bundled() and len(sys.argv) == 1: - # for the bundled builds, if we have no arguments, the user - # probably wants the gui. Users of the bundled build who want the command line - # interface shouuld specify at least one option, possibly setting a value to a - # default if they like all the defaults - from Gui import guiMain - close_console() - guiMain() - sys.exit(0) - - # ToDo: Validate files further than mere existance - if not os.path.isfile(args.rom): - input( - 'Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom) - sys.exit(1) - if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in - args.sprite.values()]): - input('Could not find link sprite sheet at given location. \nPress Enter to exit.') - sys.exit(1) - - # set up logger - loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel] - logging.basicConfig(format='%(message)s', level=loglevel) - - if args.gui: - from Gui import guiMain - guiMain(args) - elif args.count is not None: - seed = args.seed - for _ in range(args.count): - main(seed=seed, args=args) - seed = get_seed() - else: - main(seed=args.seed, args=args) - -if __name__ == '__main__': - start() + return ret \ No newline at end of file diff --git a/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py similarity index 99% rename from EntranceShuffle.py rename to worlds/alttp/EntranceShuffle.py index a89ac837..49e6791e 100644 --- a/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1941,7 +1941,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): invalid_cave_connections = defaultdict(set) if world.logic[player] in ['owglitches', 'nologic']: - import OverworldGlitchRules + from worlds.alttp import OverworldGlitchRules for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): invalid_connections[entrance] = set() if entrance in must_be_exits: diff --git a/InvertedRegions.py b/worlds/alttp/InvertedRegions.py similarity index 99% rename from InvertedRegions.py rename to worlds/alttp/InvertedRegions.py index 0879b9e7..7d8711c0 100644 --- a/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -1,6 +1,6 @@ import collections from BaseClasses import RegionType -from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region +from worlds.alttp.Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region def create_inverted_regions(world, player): diff --git a/ItemPool.py b/worlds/alttp/ItemPool.py similarity index 99% rename from ItemPool.py rename to worlds/alttp/ItemPool.py index 132785a1..30894858 100644 --- a/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -2,11 +2,11 @@ from collections import namedtuple import logging from BaseClasses import Region, RegionType, ShopType, Location, TakeAny -from Bosses import place_bosses -from Dungeons import get_dungeon_item_pool -from EntranceShuffle import connect_entrance +from worlds.alttp.Bosses import place_bosses +from worlds.alttp.Dungeons import get_dungeon_item_pool +from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError, fill_restrictive -from Items import ItemFactory +from worlds.alttp.Items import ItemFactory # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. diff --git a/Items.py b/worlds/alttp/Items.py similarity index 100% rename from Items.py rename to worlds/alttp/Items.py diff --git a/Main.py b/worlds/alttp/Main.py similarity index 95% rename from Main.py rename to worlds/alttp/Main.py index 51f76a3d..ab450064 100644 --- a/Main.py +++ b/worlds/alttp/Main.py @@ -8,16 +8,16 @@ import time import zlib import concurrent.futures -from BaseClasses import World, CollectionState, Item, Region, Location, Shop -from Items import ItemFactory -from Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance -from InvertedRegions import create_inverted_regions, mark_dark_world_regions -from EntranceShuffle import link_entrances, link_inverted_entrances -from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string -from Rules import set_rules -from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive +from BaseClasses import MultiWorld, CollectionState, Item, Region, Location +from worlds.alttp.Items import ItemFactory +from worlds.alttp.Regions import create_regions, create_shops, mark_light_world_regions, lookup_vanilla_location_to_entrance +from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions +from worlds.alttp.EntranceShuffle import link_entrances, link_inverted_entrances +from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string +from worlds.alttp.Rules import set_rules +from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression -from ItemPool import generate_itempool, difficulties, fill_prizes +from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple import Patch @@ -39,9 +39,9 @@ def main(args, seed=None): start = time.perf_counter() # initialize the world - world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, - args.item_functionality, args.timer, args.progressive.copy(), args.goal, args.algorithm, - args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) + world = MultiWorld(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, + args.item_functionality, args.timer, args.progressive.copy(), args.goal, args.algorithm, + args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) logger = logging.getLogger('') world.seed = get_seed(seed) @@ -278,7 +278,7 @@ def main(args, seed=None): # collect ER hint info er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla"} - from Regions import RegionType + from worlds.alttp.Regions import RegionType for region in world.regions: if region.player in er_hint_data and region.locations: main_entrance = get_entrance_to_region(region) @@ -333,7 +333,7 @@ def main(args, seed=None): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) + ret = MultiWorld(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() diff --git a/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py similarity index 100% rename from OverworldGlitchRules.py rename to worlds/alttp/OverworldGlitchRules.py diff --git a/Regions.py b/worlds/alttp/Regions.py similarity index 100% rename from Regions.py rename to worlds/alttp/Regions.py diff --git a/Rom.py b/worlds/alttp/Rom.py similarity index 99% rename from Rom.py rename to worlds/alttp/Rom.py index b37ce38a..b625c95b 100644 --- a/Rom.py +++ b/worlds/alttp/Rom.py @@ -15,17 +15,17 @@ import concurrent.futures from typing import Optional from BaseClasses import CollectionState, ShopType, Region, Location -from Dungeons import dungeon_music_addresses -from Regions import location_table -from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable -from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts +from worlds.alttp.Dungeons import dungeon_music_addresses +from worlds.alttp.Regions import location_table +from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable +from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts -from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, \ +from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ 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_bundled -from Items import ItemFactory -from EntranceShuffle import door_addresses +from worlds.alttp.Items import ItemFactory +from worlds.alttp.EntranceShuffle import door_addresses import Patch try: @@ -455,7 +455,7 @@ class Sprite(object): @staticmethod def default_link_sprite(): - return Sprite(local_path('data', 'default.zspr')) + return Sprite(local_path('../../data', 'default.zspr')) def decode8(self, pos): arr = [[0 for _ in range(8)] for _ in range(8)] @@ -1397,7 +1397,7 @@ def patch_rom(world, rom, player, team, enemized): # set rom name # 21 bytes - from Main import __version__ + from worlds.alttp.Main import __version__ # TODO: Adjust Enemizer to accept AP and AD rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21] rom.name.extend([0] * (21 - len(rom.name))) @@ -1544,7 +1544,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr "randomize_overworld": ow_palettes == 'random' } if any(options.values()): - data_dir = local_path("data") if is_bundled() else None + data_dir = local_path("../../data") if is_bundled() else None offsets_array = build_offset_collections(options, data_dir) ColorF = z3pr.ColorF diff --git a/Rules.py b/worlds/alttp/Rules.py similarity index 99% rename from Rules.py rename to worlds/alttp/Rules.py index b86060bc..1d459c21 100644 --- a/Rules.py +++ b/worlds/alttp/Rules.py @@ -1,10 +1,10 @@ import collections import logging -import OverworldGlitchRules -from BaseClasses import RegionType, World, Entrance -from Items import ItemFactory, progression_items, item_name_groups -from OverworldGlitchRules import overworld_glitches_rules, no_logic_rules -from Bosses import GanonDefeatRule +from worlds.alttp import OverworldGlitchRules +from BaseClasses import RegionType, MultiWorld, Entrance +from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups +from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules +from worlds.alttp.Bosses import GanonDefeatRule def set_rules(world, player): @@ -124,7 +124,7 @@ def add_rule(spot, rule, combine='and'): spot.access_rule = lambda state: rule(state) and old_rule(state) -def add_lamp_requirement(world: World, spot, player: int, has_accessible_torch: bool = False): +def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False): if world.dark_room_logic[player] == "lamp": add_rule(spot, lambda state: state.has('Lamp', player)) elif world.dark_room_logic[player] == "torches": # implicitly lamp as well @@ -1371,7 +1371,7 @@ def set_inverted_big_bomb_rules(world, player): raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) -def set_bunny_rules(world: World, player: int, inverted: bool): +def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # regions for the exits of multi-entrance caves/drops that bunny cannot pass # Note spiral cave and two brothers house are passable in superbunny state for glitch logic with extra requirements. @@ -1453,7 +1453,7 @@ def set_bunny_rules(world: World, player: int, inverted: bool): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has_Mirror(player) and state.has_sword(player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() - or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()): + or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has_Mirror(player) and state.has_Boots(player)) elif location is not None and location.name in OverworldGlitchRules.get_superbunny_accessible_locations(): if new_region.name == 'Superbunny Cave (Bottom)' or region.name == 'Kakariko Well (top)': diff --git a/Text.py b/worlds/alttp/Text.py similarity index 100% rename from Text.py rename to worlds/alttp/Text.py diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py new file mode 100644 index 00000000..573a44b2 --- /dev/null +++ b/worlds/alttp/__init__.py @@ -0,0 +1,109 @@ +from BaseClasses import World + + +class ALTTPWorld(World): + def __init__(self, options, slot: int): + self._region_cache = {} + self.slot = slot + self.shuffle = shuffle + self.logic = logic + self.mode = mode + self.swords = swords + self.difficulty = difficulty + self.difficulty_adjustments = difficulty_adjustments + self.timer = timer + self.progressive = progressive + self.goal = goal + self.dungeons = [] + self.regions = [] + self.shops = [] + self.itempool = [] + self.seed = None + self.precollected_items = [] + self.state = CollectionState(self) + self._cached_entrances = None + self._cached_locations = None + self._entrance_cache = {} + self._location_cache = {} + self.required_locations = [] + self.light_world_light_cone = False + self.dark_world_light_cone = False + self.rupoor_cost = 10 + self.aga_randomness = True + self.lock_aga_door_in_escape = False + self.save_and_quit_from_boss = True + self.accessibility = accessibility + self.shuffle_ganon = shuffle_ganon + self.fix_gtower_exit = self.shuffle_ganon + self.retro = retro + self.custom = custom + self.customitemarray: List[int] = customitemarray + self.hints = hints + self.dynamic_regions = [] + self.dynamic_locations = [] + + + self.remote_items = False + self.required_medallions = ['Ether', 'Quake'] + self.swamp_patch_required = False + self.powder_patch_required = False + self.ganon_at_pyramid = True + self.ganonstower_vanilla = True + + + self.can_access_trock_eyebridge = None + self.can_access_trock_front = None + self.can_access_trock_big_chest = None + self.can_access_trock_middle = None + self.fix_fake_world = True + self.mapshuffle = False + self.compassshuffle = False + self.keyshuffle = False + self.bigkeyshuffle = False + self.difficulty_requirements = None + self.boss_shuffle = 'none' + self.enemy_shuffle = False + self.enemy_health = 'default' + self.enemy_damage = 'default' + self.killable_thieves = False + self.tile_shuffle = False + self.bush_shuffle = False + self.beemizer = 0 + self.escape_assist = [] + self.crystals_needed_for_ganon = 7 + self.crystals_needed_for_gt = 7 + self.open_pyramid = False + self.treasure_hunt_icon = 'Triforce Piece' + self.treasure_hunt_count = 0 + self.clock_mode = False + self.can_take_damage = True + self.glitch_boots = True + self.progression_balancing = True + self.local_items = set() + self.triforce_pieces_available = 30 + self.triforce_pieces_required = 20 + self.shop_shuffle = 'off' + self.shuffle_prizes = "g" + self.sprite_pool = [] + self.dark_room_logic = "lamp" + self.restrict_dungeon_item_on_boss = False + + @property + def sewer_light_cone(self): + return self.mode == "standard" + + @property + def fix_trock_doors(self): + return self.shuffle != 'vanilla' or self.mode == 'inverted' + + @property + def fix_skullwoods_exit(self): + return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'} + + @property + def fix_palaceofdarkness_exit(self): + return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'} + + @property + def fix_trock_exit(self): + return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'} \ No newline at end of file diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py new file mode 100644 index 00000000..e69de29b