Merge pull request #121 from black-sliver/soe
Added Secret of Evermore support
This commit is contained in:
		
						commit
						b529f95798
					
				| 
						 | 
				
			
			@ -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__":
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
# Secret of Evermore
 | 
			
		||||
 | 
			
		||||
## Where is the settings page?
 | 
			
		||||
The player settings page for this game is located <a href="../player-settings">here</a>. 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
dump.py
 | 
			
		||||
pyevermizer
 | 
			
		||||
.pyevermizer
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
		Loading…
	
		Reference in New Issue