diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 32cb5c25..77ea2599 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -35,18 +35,25 @@ def update(yes = False, force = False): if not os.path.exists(path): path = os.path.join(os.path.dirname(__file__), req_file) with open(path) as requirementsfile: - requirements = pkg_resources.parse_requirements(requirementsfile) - for requirement in requirements: - requirement = str(requirement) - try: - pkg_resources.require(requirement) - except pkg_resources.ResolutionError: - if not yes: - import traceback - traceback.print_exc() - input(f'Requirement {requirement} is not satisfied, press enter to install it') - update_command() - return + for line in requirementsfile: + if line.startswith('https://'): + # extract name and version from url + url = line.split(';')[0] + wheel = line.split('/')[-1] + name, version, _ = wheel.split('-',2) + line = f'{name}=={version}' + requirements = pkg_resources.parse_requirements(line) + for requirement in requirements: + requirement = str(requirement) + try: + pkg_resources.require(requirement) + except pkg_resources.ResolutionError: + if not yes: + import traceback + traceback.print_exc() + input(f'Requirement {requirement} is not satisfied, press enter to install it') + update_command() + return if __name__ == "__main__": diff --git a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md new file mode 100644 index 00000000..209a739e --- /dev/null +++ b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md @@ -0,0 +1,29 @@ +# Secret of Evermore + +## Where is the settings page? +The player settings page for this game is located here. It contains all options +necessary to configure and export a config file. + +## What does randomization do to this game? +Items which would normally be acquired throughout the game have been moved around! Progression logic remains, +so the game is always able to be completed. However, because of the item shuffle, the player may need to access certain +areas before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any +weapon is obtained. + +Additional help can be found in the [guide](https://github.com/black-sliver/evermizer/blob/feat-mw/guide.md). + +## What items and locations get shuffled? +All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells +and the dog can be randomized using yaml options. + +## Which items can be in another player's world? +Any of the items which can be shuffled may also be placed in another player's world. +Specific items can be limited to your own world using plando. + +## What does another world's item look like in Secret of Evermore? +Secret of Evermore will display "Sent an Item". Check the client output if you want to know which. + +## What happens when the player receives an item? +When the player receives an item, a popup will appear to show which item was received. Items won't be recieved while a +script is active such as when visiting Nobilia Market or during most Boss Fights. Once all scripts have ended, items +will be recieved. diff --git a/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md b/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md new file mode 100644 index 00000000..0ada1d9f --- /dev/null +++ b/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md @@ -0,0 +1,116 @@ +# Secret of Evermore Setup Guide + +## Required Software +- [SNI](https://github.com/alttpo/sni/releases) (included in Archipelago if already installed) +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI with ROM access + - [snes9x-rr win32.zip](https://github.com/gocha/snes9x-rr/releases) + + [socket.dll](http://www.nyo.fr/~skarsnik/socket.dll) + + [connector.lua](https://raw.githubusercontent.com/alttpo/sni/main/lua/Connector.lua) + - or [BizHawk](http://tasvideos.org/BizHawk.html) + - or [bsnes-plus-nwa](https://github.com/black-sliver/bsnes-plus) + - Or SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware +- Your Secret of Evermore US ROM file, probably named `Secret of Evermore (USA).sfc` + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? +Your config file contains a set of configuration options which provide the generator with information about how +it should generate your game. Each player of a multiworld will provide their own config file. This setup allows +each player to enjoy an experience customized for their taste, and different players in the same multiworld +can all have different options. + +### Where do I get a config file? +The [Player Settings](/games/Secret%20of%20Evermore/player-settings) page on the website allows you to configure your +personal settings and export a config file from them. + +### Verifying your config file +If you would like to validate your config file to make sure it works, you may do so on the +[YAML Validator](/mysterycheck) page. + +## Generating a Single-Player Game +Stand-alone "Evermizer" has a way of balancing single-player games, but may not always be on par feature-wise. +Head over to [evermizer.com](https://evermizer.com) if you want to try the official stand-alone, otherwise read below. + +1. Navigate to the [Player Settings](/games/Secret%20of%20Evermore/player-settings) page, configure your options, and + click the "Generate Game" button. +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Run your patch file through [apbpatch](https://evermizer.com/apbpatch) and load it in your emulator or console. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that +is done, the host will provide you with either a link to download your patch file, or with a zip file containing +everyone's patch files. Your patch file should have a `.apsoe` extension. + +Put your patch file on your desktop or somewhere convenient, open [apbpatch](https://evermizer.com/apbpatch) and +generate your ROM from it. Load the ROM file in your emulator or console. + +### Connect to SNI + +#### With an emulator +Start SNI either from the Archipelago install folder or the stand-alone version. +If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the `Connector.lua` file you downloaded above +6. If the script window complains about missing `socket.dll` make sure the DLL is in snes9x or the lua file's directory. + +##### BizHawk +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following + these menu options: + `Config --> Cores --> SNES --> BSNES` + Once you have changed the loaded core, you must restart BizHawk. +2. Load your ROM file if it hasn't already been loaded. +3. Click on the Tools menu and click on **Lua Console** +4. Click the button to open a new Lua script. +5. Select the `Connector.lua` file you downloaded above + +##### bsnes-plus-nwa +This should automatically connect to SNI. +If this is its first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +#### With hardware +This guide assumes you have downloaded the correct firmware for your device. If you have not +done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware +[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information +[on this page](http://usb2snes.com/#supported-platforms). + +1. Copy the ROM file to your SD card. +2. Load the ROM file from the menu. + +### Open the client +Open [ap-soeclient](http://evermizer.com/apclient) in a modern browser. Do not switch tabs, open it in a new window +if you want to use the browser while playing. Do not minimize the window with the client. + +The client should automatically connect to SNI, the "SNES" status should change to green. + +### Connect to the Archipelago Server +Enter `/connect server:port` in the client's command prompt and press enter. You'll find `server:port` on the same page +that had the patch file. + +### Play the game +When the game is loaded but not yet past the intro cutscene, the "Game" status is yellow. When the client shows "AP" as +green and "Game" as yellow, you're ready to play. The intro can be skipped pressing the START button and "Game" should +change to green. Congratulations on successfully joining a multiworld game! + +## Hosting a MultiWorld game +The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the website linked above. +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, + so they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index bdf15025..50e1964f 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -290,5 +290,24 @@ ] } ] + }, + { + "gameTitle": "Secret of Evermore", + "tutorials": [ + { + "name": "Multiworld Setup Guide", + "description": "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.", + "files": [ + { + "language": "English", + "filename": "secret-of-evermore/multiworld_en.md", + "link": "secret-of-evermore/multiworld/en", + "authors": [ + "Black Sliver" + ] + } + ] + } + ] } ] diff --git a/host.yaml b/host.yaml index 9b0e819b..bfc7ec1d 100644 --- a/host.yaml +++ b/host.yaml @@ -108,4 +108,6 @@ minecraft_options: oot_options: # File name of the OoT v1.0 ROM rom_file: "The Legend of Zelda - Ocarina of Time.z64" - rom_file: "The Legend of Zelda - Ocarina of Time.z64" +soe_options: + # File name of the SoE US ROM + rom_file: "Secret of Evermore (USA).sfc" diff --git a/worlds/soe/.gitignore b/worlds/soe/.gitignore new file mode 100644 index 00000000..aa3bbd16 --- /dev/null +++ b/worlds/soe/.gitignore @@ -0,0 +1,3 @@ +dump.py +pyevermizer +.pyevermizer diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py new file mode 100644 index 00000000..f25f2ada --- /dev/null +++ b/worlds/soe/Logic.py @@ -0,0 +1,50 @@ +from BaseClasses import MultiWorld +from ..AutoWorld import LogicMixin +from typing import Set +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? + +from . import pyevermizer + +# TODO: resolve/flatten/expand rules to get rid of recursion below where possible +# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) +rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] +# Logic.items are all items excluding non-progression items and duplicates +item_names: Set[str] = set() +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items()) + if item.name not in item_names and not item_names.add(item.name)] + + +# when this module is loaded, this mixin will extend BaseClasses.CollectionState +class SecretOfEvermoreLogic(LogicMixin): + def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: + """ + Returns reached count of one of evermizer's progress steps based on collected items. + i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP + """ + n = 0 + for item in items: + for pvd in item.provides: + if pvd[1] == progress: + if self.has(item.name, player): + n += self.item_count(item.name, player) * pvd[0] + if n >= max_count > 0: + return n + for rule in rules: + for pvd in rule.provides: + if pvd[1] == progress and pvd[0] > 0: + has = True + for req in rule.requires: + if not self._soe_has(req[1], world, player, req[0]): + has = False + break + if has: + n += pvd[0] + if n >= max_count > 0: + return n + return n + + def _soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: + """ + Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE + """ + return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py new file mode 100644 index 00000000..9eddd0e7 --- /dev/null +++ b/worlds/soe/Options.py @@ -0,0 +1,154 @@ +import typing +from Options import Option, Range, Choice, Toggle, DefaultOnToggle + + +class EvermizerFlags: + flags: typing.List[str] + + def to_flag(self) -> str: + return self.flags[self.value] + + +class EvermizerFlag: + flag: str + + def to_flag(self) -> str: + return self.flag if self.value != self.default else '' + + +class OffOnChaosChoice(Choice): + option_off = 0 + option_on = 1 + option_chaos = 2 + alias_false = 0 + alias_true = 1 + + +class Difficulty(EvermizerFlags, Choice): + """Changes relative spell cost and stuff""" + displayname = "Difficulty" + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_chaos = 3 # random is reserved pre 0.2 + default = 1 + flags = ['e', 'n', 'h', 'x'] + + +class MoneyModifier(Range): + """Money multiplier in %""" + displayname = "Money Modifier" + range_start = 1 + range_end = 2500 + default = 200 + + +class ExpModifier(Range): + """EXP multiplier for Weapons, Characters and Spells in %""" + displayname = "Exp Modifier" + range_start = 1 + range_end = 2500 + default = 200 + + +class FixSequence(EvermizerFlag, DefaultOnToggle): + """Fix some sequence breaks""" + displayname = "Fix Sequence" + flag = '1' + + +class FixCheats(EvermizerFlag, DefaultOnToggle): + """Fix cheats left in by the devs (not desert skip)""" + displayname = "Fix Cheats" + flag = '2' + + +class FixInfiniteAmmo(EvermizerFlag, Toggle): + """Fix infinite ammo glitch""" + displayname = "Fix Infinite Ammo" + flag = '5' + + +class FixAtlasGlitch(EvermizerFlag, Toggle): + """Fix atlas underflowing stats""" + displayname = "Fix Atlas Glitch" + flag = '6' + + +class FixWingsGlitch(EvermizerFlag, Toggle): + """Fix wings making you invincible in some areas""" + displayname = "Fix Wings Glitch" + flag = '7' + + +class ShorterDialogs(EvermizerFlag, Toggle): + """Cuts some dialogs""" + displayname = "Shorter Dialogs" + flag = '9' + + +class ShortBossRush(EvermizerFlag, Toggle): + """Start boss rush at Magmar, cut HP in half""" + displayname = "Short Boss Rush" + flag = 'f' + + +class Ingredienizer(EvermizerFlags, OffOnChaosChoice): + """Shuffles or randomizes spell ingredients""" + displayname = "Ingredienizer" + default = 1 + flags = ['i', '', 'I'] + + +class Sniffamizer(EvermizerFlags, OffOnChaosChoice): + """Shuffles or randomizes drops in sniff locations""" + displayname = "Sniffamizer" + default = 1 + flags = ['s', '', 'S'] + + +class Callbeadamizer(EvermizerFlags, OffOnChaosChoice): + """Shuffles call bead characters or spells""" + displayname = "Callbeadamizer" + default = 1 + flags = ['c', '', 'C'] + + +class Musicmizer(EvermizerFlag, Toggle): + """Randomize music for some rooms""" + displayname = "Musicmizer" + flag = 'm' + + +class Doggomizer(EvermizerFlags, OffOnChaosChoice): + """On shuffles dog per act, Chaos randomizes dog per screen, Pupdunk gives you Everpupper everywhere""" + displayname = "Doggomizer" + option_pupdunk = 3 + default = 0 + flags = ['', 'd', 'D', 'p'] + + +class TurdoMode(EvermizerFlag, Toggle): + """Replace offensive spells by Turd Balls with varying strength and make weapons weak""" + displayname = "Turdo Mode" + flag = 't' + + +soe_options: typing.Dict[str, type(Option)] = { + "difficulty": Difficulty, + "money_modifier": MoneyModifier, + "exp_modifier": ExpModifier, + "fix_sequence": FixSequence, + "fix_cheats": FixCheats, + "fix_infinite_ammo": FixInfiniteAmmo, + "fix_atlas_glitch": FixAtlasGlitch, + "fix_wings_glitch": FixWingsGlitch, + "shorter_dialogs": ShorterDialogs, + "short_boss_rush": ShortBossRush, + "ingredienizer": Ingredienizer, + "sniffamizer": Sniffamizer, + "callbeadamizer": Callbeadamizer, + "musicmizer": Musicmizer, + "doggomizer": Doggomizer, + "turdo_mode": TurdoMode, +} diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py new file mode 100644 index 00000000..0812c3f1 --- /dev/null +++ b/worlds/soe/Patch.py @@ -0,0 +1,57 @@ +import bsdiff4 +import yaml +from typing import Optional +import Utils + + +USHASH = '6e9c94511d04fac6e0a1e582c170be3a' +current_patch_version = 2 + + +def read_rom(stream, strip_header=True) -> bytes: + """Reads rom into bytearray and optionally strips off any smc header""" + data = stream.read() + if strip_header and len(data) % 0x400 == 0x200: + return data[0x200:] + return data + + +def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: + patch = yaml.dump({"meta": metadata, + "patch": patch, + "game": "Secret of Evermore", + # minimum version of patch system expected for patching to be successful + "compatible_version": 1, + "version": current_patch_version, + "base_checksum": USHASH}) + return patch.encode(encoding="utf-8-sig") + + +def generate_patch(vanilla_file, randomized_file, metadata: Optional[dict] = None) -> bytes: + with open(vanilla_file, "rb") as f: + vanilla = read_rom(f) + with open(randomized_file, "rb") as f: + randomized = read_rom(f) + if metadata is None: + metadata = {} + patch = bsdiff4.diff(vanilla, randomized) + return generate_yaml(patch, metadata) + + +if __name__ == '__main__': + import argparse + import pathlib + import lzma + parser = argparse.ArgumentParser(description='Apply patch to Secret of Evermore.') + parser.add_argument('patch', type=pathlib.Path, help='path to .absoe file') + args = parser.parse_args() + with open(args.patch, "rb") as f: + data = Utils.parse_yaml(lzma.decompress(f.read()).decode("utf-8-sig")) + if data['game'] != 'Secret of Evermore': + raise RuntimeError('Patch is not for Secret of Evermore') + with open(Utils.get_options()['soe_options']['rom_file'], 'rb') as f: + vanilla_data = read_rom(f) + patched_data = bsdiff4.patch(vanilla_data, data["patch"]) + with open(args.patch.parent / (args.patch.stem + '.sfc'), 'wb') as f: + f.write(patched_data) + diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py new file mode 100644 index 00000000..104f2e86 --- /dev/null +++ b/worlds/soe/__init__.py @@ -0,0 +1,237 @@ +from ..AutoWorld import World +from ..generic.Rules import set_rule, add_item_rule +from BaseClasses import Region, Location, Entrance, Item +from Utils import get_options, output_path +import typing +import lzma +import os +import threading + +try: + import pyevermizer # from package +except ImportError: + import traceback + traceback.print_exc() + from . import pyevermizer # as part of the source tree + +from . import Logic # load logic mixin +from .Options import soe_options +from .Patch import generate_patch + +""" +In evermizer: + +Items are uniquely defined by a pair of (type, id). +For most items this is their vanilla location (i.e. CHECK_GOURD, number). + +Items have `provides`, which give the actual progression +instead of providing multiple events per item, we iterate through them in Logic.py + e.g. Found any weapon + +Locations have `requires` and `provides`. +Requirements have to be converted to (access) rules for AP + e.g. Chest locked behind having a weapon +Provides could be events, but instead we iterate through the entire logic in Logic.py + e.g. NPC available after fighting a Boss + +Rules are special locations that don't have a physical location +instead of implementing virtual locations and virtual items, we simply use them in Logic.py + e.g. 2DEs+Wheel+Gauge = Rocket + +Rules and Locations live on the same logic tree returned by pyevermizer.get_logic() + +TODO: for balancing we may want to generate Regions (with Entrances) for some +common rules, place the locations in those Regions and shorten the rules. +""" + +_id_base = 64000 +_id_offset: typing.Dict[int, int] = { + pyevermizer.CHECK_ALCHEMY: _id_base + 0, # alchemy 64000..64049 + pyevermizer.CHECK_BOSS: _id_base + 50, # bosses 64050..6499 + pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399 + pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499 + # TODO: sniff 64500..64799 +} + +# cache native evermizer items and locations +_items = pyevermizer.get_items() +_locations = pyevermizer.get_locations() +# fix up texts for AP +for _loc in _locations: + if _loc.type == pyevermizer.CHECK_GOURD: + _loc.name = f'{_loc.name} #{_loc.index}' + + +def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]: + name_to_id = {} + id_to_raw = {} + for loc in _locations: + apid = _id_offset[loc.type] + loc.index + id_to_raw[apid] = loc + name_to_id[loc.name] = apid + name_to_id['Done'] = None + return name_to_id, id_to_raw + + +def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]: + name_to_id = {} + id_to_raw = {} + for item in _items: + if item.name in name_to_id: + continue + apid = _id_offset[item.type] + item.index + id_to_raw[apid] = item + name_to_id[item.name] = apid + name_to_id['Victory'] = None + return name_to_id, id_to_raw + + +class SoEWorld(World): + """ + Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a + space station where the final boss must be defeated. + """ + game: str = "Secret of Evermore" + options = soe_options + topology_present: bool = False + remote_items: bool = False + data_version = 1 + + item_name_to_id, item_id_to_raw = _get_item_mapping() + location_name_to_id, location_id_to_raw = _get_location_mapping() + + evermizer_seed: int + connect_name: str + + def __init__(self, *args, **kwargs): + self.connect_name_available_event = threading.Event() + super(SoEWorld, self).__init__(*args, **kwargs) + + def create_event(self, event: str) -> Item: + progression = True + return SoEItem(event, progression, None, self.player) + + def create_item(self, item: typing.Union[pyevermizer.Item, str], force_progression: bool = False) -> Item: + if type(item) is str: + item = self.item_id_to_raw[self.item_name_to_id[item]] + return SoEItem(item.name, force_progression or item.progression, self.item_name_to_id[item.name], self.player) + + def create_regions(self): + # TODO: generate *some* regions from locations' requirements? + r = Region('Menu', None, 'Menu', self.player, self.world) + r.exits = [Entrance(self.player, 'New Game', r)] + self.world.regions += [r] + + r = Region('Ingame', None, 'Ingame', self.player, self.world) + r.locations = [SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r) + for loc in _locations] + r.locations.append(SoELocation(self.player, 'Done', None, r)) + self.world.regions += [r] + + self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player)) + + def create_items(self): + # clear precollected items since we don't support them yet + if type(self.world.precollected_items) is dict: + self.world.precollected_items[self.player] = [] + # add items to the pool + self.world.itempool += list(map(lambda item: self.create_item(item), _items)) + + def set_rules(self): + self.world.completion_condition[self.player] = lambda state: state.has('Victory', self.player) + # set Done from goal option once we have multiple goals + set_rule(self.world.get_location('Done', self.player), + lambda state: state._soe_has(pyevermizer.P_FINAL_BOSS, self.world, self.player)) + set_rule(self.world.get_entrance('New Game', self.player), lambda state: True) + for loc in _locations: + location = self.world.get_location(loc.name, self.player) + set_rule(location, self.make_rule(loc.requires)) + + def make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]: + def rule(state) -> bool: + for count, progress in requires: + if not state._soe_has(progress, self.world, self.player, count): + return False + return True + + return rule + + def make_item_type_limit_rule(self, item_type: int): + return lambda item: item.player != self.player or self.item_id_to_raw[item.code].type == item_type + + def generate_basic(self): + # place Victory event + self.world.get_location('Done', self.player).place_locked_item(self.create_event('Victory')) + # generate stuff for later + self.evermizer_seed = self.world.random.randint(0, 2**16-1) # TODO: make this an option for "full" plando? + + def generate_output(self, output_directory: str): + player_name = self.world.get_player_name(self.player) + self.connect_name = player_name[:32] + while len(self.connect_name.encode('utf-8')) > 32: + self.connect_name = self.connect_name[:-1] + self.connect_name_available_event.set() + placement_file = None + out_file = None + try: + money = self.world.money_modifier[self.player].value + exp = self.world.exp_modifier[self.player].value + rom_file = get_options()['soe_options']['rom_file'] + out_base = output_path(output_directory, f'AP_{self.world.seed_name}_P{self.player}_{player_name}') + out_file = out_base + '.sfc' + placement_file = out_base + '.txt' + patch_file = out_base + '.apsoe' + flags = 'l' # spoiler log + for option_name in self.options: + option = getattr(self.world, option_name)[self.player] + if hasattr(option, 'to_flag'): + flags += option.to_flag() + + with open(placement_file, "wb") as f: # generate placement file + for location in filter(lambda l: l.player == self.player, self.world.get_locations()): + item = location.item + if item.code is None: + continue # skip events + loc = self.location_id_to_raw[location.address] + if item.player != self.player: + line = f'{loc.type},{loc.index}:{pyevermizer.CHECK_NONE},{item.code},{item.player}\n' + else: + item = self.item_id_to_raw[item.code] + line = f'{loc.type},{loc.index}:{item.type},{item.index}\n' + f.write(line.encode('utf-8')) + + if (pyevermizer.main(rom_file, out_file, placement_file, self.world.seed_name, self.connect_name, self.evermizer_seed, + flags, money, exp)): + raise RuntimeError() + with lzma.LZMAFile(patch_file, 'wb') as f: + f.write(generate_patch(rom_file, out_file)) + except: + raise + finally: + try: + os.unlink(placement_file) + os.unlink(out_file) + os.unlink(out_file[:-4]+'_SPOILER.log') + except: + pass + + def modify_multidata(self, multidata: dict): + # wait for self.connect_name to be available. + self.connect_name_available_event.wait() + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if self.connect_name and self.connect_name != self.world.player_name[self.player]: + payload = multidata["connect_names"][self.world.player_name[self.player]] + multidata["connect_names"][self.connect_name] = payload + del (multidata["connect_names"][self.world.player_name[self.player]]) + + +class SoEItem(Item): + game: str = "Secret of Evermore" + + +class SoELocation(Location): + game: str = "Secret of Evermore" + + def __init__(self, player: int, name: str, address: typing.Optional[int], parent): + super().__init__(player, name, address, parent) + self.event = not address diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt new file mode 100644 index 00000000..f37a4a44 --- /dev/null +++ b/worlds/soe/requirements.txt @@ -0,0 +1,14 @@ +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp38-cp38-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp39-cp39-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.39.1/pyevermizer-0.39.1-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' +bsdiff4>=1.2.1 \ No newline at end of file