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

View File

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