Skip to content

Commit

Permalink
Merge branch 'main' into smw-main
Browse files Browse the repository at this point in the history
  • Loading branch information
PoryGone committed Dec 11, 2022
2 parents 7f4eb70 + 32820ba commit 61c1d2f
Show file tree
Hide file tree
Showing 214 changed files with 39,449 additions and 19,660 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Unittests
run: |
pytest test
pytest
151 changes: 151 additions & 0 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,157 @@ def parse_data(self):
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon"

def create_playthrough(self, create_paths: bool = True):
"""Destructive to the world while it is run, damage gets repaired afterwards."""
from itertools import chain
# get locations containing progress items
multiworld = self.multiworld
prog_locations = {location for location in multiworld.get_filled_locations() if location.item.advancement}
state_cache = [None]
collection_spheres: List[Set[Location]] = []
state = CollectionState(multiworld)
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:

# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres

sphere = {location for location in sphere_candidates if state.can_reach(location)}

for location in sphere:
state.collect(location.item, True, location)

sphere_candidates -= sphere
collection_spheres.append(sphere)
state_cache.append(state.copy())

logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere),
len(prog_locations))
if not sphere:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([multiworld.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
self.unreachables = sphere_candidates
break

# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if multiworld.can_beat_game(state_cache[num]):
to_delete.add(location)
restore_later[location] = old_item
else:
# still required, got to keep it around
location.item = old_item

# cull entries in spheres for spoiler walkthrough at end
sphere -= to_delete

# second phase, sphere 0
removed_precollected = []
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
multiworld.precollected_items[item.player].remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
multiworld.push_precollected(item)
else:
removed_precollected.append(item)

# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres

required_locations = {item for sphere in collection_spheres for item in sphere}
state = CollectionState(multiworld)
collection_spheres = []
while required_locations:
state.sweep_for_events(key_only=True)

sphere = set(filter(state.can_reach, required_locations))

for location in sphere:
state.collect(location.item, True, location)

required_locations -= sphere

collection_spheres.append(sphere)

logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')

# we can finally output our playthrough
self.playthrough = {"0": sorted([str(item) for item in
chain.from_iterable(multiworld.precollected_items.values())
if item.advancement])}

for i, sphere in enumerate(collection_spheres):
self.playthrough[str(i + 1)] = {
str(location): str(location.item) for location in sorted(sphere)}
if create_paths:
self.create_paths(state, collection_spheres)

# repair the multiworld again
for location, item in restore_later.items():
location.item = item

for item in removed_precollected:
multiworld.push_precollected(item)

def create_paths(self, state: CollectionState, collection_spheres: List[Set[Location]]):
from itertools import zip_longest
multiworld = self.multiworld

def flist_to_iter(node):
while node:
value, node = node
yield value

def get_path(state, region):
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)

self.paths = {}
topology_worlds = (player for player in multiworld.player_ids if multiworld.worlds[player].topology_present)
for player in topology_worlds:
self.paths.update(
{str(location): get_path(state, location.parent_region)
for sphere in collection_spheres for location in sphere
if location.player == player})
if player in multiworld.get_game_players("A Link to the Past"):
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
for (_, exit_path) in path):
if multiworld.mode[player] != 'inverted':
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Big Bomb Shop', player))
else:
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))

def to_json(self):
self.parse_data()
out = OrderedDict()
Expand Down
143 changes: 1 addition & 142 deletions Main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import collections
from itertools import zip_longest, chain
import logging
import os
import time
Expand Down Expand Up @@ -417,7 +416,7 @@ def precollect_hint(location):

if args.spoiler > 1:
logger.info('Calculating playthrough.')
create_playthrough(world)
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)

if args.spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
Expand All @@ -431,143 +430,3 @@ def precollect_hint(location):

logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world


def create_playthrough(world):
"""Destructive to the world while it is run, damage gets repaired afterwards."""
# get locations containing progress items
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
state_cache = [None]
collection_spheres = []
state = CollectionState(world)
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:

# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres

sphere = {location for location in sphere_candidates if state.can_reach(location)}

for location in sphere:
state.collect(location.item, True, location)

sphere_candidates -= sphere
collection_spheres.append(sphere)
state_cache.append(state.copy())

logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere),
len(prog_locations))
if not sphere:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
world.spoiler.unreachables = sphere_candidates
break

# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if world.can_beat_game(state_cache[num]):
to_delete.add(location)
restore_later[location] = old_item
else:
# still required, got to keep it around
location.item = old_item

# cull entries in spheres for spoiler walkthrough at end
sphere -= to_delete

# second phase, sphere 0
removed_precollected = []
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items[item.player].remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
else:
removed_precollected.append(item)

# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres

required_locations = {item for sphere in collection_spheres for item in sphere}
state = CollectionState(world)
collection_spheres = []
while required_locations:
state.sweep_for_events(key_only=True)

sphere = set(filter(state.can_reach, required_locations))

for location in sphere:
state.collect(location.item, True, location)

required_locations -= sphere

collection_spheres.append(sphere)

logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')

def flist_to_iter(node):
while node:
value, node = node
yield value

def get_path(state, region):
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)

world.spoiler.paths = {}
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
for player in topology_worlds:
world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})
if player in world.get_game_players("A Link to the Past"):
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
get_path(state, world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, world.get_region('Inverted Big Bomb Shop', player))

# we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in
chain.from_iterable(world.precollected_items.values())
if item.advancement])}

for i, sphere in enumerate(collection_spheres):
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}

# repair the world again
for location, item in restore_later.items():
location.item = item

for item in removed_precollected:
world.push_precollected(item)
12 changes: 9 additions & 3 deletions OoTAdjuster.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import random
import os
import zipfile
from itertools import chain

from BaseClasses import MultiWorld
Expand Down Expand Up @@ -217,13 +218,18 @@ def adjust(args):
# Load up the ROM
rom = Rom(file=args.rom, force_use=True)
delete_zootdec = True
elif os.path.splitext(args.rom)[-1] == '.apz5':
elif os.path.splitext(args.rom)[-1] in ['.apz5', '.zpf']:
# Load vanilla ROM
rom = Rom(file=args.vanilla_rom, force_use=True)
apz5_file = args.rom
base_name = os.path.splitext(apz5_file)[0]
# Patch file
apply_patch_file(rom, args.rom)
apply_patch_file(rom, apz5_file,
sub_file=(os.path.basename(base_name) + '.zpf'
if zipfile.is_zipfile(apz5_file)
else None))
else:
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
raise Exception("Invalid file extension; requires .n64, .z64, .apz5, .zpf")
# Call patch_cosmetics
try:
patch_cosmetics(ootworld, rom)
Expand Down
Loading

0 comments on commit 61c1d2f

Please sign in to comment.