diff --git a/Generate.py b/Generate.py index 81ef2989..99631165 100644 --- a/Generate.py +++ b/Generate.py @@ -9,10 +9,10 @@ from collections import Counter import string import ModuleUpdate -import Utils ModuleUpdate.update() +import Utils from worlds.alttp import Options as LttPOptions from worlds.generic import PlandoItem, PlandoConnection from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options 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/README.md b/README.md index 142e746a..8135798d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Currently, the following games are supported: * Risk of Rain 2 * The Legend of Zelda: Ocarina of Time * Timespinner +* Super Metroid +* Secret of Evermore For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index b90903e5..5afda798 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -22,6 +22,7 @@ from NetUtils import * from worlds.alttp import Regions, Shops from worlds.alttp import Items from worlds.alttp.Rom import ROM_PLAYER_LIMIT +from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT import Utils from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser from Patch import GAME_ALTTP, GAME_SM @@ -156,7 +157,7 @@ class Context(CommonContext): self.killing_player_task = asyncio.create_task(deathlink_kill_player(self)) super(Context, self).on_deathlink(data) - def handle_deathlink_state(self, currently_dead: bool): + async def handle_deathlink_state(self, currently_dead: bool): # in this state we only care about triggering a death send if self.death_state == DeathState.alive: if currently_dead: @@ -935,7 +936,7 @@ async def game_watcher(ctx: Context): 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 - ctx.handle_deathlink_state(currently_dead) + 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) @@ -1004,7 +1005,7 @@ async def game_watcher(ctx: Context): gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): currently_dead = gamemode[0] in SM_DEATH_MODES - ctx.handle_deathlink_state(currently_dead) + await ctx.handle_deathlink_state(currently_dead) if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES: if not ctx.finished_game: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) @@ -1048,7 +1049,7 @@ async def game_watcher(ctx: Context): item = ctx.items_received[itemOutPtr] itemId = item.item - items_start_id - playerID = (item.player-1) if item.player != 0 else (len(ctx.player_names)-1) + playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes([playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF])) itemOutPtr += 1 snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF])) @@ -1057,7 +1058,6 @@ async def game_watcher(ctx: Context): ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) - async def run_game(romfile): auto_start = Utils.get_options()["lttp_options"].get("rom_start", True) if auto_start is True: 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/inno_setup_310.iss b/inno_setup_310.iss index bd73b58a..1da72694 100644 --- a/inno_setup_310.iss +++ b/inno_setup_310.iss @@ -34,7 +34,6 @@ SignTool= signtool LicenseFile= LICENSE WizardStyle= modern SetupLogging=yes -MinVersion=6.3.9200 [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -51,11 +50,15 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom [Components] Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed Name: "generator"; Description: "Generator"; Types: full hosting +Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 +Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296 Name: "server"; Description: "Server"; Types: full hosting Name: "client"; Description: "Clients"; Types: full playing -Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing +Name: "client/sni"; Description: "SNI Client"; Types: full playing +Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing +Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing @@ -64,18 +67,20 @@ Name: "client/text"; Description: "Text, to !command and chat"; Types: full play NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; [Files] -Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp +Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp +Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm +Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp +Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text -Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp -Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp or generator/lttp +Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni +Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall @@ -85,30 +90,38 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt [Icons] Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server -Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/lttp -Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp +Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text +Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server -Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp +Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." -Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator/lttp +Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft [UninstallDelete] Type: dirifempty; Name: "{app}" +[InstallDelete] +Type: files; Name: "{app}\ArchipelagoLttPClient.exe" + [Registry] -Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp -Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp -Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp -Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp +Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni + +Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft @@ -190,38 +203,53 @@ begin ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL); end; -var ROMFilePage: TInputFileWizardPage; var R : longint; -var rom: string; + +var lttprom: string; +var LttPROMFilePage: TInputFileWizardPage; + +var smrom: string; +var SMRomFilePage: TInputFileWizardPage; + +var soerom: string; +var SoERomFilePage: TInputFileWizardPage; + var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; + var MinecraftDownloadPage: TDownloadWizardPage; -procedure AddRomPage(); +function CheckRom(name: string; hash: string): string; +var rom: string; begin - rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue()); + log('Handling ' + name) + rom := FileSearch(name, WizardDirValue()); if Length(rom) > 0 then begin log('existing ROM found'); - log(IntToStr(CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173'))); - if CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173') = 0 then + log(IntToStr(CompareStr(GetMD5OfFile(rom), hash))); + if CompareStr(GetMD5OfFile(rom), hash) = 0 then begin log('existing ROM verified'); + Result := rom; exit; end; log('existing ROM failed verification'); end; - rom := '' - ROMFilePage := +end; + +function AddRomPage(name: string): TInputFileWizardPage; +begin + Result := CreateInputFilePage( wpSelectComponents, 'Select ROM File', - 'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?', + 'Where is your ' + name + ' located?', 'Select the file, then click Next.'); - ROMFilePage.Add( + Result.Add( 'Location of ROM file:', - 'SNES ROM files|*.sfc|All files|*.*', + 'SNES ROM files|*.sfc;*.smc|All files|*.*', '.sfc'); end; @@ -291,34 +319,50 @@ begin Result := True; end; -procedure InitializeWizard(); -begin - AddOoTRomPage(); - AddRomPage(); - AddMinecraftDownloads(); -end; - - -function ShouldSkipPage(PageID: Integer): Boolean; -begin - Result := False; - if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp')); - if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/oot')); -end; - function GetROMPath(Param: string): string; begin - if Length(rom) > 0 then - Result := rom - else if Assigned(RomFilePage) then + if Length(lttprom) > 0 then + Result := lttprom + else if Assigned(LttPRomFilePage) then begin - R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') + R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') if R <> 0 then MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - - Result := ROMFilePage.Values[0] + + Result := LttPROMFilePage.Values[0] + end + else + Result := ''; + end; + +function GetSMROMPath(Param: string): string; +begin + if Length(smrom) > 0 then + Result := smrom + else if Assigned(SMRomFilePage) then + begin + R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') + if R <> 0 then + MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := SMROMFilePage.Values[0] + end + else + Result := ''; + end; + +function GetSoEROMPath(Param: string): string; +begin + if Length(soerom) > 0 then + Result := soerom + else if Assigned(SoERomFilePage) then + begin + R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') + log(GetMD5OfFile(SoEROMFilePage.Values[0])) + if R <> 0 then + MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := SoEROMFilePage.Values[0] end else Result := ''; @@ -333,9 +377,42 @@ begin R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f'); if R <> 0 then MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - + Result := OoTROMFilePage.Values[0] end else Result := ''; end; + +procedure InitializeWizard(); +begin + AddOoTRomPage(); + + lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173'); + if Length(lttprom) = 0 then + LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'); + + smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675'); + if Length(smrom) = 0 then + SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc'); + + soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); + if Length(soerom) = 0 then + SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); + + AddMinecraftDownloads(); +end; + + +function ShouldSkipPage(PageID: Integer): Boolean; +begin + Result := False; + if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp')); + if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm')); + if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/soe')); + if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/oot')); +end; \ No newline at end of file diff --git a/inno_setup_38.iss b/inno_setup_38.iss index 49caf36e..f9387c1a 100644 --- a/inno_setup_38.iss +++ b/inno_setup_38.iss @@ -50,11 +50,15 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom [Components] Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed Name: "generator"; Description: "Generator"; Types: full hosting +Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 +Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728 Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680 Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296 Name: "server"; Description: "Server"; Types: full hosting Name: "client"; Description: "Clients"; Types: full playing -Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing +Name: "client/sni"; Description: "SNI Client"; Types: full playing +Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing +Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing @@ -63,18 +67,20 @@ Name: "client/text"; Description: "Text, to !command and chat"; Types: full play NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; [Files] -Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp +Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp +Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm +Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp +Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text -Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp -Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp or generator/lttp +Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni +Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall @@ -84,30 +90,38 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt [Icons] Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server -Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/lttp -Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp +Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text +Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server -Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp +Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio [Run] Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." -Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator/lttp +Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft [UninstallDelete] Type: dirifempty; Name: "{app}" +[InstallDelete] +Type: files; Name: "{app}\ArchipelagoLttPClient.exe" + [Registry] -Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp -Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp -Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp -Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp +Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni + +Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni +Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft @@ -189,38 +203,53 @@ begin ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL); end; -var ROMFilePage: TInputFileWizardPage; var R : longint; -var rom: string; + +var lttprom: string; +var LttPROMFilePage: TInputFileWizardPage; + +var smrom: string; +var SMRomFilePage: TInputFileWizardPage; + +var soerom: string; +var SoERomFilePage: TInputFileWizardPage; + var ootrom: string; var OoTROMFilePage: TInputFileWizardPage; + var MinecraftDownloadPage: TDownloadWizardPage; -procedure AddRomPage(); +function CheckRom(name: string; hash: string): string; +var rom: string; begin - rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue()); + log('Handling ' + name) + rom := FileSearch(name, WizardDirValue()); if Length(rom) > 0 then begin log('existing ROM found'); - log(IntToStr(CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173'))); - if CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173') = 0 then + log(IntToStr(CompareStr(GetMD5OfFile(rom), hash))); + if CompareStr(GetMD5OfFile(rom), hash) = 0 then begin log('existing ROM verified'); + Result := rom; exit; end; log('existing ROM failed verification'); end; - rom := '' - ROMFilePage := +end; + +function AddRomPage(name: string): TInputFileWizardPage; +begin + Result := CreateInputFilePage( wpSelectComponents, 'Select ROM File', - 'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?', + 'Where is your ' + name + ' located?', 'Select the file, then click Next.'); - ROMFilePage.Add( + Result.Add( 'Location of ROM file:', - 'SNES ROM files|*.sfc|All files|*.*', + 'SNES ROM files|*.sfc;*.smc|All files|*.*', '.sfc'); end; @@ -290,34 +319,50 @@ begin Result := True; end; -procedure InitializeWizard(); -begin - AddOoTRomPage(); - AddRomPage(); - AddMinecraftDownloads(); -end; - - -function ShouldSkipPage(PageID: Integer): Boolean; -begin - Result := False; - if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then - Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp')); - if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then - Result := not (WizardIsComponentSelected('generator/oot')); -end; - function GetROMPath(Param: string): string; begin - if Length(rom) > 0 then - Result := rom - else if Assigned(RomFilePage) then + if Length(lttprom) > 0 then + Result := lttprom + else if Assigned(LttPRomFilePage) then begin - R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') + R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173') if R <> 0 then MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); - Result := ROMFilePage.Values[0] + Result := LttPROMFilePage.Values[0] + end + else + Result := ''; + end; + +function GetSMROMPath(Param: string): string; +begin + if Length(smrom) > 0 then + Result := smrom + else if Assigned(SMRomFilePage) then + begin + R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675') + if R <> 0 then + MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := SMROMFilePage.Values[0] + end + else + Result := ''; + end; + +function GetSoEROMPath(Param: string): string; +begin + if Length(soerom) > 0 then + Result := soerom + else if Assigned(SoERomFilePage) then + begin + R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a') + log(GetMD5OfFile(SoEROMFilePage.Values[0])) + if R <> 0 then + MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK); + + Result := SoEROMFilePage.Values[0] end else Result := ''; @@ -338,3 +383,36 @@ begin else Result := ''; end; + +procedure InitializeWizard(); +begin + AddOoTRomPage(); + + lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173'); + if Length(lttprom) = 0 then + LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'); + + smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675'); + if Length(smrom) = 0 then + SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc'); + + soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a'); + if Length(soerom) = 0 then + SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc'); + + AddMinecraftDownloads(); +end; + + +function ShouldSkipPage(PageID: Integer): Boolean; +begin + Result := False; + if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp')); + if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then + Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm')); + if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/soe')); + if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then + Result := not (WizardIsComponentSelected('generator/oot')); +end; \ No newline at end of file diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 04736a2e..fbf283d3 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -85,7 +85,8 @@ class CustomTechnology(Technology): def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): ingredients = origin.ingredients & allowed_packs military_allowed = "military-science-pack" in allowed_packs \ - and (ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) + and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) + or origin.name == "rocket-silo") self.player = player if origin.name not in world.worlds[player].static_nodes: if military_allowed: diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index efddc17a..5d7ab709 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -2,6 +2,7 @@ import Utils from Patch import read_rom JAP10HASH = '21f3e98df4780ee1c667b84e57d88675' +ROM_PLAYER_LIMIT = 255 import hashlib import os @@ -27,4 +28,4 @@ def get_base_rom_path(file_name: str = "") -> str: file_name = options["sm_options"]["rom_file"] if not os.path.exists(file_name): file_name = Utils.local_path(file_name) - return file_name \ No newline at end of file + return file_name diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index db156dcd..808f1bde 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -11,7 +11,7 @@ from .Items import lookup_name_to_id as items_lookup_name_to_id from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options -from .Rom import get_base_rom_path +from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT import Utils from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState @@ -242,7 +242,7 @@ class SMWorld(World): idx += 1 (w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1) (w2, w3) = self.getWord(itemId) - (w4, w5) = self.getWord(itemLoc.item.player - 1) + (w4, w5) = self.getWord(itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0) (w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1) multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7] @@ -268,9 +268,10 @@ class SMWorld(World): romPatcher.applyIPSPatchDict(patchDict) playerNames = {} - for p in range(1, self.world.players + 1): - playerNames[0x1C5000 + (p - 1) * 16] = self.world.player_name[p][:16].upper().center(16).encode() - playerNames[0x1C5000 + (self.world.players) * 16] = "Archipelago".upper().center(16).encode() + playerNames[0x1C5000] = "Archipelago".upper().center(16).encode() + for p in range(1, min(self.world.players, ROM_PLAYER_LIMIT) + 1): + playerNames[0x1C5000 + p * 16] = self.world.player_name[p][:16].upper().center(16).encode() + romPatcher.applyIPSPatch('PlayerName', { 'PlayerName': playerNames }) diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 471f982a..22b83ceb 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -573,12 +573,12 @@ class RomPatcher: self.writeCreditsStringBig(address, line, top=False) address += 0x80 - value = " "+settings.progSpeed.upper() + value = " "+"NA" # settings.progSpeed.upper() line = " PROGRESSION SPEED ....%s " % value.rjust(8, '.') self.writeCreditsString(address, 0x04, line) address += 0x40 - line = " PROGRESSION DIFFICULTY %s " % settings.progDiff.upper() + line = " PROGRESSION DIFFICULTY %s " % value.rjust(7, '.') # settings.progDiff.upper() self.writeCreditsString(address, 0x04, line) address += 0x80 # skip item distrib title 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