SoE: implement everything else

This commit is contained in:
black-sliver 2021-11-07 15:38:02 +01:00
parent 5d0d9c2890
commit 655d14ed6e
6 changed files with 372 additions and 68 deletions

3
worlds/soe/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dumpy.py
pyevermizer
.pyevermizer

View File

@ -1,7 +1,6 @@
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin from ..AutoWorld import LogicMixin
from typing import Set from typing import Set
# TODO: import Options
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? # TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
from . import pyevermizer from . import pyevermizer
@ -19,8 +18,8 @@ items = [item for item in filter(lambda item: item.progression, pyevermizer.get_
class SecretOfEvermoreLogic(LogicMixin): class SecretOfEvermoreLogic(LogicMixin):
def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: 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 Returns reached count of one of evermizer's progress steps based on collected items.
collected items. i.e. returns 0-3 for P_DE based on items giving CHECK_BOSS,DIAMOND_EYE_DROP i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
""" """
n = 0 n = 0
for item in items: 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: 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 Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
on collected items. i.e. 2 * P_DE
""" """
return self._soe_count(progress, world, player, count) >= count return self._soe_count(progress, world, player, count) >= count

View File

@ -1,7 +1,154 @@
import typing 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)] = { 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,
} }

52
worlds/soe/Patch.py Normal file
View File

@ -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)

View File

@ -1,15 +1,23 @@
from .Options import soe_options
from ..AutoWorld import World 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 BaseClasses import Region, Location, Entrance, Item
from Utils import get_options, output_path
import typing import typing
from . import Logic # load logic mixin import lzma
import os
import threading
try: try:
import pyevermizer # from package import pyevermizer # from package
except ImportError: except ImportError:
import traceback
traceback.print_exc()
from . import pyevermizer # as part of the source tree 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: 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. common rules, place the locations in those Regions and shorten the rules.
""" """
GAME_NAME = "Secret of Evermore" _id_base = 64000
ID_OFF_BASE = 64000 _id_offset: typing.Dict[int, int] = {
ID_OFFS: typing.Dict[int,int] = { pyevermizer.CHECK_ALCHEMY: _id_base + 0, # alchemy 64000..64049
pyevermizer.CHECK_ALCHEMY: ID_OFF_BASE + 0, # alchemy 64000..64049 pyevermizer.CHECK_BOSS: _id_base + 50, # bosses 64050..6499
pyevermizer.CHECK_BOSS: ID_OFF_BASE + 50, # bosses 64050..6499 pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399
pyevermizer.CHECK_GOURD: ID_OFF_BASE + 100, # gourds 64100..64399 pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499
pyevermizer.CHECK_NPC: ID_OFF_BASE + 400, # npc 64400..64499
# TODO: sniff 64500..64799 # TODO: sniff 64500..64799
} }
# cache native evermizer items and locations
def _get_locations(): _items = pyevermizer.get_items()
locs = pyevermizer.get_locations() _locations = pyevermizer.get_locations()
for loc in locs: # fix up texts for AP
if loc.type == 3: # TODO: CHECK_GOURD for _loc in _locations:
loc.name = f'{loc.name} #{loc.index}' if _loc.type == pyevermizer.CHECK_GOURD:
return locs _loc.name = f'{_loc.name} #{_loc.index}'
def _get_location_ids(): def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]:
m = {} name_to_id = {}
for loc in _get_locations(): id_to_raw = {}
m[loc.name] = ID_OFFS[loc.type] + loc.index for loc in _locations:
m['Done'] = None apid = _id_offset[loc.type] + loc.index
return m 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(): def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]:
return pyevermizer.get_items() name_to_id = {}
id_to_raw = {}
for item in _items:
def _get_item_ids(): if item.name in name_to_id:
m = {} continue
for item in _get_items(): apid = _id_offset[item.type] + item.index
if item.name in m: continue id_to_raw[apid] = item
m[item.name] = ID_OFFS[item.type] + item.index name_to_id[item.name] = apid
m['Victory'] = None name_to_id['Victory'] = None
return m return name_to_id, id_to_raw
class SoEWorld(World): 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 game: str = "Secret of Evermore"
# options = soe_options options = soe_options
topology_present: bool = True topology_present: bool = True
remote_items: bool = False # True only for testing
data_version = 0
item_name_to_id = _get_item_ids() item_name_to_id, item_id_to_raw = _get_item_mapping()
location_name_to_id = _get_location_ids() 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): def __init__(self, *args, **kwargs):
print('SoE: generate_basic') self.connect_name_available_event = threading.Event()
itempool = [item for item in map(lambda item: self.create_item(item), _get_items())] super(SoEWorld, self).__init__(*args, **kwargs)
self.world.itempool += itempool
self.world.get_location('Done', self.player).place_locked_item(self.create_event('Victory')) 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): 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 = Region('Menu', None, 'Menu', self.player, self.world)
r.exits = [Entrance(self.player, 'New Game', r)] r.exits = [Entrance(self.player, 'New Game', r)]
self.world.regions += [r] self.world.regions += [r]
r = Region('Ingame', None, 'Ingame', self.player, self.world) r = Region('Ingame', None, 'Ingame', self.player, self.world)
r.locations = [SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r) 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)) r.locations.append(SoELocation(self.player, 'Done', None, r))
self.world.regions += [r] self.world.regions += [r]
self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player)) self.world.get_entrance('New Game', self.player).connect(self.world.get_region('Ingame', self.player))
def create_event(self, event: str) -> Item: def create_items(self):
progression = True # clear precollected items since we don't support them yet
return SoEItem(event, progression, None, self.player) if type(self.world.precollected_items) is dict:
self.world.precollected_items[self.player] = []
def create_item(self, item) -> Item: # add items to the pool
# TODO: if item is string: look up item by name self.world.itempool += [item for item in
return SoEItem(item.name, item.progression, self.item_name_to_id[item.name], self.player) map(lambda item: self.create_item(item, self.restrict_item_placement), _items)]
def set_rules(self): def set_rules(self):
print('SoE: set_rules')
self.world.completion_condition[self.player] = lambda state: state.has('Victory', self.player) self.world.completion_condition[self.player] = lambda state: state.has('Victory', self.player)
# set Done from goal option once we have multiple goals # set Done from goal option once we have multiple goals
set_rule(self.world.get_location('Done', self.player), set_rule(self.world.get_location('Done', self.player),
lambda state: state._soe_has(pyevermizer.P_FINAL_BOSS, self.world, 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) set_rule(self.world.get_entrance('New Game', self.player), lambda state: True)
for loc in _get_locations(): for loc in _locations:
set_rule(self.world.get_location(loc.name, self.player), self.make_rule(loc.requires)) 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 make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]:
def rule(state): def rule(state) -> bool:
for count, progress in requires: for count, progress in requires:
if not state._soe_has(progress, self.world, self.player, count): if not state._soe_has(progress, self.world, self.player, count):
return False return False
@ -136,13 +160,79 @@ class SoEWorld(World):
return rule 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): class SoEItem(Item):
game: str = GAME_NAME game: str = "Secret of Evermore"
class SoELocation(Location): class SoELocation(Location):
game: str = GAME_NAME game: str = "Secret of Evermore"
def __init__(self, player: int, name: str, address: typing.Optional[int], parent): def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
super().__init__(player, name, address, parent) super().__init__(player, name, address, parent)

View File

@ -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