From f10163e7d2ba79f512f39ba47d21c40a89774006 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Wed, 29 Sep 2021 09:12:23 +0200
Subject: [PATCH 01/11] SoE: implement logic
---
worlds/soe/Logic.py | 52 ++++++++++++++
worlds/soe/Options.py | 7 ++
worlds/soe/__init__.py | 149 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 208 insertions(+)
create mode 100644 worlds/soe/Logic.py
create mode 100644 worlds/soe/Options.py
create mode 100644 worlds/soe/__init__.py
diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py
new file mode 100644
index 00000000..90d52cf1
--- /dev/null
+++ b/worlds/soe/Logic.py
@@ -0,0 +1,52 @@
+from BaseClasses import MultiWorld
+from ..AutoWorld import LogicMixin
+from typing import Set
+# TODO: import Options
+# 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 giving 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 an evermizer progress steps are 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..57b32bd3
--- /dev/null
+++ b/worlds/soe/Options.py
@@ -0,0 +1,7 @@
+import typing
+from Options import Option
+
+# TODO: add options
+
+soe_options: typing.Dict[str, type(Option)] = {
+}
diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py
new file mode 100644
index 00000000..9628ba50
--- /dev/null
+++ b/worlds/soe/__init__.py
@@ -0,0 +1,149 @@
+from .Options import soe_options
+from ..AutoWorld import World
+from ..generic.Rules import set_rule
+from BaseClasses import Region, Location, Entrance, Item
+import typing
+from . import Logic # load logic mixin
+
+try:
+ import pyevermizer # from package
+except ImportError:
+ from . import pyevermizer # as part of the source tree
+
+"""
+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.
+"""
+
+GAME_NAME = "Secret of Evermore"
+ID_OFF_BASE = 64000
+ID_OFFS: typing.Dict[int,int] = {
+ pyevermizer.CHECK_ALCHEMY: ID_OFF_BASE + 0, # alchemy 64000..64049
+ pyevermizer.CHECK_BOSS: ID_OFF_BASE + 50, # bosses 64050..6499
+ pyevermizer.CHECK_GOURD: ID_OFF_BASE + 100, # gourds 64100..64399
+ pyevermizer.CHECK_NPC: ID_OFF_BASE + 400, # npc 64400..64499
+ # TODO: sniff 64500..64799
+}
+
+
+def _get_locations():
+ locs = pyevermizer.get_locations()
+ for loc in locs:
+ if loc.type == 3: # TODO: CHECK_GOURD
+ loc.name = f'{loc.name} #{loc.index}'
+ return locs
+
+
+def _get_location_ids():
+ m = {}
+ for loc in _get_locations():
+ m[loc.name] = ID_OFFS[loc.type] + loc.index
+ m['Done'] = None
+ return m
+
+
+def _get_items():
+ return pyevermizer.get_items()
+
+
+def _get_item_ids():
+ m = {}
+ for item in _get_items():
+ if item.name in m: continue
+ m[item.name] = ID_OFFS[item.type] + item.index
+ m['Victory'] = None
+ return m
+
+
+class SoEWorld(World):
+ """
+ TODO: insert game description here
+ """
+ game: str = GAME_NAME
+ # options = soe_options
+ topology_present: bool = True
+
+ item_name_to_id = _get_item_ids()
+ location_name_to_id = _get_location_ids()
+
+ remote_items: bool = True # False # True only for testing
+
+ def generate_basic(self):
+ print('SoE: generate_basic')
+ itempool = [item for item in map(lambda item: self.create_item(item), _get_items())]
+ self.world.itempool += itempool
+ self.world.get_location('Done', self.player).place_locked_item(self.create_event('Victory'))
+
+ 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 _get_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_event(self, event: str) -> Item:
+ progression = True
+ return SoEItem(event, progression, None, self.player)
+
+ def create_item(self, item) -> Item:
+ # TODO: if item is string: look up item by name
+ return SoEItem(item.name, item.progression, self.item_name_to_id[item.name], self.player)
+
+ def set_rules(self):
+ print('SoE: set_rules')
+ 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 _get_locations():
+ set_rule(self.world.get_location(loc.name, self.player), self.make_rule(loc.requires))
+
+ def make_rule(self, requires):
+ def rule(state):
+ for count, progress in requires:
+ if not state._soe_has(progress, self.world, self.player, count):
+ return False
+ return True
+
+ return rule
+
+
+class SoEItem(Item):
+ game: str = GAME_NAME
+
+
+class SoELocation(Location):
+ game: str = GAME_NAME
+
+ def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
+ super().__init__(player, name, address, parent)
+ self.event = not address
From 5d0d9c28900739f7d75420e29947cc0af184f94b Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 7 Nov 2021 14:00:13 +0100
Subject: [PATCH 02/11] allow requirements to point to urls
---
ModuleUpdate.py | 31 +++++++++++++++++++------------
1 file changed, 19 insertions(+), 12 deletions(-)
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__":
From 655d14ed6e228f0e331ed8945ac5febfc7c4081b Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 7 Nov 2021 15:38:02 +0100
Subject: [PATCH 03/11] SoE: implement everything else
---
worlds/soe/.gitignore | 3 +
worlds/soe/Logic.py | 8 +-
worlds/soe/Options.py | 151 ++++++++++++++++++++++++-
worlds/soe/Patch.py | 52 +++++++++
worlds/soe/__init__.py | 212 +++++++++++++++++++++++++-----------
worlds/soe/requirements.txt | 14 +++
6 files changed, 372 insertions(+), 68 deletions(-)
create mode 100644 worlds/soe/.gitignore
create mode 100644 worlds/soe/Patch.py
create mode 100644 worlds/soe/requirements.txt
diff --git a/worlds/soe/.gitignore b/worlds/soe/.gitignore
new file mode 100644
index 00000000..e346fd15
--- /dev/null
+++ b/worlds/soe/.gitignore
@@ -0,0 +1,3 @@
+dumpy.py
+pyevermizer
+.pyevermizer
diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py
index 90d52cf1..f25f2ada 100644
--- a/worlds/soe/Logic.py
+++ b/worlds/soe/Logic.py
@@ -1,7 +1,6 @@
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
from typing import Set
-# TODO: import Options
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
from . import pyevermizer
@@ -19,8 +18,8 @@ items = [item for item in filter(lambda item: item.progression, pyevermizer.get_
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 giving CHECK_BOSS,DIAMOND_EYE_DROP
+ 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:
@@ -46,7 +45,6 @@ class SecretOfEvermoreLogic(LogicMixin):
def _soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool:
"""
- Returns True if count of an evermizer progress steps are reached based
- on collected items. i.e. 2 * P_DE
+ 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
index 57b32bd3..9eddd0e7 100644
--- a/worlds/soe/Options.py
+++ b/worlds/soe/Options.py
@@ -1,7 +1,154 @@
import typing
-from Options import Option
+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'
-# TODO: add options
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..a9c1bade
--- /dev/null
+++ b/worlds/soe/Patch.py
@@ -0,0 +1,52 @@
+import bsdiff4
+import yaml
+from typing import Optional
+import Utils
+
+
+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": 1})
+ 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
index 9628ba50..42b74fdb 100644
--- a/worlds/soe/__init__.py
+++ b/worlds/soe/__init__.py
@@ -1,15 +1,23 @@
-from .Options import soe_options
from ..AutoWorld import World
-from ..generic.Rules import set_rule
+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
-from . import Logic # load logic mixin
+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:
@@ -36,99 +44,115 @@ 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.
"""
-GAME_NAME = "Secret of Evermore"
-ID_OFF_BASE = 64000
-ID_OFFS: typing.Dict[int,int] = {
- pyevermizer.CHECK_ALCHEMY: ID_OFF_BASE + 0, # alchemy 64000..64049
- pyevermizer.CHECK_BOSS: ID_OFF_BASE + 50, # bosses 64050..6499
- pyevermizer.CHECK_GOURD: ID_OFF_BASE + 100, # gourds 64100..64399
- pyevermizer.CHECK_NPC: ID_OFF_BASE + 400, # npc 64400..64499
+_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
}
-
-def _get_locations():
- locs = pyevermizer.get_locations()
- for loc in locs:
- if loc.type == 3: # TODO: CHECK_GOURD
- loc.name = f'{loc.name} #{loc.index}'
- return locs
+# 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_ids():
- m = {}
- for loc in _get_locations():
- m[loc.name] = ID_OFFS[loc.type] + loc.index
- m['Done'] = None
- return m
+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_items():
- return pyevermizer.get_items()
-
-
-def _get_item_ids():
- m = {}
- for item in _get_items():
- if item.name in m: continue
- m[item.name] = ID_OFFS[item.type] + item.index
- m['Victory'] = None
- return m
+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):
"""
- TODO: insert game description here
+ 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 = GAME_NAME
- # options = soe_options
+ game: str = "Secret of Evermore"
+ options = soe_options
topology_present: bool = True
+ remote_items: bool = False # True only for testing
+ data_version = 0
- item_name_to_id = _get_item_ids()
- location_name_to_id = _get_location_ids()
+ item_name_to_id, item_id_to_raw = _get_item_mapping()
+ location_name_to_id, location_id_to_raw = _get_location_mapping()
- remote_items: bool = True # False # True only for testing
+ evermizer_seed: int
+ restrict_item_placement: bool = False # placeholder to force certain item types to certain pools
- def generate_basic(self):
- print('SoE: generate_basic')
- itempool = [item for item in map(lambda item: self.create_item(item), _get_items())]
- self.world.itempool += itempool
- self.world.get_location('Done', self.player).place_locked_item(self.create_event('Victory'))
+ 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
+ # 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 _get_locations()]
+ 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_event(self, event: str) -> Item:
- progression = True
- return SoEItem(event, progression, None, self.player)
-
- def create_item(self, item) -> Item:
- # TODO: if item is string: look up item by name
- return SoEItem(item.name, item.progression, self.item_name_to_id[item.name], 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 += [item for item in
+ map(lambda item: self.create_item(item, self.restrict_item_placement), _items)]
def set_rules(self):
- print('SoE: set_rules')
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 _get_locations():
- set_rule(self.world.get_location(loc.name, self.player), self.make_rule(loc.requires))
+ for loc in _locations:
+ location = self.world.get_location(loc.name, self.player)
+ set_rule(location, self.make_rule(loc.requires))
+ # limit location pool by item type
+ if self.restrict_item_placement:
+ add_item_rule(location, self.make_item_type_limit_rule(loc.type))
- def make_rule(self, requires):
- def rule(state):
+ 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
@@ -136,13 +160,79 @@ class SoEWorld(World):
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 post_fill(self):
+ # fix up the advancement property of items so they are displayed correctly in other games
+ if self.restrict_item_placement:
+ for location in self.world.get_locations():
+ item = location.item
+ if item.code and item.player == self.player and not self.item_id_to_raw[location.item.code].progression:
+ item.advancement = False
+
+ 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
class SoEItem(Item):
- game: str = GAME_NAME
+ game: str = "Secret of Evermore"
class SoELocation(Location):
- game: str = GAME_NAME
+ game: str = "Secret of Evermore"
def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
super().__init__(player, name, address, parent)
diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt
new file mode 100644
index 00000000..c0ac8ae7
--- /dev/null
+++ b/worlds/soe/requirements.txt
@@ -0,0 +1,14 @@
+https://github.com/black-sliver/pyevermizer/releases/download/v0.39.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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
From 79041bdf2115b96d3161dd8b10b3cf52f277445a Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 7 Nov 2021 15:43:07 +0100
Subject: [PATCH 04/11] update host.yaml for SoE
---
host.yaml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/host.yaml b/host.yaml
index 66392259..dd88d269 100644
--- a/host.yaml
+++ b/host.yaml
@@ -98,4 +98,7 @@ minecraft_options:
max_heap_size: "2G"
oot_options:
# File name of the OoT v1.0 ROM
- rom_file: "The Legend of Zelda - Ocarina of Time.z64"
\ No newline at end of file
+ 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"
From 449f4ee92fee948a2a0bd47a94e8584539a7eab2 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 7 Nov 2021 15:56:43 +0100
Subject: [PATCH 05/11] SoE: apply cut slot name to multidata
---
worlds/soe/__init__.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py
index 42b74fdb..7e554356 100644
--- a/worlds/soe/__init__.py
+++ b/worlds/soe/__init__.py
@@ -102,6 +102,7 @@ class SoEWorld(World):
evermizer_seed: int
restrict_item_placement: bool = False # placeholder to force certain item types to certain pools
+ connect_name: str
def __init__(self, *args, **kwargs):
self.connect_name_available_event = threading.Event()
@@ -227,6 +228,16 @@ class SoEWorld(World):
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"
From c32f3d6e966ce6e2111e204fbd48d2428d607bcc Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sun, 7 Nov 2021 23:15:35 +0100
Subject: [PATCH 06/11] SoE: data_version bump, disable topology, clean up
---
worlds/soe/.gitignore | 2 +-
worlds/soe/__init__.py | 21 ++++-----------------
2 files changed, 5 insertions(+), 18 deletions(-)
diff --git a/worlds/soe/.gitignore b/worlds/soe/.gitignore
index e346fd15..aa3bbd16 100644
--- a/worlds/soe/.gitignore
+++ b/worlds/soe/.gitignore
@@ -1,3 +1,3 @@
-dumpy.py
+dump.py
pyevermizer
.pyevermizer
diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py
index 7e554356..104f2e86 100644
--- a/worlds/soe/__init__.py
+++ b/worlds/soe/__init__.py
@@ -93,15 +93,14 @@ class SoEWorld(World):
"""
game: str = "Secret of Evermore"
options = soe_options
- topology_present: bool = True
- remote_items: bool = False # True only for testing
- data_version = 0
+ 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
- restrict_item_placement: bool = False # placeholder to force certain item types to certain pools
connect_name: str
def __init__(self, *args, **kwargs):
@@ -136,8 +135,7 @@ class SoEWorld(World):
if type(self.world.precollected_items) is dict:
self.world.precollected_items[self.player] = []
# add items to the pool
- self.world.itempool += [item for item in
- map(lambda item: self.create_item(item, self.restrict_item_placement), _items)]
+ 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)
@@ -148,9 +146,6 @@ class SoEWorld(World):
for loc in _locations:
location = self.world.get_location(loc.name, self.player)
set_rule(location, self.make_rule(loc.requires))
- # limit location pool by item type
- if self.restrict_item_placement:
- add_item_rule(location, self.make_item_type_limit_rule(loc.type))
def make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]:
def rule(state) -> bool:
@@ -170,14 +165,6 @@ class SoEWorld(World):
# generate stuff for later
self.evermizer_seed = self.world.random.randint(0, 2**16-1) # TODO: make this an option for "full" plando?
- def post_fill(self):
- # fix up the advancement property of items so they are displayed correctly in other games
- if self.restrict_item_placement:
- for location in self.world.get_locations():
- item = location.item
- if item.code and item.player == self.player and not self.item_id_to_raw[location.item.code].progression:
- item.advancement = False
-
def generate_output(self, output_directory: str):
player_name = self.world.get_player_name(self.player)
self.connect_name = player_name[:32]
From 9ada4df15113fb5ebfa3dd2afa1c852b4b626105 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Wed, 10 Nov 2021 09:17:27 +0100
Subject: [PATCH 07/11] SoE: include base_checksum in apbp
---
worlds/soe/Patch.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py
index a9c1bade..0812c3f1 100644
--- a/worlds/soe/Patch.py
+++ b/worlds/soe/Patch.py
@@ -4,6 +4,10 @@ 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()
@@ -18,7 +22,8 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
"game": "Secret of Evermore",
# minimum version of patch system expected for patching to be successful
"compatible_version": 1,
- "version": 1})
+ "version": current_patch_version,
+ "base_checksum": USHASH})
return patch.encode(encoding="utf-8-sig")
From 0d6c23e4f29c9a640220aa5aa27761ce611f6168 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Thu, 11 Nov 2021 00:06:30 +0100
Subject: [PATCH 08/11] SoE: add documentation to webhost
---
.../assets/gameInfo/en_Secret of Evermore.md | 26 ++++
.../secret-of-evermore/multiworld_en.md | 116 ++++++++++++++++++
.../static/assets/tutorial/tutorials.json | 19 +++
3 files changed, 161 insertions(+)
create mode 100644 WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
create mode 100644 WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md
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..f744d7cb
--- /dev/null
+++ b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
@@ -0,0 +1,26 @@
+# Secret of Evermore
+
+## Where is the settings page?
+The player settings page for this game is located here. It contains all the options
+you need to configure and export a config file.
+
+## What does randomization do to this game?
+Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
+is always able to be completed, but because of the item shuffle the player may need to access certain areas before
+they would in the vanilla game.
+
+## What items and locations get shuffled?
+All gourds/chests/pots, boss drops and alchemists are shuffled. Additionally you may choose to also shuffle alchemy
+ingredients, sniff spot items, call bead spells and the dog.
+
+## Which items can be in another player's world?
+Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
+limit certain items to your own world.
+
+## What does another world's item look like in Secret of Evermore?
+Secret of Evermore will just display "Sent an Item". Check the client output if you want to know which.
+
+## When the player receives an item, what happens?
+When the player receives an item, a borderless text will appear to show which item was received. You can not receive
+items while a script is active, for example in Nobilia Market or during most Boss Fights. You will receive the items
+once no more scripts are running.
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..0d6005ec
--- /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](https://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, the "Game" status is yellow. The intro can be skipped pressing
+START. When the client shows both "Game" and "AP" as green, you're ready to play.
+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"
+ ]
+ }
+ ]
+ }
+ ]
}
]
From 3ed7b9f60c1f72a0a7f875efacaf878c737a27ca Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Thu, 11 Nov 2021 01:03:31 +0100
Subject: [PATCH 09/11] SoE: reword webhost doc
Thanks Fainspirit
---
.../assets/gameInfo/en_Secret of Evermore.md | 31 ++++++++++---------
.../secret-of-evermore/multiworld_en.md | 6 ++--
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
index f744d7cb..209a739e 100644
--- a/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
+++ b/WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
@@ -1,26 +1,29 @@
# Secret of Evermore
## Where is the settings page?
-The player settings page for this game is located here. It contains all the options
-you need to configure and export a config file.
+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 the player would normally acquire throughout the game have been moved around. Logic remains, so the game
-is always able to be completed, but because of the item shuffle the player may need to access certain areas before
-they would in the vanilla 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. Additionally you may choose to also shuffle alchemy
-ingredients, sniff spot items, call bead spells and the dog.
+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 into another player's world. It is possible to choose to
-limit certain items to your own 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 just display "Sent an Item". Check the client output if you want to know which.
+Secret of Evermore will display "Sent an Item". Check the client output if you want to know which.
-## When the player receives an item, what happens?
-When the player receives an item, a borderless text will appear to show which item was received. You can not receive
-items while a script is active, for example in Nobilia Market or during most Boss Fights. You will receive the items
-once no more scripts are running.
+## 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
index 0d6005ec..f0610ab6 100644
--- a/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md
+++ b/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md
@@ -97,9 +97,9 @@ Enter `/connect server:port` in the client's command prompt and press enter. You
that had the patch file.
### Play the game
-When the game is loaded but not yet past the intro, the "Game" status is yellow. The intro can be skipped pressing
-START. When the client shows both "Game" and "AP" as green, you're ready to play.
-Congratulations on successfully joining a multiworld 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:
From 24596899c9ce5d01b17aa342d2df9662282a1891 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Fri, 12 Nov 2021 21:53:43 +0100
Subject: [PATCH 10/11] SoE doc: change apclient link to http:// for now
---
.../static/assets/tutorial/secret-of-evermore/multiworld_en.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md b/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md
index f0610ab6..0ada1d9f 100644
--- a/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md
+++ b/WebHostLib/static/assets/tutorial/secret-of-evermore/multiworld_en.md
@@ -87,7 +87,7 @@ done so already, please do this now. SD2SNES and FXPak Pro users may download th
2. Load the ROM file from the menu.
### Open the client
-Open [ap-soeclient](https://evermizer.com/apclient) in a modern browser. Do not switch tabs, open it in a new window
+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.
From 62e0e0bb555bb4a7219b654fc78c02fa3da8b609 Mon Sep 17 00:00:00 2001
From: black-sliver <59490463+black-sliver@users.noreply.github.com>
Date: Sat, 13 Nov 2021 00:42:40 +0100
Subject: [PATCH 11/11] SoE: update pyevermizer to 0.39.1
* Fix softlock when talking to drain guy again
* Disable receiving items while screen is fading (avoids crashes while closing fullscreen windows)
---
worlds/soe/requirements.txt | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt
index c0ac8ae7..f37a4a44 100644
--- a/worlds/soe/requirements.txt
+++ b/worlds/soe/requirements.txt
@@ -1,14 +1,14 @@
-https://github.com/black-sliver/pyevermizer/releases/download/v0.39.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-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.0/pyevermizer-0.39-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
+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