2022-10-21 21:26:40 +00:00
|
|
|
import itertools
|
2021-11-07 14:38:02 +00:00
|
|
|
import os
|
2021-11-14 14:46:07 +00:00
|
|
|
import os.path
|
2021-11-07 14:38:02 +00:00
|
|
|
import threading
|
2022-10-21 21:26:40 +00:00
|
|
|
import typing
|
2023-07-05 20:39:35 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
# from . import pyevermizer # as part of the source tree
|
|
|
|
import pyevermizer # from package
|
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
import settings
|
2024-01-12 00:07:40 +00:00
|
|
|
from BaseClasses import Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
|
|
|
|
from Utils import output_path
|
2022-10-21 21:26:40 +00:00
|
|
|
from worlds.AutoWorld import WebWorld, World
|
|
|
|
from worlds.generic.Rules import add_item_rule, set_rule
|
2024-01-12 00:07:40 +00:00
|
|
|
from .logic import SoEPlayerLogic
|
2024-03-29 00:01:31 +00:00
|
|
|
from .options import Difficulty, EnergyCore, Sniffamizer, SniffIngredients, SoEOptions
|
2024-01-12 00:07:40 +00:00
|
|
|
from .patch import SoEDeltaPatch, get_base_rom_path
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
from BaseClasses import MultiWorld, CollectionState
|
2021-11-07 14:38:02 +00:00
|
|
|
|
2024-01-15 08:17:46 +00:00
|
|
|
__all__ = ["pyevermizer", "SoEWorld"]
|
|
|
|
|
|
|
|
|
2021-09-29 07:12:23 +00:00
|
|
|
"""
|
|
|
|
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
|
2024-01-12 00:07:40 +00:00
|
|
|
instead of providing multiple events per item, we iterate through them in logic.py
|
2021-09-29 07:12:23 +00:00
|
|
|
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
|
2024-01-12 00:07:40 +00:00
|
|
|
Provides could be events, but instead we iterate through the entire logic in logic.py
|
2021-09-29 07:12:23 +00:00
|
|
|
e.g. NPC available after fighting a Boss
|
|
|
|
|
|
|
|
Rules are special locations that don't have a physical location
|
2024-01-12 00:07:40 +00:00
|
|
|
instead of implementing virtual locations and virtual items, we simply use them in logic.py
|
2021-09-29 07:12:23 +00:00
|
|
|
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.
|
2022-03-21 21:34:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
Item grouping currently supports
|
|
|
|
* Any <ingredient name> - "Any Water" matches all Water drops
|
|
|
|
* Any <healing item name> - "Any Petal" matches all Petal drops
|
|
|
|
* Any Moniez - Matches the talon/jewel/gold coin/credit drops from chests (not market, fountain or Mungola)
|
|
|
|
* Ingredients - Matches all ingredient drops
|
|
|
|
* Alchemy - Matches all alchemy formulas
|
|
|
|
* Weapons - Matches all weapons but Bazooka, Bone Crusher, Neutron Blade
|
|
|
|
* Traps - Matches all traps
|
2021-09-29 07:12:23 +00:00
|
|
|
"""
|
|
|
|
|
2021-11-07 14:38:02 +00:00
|
|
|
_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
|
2024-03-29 00:01:31 +00:00
|
|
|
# blank 64500..64799
|
2022-07-15 16:01:07 +00:00
|
|
|
pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899
|
|
|
|
pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999
|
2024-03-29 00:01:31 +00:00
|
|
|
pyevermizer.CHECK_SNIFF: _id_base + 1000 # sniff 65000..65592
|
2021-09-29 07:12:23 +00:00
|
|
|
}
|
|
|
|
|
2021-11-07 14:38:02 +00:00
|
|
|
# cache native evermizer items and locations
|
|
|
|
_items = pyevermizer.get_items()
|
2024-03-29 00:01:31 +00:00
|
|
|
_sniff_items = pyevermizer.get_sniff_items() # optional, not part of the default location pool
|
2022-03-22 00:13:30 +00:00
|
|
|
_traps = pyevermizer.get_traps()
|
2022-07-15 16:01:07 +00:00
|
|
|
_extras = pyevermizer.get_extra_items() # items that are not placed by default
|
2021-11-07 14:38:02 +00:00
|
|
|
_locations = pyevermizer.get_locations()
|
2024-03-29 00:01:31 +00:00
|
|
|
_sniff_locations = pyevermizer.get_sniff_locations() # optional, not part of the default location pool
|
2021-11-07 14:38:02 +00:00
|
|
|
# fix up texts for AP
|
|
|
|
for _loc in _locations:
|
|
|
|
if _loc.type == pyevermizer.CHECK_GOURD:
|
2024-03-29 00:01:31 +00:00
|
|
|
_loc.name = f"{_loc.name} #{_loc.index}"
|
|
|
|
for _loc in _sniff_locations:
|
|
|
|
if _loc.type == pyevermizer.CHECK_SNIFF:
|
|
|
|
_loc.name = f"{_loc.name} Sniff #{_loc.index}"
|
|
|
|
del _loc
|
|
|
|
|
2022-03-21 21:34:28 +00:00
|
|
|
# item helpers
|
|
|
|
_ingredients = (
|
|
|
|
'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron',
|
2024-01-21 18:34:24 +00:00
|
|
|
'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Medallion',
|
2022-03-21 21:34:28 +00:00
|
|
|
'Ash', 'Acorn'
|
|
|
|
)
|
|
|
|
_other_items = (
|
|
|
|
'Call bead', 'Petal', 'Biscuit', 'Pixie Dust', 'Nectar', 'Honey', 'Moniez'
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def _match_item_name(item: pyevermizer.Item, substr: str) -> bool:
|
|
|
|
sub: str = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
|
2022-03-21 21:34:28 +00:00
|
|
|
return sub == substr or sub == substr+'s'
|
2021-11-07 14:38:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]:
|
|
|
|
name_to_id = {}
|
|
|
|
id_to_raw = {}
|
2024-03-29 00:01:31 +00:00
|
|
|
for loc in itertools.chain(_locations, _sniff_locations):
|
2022-02-05 13:54:22 +00:00
|
|
|
ap_id = _id_offset[loc.type] + loc.index
|
|
|
|
id_to_raw[ap_id] = loc
|
|
|
|
name_to_id[loc.name] = ap_id
|
2021-11-07 14:38:02 +00:00
|
|
|
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 = {}
|
2024-03-29 00:01:31 +00:00
|
|
|
for item in itertools.chain(_items, _sniff_items, _extras, _traps):
|
2021-11-07 14:38:02 +00:00
|
|
|
if item.name in name_to_id:
|
|
|
|
continue
|
2022-02-05 13:54:22 +00:00
|
|
|
ap_id = _id_offset[item.type] + item.index
|
|
|
|
id_to_raw[ap_id] = item
|
|
|
|
name_to_id[item.name] = ap_id
|
2021-11-07 14:38:02 +00:00
|
|
|
name_to_id['Victory'] = None
|
|
|
|
return name_to_id, id_to_raw
|
2021-09-29 07:12:23 +00:00
|
|
|
|
|
|
|
|
2022-03-21 21:34:28 +00:00
|
|
|
def _get_item_grouping() -> typing.Dict[str, typing.Set[str]]:
|
|
|
|
groups = {}
|
|
|
|
ingredients_group = set()
|
|
|
|
for ingredient in _ingredients:
|
|
|
|
group = set(item.name for item in _items if _match_item_name(item, ingredient))
|
|
|
|
groups[f'Any {ingredient}'] = group
|
|
|
|
ingredients_group |= group
|
|
|
|
groups['Ingredients'] = ingredients_group
|
|
|
|
for other in _other_items:
|
|
|
|
groups[f'Any {other}'] = set(item.name for item in _items if _match_item_name(item, other))
|
|
|
|
groups['Alchemy'] = set(item.name for item in _items if item.type == pyevermizer.CHECK_ALCHEMY)
|
|
|
|
groups['Weapons'] = {'Spider Claw', 'Horn Spear', 'Gladiator Sword', 'Bronze Axe', 'Bronze Spear', 'Crusader Sword',
|
|
|
|
'Lance (Weapon)', 'Knight Basher', 'Atom Smasher', 'Laser Lance'}
|
|
|
|
groups['Traps'] = {trap.name for trap in _traps}
|
|
|
|
return groups
|
|
|
|
|
|
|
|
|
2022-04-03 02:48:43 +00:00
|
|
|
class SoEWebWorld(WebWorld):
|
|
|
|
theme = 'jungle'
|
2022-05-11 18:05:53 +00:00
|
|
|
tutorials = [Tutorial(
|
|
|
|
"Multiworld Setup Guide",
|
2022-07-15 16:01:07 +00:00
|
|
|
"A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related"
|
|
|
|
" software.",
|
2022-05-11 18:05:53 +00:00
|
|
|
"English",
|
|
|
|
"multiworld_en.md",
|
|
|
|
"multiworld/en",
|
|
|
|
["Black Sliver"]
|
|
|
|
)]
|
2022-04-03 02:48:43 +00:00
|
|
|
|
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
class SoESettings(settings.Group):
|
|
|
|
class RomFile(settings.SNESRomPath):
|
|
|
|
"""File name of the SoE US ROM"""
|
|
|
|
description = "Secret of Evermore (USA) ROM"
|
|
|
|
copy_to = "Secret of Evermore (USA).sfc"
|
|
|
|
md5s = [SoEDeltaPatch.hash]
|
|
|
|
|
|
|
|
rom_file: RomFile = RomFile(RomFile.copy_to)
|
|
|
|
|
|
|
|
|
2021-09-29 07:12:23 +00:00
|
|
|
class SoEWorld(World):
|
|
|
|
"""
|
2021-11-07 14:38:02 +00:00
|
|
|
Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a
|
2024-01-15 08:17:46 +00:00
|
|
|
space station where the final boss must be defeated.
|
2021-09-29 07:12:23 +00:00
|
|
|
"""
|
2024-01-12 00:07:40 +00:00
|
|
|
game: typing.ClassVar[str] = "Secret of Evermore"
|
|
|
|
options_dataclass = SoEOptions
|
|
|
|
options: SoEOptions
|
2023-07-05 20:39:35 +00:00
|
|
|
settings: typing.ClassVar[SoESettings]
|
2022-04-03 02:48:43 +00:00
|
|
|
topology_present = False
|
2024-03-29 00:01:31 +00:00
|
|
|
data_version = 5
|
2022-04-03 02:48:43 +00:00
|
|
|
web = SoEWebWorld()
|
2024-03-29 00:01:31 +00:00
|
|
|
required_client_version = (0, 4, 4)
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2021-11-07 14:38:02 +00:00
|
|
|
item_name_to_id, item_id_to_raw = _get_item_mapping()
|
|
|
|
location_name_to_id, location_id_to_raw = _get_location_mapping()
|
2022-03-21 21:34:28 +00:00
|
|
|
item_name_groups = _get_item_grouping()
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
logic: SoEPlayerLogic
|
2021-11-07 14:38:02 +00:00
|
|
|
evermizer_seed: int
|
2021-11-07 14:56:43 +00:00
|
|
|
connect_name: str
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2022-01-22 00:29:20 +00:00
|
|
|
_halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name]
|
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def __init__(self, multiworld: "MultiWorld", player: int):
|
2021-11-07 14:38:02 +00:00
|
|
|
self.connect_name_available_event = threading.Event()
|
2024-01-12 00:07:40 +00:00
|
|
|
super(SoEWorld, self).__init__(multiworld, player)
|
2021-11-07 14:38:02 +00:00
|
|
|
|
2022-07-15 16:01:07 +00:00
|
|
|
def generate_early(self) -> None:
|
2024-01-12 00:07:40 +00:00
|
|
|
# create logic from options
|
|
|
|
if self.options.required_fragments.value > self.options.available_fragments.value:
|
|
|
|
self.options.available_fragments.value = self.options.required_fragments.value
|
|
|
|
self.logic = SoEPlayerLogic(self.player, self.options)
|
2022-07-15 16:01:07 +00:00
|
|
|
|
2021-11-07 14:38:02 +00:00
|
|
|
def create_event(self, event: str) -> Item:
|
2022-06-17 01:23:27 +00:00
|
|
|
return SoEItem(event, ItemClassification.progression, None, self.player)
|
2021-11-07 14:38:02 +00:00
|
|
|
|
2022-03-24 00:39:18 +00:00
|
|
|
def create_item(self, item: typing.Union[pyevermizer.Item, str]) -> Item:
|
2022-10-21 21:26:40 +00:00
|
|
|
if isinstance(item, str):
|
2021-11-07 14:38:02 +00:00
|
|
|
item = self.item_id_to_raw[self.item_name_to_id[item]]
|
2022-06-17 01:23:27 +00:00
|
|
|
if item.type == pyevermizer.CHECK_TRAP:
|
|
|
|
classification = ItemClassification.trap
|
|
|
|
elif item.progression:
|
|
|
|
classification = ItemClassification.progression
|
2022-07-15 16:01:07 +00:00
|
|
|
elif item.useful:
|
|
|
|
classification = ItemClassification.useful
|
2022-06-17 01:23:27 +00:00
|
|
|
else:
|
|
|
|
classification = ItemClassification.filler
|
|
|
|
|
|
|
|
return SoEItem(item.name, classification, self.item_name_to_id[item.name], self.player)
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2022-04-30 01:37:28 +00:00
|
|
|
@classmethod
|
2024-01-12 00:07:40 +00:00
|
|
|
def stage_assert_generate(cls, _: "MultiWorld") -> None:
|
2022-04-30 01:37:28 +00:00
|
|
|
rom_file = get_base_rom_path()
|
|
|
|
if not os.path.exists(rom_file):
|
|
|
|
raise FileNotFoundError(rom_file)
|
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def create_regions(self) -> None:
|
2022-10-21 21:26:40 +00:00
|
|
|
# exclude 'hidden' on easy
|
2024-01-12 00:07:40 +00:00
|
|
|
max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256
|
2022-10-21 21:26:40 +00:00
|
|
|
|
2021-11-07 14:38:02 +00:00
|
|
|
# TODO: generate *some* regions from locations' requirements?
|
2023-10-25 07:34:59 +00:00
|
|
|
menu = Region('Menu', self.player, self.multiworld)
|
|
|
|
self.multiworld.regions += [menu]
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int:
|
2022-11-01 12:14:38 +00:00
|
|
|
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
|
|
|
|
if len(evermizer_loc.requires) == 1 and evermizer_loc.requires[0][1] != pyevermizer.P_WEAPON:
|
|
|
|
return 2
|
|
|
|
return min(2, len(evermizer_loc.requires))
|
|
|
|
|
2023-10-25 07:34:59 +00:00
|
|
|
# create ingame region
|
|
|
|
ingame = Region('Ingame', self.player, self.multiworld)
|
|
|
|
|
2022-10-21 21:26:40 +00:00
|
|
|
# group locations into spheres (1, 2, 3+ at index 0, 1, 2)
|
|
|
|
spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {}
|
|
|
|
for loc in _locations:
|
2022-11-01 12:14:38 +00:00
|
|
|
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
|
2023-10-25 07:34:59 +00:00
|
|
|
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
|
2022-10-21 21:26:40 +00:00
|
|
|
loc.difficulty > max_difficulty))
|
2024-03-29 00:01:31 +00:00
|
|
|
# extend pool if feature and setting enabled
|
|
|
|
if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
|
|
|
|
for loc in _sniff_locations:
|
|
|
|
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
|
|
|
|
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
|
|
|
|
loc.difficulty > max_difficulty))
|
2022-10-21 21:26:40 +00:00
|
|
|
|
|
|
|
# location balancing data
|
|
|
|
trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = {
|
2024-03-29 00:01:31 +00:00
|
|
|
0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40), # remove up to 40 gourds from sphere 1
|
|
|
|
pyevermizer.CHECK_SNIFF: (100, 130, 130, 130)}, # remove up to 130 sniff spots from sphere 1
|
|
|
|
1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90), # remove up to 90 gourds from sphere 2
|
|
|
|
pyevermizer.CHECK_SNIFF: (160, 200, 200, 200)}, # remove up to 200 sniff spots from sphere 2
|
2022-10-21 21:26:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# mark some as excluded based on numbers above
|
|
|
|
for trash_sphere, fills in trash_fills.items():
|
|
|
|
for typ, counts in fills.items():
|
2024-03-29 00:01:31 +00:00
|
|
|
if typ not in spheres[trash_sphere]:
|
|
|
|
continue # e.g. player does not have sniff locations
|
2024-01-12 00:07:40 +00:00
|
|
|
count = counts[self.options.difficulty.value]
|
|
|
|
for location in self.random.sample(spheres[trash_sphere][typ], count):
|
2022-11-01 12:14:38 +00:00
|
|
|
assert location.name != "Energy Core #285", "Error in sphere generation"
|
2022-10-21 21:26:40 +00:00
|
|
|
location.progress_type = LocationProgressType.EXCLUDED
|
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def sphere1_blocked_items_rule(item: pyevermizer.Item) -> bool:
|
2022-10-21 21:26:40 +00:00
|
|
|
if isinstance(item, SoEItem):
|
|
|
|
# disable certain items in sphere 1
|
|
|
|
if item.name in {"Gauge", "Wheel"}:
|
|
|
|
return False
|
|
|
|
# and some more for non-easy, non-mystery
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery):
|
2022-10-21 21:26:40 +00:00
|
|
|
if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
for locations in spheres[0].values():
|
|
|
|
for location in locations:
|
|
|
|
add_item_rule(location, sphere1_blocked_items_rule)
|
|
|
|
|
|
|
|
# make some logically late(r) bosses priority locations to increase complexity
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.difficulty == Difficulty.option_mystery:
|
|
|
|
late_count = self.random.randint(0, 2)
|
2022-10-21 21:26:40 +00:00
|
|
|
else:
|
2024-01-12 00:07:40 +00:00
|
|
|
late_count = self.options.difficulty.value
|
2022-10-21 21:26:40 +00:00
|
|
|
late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala",
|
|
|
|
"Mungola", "Lightning Storm", "Magmar", "Volcano Viper")
|
2024-01-12 00:07:40 +00:00
|
|
|
late_locations = self.random.sample(late_bosses, late_count)
|
2022-10-21 21:26:40 +00:00
|
|
|
|
|
|
|
# add locations to the world
|
|
|
|
for sphere in spheres.values():
|
|
|
|
for locations in sphere.values():
|
|
|
|
for location in locations:
|
2023-10-25 07:34:59 +00:00
|
|
|
ingame.locations.append(location)
|
2022-10-21 21:26:40 +00:00
|
|
|
if location.name in late_locations:
|
|
|
|
location.progress_type = LocationProgressType.PRIORITY
|
|
|
|
|
2023-10-25 07:34:59 +00:00
|
|
|
ingame.locations.append(SoELocation(self.player, 'Done', None, ingame))
|
|
|
|
menu.connect(ingame, "New Game")
|
|
|
|
self.multiworld.regions += [ingame]
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def create_items(self) -> None:
|
2022-07-15 16:01:07 +00:00
|
|
|
# add regular items to the pool
|
|
|
|
exclusions: typing.List[str] = []
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.energy_core != EnergyCore.option_shuffle:
|
2022-07-15 16:01:07 +00:00
|
|
|
exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below
|
|
|
|
items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions)))
|
|
|
|
|
|
|
|
# remove one pair of wings that will be placed in generate_basic
|
|
|
|
items.remove(self.create_item("Wings"))
|
2022-03-22 00:13:30 +00:00
|
|
|
|
2024-03-29 00:01:31 +00:00
|
|
|
# extend pool if feature and setting enabled
|
|
|
|
if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
|
|
|
|
if self.options.sniff_ingredients == SniffIngredients.option_vanilla_ingredients:
|
|
|
|
# vanilla ingredients
|
|
|
|
items += list(map(lambda item: self.create_item(item), _sniff_items))
|
|
|
|
else:
|
|
|
|
# random ingredients
|
|
|
|
items += [self.create_item(self.get_filler_item_name()) for _ in _sniff_items]
|
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def is_ingredient(item: pyevermizer.Item) -> bool:
|
2022-07-15 16:01:07 +00:00
|
|
|
for ingredient in _ingredients:
|
|
|
|
if _match_item_name(item, ingredient):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
# add energy core fragments to the pool
|
|
|
|
ingredients = [n for n, item in enumerate(items) if is_ingredient(item)]
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.energy_core == EnergyCore.option_fragments:
|
2022-07-15 16:01:07 +00:00
|
|
|
items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core
|
2024-01-12 00:07:40 +00:00
|
|
|
for _ in range(self.options.available_fragments - 1):
|
2022-07-15 16:01:07 +00:00
|
|
|
if len(ingredients) < 1:
|
|
|
|
break # out of ingredients to replace
|
2024-01-12 00:07:40 +00:00
|
|
|
r = self.random.choice(ingredients)
|
2022-07-15 16:01:07 +00:00
|
|
|
ingredients.remove(r)
|
|
|
|
items[r] = self.create_item("Energy Core Fragment")
|
|
|
|
|
|
|
|
# add traps to the pool
|
2024-01-12 00:07:40 +00:00
|
|
|
trap_count = self.options.trap_count.value
|
|
|
|
trap_names: typing.List[str] = []
|
|
|
|
trap_weights: typing.List[int] = []
|
2022-03-22 00:13:30 +00:00
|
|
|
if trap_count > 0:
|
2024-01-12 00:07:40 +00:00
|
|
|
for trap_option in self.options.trap_chances:
|
|
|
|
trap_names.append(trap_option.item_name)
|
|
|
|
trap_weights.append(trap_option.value)
|
|
|
|
if sum(trap_weights) == 0:
|
|
|
|
trap_weights = [1 for _ in trap_weights]
|
2022-03-22 00:13:30 +00:00
|
|
|
|
2022-03-24 00:39:18 +00:00
|
|
|
def create_trap() -> Item:
|
2024-01-12 00:07:40 +00:00
|
|
|
return self.create_item(self.random.choices(trap_names, trap_weights)[0])
|
2022-03-22 00:13:30 +00:00
|
|
|
|
2022-07-15 16:01:07 +00:00
|
|
|
for _ in range(trap_count):
|
|
|
|
if len(ingredients) < 1:
|
|
|
|
break # out of ingredients to replace
|
2024-01-12 00:07:40 +00:00
|
|
|
r = self.random.choice(ingredients)
|
2022-07-15 16:01:07 +00:00
|
|
|
ingredients.remove(r)
|
|
|
|
items[r] = create_trap()
|
2022-03-22 00:13:30 +00:00
|
|
|
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.itempool += items
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def set_rules(self) -> None:
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.completion_condition[self.player] = lambda state: state.has('Victory', self.player)
|
2021-09-29 07:12:23 +00:00
|
|
|
# set Done from goal option once we have multiple goals
|
2022-11-01 02:41:21 +00:00
|
|
|
set_rule(self.multiworld.get_location('Done', self.player),
|
2024-01-12 00:07:40 +00:00
|
|
|
lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS))
|
2022-11-01 02:41:21 +00:00
|
|
|
set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True)
|
2024-03-29 00:01:31 +00:00
|
|
|
locations: typing.Iterable[pyevermizer.Location]
|
|
|
|
if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
|
|
|
|
locations = itertools.chain(_locations, _sniff_locations)
|
|
|
|
else:
|
|
|
|
locations = _locations
|
|
|
|
for loc in locations:
|
2022-11-01 02:41:21 +00:00
|
|
|
location = self.multiworld.get_location(loc.name, self.player)
|
2021-11-07 14:38:02 +00:00
|
|
|
set_rule(location, self.make_rule(loc.requires))
|
|
|
|
|
2022-10-21 21:26:40 +00:00
|
|
|
def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]:
|
2024-01-12 00:07:40 +00:00
|
|
|
def rule(state: "CollectionState") -> bool:
|
2021-09-29 07:12:23 +00:00
|
|
|
for count, progress in requires:
|
2024-01-12 00:07:40 +00:00
|
|
|
if not self.logic.has(state, progress, count):
|
2021-09-29 07:12:23 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
return rule
|
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def generate_basic(self) -> None:
|
2021-11-07 14:38:02 +00:00
|
|
|
# place Victory event
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.get_location('Done', self.player).place_locked_item(self.create_event('Victory'))
|
2022-01-22 00:29:20 +00:00
|
|
|
# place wings in halls NE to avoid softlock
|
2024-01-12 00:07:40 +00:00
|
|
|
wings_location = self.random.choice(self._halls_ne_chest_names)
|
2022-01-22 00:29:20 +00:00
|
|
|
wings_item = self.create_item('Wings')
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.get_location(wings_location, self.player).place_locked_item(wings_item)
|
2022-07-15 16:01:07 +00:00
|
|
|
# place energy core at vanilla location for vanilla mode
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.energy_core == EnergyCore.option_vanilla:
|
2022-07-15 16:01:07 +00:00
|
|
|
energy_core = self.create_item('Energy Core')
|
2022-11-01 02:41:21 +00:00
|
|
|
self.multiworld.get_location('Energy Core #285', self.player).place_locked_item(energy_core)
|
2021-11-07 14:38:02 +00:00
|
|
|
# generate stuff for later
|
2024-01-12 00:07:40 +00:00
|
|
|
self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
|
|
|
|
|
|
|
|
def generate_output(self, output_directory: str) -> None:
|
2022-11-01 02:41:21 +00:00
|
|
|
player_name = self.multiworld.get_player_name(self.player)
|
2021-11-07 14:38:02 +00:00
|
|
|
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()
|
2022-10-21 21:26:40 +00:00
|
|
|
placement_file = ""
|
|
|
|
out_file = ""
|
2021-11-07 14:38:02 +00:00
|
|
|
try:
|
2024-01-12 00:07:40 +00:00
|
|
|
money = self.options.money_modifier.value
|
|
|
|
exp = self.options.exp_modifier.value
|
2022-07-15 16:01:07 +00:00
|
|
|
switches: typing.List[str] = []
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.death_link.value:
|
2022-03-22 00:13:30 +00:00
|
|
|
switches.append("--death-link")
|
2024-01-12 00:07:40 +00:00
|
|
|
if self.options.energy_core == EnergyCore.option_fragments:
|
|
|
|
switches.extend(('--available-fragments', str(self.options.available_fragments.value),
|
|
|
|
'--required-fragments', str(self.options.required_fragments.value)))
|
2022-03-18 03:53:09 +00:00
|
|
|
rom_file = get_base_rom_path()
|
2022-11-01 02:41:21 +00:00
|
|
|
out_base = output_path(output_directory, self.multiworld.get_out_file_name_base(self.player))
|
2021-11-07 14:38:02 +00:00
|
|
|
out_file = out_base + '.sfc'
|
|
|
|
placement_file = out_base + '.txt'
|
|
|
|
patch_file = out_base + '.apsoe'
|
|
|
|
flags = 'l' # spoiler log
|
2024-01-12 00:07:40 +00:00
|
|
|
flags += self.options.flags
|
2021-11-07 14:38:02 +00:00
|
|
|
|
|
|
|
with open(placement_file, "wb") as f: # generate placement file
|
2023-10-29 18:47:37 +00:00
|
|
|
for location in self.multiworld.get_locations(self.player):
|
2021-11-07 14:38:02 +00:00
|
|
|
item = location.item
|
2022-10-21 21:26:40 +00:00
|
|
|
assert item is not None, "Can't handle unfilled location"
|
|
|
|
if item.code is None or location.address is None:
|
2021-11-07 14:38:02 +00:00
|
|
|
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:
|
2022-10-21 21:26:40 +00:00
|
|
|
soe_item = self.item_id_to_raw[item.code]
|
|
|
|
line = f'{loc.type},{loc.index}:{soe_item.type},{soe_item.index}\n'
|
2021-11-07 14:38:02 +00:00
|
|
|
f.write(line.encode('utf-8'))
|
|
|
|
|
2021-11-14 14:46:07 +00:00
|
|
|
if not os.path.exists(rom_file):
|
|
|
|
raise FileNotFoundError(rom_file)
|
2022-11-01 02:41:21 +00:00
|
|
|
if (pyevermizer.main(rom_file, out_file, placement_file, self.multiworld.seed_name, self.connect_name,
|
2022-03-22 00:13:30 +00:00
|
|
|
self.evermizer_seed, flags, money, exp, switches)):
|
2021-11-07 14:38:02 +00:00
|
|
|
raise RuntimeError()
|
2022-03-18 03:53:09 +00:00
|
|
|
patch = SoEDeltaPatch(patch_file, player=self.player,
|
|
|
|
player_name=player_name, patched_path=out_file)
|
|
|
|
patch.write()
|
2022-10-21 21:26:40 +00:00
|
|
|
except Exception:
|
2021-11-07 14:38:02 +00:00
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
try:
|
|
|
|
os.unlink(placement_file)
|
|
|
|
os.unlink(out_file)
|
2022-02-22 10:48:08 +00:00
|
|
|
os.unlink(out_file[:-4] + '_SPOILER.log')
|
2022-10-21 21:26:40 +00:00
|
|
|
except FileNotFoundError:
|
2021-11-07 14:38:02 +00:00
|
|
|
pass
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2024-01-12 00:07:40 +00:00
|
|
|
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None:
|
2021-11-07 14:56:43 +00:00
|
|
|
# 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
|
2022-11-01 02:41:21 +00:00
|
|
|
if self.connect_name and self.connect_name != self.multiworld.player_name[self.player]:
|
|
|
|
payload = multidata["connect_names"][self.multiworld.player_name[self.player]]
|
2021-11-07 14:56:43 +00:00
|
|
|
multidata["connect_names"][self.connect_name] = payload
|
2022-02-09 20:06:34 +00:00
|
|
|
|
2022-05-19 13:37:26 +00:00
|
|
|
def get_filler_item_name(self) -> str:
|
2024-01-12 00:07:40 +00:00
|
|
|
return self.random.choice(list(self.item_name_groups["Ingredients"]))
|
2022-05-19 13:37:26 +00:00
|
|
|
|
2022-06-17 01:23:27 +00:00
|
|
|
|
2021-09-29 07:12:23 +00:00
|
|
|
class SoEItem(Item):
|
2021-11-07 14:38:02 +00:00
|
|
|
game: str = "Secret of Evermore"
|
2022-10-21 21:26:40 +00:00
|
|
|
__slots__ = () # disable __dict__
|
2021-09-29 07:12:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SoELocation(Location):
|
2021-11-07 14:38:02 +00:00
|
|
|
game: str = "Secret of Evermore"
|
2022-10-21 21:26:40 +00:00
|
|
|
__slots__ = () # disables __dict__ once Location has __slots__
|
2021-09-29 07:12:23 +00:00
|
|
|
|
2022-10-21 21:26:40 +00:00
|
|
|
def __init__(self, player: int, name: str, address: typing.Optional[int], parent: Region, exclude: bool = False):
|
2021-09-29 07:12:23 +00:00
|
|
|
super().__init__(player, name, address, parent)
|
2022-10-21 21:26:40 +00:00
|
|
|
# unconditional assignments favor a split dict, saving memory
|
|
|
|
self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT
|